diff --git a/package-lock.json b/package-lock.json
index 7e696ee21..bf671ae79 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28450,6 +28450,7 @@
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
+ "mime-types": "^3.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
@@ -30007,6 +30008,31 @@
"uc.micro": "^2.0.0"
}
},
+ "packages/bruno-app/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "packages/bruno-app/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"packages/bruno-app/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json
index ca6441f91..496aebc88 100644
--- a/packages/bruno-app/package.json
+++ b/packages/bruno-app/package.json
@@ -44,9 +44,8 @@
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",
- "jsesc": "^3.0.2",
"js-yaml": "^4.1.0",
- "xml2js": "^0.6.2",
+ "jsesc": "^3.0.2",
"jshint": "^2.13.6",
"json5": "^2.2.3",
"jsonc-parser": "^3.2.1",
@@ -56,6 +55,7 @@
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
+ "mime-types": "^3.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
@@ -89,6 +89,7 @@
"system": "^2.0.1",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",
+ "xml2js": "^0.6.2",
"yup": "^0.32.11"
},
"devDependencies": {
diff --git a/packages/bruno-app/src/components/BodyModeSelector/StyledWrapper.js b/packages/bruno-app/src/components/BodyModeSelector/StyledWrapper.js
new file mode 100644
index 000000000..e810ef66d
--- /dev/null
+++ b/packages/bruno-app/src/components/BodyModeSelector/StyledWrapper.js
@@ -0,0 +1,38 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ font-size: ${(props) => props.theme.font.size.base};
+
+ .body-mode-selector {
+ background: transparent;
+ border-radius: 3px;
+
+ .dropdown-item {
+ padding: 0.2rem 0.6rem !important;
+ padding-left: 1.5rem !important;
+ display: flex;
+ align-items: center;
+ }
+
+ .label-item {
+ padding: 0.2rem 0.6rem !important;
+ }
+
+ .selected-body-mode {
+ color: ${(props) => props.theme.colors.text.yellow};
+ }
+
+ .dropdown-icon {
+ display: flex;
+ align-items: center;
+ margin-right: 0.5rem;
+ }
+ }
+
+ .caret {
+ color: ${(props) => props.theme.colors.text.muted};
+ fill: ${(props) => props.theme.colors.text.muted};
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/BodyModeSelector/index.js b/packages/bruno-app/src/components/BodyModeSelector/index.js
index 842c3f9ce..781bfbffc 100644
--- a/packages/bruno-app/src/components/BodyModeSelector/index.js
+++ b/packages/bruno-app/src/components/BodyModeSelector/index.js
@@ -1,17 +1,27 @@
import React, { useRef, forwardRef } from 'react';
-import { IconCaretDown } from '@tabler/icons';
+import {
+ IconCaretDown,
+ IconForms,
+ IconBraces,
+ IconCode,
+ IconFileText,
+ IconDatabase,
+ IconFile,
+ IconX
+} from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { humanizeRequestBodyMode } from 'utils/collections';
+import StyledWrapper from './StyledWrapper';
const DEFAULT_MODES = [
- { key: 'multipartForm', label: 'Multipart Form', category: 'Form' },
- { key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form' },
- { key: 'json', label: 'JSON', category: 'Raw' },
- { key: 'xml', label: 'XML', category: 'Raw' },
- { key: 'text', label: 'TEXT', category: 'Raw' },
- { key: 'sparql', label: 'SPARQL', category: 'Raw' },
- { key: 'file', label: 'File / Binary', category: 'Other' },
- { key: 'none', label: 'None', category: 'Other' }
+ { key: 'multipartForm', label: 'Multipart Form', category: 'Form', icon: IconForms },
+ { key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form', icon: IconForms },
+ { key: 'json', label: 'JSON', category: 'Raw', icon: IconBraces },
+ { key: 'xml', label: 'XML', category: 'Raw', icon: IconCode },
+ { key: 'text', label: 'TEXT', category: 'Raw', icon: IconFileText },
+ { key: 'sparql', label: 'SPARQL', category: 'Raw', icon: IconDatabase },
+ { key: 'file', label: 'File / Binary', category: 'Other', icon: IconFile },
+ { key: 'none', label: 'No Body', category: 'Other', icon: IconX }
];
const BodyModeSelector = ({
@@ -53,30 +63,40 @@ const BodyModeSelector = ({
}, {});
return (
-
-
}
- placement={placement}
- disabled={disabled}
- className={className}
- >
- {Object.entries(groupedModes).map(([category, categoryModes]) => (
-
- {showCategories && {category}
}
- {categoryModes.map((mode) => (
- onModeSelect(mode.key)}
- >
- {mode.label}
-
- ))}
-
- ))}
-
-
+
+
+
}
+ placement={placement}
+ disabled={disabled}
+ className={className}
+ >
+ {Object.entries(groupedModes).map(([category, categoryModes]) => (
+
+ {showCategories && {category}
}
+ {categoryModes.map((mode) => {
+ const ModeIcon = mode.icon;
+ return (
+ onModeSelect(mode.key)}
+ >
+ {ModeIcon && (
+
+
+
+ )}
+ {mode.label}
+
+ );
+ })}
+
+ ))}
+
+
+
);
};
diff --git a/packages/bruno-app/src/components/EditableTable/StyledWrapper.js b/packages/bruno-app/src/components/EditableTable/StyledWrapper.js
index 1202a210d..f8b962063 100644
--- a/packages/bruno-app/src/components/EditableTable/StyledWrapper.js
+++ b/packages/bruno-app/src/components/EditableTable/StyledWrapper.js
@@ -35,17 +35,17 @@ const StyledWrapper = styled.div`
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
vertical-align: middle;
- &:nth-child(1) {
- width: 25px !important;
- border-right: none;
- }
-
&:last-child {
border-right: none;
}
}
}
+ &.has-checkbox thead td:nth-child(1) {
+ width: 25px !important;
+ border-right: none;
+ }
+
tbody {
tr {
transition: background 0.1s ease;
@@ -62,19 +62,6 @@ const StyledWrapper = styled.div`
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
vertical-align: middle;
- &:nth-child(1) {
- width: 25px;
- border-right: none;
- text-align: center;
- vertical-align: middle;
- line-height: 1;
-
- input[type='checkbox'] {
- vertical-align: baseline;
- display: inline-block;
- }
- }
-
&:last-child {
border-right: none;
}
@@ -82,6 +69,19 @@ const StyledWrapper = styled.div`
}
}
+ &.has-checkbox tbody td:nth-child(1) {
+ width: 25px;
+ border-right: none;
+ text-align: center;
+ vertical-align: middle;
+ line-height: 1;
+
+ input[type='checkbox'] {
+ vertical-align: baseline;
+ display: inline-block;
+ }
+ }
+
.tooltip-mod {
font-size: 11px !important;
max-width: 200px !important;
diff --git a/packages/bruno-app/src/components/EditableTable/index.js b/packages/bruno-app/src/components/EditableTable/index.js
index 7a78e3ab4..5fcf171b3 100644
--- a/packages/bruno-app/src/components/EditableTable/index.js
+++ b/packages/bruno-app/src/components/EditableTable/index.js
@@ -223,7 +223,7 @@ const EditableTable = ({
const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length;
return (
-
+
diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js
index 126cc5938..758a9910e 100644
--- a/packages/bruno-app/src/components/FilePickerEditor/index.js
+++ b/packages/bruno-app/src/components/FilePickerEditor/index.js
@@ -55,16 +55,30 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
return filenames.length > 0 ? (
{!readOnly && (
-
) : (
diff --git a/packages/bruno-app/src/components/Icons/ExampleIcon/index.js b/packages/bruno-app/src/components/Icons/ExampleIcon/index.js
index aa7a5c47b..9826c5fce 100644
--- a/packages/bruno-app/src/components/Icons/ExampleIcon/index.js
+++ b/packages/bruno-app/src/components/Icons/ExampleIcon/index.js
@@ -1,16 +1,16 @@
import React from 'react';
-const ExampleIcon = ({ color = 'white', size = 16, ...props }) => {
+const ExampleIcon = ({ color = 'currentColor', size = 16, ...props }) => {
return (
diff --git a/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js
index a119245e8..7485e7f62 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js
@@ -9,6 +9,14 @@ const StyledWrapper = styled.div`
}
}
+ .request-pane {
+ flex-shrink: 0;
+ }
+
+ .response-pane {
+ min-width: 0;
+ }
+
div.dragbar-wrapper {
display: flex;
align-items: center;
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js
index 220f12200..be167ebdb 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { IconAlertTriangle } from '@tabler/icons';
-import CloseTabIcon from './CloseTabIcon';
+import GradientCloseButton from './GradientCloseButton';
const RequestTabNotFound = ({ handleCloseClick }) => {
const [showErrorMessage, setShowErrorMessage] = useState(false);
@@ -28,9 +28,7 @@ const RequestTabNotFound = ({ handleCloseClick }) => {
>
) : null}
- handleCloseClick(e)}>
-
-
+
>
);
};
diff --git a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBodyMode/index.js b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBodyMode/index.js
index d471245e7..7b8badc52 100644
--- a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBodyMode/index.js
+++ b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBodyMode/index.js
@@ -14,36 +14,34 @@ const ResponseExampleBodyMode = ({ item, collection, exampleUid, body, bodyMode,
// Initialize the new body structure based on the selected mode
let newBody = { mode: value };
- // Preserve existing data for the new mode if it exists
- if (body) {
- switch (value) {
- case 'json':
- newBody.json = body.json || '';
- break;
- case 'text':
- newBody.text = body.text || '';
- break;
- case 'xml':
- newBody.xml = body.xml || '';
- break;
- case 'sparql':
- newBody.sparql = body.sparql || '';
- break;
- case 'formUrlEncoded':
- newBody.formUrlEncoded = body.formUrlEncoded || [];
- break;
- case 'multipartForm':
- newBody.multipartForm = body.multipartForm || [];
- break;
- case 'file':
- newBody.file = body.file || { name: '', data: '' };
- break;
- case 'none':
- // No additional data needed for 'none' mode
- break;
- default:
- break;
- }
+ // Initialize body content based on selected mode
+ switch (value) {
+ case 'json':
+ newBody.json = body?.json || '';
+ break;
+ case 'text':
+ newBody.text = body?.text || '';
+ break;
+ case 'xml':
+ newBody.xml = body?.xml || '';
+ break;
+ case 'sparql':
+ newBody.sparql = body?.sparql || '';
+ break;
+ case 'formUrlEncoded':
+ newBody.formUrlEncoded = body?.formUrlEncoded || [];
+ break;
+ case 'multipartForm':
+ newBody.multipartForm = body?.multipartForm || [];
+ break;
+ case 'file':
+ newBody.file = Array.isArray(body?.file) ? body.file : [];
+ break;
+ case 'none':
+ // No additional data needed for 'none' mode
+ break;
+ default:
+ break;
}
dispatch(updateResponseExampleRequest({
diff --git a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFileBody/index.js b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFileBody/index.js
index 72b7caab0..1a231d097 100644
--- a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFileBody/index.js
+++ b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFileBody/index.js
@@ -1,16 +1,14 @@
-import React, { useState, useMemo } from 'react';
-import { get, cloneDeep } from 'lodash';
-import { IconTrash } from '@tabler/icons';
+import React, { useState, useMemo, useCallback } from 'react';
+import { get } from 'lodash';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { updateResponseExampleFileBodyParams } from 'providers/ReduxStore/slices/collections';
import mime from 'mime-types';
import path from 'utils/common/path';
+import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
import FilePickerEditor from 'components/FilePickerEditor/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
-import Table from 'components/Table-v2';
-import ReorderTable from 'components/ReorderTable';
import RadioButton from 'components/RadioButton';
const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = false }) => {
@@ -25,16 +23,8 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
return Array.isArray(_params) ? _params : [];
}, [item.draft, item.examples, item, exampleUid]);
- const [enabledFileUid, setEnableFileUid] = useState(params.length > 0 ? params[0].uid : '');
-
- const addFile = () => {
- const newParam = {
- filePath: '',
- contentType: '',
- selected: true
- };
-
- const updatedParams = [...params, newParam];
+ const handleParamsChange = useCallback((updatedParams) => {
+ if (!editMode) return;
dispatch(updateResponseExampleFileBodyParams({
itemUid: item.uid,
@@ -42,75 +32,70 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
exampleUid: exampleUid,
params: updatedParams
}));
- };
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
- const handleParamChange = (e, _param, type) => {
+ const handleFilePathChange = useCallback((row, newFilePath, onChange) => {
if (!editMode) return;
- const param = cloneDeep(_param);
- switch (type) {
- case 'filePath': {
- param.filePath = e.target.filePath;
- // Auto-detect content type from file extension using mime library (same as updateFile)
- const contentType = mime.contentType(path.extname(e.target.filePath));
- param.contentType = contentType || '';
- break;
- }
- case 'contentType': {
- param.contentType = e.target.contentType;
- break;
- }
- case 'selected': {
- // When a file is selected, deselect all others and select this one
- const updatedParams = params.map((p) => ({
- ...p,
- selected: p.uid === param.uid ? e.target.checked : false
- }));
+ const currentParams = params || [];
+ const existingParam = currentParams.find((p) => p.uid === row.uid);
- dispatch(updateResponseExampleFileBodyParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
-
- // Update the enabled file UID state
- if (e.target.checked) {
- setEnableFileUid(param.uid);
+ let updatedParams;
+ if (existingParam) {
+ // Update existing param
+ updatedParams = currentParams.map((p) => {
+ if (p.uid === row.uid) {
+ const updated = { ...p, filePath: newFilePath };
+ // Auto-detect content type from file extension
+ if (newFilePath) {
+ const contentType = mime.contentType(path.extname(newFilePath));
+ updated.contentType = contentType || '';
+ } else {
+ updated.contentType = '';
+ }
+ return updated;
}
- return; // Early return since we already dispatched
+ return p;
+ });
+ } else {
+ // Add new param (from EditableTable's empty row)
+ // Deselect all existing params and select the new one
+ const deselectedParams = currentParams.map((p) => ({ ...p, selected: false }));
+ const newParam = {
+ uid: row.uid,
+ filePath: newFilePath,
+ contentType: '',
+ selected: true
+ };
+ // Auto-detect content type from file extension
+ if (newFilePath) {
+ const contentType = mime.contentType(path.extname(newFilePath));
+ newParam.contentType = contentType || '';
}
+ updatedParams = [...deselectedParams, newParam];
}
- const updatedParams = params.map((p) => p.uid === param.uid ? param : p);
+ handleParamsChange(updatedParams);
+ }, [editMode, params, handleParamsChange]);
- dispatch(updateResponseExampleFileBodyParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
- };
-
- const handleRemoveParams = (param) => {
+ const handleSelectedChange = useCallback((row, checked) => {
if (!editMode) return;
- const updatedParams = params.filter((p) => p.uid !== param.uid);
-
- dispatch(updateResponseExampleFileBodyParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
+ // When a file is selected, deselect all others and select this one
+ const updatedParams = params.map((p) => ({
+ ...p,
+ selected: p.uid === row.uid ? checked : false
}));
- };
- const handleParamDrag = ({ updateReorderedItem }) => {
+ handleParamsChange(updatedParams);
+ }, [editMode, params, handleParamsChange]);
+
+ const handleParamDrag = useCallback(({ updateReorderedItem }) => {
if (!editMode) return;
const reorderedParams = updateReorderedItem.map((uid) => {
return params.find((p) => p.uid === uid);
- });
+ }).filter(Boolean);
dispatch(updateResponseExampleFileBodyParams({
itemUid: item.uid,
@@ -118,6 +103,73 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
exampleUid: exampleUid,
params: reorderedParams
}));
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid, params]);
+
+ const columns = [
+ {
+ key: 'filePath',
+ name: 'File',
+ isKeyField: true,
+ placeholder: 'File',
+ width: '50%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ handleFilePathChange(row, newPath, onChange)}
+ collection={collection}
+ readOnly={!editMode}
+ />
+ )
+ },
+ {
+ key: 'contentType',
+ name: 'Content-Type',
+ placeholder: 'Auto',
+ width: '30%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ theme={storedTheme}
+ placeholder={isLastEmptyRow ? 'Auto' : ''}
+ value={value || ''}
+ onChange={onChange}
+ onRun={() => {}}
+ collection={collection}
+ readOnly={!editMode}
+ />
+ )
+ },
+ {
+ key: 'selected',
+ name: 'Selected',
+ width: '20%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow, rowIndex }) => (
+
+ handleSelectedChange(row, e.target.checked)}
+ disabled={!editMode}
+ className="mr-1 mousetrap"
+ dataTestId={`file-radio-button-${rowIndex}`}
+ />
+
+ )
+ }
+ ];
+
+ const defaultRow = {
+ filePath: '',
+ contentType: '',
+ selected: false
};
if (params.length === 0 && !editMode) {
@@ -126,95 +178,16 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
return (
-
-
- {params && params.length
- ? params.map((param, index) => {
- return (
-
- |
-
- handleParamChange({
- target: {
- filePath: path
- }
- },
- param,
- 'filePath') : () => {}}
- collection={collection}
- readOnly={!editMode}
- />
- |
-
-
- {}}
- theme={storedTheme}
- placeholder="Auto"
- value={param.contentType}
- onChange={editMode ? (newValue) =>
- handleParamChange({
- target: {
- contentType: newValue
- }
- },
- param,
- 'contentType') : () => {}}
- onRun={() => {}}
- collection={collection}
- />
-
- |
-
-
- handleParamChange(e, param, 'selected') : () => {}}
- disabled={!editMode}
- className="mr-1 mousetrap"
- dataTestId={`file-radio-button-${index}`}
- />
- handleRemoveParams(param)}
- className={`delete-button ${editMode ? 'edit-mode' : ''}`}
- disabled={!editMode}
- >
-
-
-
- |
-
- );
- })
- : null}
-
-
-
- {editMode && (
-
-
- + Add File
-
-
- )}
+
);
};
diff --git a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFormUrlEncodedParams/index.js b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFormUrlEncodedParams/index.js
index f4f88fb02..e0b0ef23c 100644
--- a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFormUrlEncodedParams/index.js
+++ b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFormUrlEncodedParams/index.js
@@ -1,15 +1,11 @@
-import React, { useMemo } from 'react';
+import React, { useMemo, useCallback } from 'react';
import get from 'lodash/get';
-import cloneDeep from 'lodash/cloneDeep';
-import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { updateResponseExampleFormUrlEncodedParams } from 'providers/ReduxStore/slices/collections';
+import EditableTable from 'components/EditableTable';
import MultiLineEditor from 'components/MultiLineEditor';
import StyledWrapper from './StyledWrapper';
-import ReorderTable from 'components/ReorderTable/index';
-import Table from 'components/Table-v2';
-import Checkbox from 'components/Checkbox';
const ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, editMode = false }) => {
const dispatch = useDispatch();
@@ -21,72 +17,67 @@ const ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, edi
: get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.body?.formUrlEncoded || [];
}, [item, exampleUid]);
- const addParam = () => {
- const newParam = {
- name: '',
- value: '',
- enabled: true
- };
-
- const updatedParams = [...params, newParam];
-
- dispatch(updateResponseExampleFormUrlEncodedParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
- };
-
- const handleParamChange = (e, _param, type) => {
+ const handleParamsChange = useCallback((updatedParams) => {
if (!editMode) return;
- const param = cloneDeep(_param);
- switch (type) {
- case 'name': {
- param.name = e.target.value;
- break;
- }
- case 'value': {
- param.value = e.target.value;
- break;
- }
- case 'enabled': {
- param.enabled = e.target.checked;
- break;
- }
+ dispatch(updateResponseExampleFormUrlEncodedParams({
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ exampleUid: exampleUid,
+ params: updatedParams
+ }));
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
+
+ const handleParamDrag = useCallback(({ updateReorderedItem }) => {
+ if (!editMode) return;
+
+ const reorderedParams = updateReorderedItem.map((uid) => {
+ return params.find((p) => p.uid === uid);
+ }).filter(Boolean);
+
+ dispatch(updateResponseExampleFormUrlEncodedParams({
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ exampleUid: exampleUid,
+ params: reorderedParams
+ }));
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid, params]);
+
+ const columns = [
+ {
+ key: 'name',
+ name: 'Key',
+ isKeyField: true,
+ placeholder: 'Key',
+ width: '40%',
+ readOnly: !editMode
+ },
+ {
+ key: 'value',
+ name: 'Value',
+ placeholder: 'Value',
+ width: '60%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ onChange={onChange}
+ allowNewlines={true}
+ onRun={() => {}}
+ collection={collection}
+ item={item}
+ placeholder={isLastEmptyRow ? 'Value' : ''}
+ />
+ )
}
+ ];
- const updatedParams = params.map((p) => p.uid === param.uid ? param : p);
-
- dispatch(updateResponseExampleFormUrlEncodedParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
- };
-
- const handleRemoveParams = (param) => {
- const updatedParams = params.filter((p) => p.uid !== param.uid);
-
- dispatch(updateResponseExampleFormUrlEncodedParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
- };
-
- const handleParamDrag = ({ updateReorderedItem }) => {
- const updatedParams = updateReorderedItem(params);
-
- dispatch(updateResponseExampleFormUrlEncodedParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
+ const defaultRow = {
+ name: '',
+ value: '',
+ enabled: true
};
if (params.length === 0 && !editMode) {
@@ -95,84 +86,15 @@ const ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, edi
return (
-
-
- {editMode && (
-
-
- + Add Param
-
-
- )}
+
);
};
diff --git a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleHeaders/index.js b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleHeaders/index.js
index beb1266ed..28a156d05 100644
--- a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleHeaders/index.js
+++ b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleHeaders/index.js
@@ -1,14 +1,11 @@
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
-import { IconTrash } from '@tabler/icons';
import get from 'lodash/get';
-import { addResponseExampleRequestHeader, updateResponseExampleRequestHeader, deleteResponseExampleRequestHeader, moveResponseExampleRequestHeader, setResponseExampleRequestHeaders } from 'providers/ReduxStore/slices/collections';
-import Table from 'components/Table-v2';
-import ReorderTable from 'components/ReorderTable';
+import { moveResponseExampleRequestHeader, setResponseExampleRequestHeaders } from 'providers/ReduxStore/slices/collections';
+import EditableTable from 'components/EditableTable';
import SingleLineEditor from 'components/SingleLineEditor';
import BulkEditor from 'components/BulkEditor';
-import Checkbox from 'components/Checkbox';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
import StyledWrapper from './StyledWrapper';
@@ -26,55 +23,18 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
: get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.headers || [];
}, [item, exampleUid]);
- const handleAddHeader = () => {
+ const handleHeadersChange = useCallback((updatedHeaders) => {
if (editMode) {
- dispatch(addResponseExampleRequestHeader({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid
- }));
- }
- };
-
- const handleHeaderValueChange = (e, header, type) => {
- if (editMode) {
- const updatedHeader = { ...header };
- switch (type) {
- case 'name': {
- updatedHeader.name = e.target.value;
- break;
- }
- case 'value': {
- updatedHeader.value = e.target.value;
- break;
- }
- case 'enabled': {
- updatedHeader.enabled = e.target.checked;
- break;
- }
- }
-
- dispatch(updateResponseExampleRequestHeader({
+ dispatch(setResponseExampleRequestHeaders({
itemUid: item.uid,
collectionUid: collection.uid,
exampleUid: exampleUid,
- header: updatedHeader
+ headers: updatedHeaders
}));
}
- };
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
- const handleRemoveHeader = (header) => {
- if (editMode) {
- dispatch(deleteResponseExampleRequestHeader({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- headerUid: header.uid
- }));
- }
- };
-
- const handleHeaderDrag = ({ updateReorderedItem }) => {
+ const handleHeaderDrag = useCallback(({ updateReorderedItem }) => {
if (editMode) {
dispatch(moveResponseExampleRequestHeader({
itemUid: item.uid,
@@ -83,7 +43,7 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
updateReorderedItem
}));
}
- };
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
@@ -100,6 +60,58 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
}
};
+ const columns = [
+ {
+ key: 'name',
+ name: 'Key',
+ isKeyField: true,
+ placeholder: 'Key',
+ width: '40%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
+ autocomplete={headerAutoCompleteList}
+ onRun={() => {}}
+ collection={collection}
+ placeholder={isLastEmptyRow ? 'Key' : ''}
+ />
+ )
+ },
+ {
+ key: 'value',
+ name: 'Value',
+ placeholder: 'Value',
+ width: '60%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ onChange={onChange}
+ onRun={() => {}}
+ autocomplete={MimeTypes}
+ allowNewlines={true}
+ collection={collection}
+ item={item}
+ placeholder={isLastEmptyRow ? 'Value' : ''}
+ />
+ )
+ }
+ ];
+
+ const defaultRow = {
+ name: '',
+ value: '',
+ enabled: true
+ };
+
if (isBulkEditMode && editMode) {
return (
@@ -119,85 +131,17 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
return (
Headers
-
-
- {headers && headers.length
- ? headers.map((header, index) => (
-
- |
-
- handleHeaderValueChange(e, header, 'enabled')}
- dataTestId={`header-checkbox-${index}`}
- />
-
- {}}
- onChange={(newValue) =>
- handleHeaderValueChange({
- target: {
- value: newValue
- }
- },
- header,
- 'name')}
- autocomplete={headerAutoCompleteList}
- onRun={() => {}}
- collection={collection}
- />
- |
-
-
- {}}
- onChange={(newValue) =>
- handleHeaderValueChange({
- target: {
- value: newValue
- }
- },
- header,
- 'value')}
- onRun={() => {}}
- autocomplete={MimeTypes}
- allowNewlines={true}
- collection={collection}
- item={item}
- />
- {editMode && (
- handleRemoveHeader(header)} className="delete-button">
-
-
- )}
-
- |
-
- ))
- : null}
-
-
-
+
{editMode && (
-
-
- + Add Header
-
+
{
const dispatch = useDispatch();
@@ -25,95 +23,109 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
: get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.body?.multipartForm || [];
}, [item, exampleUid]);
- const addParam = () => {
- const newParam = {
- name: '',
- value: '',
- contentType: '',
- enabled: true,
- type: 'text'
- };
-
- const updatedParams = [...params, newParam];
-
- dispatch(updateResponseExampleMultipartFormParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
- };
-
- const addFile = () => {
- const newParam = {
- name: '',
- value: [],
- contentType: '',
- enabled: true,
- type: 'file'
- };
-
- const updatedParams = [...params, newParam];
-
- dispatch(updateResponseExampleMultipartFormParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
- };
-
- const handleParamChange = (e, _param, type) => {
+ const handleParamsChange = useCallback((updatedParams) => {
if (!editMode) return;
- const param = cloneDeep(_param);
- switch (type) {
- case 'name': {
- param.name = e.target.value;
- break;
- }
- case 'value': {
- param.value = e.target.value;
- if (param.type === 'file' && e.target.value) {
- const contentType = mime.contentType(path.extname(e.target.value));
- param.contentType = contentType || '';
+ dispatch(updateResponseExampleMultipartFormParams({
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ exampleUid: exampleUid,
+ params: updatedParams
+ }));
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
+
+ const handleBrowseFiles = useCallback((row, onChange) => {
+ if (!editMode) return;
+
+ dispatch(browseFiles())
+ .then((filePaths) => {
+ const processedPaths = filePaths.map((filePath) => {
+ const collectionDir = collection.pathname;
+ if (filePath.startsWith(collectionDir)) {
+ return path.relative(collectionDir, filePath);
+ }
+ return filePath;
+ });
+
+ const currentParams = params || [];
+ const existingParam = currentParams.find((p) => p.uid === row.uid);
+
+ let updatedParams;
+ if (existingParam) {
+ // Update existing param
+ updatedParams = currentParams.map((p) => {
+ if (p.uid === row.uid) {
+ const updated = { ...p, type: 'file', value: processedPaths };
+ // Auto-detect content type from first file
+ if (processedPaths.length > 0) {
+ const contentType = mime.contentType(path.extname(processedPaths[0]));
+ updated.contentType = contentType || '';
+ }
+ return updated;
+ }
+ return p;
+ });
+ } else {
+ // Add new param (from EditableTable's empty row)
+ const newParam = {
+ uid: row.uid,
+ name: row.name || '',
+ type: 'file',
+ value: processedPaths,
+ contentType: '',
+ enabled: true
+ };
+ // Auto-detect content type from first file
+ if (processedPaths.length > 0) {
+ const contentType = mime.contentType(path.extname(processedPaths[0]));
+ newParam.contentType = contentType || '';
+ }
+ updatedParams = [...currentParams, newParam];
}
- break;
- }
- case 'contentType': {
- param.contentType = e.target.value;
- break;
- }
- case 'enabled': {
- param.enabled = e.target.checked;
- break;
- }
- }
- const updatedParams = params.map((p) => p.uid === param.uid ? param : p);
+ handleParamsChange(updatedParams);
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ }, [editMode, dispatch, collection.pathname, params, handleParamsChange]);
- dispatch(updateResponseExampleMultipartFormParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
- };
-
- const handleRemoveParams = (param) => {
+ const handleClearFile = useCallback((row) => {
if (!editMode) return;
- const updatedParams = params.filter((p) => p.uid !== param.uid);
+ const currentParams = params || [];
+ const existingParam = currentParams.find((p) => p.uid === row.uid);
- dispatch(updateResponseExampleMultipartFormParams({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- params: updatedParams
- }));
- };
+ if (existingParam) {
+ const updatedParams = currentParams.map((p) => {
+ if (p.uid === row.uid) {
+ return { ...p, type: 'text', value: '' };
+ }
+ return p;
+ });
+ handleParamsChange(updatedParams);
+ }
+ }, [editMode, params, handleParamsChange]);
- const handleParamDrag = ({ updateReorderedItem }) => {
+ const handleValueChange = useCallback((row, newValue, onChange) => {
+ if (!editMode) return;
+
+ const currentParams = params || [];
+ const existingParam = currentParams.find((p) => p.uid === row.uid);
+ if (existingParam) {
+ const updatedParams = currentParams.map((p) => {
+ if (p.uid === row.uid) {
+ return { ...p, type: 'text', value: newValue };
+ }
+ return p;
+ });
+ handleParamsChange(updatedParams);
+ } else {
+ onChange(newValue);
+ }
+ }, [editMode, params, handleParamsChange]);
+
+ const handleParamDrag = useCallback(({ updateReorderedItem }) => {
if (!editMode) return;
const reorderedParams = updateReorderedItem.map((uid) => {
@@ -126,6 +138,117 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
exampleUid: exampleUid,
params: reorderedParams
}));
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid, params]);
+
+ const getFileName = (filePaths) => {
+ if (!filePaths || (Array.isArray(filePaths) && filePaths.length === 0)) {
+ return null;
+ }
+ const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
+ const validPaths = paths.filter((v) => v != null && v !== '');
+ if (validPaths.length === 0) return null;
+
+ const separator = isWindowsOS() ? '\\' : '/';
+ if (validPaths.length === 1) {
+ return validPaths[0].split(separator).pop();
+ }
+ return `${validPaths.length} file(s)`;
+ };
+
+ const columns = [
+ {
+ key: 'name',
+ name: 'Key',
+ isKeyField: true,
+ placeholder: 'Key',
+ width: '30%',
+ readOnly: !editMode
+ },
+ {
+ key: 'value',
+ name: 'Value',
+ placeholder: 'Value',
+ width: '40%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => {
+ const isFile = row.type === 'file';
+ const fileName = isFile ? getFileName(value) : null;
+ const hasTextValue = !isFile && value && value.length > 0;
+
+ if (fileName) {
+ return (
+
+
+
+ {fileName}
+
+ handleClearFile(row)}
+ title="Remove file"
+ >
+
+
+
+ );
+ }
+
+ return (
+
+
+ {}}
+ theme={storedTheme}
+ value={value || ''}
+ onChange={(newValue) => handleValueChange(row, newValue, onChange)}
+ onRun={() => {}}
+ allowNewlines={true}
+ collection={collection}
+ item={item}
+ readOnly={!editMode}
+ placeholder={isLastEmptyRow ? 'Value' : ''}
+ />
+
+ {!hasTextValue && !isLastEmptyRow && (
+
handleBrowseFiles(row, onChange)}
+ title="Select file"
+ >
+
+
+ )}
+
+ );
+ }
+ },
+ {
+ key: 'contentType',
+ name: 'Content-Type',
+ placeholder: 'Auto',
+ width: '30%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ theme={storedTheme}
+ placeholder={isLastEmptyRow ? 'Auto' : ''}
+ value={value || ''}
+ onChange={onChange}
+ onRun={() => {}}
+ collection={collection}
+ readOnly={!editMode}
+ />
+ )
+ }
+ ];
+
+ const defaultRow = {
+ name: '',
+ value: '',
+ contentType: '',
+ enabled: true,
+ type: 'text'
};
if (params.length === 0 && !editMode) {
@@ -134,129 +257,15 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
return (
-
-
- {params && params.length
- ? params.map((param, index) => {
- return (
-
- |
-
- handleParamChange(e, param, 'enabled')}
- dataTestId={`multipart-form-param-checkbox-${index}`}
- />
-
- handleParamChange(e, param, 'name')}
- disabled={!editMode}
- />
- |
-
-
- {param.type === 'file' ? (
-
- handleParamChange({
- target: {
- value: newValue
- }
- },
- param,
- 'value')}
- collection={collection}
- readOnly={!editMode}
- />
- ) : (
- {}}
- theme={storedTheme}
- value={param.value}
- onChange={(newValue) =>
- handleParamChange({
- target: {
- value: newValue
- }
- },
- param,
- 'value')}
- onRun={() => {}}
- allowNewlines={true}
- collection={collection}
- item={item}
- readOnly={!editMode}
- />
- )}
-
- |
-
-
- {}}
- theme={storedTheme}
- placeholder="Auto"
- value={param.contentType}
- onChange={(newValue) =>
- handleParamChange({
- target: {
- value: newValue
- }
- },
- param,
- 'contentType')}
- onRun={() => {}}
- collection={collection}
- readOnly={!editMode}
- />
- handleRemoveParams(param)}
- className={`delete-button ${editMode ? 'edit-mode' : ''}`}
- disabled={!editMode}
- >
-
-
-
- |
-
- );
- })
- : null}
-
-
-
- {editMode && (
-
-
- + Add Param
-
-
- + Add File
-
-
- )}
+
);
};
diff --git a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/StyledWrapper.js b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/StyledWrapper.js
index e566a1390..e2baec877 100644
--- a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/StyledWrapper.js
+++ b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/StyledWrapper.js
@@ -43,13 +43,17 @@ const StyledWrapper = styled.div`
tbody {
tr {
border-bottom: 1px solid ${(props) => props.theme.table.border};
-
- &:hover {
- background: ${(props) => props.theme.plainGrid.hoverBg};
- }
}
}
}
+
+ /* Override styles for EditableTable to prevent uppercase transformation and ensure proper spacing */
+ /* The .table-container is from EditableTable component */
+ .table-container table thead td {
+ text-transform: none !important;
+ letter-spacing: normal !important;
+ padding: 8px 10px !important;
+ }
tr {
diff --git a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/index.js b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/index.js
index 1fcf0ef2a..4b550df28 100644
--- a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/index.js
+++ b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/index.js
@@ -1,14 +1,11 @@
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
-import { IconTrash } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import get from 'lodash/get';
-import { addResponseExampleParam, updateResponseExampleParam, deleteResponseExampleParam, moveResponseExampleParam, setResponseExampleParams } from 'providers/ReduxStore/slices/collections';
-import Table from 'components/Table-v2';
-import ReorderTable from 'components/ReorderTable';
+import { moveResponseExampleParam, setResponseExampleParams } from 'providers/ReduxStore/slices/collections';
+import EditableTable from 'components/EditableTable';
import SingleLineEditor from 'components/SingleLineEditor';
import BulkEditor from 'components/BulkEditor';
-import Checkbox from 'components/Checkbox';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
@@ -26,61 +23,22 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
const queryParams = params.filter((param) => param.type === 'query');
const pathParams = params.filter((param) => param.type === 'path');
- const handleAddQueryParam = () => {
+ const handleQueryParamsChange = useCallback((updatedQueryParams) => {
if (!editMode) {
return;
}
- dispatch(addResponseExampleParam({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid
- }));
- };
-
- const handleQueryParamChange = (e, data, key) => {
- if (!editMode) {
- return;
- }
-
- const updatedParam = { ...data };
- switch (key) {
- case 'name': {
- updatedParam.name = e.target.value;
- break;
- }
- case 'value': {
- updatedParam.value = e.target.value;
- break;
- }
- case 'enabled': {
- updatedParam.enabled = e.target.checked;
- break;
- }
- }
-
- dispatch(updateResponseExampleParam({
+ // Merge updated query params with path params
+ const allParams = [...updatedQueryParams, ...pathParams];
+ dispatch(setResponseExampleParams({
itemUid: item.uid,
collectionUid: collection.uid,
exampleUid: exampleUid,
- param: updatedParam
+ params: allParams
}));
- };
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid, pathParams]);
- const handleRemoveQueryParam = (param) => {
- if (!editMode) {
- return;
- }
-
- dispatch(deleteResponseExampleParam({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- paramUid: param.uid
- }));
- };
-
- const handleQueryParamDrag = ({ updateReorderedItem }) => {
+ const handleQueryParamDrag = useCallback(({ updateReorderedItem }) => {
if (!editMode) {
return;
}
@@ -91,7 +49,22 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
exampleUid: exampleUid,
updateReorderedItem
}));
- };
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
+
+ const handlePathParamsChange = useCallback((updatedPathParams) => {
+ if (!editMode) {
+ return;
+ }
+
+ // Merge updated path params with query params
+ const allParams = [...queryParams, ...updatedPathParams];
+ dispatch(setResponseExampleParams({
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ exampleUid: exampleUid,
+ params: allParams
+ }));
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid, queryParams]);
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
@@ -102,27 +75,13 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
return;
}
+ // Merge bulk edited query params with path params
+ const allParams = [...newParams, ...pathParams];
dispatch(setResponseExampleParams({
itemUid: item.uid,
collectionUid: collection.uid,
exampleUid: exampleUid,
- params: newParams
- }));
- };
-
- const handlePathParamChange = (e, data) => {
- if (!editMode) {
- return;
- }
-
- const updatedParam = { ...data };
- updatedParam.value = e.target.value;
-
- dispatch(updateResponseExampleParam({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- param: updatedParam
+ params: allParams
}));
};
@@ -138,6 +97,86 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
);
}
+ const queryColumns = [
+ {
+ key: 'name',
+ name: 'Name',
+ isKeyField: true,
+ placeholder: 'Name',
+ width: '40%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ onChange={onChange}
+ onRun={() => {}}
+ collection={collection}
+ variablesAutocomplete={true}
+ readOnly={!editMode}
+ placeholder={isLastEmptyRow ? 'Name' : ''}
+ />
+ )
+ },
+ {
+ key: 'value',
+ name: 'Value',
+ placeholder: 'Value',
+ width: '60%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ onChange={onChange}
+ onRun={() => {}}
+ collection={collection}
+ variablesAutocomplete={true}
+ readOnly={!editMode}
+ placeholder={isLastEmptyRow ? 'Value' : ''}
+ />
+ )
+ }
+ ];
+
+ const pathColumns = [
+ {
+ key: 'name',
+ name: 'Name',
+ readOnly: true,
+ width: '40%'
+ },
+ {
+ key: 'value',
+ name: 'Value',
+ placeholder: 'Value',
+ width: '60%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ onChange={onChange}
+ onRun={() => {}}
+ collection={collection}
+ variablesAutocomplete={true}
+ readOnly={!editMode}
+ placeholder={isLastEmptyRow ? 'Value' : ''}
+ />
+ )
+ }
+ ];
+
+ const defaultQueryRow = {
+ name: '',
+ value: '',
+ enabled: true,
+ type: 'query'
+ };
+
if (queryParams.length === 0 && pathParams.length === 0 && !editMode) {
return null;
}
@@ -145,69 +184,17 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
return (
Query parameters
-
-
- {queryParams && queryParams.length
- ? queryParams.map((param, index) => (
-
- |
-
- handleQueryParamChange(e, param, 'enabled')}
- dataTestId={`query-param-checkbox-${index}`}
- />
-
- {}}
- onChange={(newValue) => handleQueryParamChange({ target: { value: newValue } }, param, 'name')}
- onRun={() => {}}
- collection={collection}
- variablesAutocomplete={true}
- readOnly={!editMode}
- />
- |
-
-
- {}}
- onChange={(newValue) => handleQueryParamChange({ target: { value: newValue } }, param, 'value')}
- onRun={() => {}}
- collection={collection}
- variablesAutocomplete={true}
- readOnly={!editMode}
- />
- {editMode && (
- handleRemoveQueryParam(param)} className="delete-button">
-
-
- )}
-
- |
-
- ))
- : null}
-
-
-
+
{editMode && (
-
-
- {pathParams && pathParams.length
- ? pathParams.map((path, index) => {
- return (
-
- |
- {path.name}
- |
-
- {}}
- onChange={(newValue) => handlePathParamChange({ target: { value: newValue } }, path)}
- onRun={() => {}}
- collection={collection}
- variablesAutocomplete={true}
- readOnly={!editMode}
- />
- |
-
- );
- })
- : null}
-
- {pathParams.length === 0 && No path parameters defined
}
+
>
)}
diff --git a/packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseHeaders/index.js b/packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseHeaders/index.js
index e2ffd6573..499018b4b 100644
--- a/packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseHeaders/index.js
+++ b/packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseHeaders/index.js
@@ -1,12 +1,10 @@
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
-import { IconTrash } from '@tabler/icons';
import get from 'lodash/get';
-import { addResponseExampleHeader, updateResponseExampleHeader, deleteResponseExampleHeader, moveResponseExampleHeader, setResponseExampleHeaders, updateResponseExampleResponse } from 'providers/ReduxStore/slices/collections';
+import { moveResponseExampleHeader, setResponseExampleHeaders, updateResponseExampleResponse } from 'providers/ReduxStore/slices/collections';
import { getBodyType } from 'utils/responseBodyProcessor';
-import Table from 'components/Table-v2';
-import ReorderTable from 'components/ReorderTable';
+import EditableTable from 'components/EditableTable';
import SingleLineEditor from 'components/SingleLineEditor';
import BulkEditor from 'components/BulkEditor';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
@@ -28,45 +26,17 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.response || {} : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.response || {};
}, [item, exampleUid]);
- const handleAddHeader = () => {
+ const handleHeadersChange = useCallback((updatedHeaders) => {
if (!editMode) {
return;
}
- dispatch(addResponseExampleHeader({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid
- }));
- };
+ // Check if content-type header was updated
+ const contentTypeHeader = updatedHeaders.find((h) => h.name?.toLowerCase() === 'content-type');
+ const oldContentTypeHeader = headers.find((h) => h.name?.toLowerCase() === 'content-type');
- const handleHeaderValueChange = (e, header, type) => {
- if (!editMode) {
- return;
- }
-
- const updatedHeader = { ...header };
- switch (type) {
- case 'name': {
- updatedHeader.name = e.target.value;
- break;
- }
- case 'value': {
- updatedHeader.value = e.target.value;
- break;
- }
- }
-
- dispatch(updateResponseExampleHeader({
- itemUid: item.uid,
- collectionUid: collection.uid,
- exampleUid: exampleUid,
- header: updatedHeader
- }));
-
- // If content-type header is being updated, automatically update the body type
- if (header.name?.toLowerCase() === 'content-type' && type === 'value') {
- const newContentType = updatedHeader.value?.toLowerCase() || '';
+ if (contentTypeHeader && oldContentTypeHeader && contentTypeHeader.value !== oldContentTypeHeader.value) {
+ const newContentType = contentTypeHeader.value?.toLowerCase() || '';
const newBodyType = getBodyType(newContentType);
const currentBodyType = response.body?.type || 'text';
@@ -85,22 +55,16 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
}));
}
}
- };
- const handleRemoveHeader = (header) => {
- if (!editMode) {
- return;
- }
-
- dispatch(deleteResponseExampleHeader({
+ dispatch(setResponseExampleHeaders({
itemUid: item.uid,
collectionUid: collection.uid,
exampleUid: exampleUid,
- headerUid: header.uid
+ headers: updatedHeaders
}));
- };
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid, headers, response]);
- const handleHeaderDrag = ({ updateReorderedItem }) => {
+ const handleHeaderDrag = useCallback(({ updateReorderedItem }) => {
if (!editMode) {
return;
}
@@ -111,7 +75,7 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
exampleUid: exampleUid,
updateReorderedItem
}));
- };
+ }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
@@ -152,79 +116,71 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
);
}
+ const columns = [
+ {
+ key: 'name',
+ name: 'Key',
+ isKeyField: true,
+ placeholder: 'Key',
+ width: '40%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
+ autocomplete={headerAutoCompleteList}
+ onRun={() => {}}
+ collection={collection}
+ readOnly={!editMode}
+ placeholder={isLastEmptyRow ? 'Key' : ''}
+ />
+ )
+ },
+ {
+ key: 'value',
+ name: 'Value',
+ placeholder: 'Value',
+ width: '60%',
+ readOnly: !editMode,
+ render: ({ row, value, onChange, isLastEmptyRow }) => (
+ {}}
+ onChange={onChange}
+ onRun={() => {}}
+ autocomplete={MimeTypes}
+ allowNewlines={true}
+ collection={collection}
+ item={item}
+ readOnly={!editMode}
+ placeholder={isLastEmptyRow ? 'Value' : ''}
+ />
+ )
+ }
+ ];
+
+ const defaultRow = {
+ name: '',
+ value: ''
+ };
+
return (
-
-
- {headers && headers.length
- ? headers.map((header) => (
-
- |
- {}}
- onChange={(newValue) =>
- handleHeaderValueChange({
- target: {
- value: newValue
- }
- },
- header,
- 'name')}
- autocomplete={headerAutoCompleteList}
- onRun={() => {}}
- collection={collection}
- readOnly={!editMode}
- />
- |
-
-
- {}}
- onChange={(newValue) =>
- handleHeaderValueChange({
- target: {
- value: newValue
- }
- },
- header,
- 'value')}
- onRun={() => {}}
- autocomplete={MimeTypes}
- allowNewlines={true}
- collection={collection}
- item={item}
- readOnly={!editMode}
- />
- {editMode && (
- handleRemoveHeader(header)} className="delete-button">
-
-
- )}
-
- |
-
- ))
- : null}
-
-
-
+
{editMode && (
-
-
- + Add Header
-
+
props.theme.sidebar.dropdownIcon.color};
+ visibility: hidden;
.dropdown {
div[aria-expanded='true'] {
visibility: visible;
}
div[aria-expanded='false'] {
- visibility: hidden;
+ visibility: visible;
}
}
}
@@ -36,11 +37,7 @@ const StyledWrapper = styled.div`
&.item-hovered {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
.menu-icon {
- .dropdown {
- div[aria-expanded='false'] {
- visibility: visible;
- }
- }
+ visibility: visible;
}
}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/index.js
index 927198558..bc4e983c5 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/index.js
@@ -1,4 +1,4 @@
-import React, { useState, useRef, forwardRef, useEffect } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import {
@@ -8,13 +8,16 @@ import {
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
import { uuid } from 'utils/common';
-import { IconDots } from '@tabler/icons';
+import { IconDots, IconEdit, IconCopy, IconTrash, IconCode } from '@tabler/icons';
import ExampleIcon from 'components/Icons/ExampleIcon';
import range from 'lodash/range';
import classnames from 'classnames';
-import Dropdown from 'components/Dropdown';
+import MenuDropdown from 'ui/MenuDropdown';
+import ActionIcon from 'ui/ActionIcon';
import Modal from 'components/Modal';
import DeleteResponseExampleModal from './DeleteResponseExampleModal';
+import GenerateCodeItem from '../GenerateCodeItem';
+import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
const ExampleItem = ({ example, item, collection }) => {
@@ -25,8 +28,9 @@ const ExampleItem = ({ example, item, collection }) => {
const [editName, setEditName] = useState(example.name);
const [showRenameModal, setShowRenameModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
- const dropdownTippyRef = useRef(null);
+ const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
const exampleRef = useRef(null);
+ const menuDropdownRef = useRef(null);
// Calculate indentation: item depth + 1 for examples
const indents = range((item.depth || 0) + 1);
@@ -48,9 +52,6 @@ const ExampleItem = ({ example, item, collection }) => {
const handleRename = () => {
setEditName(example.name); // Set current name when opening modal
setShowRenameModal(true);
- if (dropdownTippyRef.current) {
- dropdownTippyRef.current.hide();
- }
};
// Update editName when example changes
@@ -93,26 +94,22 @@ const ExampleItem = ({ example, item, collection }) => {
itemUid: item.uid,
exampleIndex: clonedExampleIndex
}));
-
- if (dropdownTippyRef.current) {
- dropdownTippyRef.current.hide();
- }
};
const handleDelete = () => {
setShowDeleteModal(true);
- if (dropdownTippyRef.current) {
- dropdownTippyRef.current.hide();
- }
};
- const handleRightClick = (e) => {
- e.preventDefault();
- e.stopPropagation();
-
- // Show the dropdown menu programmatically
- if (dropdownTippyRef.current) {
- dropdownTippyRef.current.show();
+ const handleGenerateCode = () => {
+ // Check if example has a request URL
+ if (
+ (example?.request?.url !== '' && example?.request?.url !== undefined)
+ || (item?.request?.url !== '' && item?.request?.url !== undefined)
+ || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')
+ ) {
+ setGenerateCodeItemModalOpen(true);
+ } else {
+ toast.error('URL is required');
}
};
@@ -130,17 +127,48 @@ const ExampleItem = ({ example, item, collection }) => {
setShowRenameModal(false);
};
- const onDropdownCreate = (instance) => {
- dropdownTippyRef.current = instance;
+ // Build menu items for MenuDropdown
+ const buildMenuItems = () => {
+ return [
+ {
+ id: 'rename',
+ leftSection: IconEdit,
+ label: 'Rename',
+ onClick: handleRename,
+ testId: 'response-example-rename-option'
+ },
+ {
+ id: 'clone',
+ leftSection: IconCopy,
+ label: 'Clone',
+ onClick: handleClone,
+ testId: 'response-example-clone-option'
+ },
+ {
+ id: 'generate-code',
+ leftSection: IconCode,
+ label: 'Generate Code',
+ onClick: handleGenerateCode,
+ testId: 'response-example-generate-code-option'
+ },
+ { id: 'separator-1', type: 'divider' },
+ {
+ id: 'delete',
+ leftSection: IconTrash,
+ label: 'Delete',
+ className: 'delete-item',
+ onClick: handleDelete,
+ testId: 'response-example-delete-option'
+ }
+ ];
};
- const MenuIcon = forwardRef((props, ref) => {
- return (
-
-
-
- );
- });
+ // Handle right-click context menu
+ const handleContextMenu = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ menuDropdownRef.current?.open();
+ };
const itemRowClassName = classnames('flex collection-item-name relative items-center', {
'item-focused-in-tab': isExampleActive
@@ -152,7 +180,7 @@ const ExampleItem = ({ example, item, collection }) => {
className={itemRowClassName}
onClick={handleExampleClick}
onDoubleClick={handleDoubleClick}
- onContextMenu={handleRightClick}
+ onContextMenu={handleContextMenu}
>
{indents && indents.length
? indents.map((i) => (
@@ -160,7 +188,6 @@ const ExampleItem = ({ example, item, collection }) => {
className="indent-block"
key={i}
style={{ width: 16, minWidth: 16, height: '100%' }}
- onContextMenu={handleRightClick}
>
{/* Indent */}
@@ -169,45 +196,20 @@ const ExampleItem = ({ example, item, collection }) => {
-
} placement="bottom-start">
-
{
- dropdownTippyRef.current.hide();
- handleRename();
- }}
- data-testid="response-example-rename-option"
- >
- Rename
-
-
{
- dropdownTippyRef.current.hide();
- handleClone();
- }}
- data-testid="response-example-clone-option"
- >
- Clone
-
-
{
- dropdownTippyRef.current.hide();
- handleDelete();
- }}
- data-testid="response-example-delete-option"
- >
- Delete
-
-
+
+
+
{showRenameModal && (
@@ -250,6 +252,16 @@ const ExampleItem = ({ example, item, collection }) => {
collection={collection}
/>
)}
+
+ {generateCodeItemModalOpen && (
+
setGenerateCodeItemModalOpen(false)}
+ isExample={true}
+ exampleUid={example.uid}
+ />
+ )}
);
};
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js
index d5b9516c6..a8901c4a1 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js
@@ -4,13 +4,14 @@ const Wrapper = styled.div`
position: relative;
.menu-icon {
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
+ visibility: hidden;
.dropdown {
div[aria-expanded='true'] {
visibility: visible;
}
div[aria-expanded='false'] {
- visibility: hidden;
+ visibility: visible;
}
}
}
@@ -97,11 +98,7 @@ const Wrapper = styled.div`
&.item-hovered {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
.menu-icon {
- .dropdown {
- div[aria-expanded='false'] {
- visibility: visible;
- }
- }
+ visibility: visible;
}
}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
index 536b7a2d7..2b56afdbb 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
@@ -1,4 +1,4 @@
-import React, { useState, useRef, forwardRef, useEffect } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import { getEmptyImage } from 'react-dnd-html5-backend';
import range from 'lodash/range';
import filter from 'lodash/filter';
@@ -14,7 +14,6 @@ import {
IconCopy,
IconClipboard,
IconCode,
- IconPhoto,
IconFolder,
IconTrash,
IconSettings,
@@ -28,7 +27,7 @@ import { toggleCollectionItem, addResponseExample } from 'providers/ReduxStore/s
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
import { uuid } from 'utils/common';
import { copyRequest } from 'providers/ReduxStore/slices/app';
-import Dropdown from 'components/Dropdown';
+import MenuDropdown from 'ui/MenuDropdown';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
import RenameCollectionItem from './RenameCollectionItem';
@@ -46,6 +45,7 @@ import NetworkError from 'components/ResponsePane/NetworkError/index';
import CollectionItemInfo from './CollectionItemInfo/index';
import CollectionItemIcon from './CollectionItemIcon';
import ExampleItem from './ExampleItem';
+import ExampleIcon from 'components/Icons/ExampleIcon';
import { scrollToTheActiveTab } from 'utils/tabs';
import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';
import { isEqual } from 'lodash';
@@ -68,6 +68,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
// We use a single ref for drag and drop.
const ref = useRef(null);
+ const menuDropdownRef = useRef(null);
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
@@ -182,15 +183,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
})
});
- const dropdownTippyRef = useRef();
- const MenuIcon = forwardRef((props, ref) => {
- return (
-
-
-
- );
- });
-
const iconClassName = classnames({
'rotate-90': !itemIsCollapsed
});
@@ -289,19 +281,138 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
e.preventDefault();
};
- const handleRightClick = (event) => {
- const _menuDropdown = dropdownTippyRef.current;
- if (_menuDropdown) {
- let menuDropdownBehavior = 'show';
- if (_menuDropdown.state.isShown) {
- menuDropdownBehavior = 'hide';
- }
- _menuDropdown[menuDropdownBehavior]();
- }
+ // Handle right-click context menu
+ const handleContextMenu = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ menuDropdownRef.current?.open();
};
let indents = range(item.depth);
- const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
+
+ // Build menu items for MenuDropdown
+ const buildMenuItems = () => {
+ const items = [
+ {
+ id: 'info',
+ leftSection: IconInfoCircle,
+ label: 'Info',
+ onClick: () => setItemInfoModalOpen(true)
+ }
+ ];
+
+ if (isFolder) {
+ items.push(
+ {
+ id: 'new-request',
+ leftSection: IconFilePlus,
+ label: 'New Request',
+ onClick: () => setNewRequestModalOpen(true)
+ },
+ {
+ id: 'new-folder',
+ leftSection: IconFolderPlus,
+ label: 'New Folder',
+ onClick: () => setNewFolderModalOpen(true)
+ },
+ {
+ id: 'run',
+ leftSection: IconPlayerPlay,
+ label: 'Run',
+ onClick: () => setRunCollectionModalOpen(true)
+ }
+ );
+ }
+
+ items.push(
+ {
+ id: 'clone',
+ leftSection: IconCopy,
+ label: 'Clone',
+ onClick: () => setCloneItemModalOpen(true)
+ },
+ {
+ id: 'copy',
+ leftSection: IconCopy,
+ label: 'Copy',
+ onClick: handleCopyItem
+ }
+ );
+
+ if (isFolder && hasCopiedItems) {
+ items.push({
+ id: 'paste',
+ leftSection: IconClipboard,
+ label: 'Paste',
+ onClick: handlePasteItem
+ });
+ }
+
+ items.push(
+ {
+ id: 'rename',
+ leftSection: IconEdit,
+ label: 'Rename',
+ onClick: () => setRenameItemModalOpen(true)
+ },
+ {
+ id: 'show-in-folder',
+ leftSection: IconFolder,
+ label: 'Show in Folder',
+ onClick: handleShowInFolder
+ }
+ );
+
+ if (!isFolder && (item.type === 'http-request' || item.type === 'graphql-request')) {
+ items.push({
+ id: 'generate-code',
+ leftSection: IconCode,
+ label: 'Generate Code',
+ onClick: handleGenerateCode
+ });
+ }
+
+ if (!isFolder && isItemARequest(item) && item.type === 'http-request') {
+ items.push({
+ id: 'create-example',
+ leftSection: ExampleIcon,
+ label: 'Create Example',
+ onClick: () => setCreateExampleModalOpen(true)
+ });
+ }
+
+ items.push({ id: 'separator-1', type: 'divider' });
+
+ if (isFolder) {
+ items.push(
+ {
+ id: 'settings',
+ leftSection: IconSettings,
+ label: 'Settings',
+ onClick: viewFolderSettings
+ },
+ {
+ id: 'open-terminal',
+ leftSection: IconTerminal2,
+ label: 'Open in Terminal',
+ onClick: async () => {
+ const folderCwd = item.pathname || collectionPathname;
+ await openDevtoolsAndSwitchToTerminal(dispatch, folderCwd);
+ }
+ }
+ );
+ }
+
+ items.push({
+ id: 'delete',
+ leftSection: IconTrash,
+ label: 'Delete',
+ className: 'delete-item',
+ onClick: () => setDeleteItemModalOpen(true)
+ });
+
+ return items;
+ };
const className = classnames('flex flex-col w-full', {
'is-sidebar-dragging': isSidebarDragging
@@ -382,9 +493,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const folderItems = sortByNameThenSequence(filter(item.items, (i) => isItemAFolder(i)));
const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i)));
- const handleGenerateCode = (e) => {
- e.stopPropagation();
- dropdownTippyRef.current.hide();
+ const handleGenerateCode = () => {
if (
(item?.request?.url !== '')
|| (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')
@@ -412,15 +521,12 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
};
const handleCopyItem = () => {
- dropdownTippyRef.current.hide();
dispatch(copyRequest(item));
const itemType = isFolder ? 'Folder' : 'Request';
toast.success(`${itemType} copied to clipboard`);
};
const handlePasteItem = () => {
- dropdownTippyRef.current.hide();
-
// Only allow paste into folders
if (!isFolder) {
toast.error('Paste is only available for folders');
@@ -504,6 +610,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
+ onContextMenu={handleContextMenu}
data-testid="sidebar-collection-item-row"
>
@@ -511,7 +618,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
? indents.map((i) => (
@@ -559,186 +664,14 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
-
} placement="bottom-start">
-
{
- dropdownTippyRef.current.hide();
- setItemInfoModalOpen(true);
- }}
- >
-
-
-
- Info
-
- {isFolder && (
- <>
-
{
- dropdownTippyRef.current.hide();
- setNewRequestModalOpen(true);
- }}
- >
-
-
-
- New Request
-
-
{
- dropdownTippyRef.current.hide();
- setNewFolderModalOpen(true);
- }}
- >
-
-
-
- New Folder
-
-
{
- dropdownTippyRef.current.hide();
- setRunCollectionModalOpen(true);
- }}
- >
-
-
-
- Run
-
- >
- )}
-
{
- dropdownTippyRef.current.hide();
- setCloneItemModalOpen(true);
- }}
- >
-
-
-
- Clone
-
-
-
-
-
- Copy
-
- {isFolder && hasCopiedItems && (
-
-
-
-
- Paste
-
- )}
-
{
- dropdownTippyRef.current.hide();
- setRenameItemModalOpen(true);
- }}
- >
-
-
-
- Rename
-
-
{
- dropdownTippyRef.current.hide();
- handleShowInFolder();
- }}
- >
-
-
-
- Show in Folder
-
- {!isFolder && (item.type === 'http-request' || item.type === 'graphql-request') && (
-
{
- handleGenerateCode(e);
- }}
- >
-
-
-
- Generate Code
-
- )}
- {!isFolder && isItemARequest(item) && item.type === 'http-request' && (
-
{
- dropdownTippyRef.current.hide();
- setCreateExampleModalOpen(true);
- }}
- >
-
-
-
- Create Example
-
- )}
-
- {isFolder && (
-
{
- dropdownTippyRef.current.hide();
- viewFolderSettings();
- }}
- >
-
-
-
- Settings
-
- )}
- {isFolder && (
-
{
- dropdownTippyRef.current.hide();
- // Get folder pathname
- const folderCwd = item.pathname || collectionPathname;
- await openDevtoolsAndSwitchToTerminal(dispatch, folderCwd);
- }}
- >
-
-
-
- Open in Terminal
-
- )}
-
{
- dropdownTippyRef.current.hide();
- setDeleteItemModalOpen(true);
- }}
- >
-
-
-
- Delete
-
-
+
+
+
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/exampleReducers.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/exampleReducers.js
index f0335f08e..16ae21a30 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/exampleReducers.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/exampleReducers.js
@@ -329,8 +329,8 @@ export const setResponseExampleHeaders = (state, action) => {
const example = item.draft.examples.find((e) => e.uid === exampleUid);
if (!example) return;
- example.response.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
- uid: uuid(),
+ example.response.headers = map(headers, ({ uid, name = '', value = '', enabled = true }) => ({
+ uid: uid || uuid(),
name: name,
value: value,
description: '',
@@ -921,8 +921,8 @@ export const setResponseExampleRequestHeaders = (state, action) => {
const example = item.draft.examples.find((e) => e.uid === exampleUid);
if (!example) return;
- example.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
- uid: uuid(),
+ example.request.headers = map(headers, ({ uid, name = '', value = '', enabled = true }) => ({
+ uid: uid || uuid(),
name: name,
value: value,
description: '',
@@ -950,14 +950,36 @@ export const setResponseExampleParams = (state, action) => {
const example = item.draft.examples.find((e) => e.uid === exampleUid);
if (!example) return;
- example.request.params = map(params, ({ name = '', value = '', enabled = true, type = 'query' }) => ({
- uid: uuid(),
+ example.request.params = map(params, ({ uid, name = '', value = '', enabled = true, type = 'query' }) => ({
+ uid: uid || uuid(),
name: name,
value: value,
description: '',
enabled: enabled,
type: type
}));
+
+ // Update URL when query parameters change
+ const queryParams = filter(example.request.params, (p) => p.enabled && p.type === 'query');
+ const query = stringifyQueryParams(queryParams);
+
+ if (!example.request.url) {
+ example.request.url = '';
+ }
+
+ const parts = splitOnFirst(example.request.url, '?');
+
+ if (!query || !query.length) {
+ if (parts.length) {
+ example.request.url = parts[0];
+ }
+ } else {
+ if (!parts.length) {
+ example.request.url += '?' + query;
+ } else {
+ example.request.url = parts[0] + '?' + query;
+ }
+ }
};
// Response Example Body Types
diff --git a/packages/bruno-app/src/ui/HeightBoundContainer/StyledWrapper.js b/packages/bruno-app/src/ui/HeightBoundContainer/StyledWrapper.js
index 6d261b9bf..9653b266b 100644
--- a/packages/bruno-app/src/ui/HeightBoundContainer/StyledWrapper.js
+++ b/packages/bruno-app/src/ui/HeightBoundContainer/StyledWrapper.js
@@ -17,8 +17,15 @@ const StyledWrapper = styled.div`
/* flex container - enforces boundaries */
.flex-boundary {
width: 100%;
+ min-width: 0;
display: flex;
+ flex-direction: column;
overflow-y: auto;
+
+ > * {
+ flex: 1 1 0;
+ min-height: 0;
+ }
}
`;
diff --git a/packages/bruno-app/src/ui/MenuDropdown/index.js b/packages/bruno-app/src/ui/MenuDropdown/index.js
index e4ef53fc2..f8152b989 100644
--- a/packages/bruno-app/src/ui/MenuDropdown/index.js
+++ b/packages/bruno-app/src/ui/MenuDropdown/index.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { forwardRef, useImperativeHandle } from 'react';
import { useRef, useCallback, useState } from 'react';
import Dropdown from 'components/Dropdown';
@@ -36,8 +36,9 @@ const getNextIndex = (currentIndex, total, key, noFocus) => {
* @param {string} props.className - Optional className for the dropdown
* @param {string} props.selectedItemId - Optional ID of the selected/active item to focus on open
* @param {Object} props.dropdownProps - Other props passed to underlying Dropdown component
+ * @param {React.Ref} ref - Optional ref to expose open/close methods
*/
-const MenuDropdown = ({
+const MenuDropdown = forwardRef(({
items = [],
children,
placement = 'bottom-end',
@@ -45,10 +46,17 @@ const MenuDropdown = ({
selectedItemId,
'data-testid': testId = 'menu-dropdown',
...dropdownProps
-}) => {
+}, ref) => {
const tippyRef = useRef();
const [isOpen, setIsOpen] = useState(false);
+ // Expose open/close methods via ref
+ useImperativeHandle(ref, () => ({
+ open: () => setIsOpen(true),
+ close: () => setIsOpen(false),
+ toggle: () => setIsOpen((prev) => !prev)
+ }), []);
+
// Get all focusable menu items from the menu dropdown
const getMenuItems = useCallback(() => {
const popper = tippyRef.current?.popper;
@@ -274,6 +282,6 @@ const MenuDropdown = ({
);
-};
+});
export default MenuDropdown;
diff --git a/tests/collection/draft/draft-values-in-requests.spec.ts b/tests/collection/draft/draft-values-in-requests.spec.ts
index 225d82424..11fb9f509 100644
--- a/tests/collection/draft/draft-values-in-requests.spec.ts
+++ b/tests/collection/draft/draft-values-in-requests.spec.ts
@@ -60,7 +60,7 @@ test.describe('Draft values are used in requests', () => {
// Create a request in the collection
// Create a new request via collection menu
- await folder.locator('.menu-icon').hover();
+ await folder.hover();
await folder.locator('.menu-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
diff --git a/tests/request/copy-request/copy-request.spec.ts b/tests/request/copy-request/copy-request.spec.ts
index bd913b4cd..2e5482949 100644
--- a/tests/request/copy-request/copy-request.spec.ts
+++ b/tests/request/copy-request/copy-request.spec.ts
@@ -11,7 +11,7 @@ test.describe('Copy and Paste Requests', () => {
// Create a new request
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });
- await collection.locator('.collection-actions').hover();
+ await collection.hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByPlaceholder('Request Name').fill('original-request');
@@ -23,6 +23,7 @@ test.describe('Copy and Paste Requests', () => {
// Copy the request
const requestItem = page.locator('.collection-item-name').filter({ hasText: 'original-request' });
+ await requestItem.hover();
await requestItem.locator('.menu-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();
@@ -37,7 +38,7 @@ test.describe('Copy and Paste Requests', () => {
test('should paste request into a folder', async ({ page, createTmpDir }) => {
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });
- await collection.locator('.collection-actions').hover();
+ await collection.hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
await page.locator('#folder-name').fill('test-folder');
diff --git a/tests/response-examples/menu-operations.spec.ts b/tests/response-examples/menu-operations.spec.ts
index 782cc4930..f287f280c 100644
--- a/tests/response-examples/menu-operations.spec.ts
+++ b/tests/response-examples/menu-operations.spec.ts
@@ -24,19 +24,19 @@ test.describe.serial('Response Example Menu Operations', () => {
await page.getByRole('button', { name: 'Create Example' }).click();
// Wait for modal to close
await page.waitForSelector('text=Save Response as Example', { state: 'detached' });
- await page.locator('.collection-item-name', { hasText: 'menu-operations' }).getByTestId('request-item-chevron').click();
+ await page.locator('.collection-item-name').filter({ hasText: 'menu-operations' }).getByTestId('request-item-chevron').click();
- const exampleItem = page.locator('.collection-item-name').getByText('Example to Clone');
- await expect(exampleItem).toBeVisible();
+ const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone' });
+ await expect(exampleRow).toBeVisible();
});
await test.step('Clone example', async () => {
- const exampleItem = page.locator('.collection-item-name').getByText('Example to Clone');
- await exampleItem.hover();
- await page.getByTestId('response-example-menu-icon').last().click();
+ const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone' });
+ await exampleRow.hover();
+ await exampleRow.locator('.menu-icon').click();
- await page.getByTestId('response-example-clone-option').click();
- const clonedExampleItem = page.locator('.collection-item-name').getByText('Example to Clone (Copy)');
+ await page.getByTestId('response-example-menu-clone').click();
+ const clonedExampleItem = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone (Copy)' });
await expect(clonedExampleItem).toBeVisible();
});
});
@@ -57,20 +57,20 @@ test.describe.serial('Response Example Menu Operations', () => {
// Wait for modal to close
await page.waitForSelector('text=Save Response as Example', { state: 'detached' });
- const exampleItem = page.locator('.collection-item-name').getByText('Example to Delete', { exact: true });
- await expect(exampleItem).toBeVisible();
+ const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Delete' });
+ await expect(exampleRow).toBeVisible();
});
await test.step('Delete example', async () => {
- const exampleItem = page.locator('.collection-item-name').getByText('Example to Delete', { exact: true });
- await expect(exampleItem).toBeVisible();
- await exampleItem.hover();
- await page.getByTestId('response-example-menu-icon').last().click();
+ const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Delete' });
+ await expect(exampleRow).toBeVisible();
+ await exampleRow.hover();
+ await exampleRow.locator('.menu-icon').click();
- await page.getByTestId('response-example-delete-option').click();
+ await page.getByTestId('response-example-menu-delete').click();
await expect(page.getByText('Delete Example')).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
- await expect(exampleItem).not.toBeVisible();
+ await expect(exampleRow).not.toBeVisible();
});
});
@@ -90,16 +90,16 @@ test.describe.serial('Response Example Menu Operations', () => {
// Wait for modal to close
await page.waitForSelector('text=Save Response as Example', { state: 'detached' });
- const exampleItem = page.locator('.collection-item-name').getByText('Example to Rename', { exact: true });
- await expect(exampleItem).toBeVisible();
+ const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Rename' });
+ await expect(exampleRow).toBeVisible();
});
await test.step('Rename example', async () => {
- const exampleItem = page.locator('.collection-item-name').getByText('Example to Rename', { exact: true });
- await expect(exampleItem).toBeVisible();
- await exampleItem.hover();
- await page.getByTestId('response-example-menu-icon').last().click();
- await page.getByTestId('response-example-rename-option').click();
+ const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Rename' });
+ await expect(exampleRow).toBeVisible();
+ await exampleRow.hover();
+ await exampleRow.locator('.menu-icon').click();
+ await page.getByTestId('response-example-menu-rename').click();
await expect(page.getByText('Rename Example')).toBeVisible();
const renameExampleNameInput = page.getByTestId('rename-example-name-input');
await renameExampleNameInput.clear();
@@ -107,10 +107,9 @@ test.describe.serial('Response Example Menu Operations', () => {
await page.getByRole('button', { name: 'Rename' }).click();
// Wait for modal to close
await page.waitForSelector('text=Rename Example', { state: 'detached' });
- const updatedExampleItem = page.locator('.collection-item-name').getByText('Renamed Example', { exact: true });
- await expect(exampleItem).not.toBeVisible();
- await expect(updatedExampleItem).toBeVisible();
- await expect(updatedExampleItem).toHaveText('Renamed Example');
+ const updatedExampleRow = page.locator('.collection-item-name').filter({ hasText: 'Renamed Example' });
+ await expect(exampleRow).not.toBeVisible();
+ await expect(updatedExampleRow).toBeVisible();
});
});
});
diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts
index 5776b1bc1..10438af01 100644
--- a/tests/utils/page/actions.ts
+++ b/tests/utils/page/actions.ts
@@ -217,6 +217,7 @@ const deleteRequest = async (page, requestName: string, collectionName: string)
const collectionWrapper = collectionContainer.locator('..');
const request = collectionWrapper.locator('.collection-item-name').filter({ hasText: requestName });
+ await request.hover();
await request.locator('.menu-icon').click();
await locators.dropdown.item('Delete').click();
await locators.modal.button('Delete').click();