refactor: implementation of bulk edit functionality for query parameters and request headers

refactor: integrate BulkEditCodeEditor for bulk editing of query parameters and request headers

refactor: refactor BulkEditCodeEditor component folder structure nad fix Bulk Edit button styles

refactor: now the queryparams are updated in both the ways

style: fix indentation

reverting the style changes which  fixes the alignment of the bulkedit button

refactor: add onSave prop to BulkEditCodeEditor and update value handling

feat: add onSave prop to BulkEditCodeEditor for improved header management

added onRun prop to BulkEditCodeEditor, QueryParams, and RequestHeaders

refactor: renamed BulkEditCodeEditor to BulkEditor and update the references, and updated names for bulkEdit states
This commit is contained in:
sanjai0py
2025-06-22 22:24:19 +05:30
parent e8eab46f48
commit f2b5b6f783
7 changed files with 200 additions and 38 deletions

View File

@@ -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 (
<>
<div className="h-[200px]">
<CodeEditor
mode="text/plain"
theme={displayedTheme}
font={preferences.codeFont || 'default'}
value={parsedParams}
onEdit={handleEdit}
onSave={onSave}
onRun={onRun}
/>
</div>
<div className="flex btn-action justify-between items-center mt-3">
<button className="text-link select-none ml-auto" onClick={onToggle}>
Key/Value Edit
</button>
</div>
</>
);
};
export default BulkEditor;

View File

@@ -31,7 +31,7 @@ const Wrapper = styled.div`
}
}
.btn-add-param {
.btn-action {
font-size: 0.8125rem;
&:hover span {
text-decoration: underline;

View File

@@ -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 (
<StyledWrapper className="w-full mt-3">
<BulkEditor
params={queryParams}
onChange={handleBulkParamsChange}
onToggle={toggleBulkEditMode}
onSave={onSave}
onRun={handleRun}
/>
</StyledWrapper>
);
}
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
@@ -171,9 +198,14 @@ const QueryParams = ({ item, collection }) => {
</ReorderTable>
</Table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
+&nbsp;<span>Add Param</span>
</button>
<div className="flex justify-between mt-2">
<button className="btn-action text-link pr-2 py-3 select-none" onClick={handleAddQueryParam}>
+&nbsp;<span>Add Param</span>
</button>
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
Bulk Edit
</button>
</div>
<div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<InfoTip infotipId="path-param-InfoTip">

View File

@@ -22,18 +22,11 @@ const Wrapper = styled.div`
}
}
.top-controls {
display: flex;
justify-content: right;
.btn-action {
font-size: 0.8125rem;
}
.bottom-controls {
font-size: 0.8125rem;
}
div.CodeMirror {
height: 100%;
&:hover span {
text-decoration: underline;
}
}
input[type='text'] {

View File

@@ -4,7 +4,7 @@ import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
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 (
<StyledWrapper className="w-full mt-3">
<BulkEditor
params={headers}
onChange={handleBulkHeadersChange}
onToggle={toggleBulkEditMode}
onSave={onSave}
onRun={handleRun}
/>
</StyledWrapper>
);
}
return (
<StyledWrapper className="w-full">
<Table
@@ -153,9 +179,14 @@ const RequestHeaders = ({ item, collection }) => {
: null}
</ReorderTable>
</Table>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
<div className="flex justify-between mt-2">
<button className="btn-action text-link pr-2 py-3 select-none" onClick={addHeader}>
+ Add Header
</button>
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
Bulk Edit
</button>
</div>
</StyledWrapper>
);
};

View File

@@ -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);
@@ -786,24 +827,28 @@ export const collectionsSlice = createSlice({
}
},
setRequestHeaders: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const { collectionUid, itemUid, headers } = action.payload;
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.headers = map(action.payload.headers, (header) => ({
uid: uuid(),
name: header.name,
value: header.value,
description: '',
enabled: true
}));
}
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);
@@ -2293,6 +2338,7 @@ export const {
requestUrlChanged,
updateAuth,
addQueryParam,
setQueryParams,
moveQueryParam,
updateQueryParam,
deleteQueryParam,

View File

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