diff --git a/packages/bruno-app/src/components/BulkEditor/index.js b/packages/bruno-app/src/components/BulkEditor/index.js new file mode 100644 index 000000000..1739c963f --- /dev/null +++ b/packages/bruno-app/src/components/BulkEditor/index.js @@ -0,0 +1,40 @@ +import React, { useMemo } from 'react'; +import CodeEditor from 'components/CodeEditor'; +import { useTheme } from 'providers/Theme'; +import { useSelector } from 'react-redux'; +import { parseBulkKeyValue, serializeBulkKeyValue } from 'utils/common/bulkKeyValueUtils'; + +const BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => { + const preferences = useSelector((state) => state.app.preferences); + const { displayedTheme } = useTheme(); + + const parsedParams = useMemo(() => serializeBulkKeyValue(params), [params]); + + const handleEdit = (value) => { + const parsed = parseBulkKeyValue(value); + onChange(parsed); + }; + + return ( + <> +
+ +
+
+ +
+ + ); +}; + +export default BulkEditor; diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js index b460c1b4f..9a23f2f9c 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js @@ -31,7 +31,7 @@ const Wrapper = styled.div` } } - .btn-add-param { + .btn-action { font-size: 0.8125rem; &:hover span { text-decoration: underline; diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index 8fe1cd00b..0b1b9df9c 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -1,16 +1,17 @@ -import React from 'react'; +import React, { useState } from 'react'; import get from 'lodash/get'; import cloneDeep from 'lodash/cloneDeep'; import InfoTip from 'components/InfoTip'; import { IconTrash } from '@tabler/icons'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useTheme } from 'providers/Theme'; import { addQueryParam, updateQueryParam, deleteQueryParam, moveQueryParam, - updatePathParam + updatePathParam, + setQueryParams } from 'providers/ReduxStore/slices/collections'; import SingleLineEditor from 'components/SingleLineEditor'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; @@ -18,6 +19,7 @@ import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collection import StyledWrapper from './StyledWrapper'; import Table from 'components/Table/index'; import ReorderTable from 'components/ReorderTable'; +import BulkEditor from '../../BulkEditor'; const QueryParams = ({ item, collection }) => { const dispatch = useDispatch(); @@ -25,6 +27,8 @@ const QueryParams = ({ item, collection }) => { const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params'); const queryParams = params.filter((param) => param.type === 'query'); const pathParams = params.filter((param) => param.type === 'path'); + + const [isBulkEditMode, setIsBulkEditMode] = useState(false); const handleAddQueryParam = () => { dispatch( @@ -113,6 +117,29 @@ const QueryParams = ({ item, collection }) => { ); }; + const toggleBulkEditMode = () => { + setIsBulkEditMode(!isBulkEditMode); + }; + + const handleBulkParamsChange = (newParams) => { + const paramsWithType = newParams.map((item) => ({ ...item, type: 'query' })); + dispatch(setQueryParams({ collectionUid: collection.uid, itemUid: item.uid, params: paramsWithType })); + }; + + if (isBulkEditMode) { + return ( + + + + ); + } + return (
@@ -171,9 +198,14 @@ const QueryParams = ({ item, collection }) => { - +
+ + +
Path diff --git a/packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js index 5b787e8bb..86cb4e365 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js @@ -22,8 +22,11 @@ const Wrapper = styled.div` } } - .btn-add-header { + .btn-action { font-size: 0.8125rem; + &:hover span { + text-decoration: underline; + } } input[type='text'] { diff --git a/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js b/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js index d88318017..ddcc62af2 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { useState } from 'react'; import get from 'lodash/get'; import cloneDeep from 'lodash/cloneDeep'; import { IconTrash } from '@tabler/icons'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useTheme } from 'providers/Theme'; -import { addRequestHeader, updateRequestHeader, deleteRequestHeader, moveRequestHeader } from 'providers/ReduxStore/slices/collections'; +import { addRequestHeader, updateRequestHeader, deleteRequestHeader, moveRequestHeader, setRequestHeaders } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import SingleLineEditor from 'components/SingleLineEditor'; import StyledWrapper from './StyledWrapper'; @@ -12,12 +12,16 @@ import { headers as StandardHTTPHeaders } from 'know-your-http-well'; import { MimeTypes } from 'utils/codemirror/autocompleteConstants'; import Table from 'components/Table/index'; import ReorderTable from 'components/ReorderTable/index'; +import BulkEditor from '../../BulkEditor'; + const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header); const RequestHeaders = ({ item, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers'); + + const [isBulkEditMode, setIsBulkEditMode] = useState(false); const addHeader = () => { dispatch( @@ -75,6 +79,28 @@ const RequestHeaders = ({ item, collection }) => { ); }; + const toggleBulkEditMode = () => { + setIsBulkEditMode(!isBulkEditMode); + }; + + const handleBulkHeadersChange = (newHeaders) => { + dispatch(setRequestHeaders({ collectionUid: collection.uid, itemUid: item.uid, headers: newHeaders })); + }; + + if (isBulkEditMode) { + return ( + + + + ); + } + return ( { : null}
- +
+ + +
); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index df1fc63bc..9139ec599 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -579,7 +579,48 @@ export const collectionsSlice = createSlice({ } } }, + setQueryParams: (state, action) => { + const { collectionUid, itemUid, params } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + if (!collection) { + return; + } + + const item = findItemInCollection(collection, itemUid); + if (!item || !isItemARequest(item)) { + return; + } + + if (!item.draft) { + item.draft = cloneDeep(item); + } + const existingOtherParams = item.draft.request.params?.filter(p => p.type !== 'query') || []; + const newQueryParams = map(params, ({ name = '', value = '', enabled = true }) => ({ + uid: uuid(), + name, + value, + description: '', + type: 'query', + enabled + })); + + item.draft.request.params = [...newQueryParams, ...existingOtherParams]; + + // Update the request URL to reflect the new query params + const parts = splitOnFirst(item.draft.request.url, '?'); + const query = stringifyQueryParams( + filter(item.draft.request.params, (p) => p.enabled && p.type === 'query') + ); + + // If there are enabled query params, append them to the URL + if (query && query.length) { + item.draft.request.url = parts[0] + '?' + query; + } else { + // If no enabled query params, remove the query part from URL + item.draft.request.url = parts[0]; + } + }, moveQueryParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -785,6 +826,30 @@ export const collectionsSlice = createSlice({ } } }, + setRequestHeaders: (state, action) => { + const { collectionUid, itemUid, headers } = action.payload; + + const collection = findCollectionByUid(state.collections, collectionUid); + if (!collection) { + return; + } + + const item = findItemInCollection(collection, itemUid); + if (!item || !isItemARequest(item)) { + return; + } + + if (!item.draft) { + item.draft = cloneDeep(item); + } + item.draft.request.headers = map(action.payload.headers, ({name = '', value = '', enabled = true}) => ({ + uid: uuid(), + name: name, + value: value, + description: '', + enabled: enabled + })); + }, addFormUrlEncodedParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -2273,6 +2338,7 @@ export const { requestUrlChanged, updateAuth, addQueryParam, + setQueryParams, moveQueryParam, updateQueryParam, deleteQueryParam, @@ -2281,6 +2347,7 @@ export const { updateRequestHeader, deleteRequestHeader, moveRequestHeader, + setRequestHeaders, addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam, diff --git a/packages/bruno-app/src/utils/common/bulkKeyValueUtils.js b/packages/bruno-app/src/utils/common/bulkKeyValueUtils.js new file mode 100644 index 000000000..b165c2f3f --- /dev/null +++ b/packages/bruno-app/src/utils/common/bulkKeyValueUtils.js @@ -0,0 +1,20 @@ +export function parseBulkKeyValue(value) { + return value + .split(/\r?\n/) + .map((pair) => { + const isEnabled = !pair.trim().startsWith('//'); + const cleanPair = pair.replace(/^\/\/\s*/, ''); + const sep = cleanPair.indexOf(':'); + if (sep < 0) return null; + return { + name: cleanPair.slice(0, sep).trim(), + value: cleanPair.slice(sep + 1).trim(), + enabled: isEnabled + }; + }) + .filter(Boolean); +} + +export function serializeBulkKeyValue(items) { + return items.map((item) => `${item.enabled ? '' : '//'}${item.name}:${item.value}`).join('\n'); +}