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 && ( - )} {!readOnly && <> } - {renderButtonText(filenames)} + + {renderButtonText(filenames)} +
) : (
- - {params && params.length - ? params.map((param, index) => { - return ( - - - - - - ); - }) - : null} - -
- - 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}`} - /> - -
-
- - {editMode && ( -
- -
- )} + ); }; 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 ( - - - {params && params.length - ? params.map((param, index) => { - return ( - - - - - ); - }) - : null} - -
-
- handleParamChange(e, param, 'enabled')} - dataTestId={`urlencoded-param-checkbox-${index}`} - /> -
- handleParamChange(e, param, 'name') : () => {}} - disabled={!editMode} - /> -
-
- {}} - onChange={editMode ? (newValue) => - handleParamChange({ - target: { - value: newValue - } - }, - param, - 'value') : () => {}} - allowNewlines={true} - onRun={() => {}} - collection={collection} - item={item} - /> - -
-
- - {editMode && ( -
- -
- )} +
); }; 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) => ( - - - - - )) - : null} - -
-
- 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 && ( - - )} -
-
- + {editMode && ( -
- +
+
+ ); + } + + 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 && ( + + )} +
+ ); + } + }, + { + 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 ( - - - - - - ); - }) - : null} - -
-
- 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} - /> - -
-
- - {editMode && ( -
- - -
- )} +
); }; 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) => ( - - - - - )) - : null} - -
-
- 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 && ( - - )} -
-
- + {editMode && ( -
- +
- - {pathParams && pathParams.length - ? pathParams.map((path, index) => { - return ( - - - - - ); - }) - : null} -
- {path.name} - - {}} - onChange={(newValue) => handlePathParamChange({ target: { value: newValue } }, path)} - onRun={() => {}} - collection={collection} - variablesAutocomplete={true} - readOnly={!editMode} - /> -
- {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) => ( - - - - - )) - : null} - -
- {}} - 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 && ( - - )} -
-
- + {editMode && ( -
- +
@@ -169,45 +196,20 @@ const ExampleItem = ({ example, item, collection }) => {
{example.name}
- } 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();