mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-28 15:14:06 +00:00
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:
40
packages/bruno-app/src/components/BulkEditor/index.js
Normal file
40
packages/bruno-app/src/components/BulkEditor/index.js
Normal 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;
|
||||
@@ -31,7 +31,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-param {
|
||||
.btn-action {
|
||||
font-size: 0.8125rem;
|
||||
&:hover span {
|
||||
text-decoration: underline;
|
||||
|
||||
@@ -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}>
|
||||
+ <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}>
|
||||
+ <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">
|
||||
|
||||
@@ -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'] {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
20
packages/bruno-app/src/utils/common/bulkKeyValueUtils.js
Normal file
20
packages/bruno-app/src/utils/common/bulkKeyValueUtils.js
Normal 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');
|
||||
}
|
||||
Reference in New Issue
Block a user