mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 14:44:07 +00:00
fix: refactor response examples to use MenuDropdown and Editable components (#6382)
* feat: use common dropdown component * fix: update example ui to match v3 * fix: test cases, bugs * fix: review comments * fix: review comments * fix: review * fix: file body/binary table within response examples * fix: file name, close btn not visible issue * fix: unnessary transition for three dots * fix: install missing deps in bruno-app * update example url when param is updated * empty commit * chore: update package-lock.json --------- Co-authored-by: Bijin A B <bijin@usebruno.com>
This commit is contained in:
@@ -14,36 +14,34 @@ const ResponseExampleBodyMode = ({ item, collection, exampleUid, body, bodyMode,
|
||||
// Initialize the new body structure based on the selected mode
|
||||
let newBody = { mode: value };
|
||||
|
||||
// Preserve existing data for the new mode if it exists
|
||||
if (body) {
|
||||
switch (value) {
|
||||
case 'json':
|
||||
newBody.json = body.json || '';
|
||||
break;
|
||||
case 'text':
|
||||
newBody.text = body.text || '';
|
||||
break;
|
||||
case 'xml':
|
||||
newBody.xml = body.xml || '';
|
||||
break;
|
||||
case 'sparql':
|
||||
newBody.sparql = body.sparql || '';
|
||||
break;
|
||||
case 'formUrlEncoded':
|
||||
newBody.formUrlEncoded = body.formUrlEncoded || [];
|
||||
break;
|
||||
case 'multipartForm':
|
||||
newBody.multipartForm = body.multipartForm || [];
|
||||
break;
|
||||
case 'file':
|
||||
newBody.file = body.file || { name: '', data: '' };
|
||||
break;
|
||||
case 'none':
|
||||
// No additional data needed for 'none' mode
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Initialize body content based on selected mode
|
||||
switch (value) {
|
||||
case 'json':
|
||||
newBody.json = body?.json || '';
|
||||
break;
|
||||
case 'text':
|
||||
newBody.text = body?.text || '';
|
||||
break;
|
||||
case 'xml':
|
||||
newBody.xml = body?.xml || '';
|
||||
break;
|
||||
case 'sparql':
|
||||
newBody.sparql = body?.sparql || '';
|
||||
break;
|
||||
case 'formUrlEncoded':
|
||||
newBody.formUrlEncoded = body?.formUrlEncoded || [];
|
||||
break;
|
||||
case 'multipartForm':
|
||||
newBody.multipartForm = body?.multipartForm || [];
|
||||
break;
|
||||
case 'file':
|
||||
newBody.file = Array.isArray(body?.file) ? body.file : [];
|
||||
break;
|
||||
case 'none':
|
||||
// No additional data needed for 'none' mode
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
dispatch(updateResponseExampleRequest({
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { get, cloneDeep } from 'lodash';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateResponseExampleFileBodyParams } from 'providers/ReduxStore/slices/collections';
|
||||
import mime from 'mime-types';
|
||||
import path from 'utils/common/path';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FilePickerEditor from 'components/FilePickerEditor/index';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import Table from 'components/Table-v2';
|
||||
import ReorderTable from 'components/ReorderTable';
|
||||
import RadioButton from 'components/RadioButton';
|
||||
|
||||
const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = false }) => {
|
||||
@@ -25,16 +23,8 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
|
||||
return Array.isArray(_params) ? _params : [];
|
||||
}, [item.draft, item.examples, item, exampleUid]);
|
||||
|
||||
const [enabledFileUid, setEnableFileUid] = useState(params.length > 0 ? params[0].uid : '');
|
||||
|
||||
const addFile = () => {
|
||||
const newParam = {
|
||||
filePath: '',
|
||||
contentType: '',
|
||||
selected: true
|
||||
};
|
||||
|
||||
const updatedParams = [...params, newParam];
|
||||
const handleParamsChange = useCallback((updatedParams) => {
|
||||
if (!editMode) return;
|
||||
|
||||
dispatch(updateResponseExampleFileBodyParams({
|
||||
itemUid: item.uid,
|
||||
@@ -42,75 +32,70 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
|
||||
exampleUid: exampleUid,
|
||||
params: updatedParams
|
||||
}));
|
||||
};
|
||||
}, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
|
||||
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const handleFilePathChange = useCallback((row, newFilePath, onChange) => {
|
||||
if (!editMode) return;
|
||||
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
case 'filePath': {
|
||||
param.filePath = e.target.filePath;
|
||||
// Auto-detect content type from file extension using mime library (same as updateFile)
|
||||
const contentType = mime.contentType(path.extname(e.target.filePath));
|
||||
param.contentType = contentType || '';
|
||||
break;
|
||||
}
|
||||
case 'contentType': {
|
||||
param.contentType = e.target.contentType;
|
||||
break;
|
||||
}
|
||||
case 'selected': {
|
||||
// When a file is selected, deselect all others and select this one
|
||||
const updatedParams = params.map((p) => ({
|
||||
...p,
|
||||
selected: p.uid === param.uid ? e.target.checked : false
|
||||
}));
|
||||
const currentParams = params || [];
|
||||
const existingParam = currentParams.find((p) => p.uid === row.uid);
|
||||
|
||||
dispatch(updateResponseExampleFileBodyParams({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
params: updatedParams
|
||||
}));
|
||||
|
||||
// Update the enabled file UID state
|
||||
if (e.target.checked) {
|
||||
setEnableFileUid(param.uid);
|
||||
let updatedParams;
|
||||
if (existingParam) {
|
||||
// Update existing param
|
||||
updatedParams = currentParams.map((p) => {
|
||||
if (p.uid === row.uid) {
|
||||
const updated = { ...p, filePath: newFilePath };
|
||||
// Auto-detect content type from file extension
|
||||
if (newFilePath) {
|
||||
const contentType = mime.contentType(path.extname(newFilePath));
|
||||
updated.contentType = contentType || '';
|
||||
} else {
|
||||
updated.contentType = '';
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
return; // Early return since we already dispatched
|
||||
return p;
|
||||
});
|
||||
} else {
|
||||
// Add new param (from EditableTable's empty row)
|
||||
// Deselect all existing params and select the new one
|
||||
const deselectedParams = currentParams.map((p) => ({ ...p, selected: false }));
|
||||
const newParam = {
|
||||
uid: row.uid,
|
||||
filePath: newFilePath,
|
||||
contentType: '',
|
||||
selected: true
|
||||
};
|
||||
// Auto-detect content type from file extension
|
||||
if (newFilePath) {
|
||||
const contentType = mime.contentType(path.extname(newFilePath));
|
||||
newParam.contentType = contentType || '';
|
||||
}
|
||||
updatedParams = [...deselectedParams, newParam];
|
||||
}
|
||||
|
||||
const updatedParams = params.map((p) => p.uid === param.uid ? param : p);
|
||||
handleParamsChange(updatedParams);
|
||||
}, [editMode, params, handleParamsChange]);
|
||||
|
||||
dispatch(updateResponseExampleFileBodyParams({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
params: updatedParams
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveParams = (param) => {
|
||||
const handleSelectedChange = useCallback((row, checked) => {
|
||||
if (!editMode) return;
|
||||
|
||||
const updatedParams = params.filter((p) => p.uid !== param.uid);
|
||||
|
||||
dispatch(updateResponseExampleFileBodyParams({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
params: updatedParams
|
||||
// When a file is selected, deselect all others and select this one
|
||||
const updatedParams = params.map((p) => ({
|
||||
...p,
|
||||
selected: p.uid === row.uid ? checked : false
|
||||
}));
|
||||
};
|
||||
|
||||
const handleParamDrag = ({ updateReorderedItem }) => {
|
||||
handleParamsChange(updatedParams);
|
||||
}, [editMode, params, handleParamsChange]);
|
||||
|
||||
const handleParamDrag = useCallback(({ updateReorderedItem }) => {
|
||||
if (!editMode) return;
|
||||
|
||||
const reorderedParams = updateReorderedItem.map((uid) => {
|
||||
return params.find((p) => p.uid === uid);
|
||||
});
|
||||
}).filter(Boolean);
|
||||
|
||||
dispatch(updateResponseExampleFileBodyParams({
|
||||
itemUid: item.uid,
|
||||
@@ -118,6 +103,73 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
|
||||
exampleUid: exampleUid,
|
||||
params: reorderedParams
|
||||
}));
|
||||
}, [editMode, dispatch, item.uid, collection.uid, exampleUid, params]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'filePath',
|
||||
name: 'File',
|
||||
isKeyField: true,
|
||||
placeholder: 'File',
|
||||
width: '50%',
|
||||
readOnly: !editMode,
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<FilePickerEditor
|
||||
isSingleFilePicker={true}
|
||||
value={value || ''}
|
||||
onChange={(newPath) => handleFilePathChange(row, newPath, onChange)}
|
||||
collection={collection}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'contentType',
|
||||
name: 'Content-Type',
|
||||
placeholder: 'Auto',
|
||||
width: '30%',
|
||||
readOnly: !editMode,
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
className="flex items-center justify-center"
|
||||
onSave={() => {}}
|
||||
theme={storedTheme}
|
||||
placeholder={isLastEmptyRow ? 'Auto' : ''}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'selected',
|
||||
name: 'Selected',
|
||||
width: '20%',
|
||||
readOnly: !editMode,
|
||||
render: ({ row, value, onChange, isLastEmptyRow, rowIndex }) => (
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<RadioButton
|
||||
key={row.uid}
|
||||
id={`file-${row.uid}`}
|
||||
name="selectedFile"
|
||||
value={row.uid}
|
||||
checked={row.selected}
|
||||
onChange={(e) => handleSelectedChange(row, e.target.checked)}
|
||||
disabled={!editMode}
|
||||
className="mr-1 mousetrap"
|
||||
dataTestId={`file-radio-button-${rowIndex}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const defaultRow = {
|
||||
filePath: '',
|
||||
contentType: '',
|
||||
selected: false
|
||||
};
|
||||
|
||||
if (params.length === 0 && !editMode) {
|
||||
@@ -126,95 +178,16 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-4">
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'File', accessor: 'file', width: '50%' },
|
||||
{ name: 'Content-Type', accessor: 'contentType', width: '30%' },
|
||||
{ name: 'Selected', accessor: 'selected', width: '20%' }
|
||||
]}
|
||||
>
|
||||
<ReorderTable updateReorderedItem={handleParamDrag}>
|
||||
{params && params.length
|
||||
? params.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid} data-uid={param.uid}>
|
||||
<td className="flex relative">
|
||||
<FilePickerEditor
|
||||
isSingleFilePicker={true}
|
||||
value={param.filePath}
|
||||
onChange={editMode ? (path) =>
|
||||
handleParamChange({
|
||||
target: {
|
||||
filePath: path
|
||||
}
|
||||
},
|
||||
param,
|
||||
'filePath') : () => {}}
|
||||
collection={collection}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<SingleLineEditor
|
||||
className="flex items-center justify-center"
|
||||
onSave={() => {}}
|
||||
theme={storedTheme}
|
||||
placeholder="Auto"
|
||||
value={param.contentType}
|
||||
onChange={editMode ? (newValue) =>
|
||||
handleParamChange({
|
||||
target: {
|
||||
contentType: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'contentType') : () => {}}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<RadioButton
|
||||
key={param.uid}
|
||||
id={`file-${param.uid}`}
|
||||
name="selectedFile"
|
||||
value={param.uid}
|
||||
checked={enabledFileUid === param.uid || param.selected}
|
||||
onChange={editMode ? (e) => handleParamChange(e, param, 'selected') : () => {}}
|
||||
disabled={!editMode}
|
||||
className="mr-1 mousetrap"
|
||||
dataTestId={`file-radio-button-${index}`}
|
||||
/>
|
||||
<button
|
||||
tabIndex="-1"
|
||||
onClick={() => handleRemoveParams(param)}
|
||||
className={`delete-button ${editMode ? 'edit-mode' : ''}`}
|
||||
disabled={!editMode}
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
{editMode && (
|
||||
<div className="flex justify-between mt-2">
|
||||
<button
|
||||
className="btn-action pr-2 py-3 select-none"
|
||||
onClick={addFile}
|
||||
>
|
||||
+ Add File
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<EditableTable
|
||||
columns={columns}
|
||||
rows={params || []}
|
||||
onChange={handleParamsChange}
|
||||
defaultRow={defaultRow}
|
||||
reorderable={editMode}
|
||||
onReorder={handleParamDrag}
|
||||
showAddRow={editMode}
|
||||
showCheckbox={false}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }) => (
|
||||
<MultiLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
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 (
|
||||
<StyledWrapper className="w-full mt-4">
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Key', accessor: 'key', width: '40%' },
|
||||
{ name: 'Value', accessor: 'value', width: '60%' }
|
||||
]}
|
||||
>
|
||||
<ReorderTable updateReorderedItem={handleParamDrag}>
|
||||
{params && params.length
|
||||
? params.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid} data-uid={param.uid}>
|
||||
<td className="flex relative">
|
||||
<div className="flex items-center justify-center mr-3">
|
||||
<Checkbox
|
||||
checked={param.enabled === true}
|
||||
disabled={!editMode}
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
dataTestId={`urlencoded-param-checkbox-${index}`}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.name}
|
||||
className="mousetrap"
|
||||
onChange={editMode ? (e) => handleParamChange(e, param, 'name') : () => {}}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<MultiLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={editMode ? (newValue) =>
|
||||
handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value') : () => {}}
|
||||
allowNewlines={true}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
<button
|
||||
tabIndex="-1"
|
||||
onClick={() => handleRemoveParams(param)}
|
||||
className={`delete-button ${editMode ? 'edit-mode' : ''}`}
|
||||
disabled={!editMode}
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
{editMode && (
|
||||
<div className="flex justify-between mt-2">
|
||||
<button
|
||||
className="btn-action text-link pr-2 py-3 select-none"
|
||||
onClick={addParam}
|
||||
>
|
||||
+ Add Param
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<EditableTable
|
||||
columns={columns}
|
||||
rows={params || []}
|
||||
onChange={handleParamsChange}
|
||||
defaultRow={defaultRow}
|
||||
reorderable={editMode}
|
||||
onReorder={handleParamDrag}
|
||||
showAddRow={editMode}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
readOnly={!editMode}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
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 }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
readOnly={!editMode}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={onChange}
|
||||
onRun={() => {}}
|
||||
autocomplete={MimeTypes}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const defaultRow = {
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
};
|
||||
|
||||
if (isBulkEditMode && editMode) {
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-3">
|
||||
@@ -119,85 +131,17 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-4">
|
||||
<div className="mb-1 title text-xs font-bold">Headers</div>
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Key', accessor: 'key', width: '40%' },
|
||||
{ name: 'Value', accessor: 'value', width: '60%' }
|
||||
]}
|
||||
>
|
||||
<ReorderTable updateReorderedItem={handleHeaderDrag}>
|
||||
{headers && headers.length
|
||||
? headers.map((header, index) => (
|
||||
<tr key={header.uid} data-uid={header.uid}>
|
||||
<td className="flex relative">
|
||||
<div className="flex items-center justify-center mr-3">
|
||||
<Checkbox
|
||||
checked={header.enabled === true}
|
||||
disabled={!editMode}
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
dataTestId={`header-checkbox-${index}`}
|
||||
/>
|
||||
</div>
|
||||
<SingleLineEditor
|
||||
value={header.name || ''}
|
||||
readOnly={!editMode}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'name')}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<SingleLineEditor
|
||||
value={header.value || ''}
|
||||
readOnly={!editMode}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'value')}
|
||||
onRun={() => {}}
|
||||
autocomplete={MimeTypes}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
{editMode && (
|
||||
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)} className="delete-button">
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: null}
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
<EditableTable
|
||||
columns={columns}
|
||||
rows={headers || []}
|
||||
onChange={handleHeadersChange}
|
||||
defaultRow={defaultRow}
|
||||
reorderable={editMode}
|
||||
onReorder={handleHeaderDrag}
|
||||
showAddRow={editMode}
|
||||
/>
|
||||
{editMode && (
|
||||
<div className="flex justify-between mt-2">
|
||||
<button
|
||||
className="btn-action text-link pr-2 py-3 select-none"
|
||||
onClick={handleAddHeader}
|
||||
>
|
||||
+ Add Header
|
||||
</button>
|
||||
<div className="flex justify-end mt-2">
|
||||
<button
|
||||
className="btn-action text-link select-none"
|
||||
onClick={toggleBulkEditMode}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
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 { IconUpload, IconX, IconFile } from '@tabler/icons';
|
||||
import { updateResponseExampleMultipartFormParams } from 'providers/ReduxStore/slices/collections';
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import mime from 'mime-types';
|
||||
import path from 'utils/common/path';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FilePickerEditor from 'components/FilePickerEditor';
|
||||
import Table from 'components/Table-v2';
|
||||
import ReorderTable from 'components/ReorderTable/index';
|
||||
import Checkbox from 'components/Checkbox';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
|
||||
const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, editMode = false }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -25,95 +23,109 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
|
||||
: get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.body?.multipartForm || [];
|
||||
}, [item, exampleUid]);
|
||||
|
||||
const addParam = () => {
|
||||
const newParam = {
|
||||
name: '',
|
||||
value: '',
|
||||
contentType: '',
|
||||
enabled: true,
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
const updatedParams = [...params, newParam];
|
||||
|
||||
dispatch(updateResponseExampleMultipartFormParams({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
params: updatedParams
|
||||
}));
|
||||
};
|
||||
|
||||
const addFile = () => {
|
||||
const newParam = {
|
||||
name: '',
|
||||
value: [],
|
||||
contentType: '',
|
||||
enabled: true,
|
||||
type: 'file'
|
||||
};
|
||||
|
||||
const updatedParams = [...params, newParam];
|
||||
|
||||
dispatch(updateResponseExampleMultipartFormParams({
|
||||
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;
|
||||
if (param.type === 'file' && e.target.value) {
|
||||
const contentType = mime.contentType(path.extname(e.target.value));
|
||||
param.contentType = contentType || '';
|
||||
dispatch(updateResponseExampleMultipartFormParams({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
params: updatedParams
|
||||
}));
|
||||
}, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
|
||||
|
||||
const handleBrowseFiles = useCallback((row, onChange) => {
|
||||
if (!editMode) return;
|
||||
|
||||
dispatch(browseFiles())
|
||||
.then((filePaths) => {
|
||||
const processedPaths = filePaths.map((filePath) => {
|
||||
const collectionDir = collection.pathname;
|
||||
if (filePath.startsWith(collectionDir)) {
|
||||
return path.relative(collectionDir, filePath);
|
||||
}
|
||||
return filePath;
|
||||
});
|
||||
|
||||
const currentParams = params || [];
|
||||
const existingParam = currentParams.find((p) => p.uid === row.uid);
|
||||
|
||||
let updatedParams;
|
||||
if (existingParam) {
|
||||
// Update existing param
|
||||
updatedParams = currentParams.map((p) => {
|
||||
if (p.uid === row.uid) {
|
||||
const updated = { ...p, type: 'file', value: processedPaths };
|
||||
// Auto-detect content type from first file
|
||||
if (processedPaths.length > 0) {
|
||||
const contentType = mime.contentType(path.extname(processedPaths[0]));
|
||||
updated.contentType = contentType || '';
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
return p;
|
||||
});
|
||||
} else {
|
||||
// Add new param (from EditableTable's empty row)
|
||||
const newParam = {
|
||||
uid: row.uid,
|
||||
name: row.name || '',
|
||||
type: 'file',
|
||||
value: processedPaths,
|
||||
contentType: '',
|
||||
enabled: true
|
||||
};
|
||||
// Auto-detect content type from first file
|
||||
if (processedPaths.length > 0) {
|
||||
const contentType = mime.contentType(path.extname(processedPaths[0]));
|
||||
newParam.contentType = contentType || '';
|
||||
}
|
||||
updatedParams = [...currentParams, newParam];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'contentType': {
|
||||
param.contentType = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedParams = params.map((p) => p.uid === param.uid ? param : p);
|
||||
handleParamsChange(updatedParams);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}, [editMode, dispatch, collection.pathname, params, handleParamsChange]);
|
||||
|
||||
dispatch(updateResponseExampleMultipartFormParams({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
params: updatedParams
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveParams = (param) => {
|
||||
const handleClearFile = useCallback((row) => {
|
||||
if (!editMode) return;
|
||||
|
||||
const updatedParams = params.filter((p) => p.uid !== param.uid);
|
||||
const currentParams = params || [];
|
||||
const existingParam = currentParams.find((p) => p.uid === row.uid);
|
||||
|
||||
dispatch(updateResponseExampleMultipartFormParams({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
params: updatedParams
|
||||
}));
|
||||
};
|
||||
if (existingParam) {
|
||||
const updatedParams = currentParams.map((p) => {
|
||||
if (p.uid === row.uid) {
|
||||
return { ...p, type: 'text', value: '' };
|
||||
}
|
||||
return p;
|
||||
});
|
||||
handleParamsChange(updatedParams);
|
||||
}
|
||||
}, [editMode, params, handleParamsChange]);
|
||||
|
||||
const handleParamDrag = ({ updateReorderedItem }) => {
|
||||
const handleValueChange = useCallback((row, newValue, onChange) => {
|
||||
if (!editMode) return;
|
||||
|
||||
const currentParams = params || [];
|
||||
const existingParam = currentParams.find((p) => p.uid === row.uid);
|
||||
if (existingParam) {
|
||||
const updatedParams = currentParams.map((p) => {
|
||||
if (p.uid === row.uid) {
|
||||
return { ...p, type: 'text', value: newValue };
|
||||
}
|
||||
return p;
|
||||
});
|
||||
handleParamsChange(updatedParams);
|
||||
} else {
|
||||
onChange(newValue);
|
||||
}
|
||||
}, [editMode, params, handleParamsChange]);
|
||||
|
||||
const handleParamDrag = useCallback(({ updateReorderedItem }) => {
|
||||
if (!editMode) return;
|
||||
|
||||
const reorderedParams = updateReorderedItem.map((uid) => {
|
||||
@@ -126,6 +138,117 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
|
||||
exampleUid: exampleUid,
|
||||
params: reorderedParams
|
||||
}));
|
||||
}, [editMode, dispatch, item.uid, collection.uid, exampleUid, params]);
|
||||
|
||||
const getFileName = (filePaths) => {
|
||||
if (!filePaths || (Array.isArray(filePaths) && filePaths.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
||||
const validPaths = paths.filter((v) => v != null && v !== '');
|
||||
if (validPaths.length === 0) return null;
|
||||
|
||||
const separator = isWindowsOS() ? '\\' : '/';
|
||||
if (validPaths.length === 1) {
|
||||
return validPaths[0].split(separator).pop();
|
||||
}
|
||||
return `${validPaths.length} file(s)`;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
name: 'Key',
|
||||
isKeyField: true,
|
||||
placeholder: 'Key',
|
||||
width: '30%',
|
||||
readOnly: !editMode
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
name: 'Value',
|
||||
placeholder: 'Value',
|
||||
width: '40%',
|
||||
readOnly: !editMode,
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => {
|
||||
const isFile = row.type === 'file';
|
||||
const fileName = isFile ? getFileName(value) : null;
|
||||
const hasTextValue = !isFile && value && value.length > 0;
|
||||
|
||||
if (fileName) {
|
||||
return (
|
||||
<div className="flex items-center file-value-cell">
|
||||
<IconFile size={16} className="text-muted mr-1" />
|
||||
<span className="file-name flex-1 truncate" title={Array.isArray(value) ? value.join(', ') : value}>
|
||||
{fileName}
|
||||
</span>
|
||||
<button
|
||||
className="clear-file-btn ml-1"
|
||||
onClick={() => handleClearFile(row)}
|
||||
title="Remove file"
|
||||
>
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center value-cell">
|
||||
<div className="flex-1">
|
||||
<MultiLineEditor
|
||||
onSave={() => {}}
|
||||
theme={storedTheme}
|
||||
value={value || ''}
|
||||
onChange={(newValue) => handleValueChange(row, newValue, onChange)}
|
||||
onRun={() => {}}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
readOnly={!editMode}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
/>
|
||||
</div>
|
||||
{!hasTextValue && !isLastEmptyRow && (
|
||||
<button
|
||||
className="upload-btn ml-1"
|
||||
onClick={() => handleBrowseFiles(row, onChange)}
|
||||
title="Select file"
|
||||
>
|
||||
<IconUpload size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'contentType',
|
||||
name: 'Content-Type',
|
||||
placeholder: 'Auto',
|
||||
width: '30%',
|
||||
readOnly: !editMode,
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
onSave={() => {}}
|
||||
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 (
|
||||
<StyledWrapper className="w-full mt-4">
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Key', accessor: 'key', width: '30%' },
|
||||
{ name: 'Value', accessor: 'value', width: '40%' },
|
||||
{ name: 'Content-Type', accessor: 'content-type', width: '30%' }
|
||||
]}
|
||||
>
|
||||
<ReorderTable updateReorderedItem={handleParamDrag}>
|
||||
{params && params.length
|
||||
? params.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid} className="w-full" data-uid={param.uid}>
|
||||
<td className="flex relative">
|
||||
<div className="flex items-center justify-center mr-3">
|
||||
<Checkbox
|
||||
checked={param.enabled === true}
|
||||
disabled={!editMode}
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
dataTestId={`multipart-form-param-checkbox-${index}`}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'name')}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
{param.type === 'file' ? (
|
||||
<FilePickerEditor
|
||||
value={param.value}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value')}
|
||||
collection={collection}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
) : (
|
||||
<MultiLineEditor
|
||||
onSave={() => {}}
|
||||
theme={storedTheme}
|
||||
value={param.value}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value')}
|
||||
onRun={() => {}}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<SingleLineEditor
|
||||
onSave={() => {}}
|
||||
theme={storedTheme}
|
||||
placeholder="Auto"
|
||||
value={param.contentType}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'contentType')}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
<button
|
||||
tabIndex="-1"
|
||||
onClick={() => handleRemoveParams(param)}
|
||||
className={`delete-button ${editMode ? 'edit-mode' : ''}`}
|
||||
disabled={!editMode}
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
{editMode && (
|
||||
<div className="flex justify-between mt-2">
|
||||
<button
|
||||
className="btn-action text-link pr-2 py-3 select-none"
|
||||
onClick={addParam}
|
||||
>
|
||||
+ Add Param
|
||||
</button>
|
||||
<button
|
||||
className="btn-action text-link pr-2 py-3 select-none"
|
||||
onClick={addFile}
|
||||
>
|
||||
+ Add File
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<EditableTable
|
||||
columns={columns}
|
||||
rows={params || []}
|
||||
onChange={handleParamsChange}
|
||||
defaultRow={defaultRow}
|
||||
reorderable={editMode}
|
||||
onReorder={handleParamDrag}
|
||||
showAddRow={editMode}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
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 }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
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 }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
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 (
|
||||
<StyledWrapper className="w-full mt-4">
|
||||
<div className="mb-1 title text-xs font-bold">Query parameters</div>
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Name', accessor: 'name', width: '40%' },
|
||||
{ name: 'Value', accessor: 'value', width: '60%' }
|
||||
]}
|
||||
>
|
||||
<ReorderTable updateReorderedItem={handleQueryParamDrag}>
|
||||
{queryParams && queryParams.length
|
||||
? queryParams.map((param, index) => (
|
||||
<tr key={param.uid} data-uid={param.uid}>
|
||||
<td className="flex relative">
|
||||
<div className="flex items-center justify-center mr-3">
|
||||
<Checkbox
|
||||
checked={param.enabled !== false}
|
||||
disabled={!editMode}
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
|
||||
dataTestId={`query-param-checkbox-${index}`}
|
||||
/>
|
||||
</div>
|
||||
<SingleLineEditor
|
||||
value={param.name || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={(newValue) => handleQueryParamChange({ target: { value: newValue } }, param, 'name')}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
variablesAutocomplete={true}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<SingleLineEditor
|
||||
value={param.value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={(newValue) => handleQueryParamChange({ target: { value: newValue } }, param, 'value')}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
variablesAutocomplete={true}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
{editMode && (
|
||||
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)} className="delete-button">
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: null}
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
<EditableTable
|
||||
columns={queryColumns}
|
||||
rows={queryParams || []}
|
||||
onChange={handleQueryParamsChange}
|
||||
defaultRow={defaultQueryRow}
|
||||
reorderable={editMode}
|
||||
onReorder={handleQueryParamDrag}
|
||||
showAddRow={editMode}
|
||||
/>
|
||||
{editMode && (
|
||||
<div className="flex justify-between mt-2">
|
||||
<button
|
||||
className="btn-action text-link pr-2 py-3 select-none"
|
||||
onClick={handleAddQueryParam}
|
||||
>
|
||||
+ Add Param
|
||||
</button>
|
||||
<div className="flex justify-end mt-2">
|
||||
<button
|
||||
className="btn-action text-link select-none"
|
||||
onClick={toggleBulkEditMode}
|
||||
@@ -231,37 +218,16 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
|
||||
</div>
|
||||
</InfoTip>
|
||||
</div>
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Name', accessor: 'name', width: '40%' },
|
||||
{ name: 'Value', accessor: 'value', width: '60%' }
|
||||
]}
|
||||
>
|
||||
{pathParams && pathParams.length
|
||||
? pathParams.map((path, index) => {
|
||||
return (
|
||||
<tr key={index} data-uid={path.uid}>
|
||||
<td>
|
||||
{path.name}
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={path.value}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={(newValue) => handlePathParamChange({ target: { value: newValue } }, path)}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
variablesAutocomplete={true}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</Table>
|
||||
{pathParams.length === 0 && <div className="title pr-2 py-3 mt-2 text-xs">No path parameters defined</div>}
|
||||
<EditableTable
|
||||
columns={pathColumns}
|
||||
rows={pathParams}
|
||||
onChange={handlePathParamsChange}
|
||||
defaultRow={{}}
|
||||
showCheckbox={false}
|
||||
showDelete={false}
|
||||
showAddRow={false}
|
||||
reorderable={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user