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 (
-
+
+
+
+
);
};
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');
+}