mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 03:41:28 +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:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -28450,6 +28450,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"mime-types": "^3.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -30007,6 +30008,31 @@
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -44,9 +44,8 @@
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"jsesc": "^3.0.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
@@ -56,6 +55,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"mime-types": "^3.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -89,6 +89,7 @@
|
||||
"system": "^2.0.1",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
padding-left: 1.5rem !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
fill: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,17 +1,27 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import {
|
||||
IconCaretDown,
|
||||
IconForms,
|
||||
IconBraces,
|
||||
IconCode,
|
||||
IconFileText,
|
||||
IconDatabase,
|
||||
IconFile,
|
||||
IconX
|
||||
} from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DEFAULT_MODES = [
|
||||
{ key: 'multipartForm', label: 'Multipart Form', category: 'Form' },
|
||||
{ key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form' },
|
||||
{ key: 'json', label: 'JSON', category: 'Raw' },
|
||||
{ key: 'xml', label: 'XML', category: 'Raw' },
|
||||
{ key: 'text', label: 'TEXT', category: 'Raw' },
|
||||
{ key: 'sparql', label: 'SPARQL', category: 'Raw' },
|
||||
{ key: 'file', label: 'File / Binary', category: 'Other' },
|
||||
{ key: 'none', label: 'None', category: 'Other' }
|
||||
{ key: 'multipartForm', label: 'Multipart Form', category: 'Form', icon: IconForms },
|
||||
{ key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form', icon: IconForms },
|
||||
{ key: 'json', label: 'JSON', category: 'Raw', icon: IconBraces },
|
||||
{ key: 'xml', label: 'XML', category: 'Raw', icon: IconCode },
|
||||
{ key: 'text', label: 'TEXT', category: 'Raw', icon: IconFileText },
|
||||
{ key: 'sparql', label: 'SPARQL', category: 'Raw', icon: IconDatabase },
|
||||
{ key: 'file', label: 'File / Binary', category: 'Other', icon: IconFile },
|
||||
{ key: 'none', label: 'No Body', category: 'Other', icon: IconX }
|
||||
];
|
||||
|
||||
const BodyModeSelector = ({
|
||||
@@ -53,30 +63,40 @@ const BodyModeSelector = ({
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'} ${wrapperClassName}`}>
|
||||
<Dropdown
|
||||
onCreate={onDropdownCreate}
|
||||
icon={<Icon />}
|
||||
placement={placement}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
>
|
||||
{Object.entries(groupedModes).map(([category, categoryModes]) => (
|
||||
<React.Fragment key={category}>
|
||||
{showCategories && <div className="label-item font-medium">{category}</div>}
|
||||
{categoryModes.map((mode) => (
|
||||
<div
|
||||
key={mode.key}
|
||||
className="dropdown-item"
|
||||
onClick={() => onModeSelect(mode.key)}
|
||||
>
|
||||
{mode.label}
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
<StyledWrapper className={wrapperClassName}>
|
||||
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'}`}>
|
||||
<Dropdown
|
||||
onCreate={onDropdownCreate}
|
||||
icon={<Icon />}
|
||||
placement={placement}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
>
|
||||
{Object.entries(groupedModes).map(([category, categoryModes]) => (
|
||||
<React.Fragment key={category}>
|
||||
{showCategories && <div className="label-item">{category}</div>}
|
||||
{categoryModes.map((mode) => {
|
||||
const ModeIcon = mode.icon;
|
||||
return (
|
||||
<div
|
||||
key={mode.key}
|
||||
className="dropdown-item"
|
||||
onClick={() => onModeSelect(mode.key)}
|
||||
>
|
||||
{ModeIcon && (
|
||||
<span className="dropdown-icon">
|
||||
<ModeIcon size={16} strokeWidth={2} />
|
||||
</span>
|
||||
)}
|
||||
{mode.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,17 +35,17 @@ const StyledWrapper = styled.div`
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
vertical-align: middle;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 25px !important;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-checkbox thead td:nth-child(1) {
|
||||
width: 25px !important;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
transition: background 0.1s ease;
|
||||
@@ -62,19 +62,6 @@ const StyledWrapper = styled.div`
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
vertical-align: middle;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 25px;
|
||||
border-right: none;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
|
||||
input[type='checkbox'] {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
@@ -82,6 +69,19 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
&.has-checkbox tbody td:nth-child(1) {
|
||||
width: 25px;
|
||||
border-right: none;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
|
||||
input[type='checkbox'] {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
max-width: 200px !important;
|
||||
|
||||
@@ -223,7 +223,7 @@ const EditableTable = ({
|
||||
const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledWrapper className={showCheckbox ? 'has-checkbox' : 'no-checkbox'}>
|
||||
<div className="table-container" ref={tableRef}>
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
@@ -55,16 +55,30 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
return filenames.length > 0 ? (
|
||||
<div
|
||||
className={buttonClass}
|
||||
style={{ fontWeight: 400, width: '100%', textOverflow: 'ellipsis', overflowX: 'hidden' }}
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
title={title}
|
||||
>
|
||||
{!readOnly && (
|
||||
<button className="align-middle" onClick={clear}>
|
||||
<button className="align-middle" onClick={clear} style={{ flexShrink: 0 }}>
|
||||
<IconX size={18} />
|
||||
</button>
|
||||
)}
|
||||
{!readOnly && <> </>}
|
||||
{renderButtonText(filenames)}
|
||||
<span style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{renderButtonText(filenames)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<button className={buttonClass} style={{ width: '100%' }} onClick={!readOnly ? browse : undefined} disabled={readOnly}>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
const ExampleIcon = ({ color = 'white', size = 16, ...props }) => {
|
||||
const ExampleIcon = ({ color = 'currentColor', size = 16, ...props }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g clipPath="url(#clip0_486_1191)">
|
||||
<path d="M2.66699 3.33329C2.66699 3.15648 2.73723 2.98691 2.86225 2.86189C2.98728 2.73686 3.15685 2.66663 3.33366 2.66663H12.667C12.8438 2.66663 13.0134 2.73686 13.1384 2.86189C13.2634 2.98691 13.3337 3.15648 13.3337 3.33329V12.6666C13.3337 12.8434 13.2634 13.013 13.1384 13.138C13.0134 13.2631 12.8438 13.3333 12.667 13.3333H3.33366C3.15685 13.3333 2.98728 13.2631 2.86225 13.138C2.73723 13.013 2.66699 12.8434 2.66699 12.6666V3.33329Z" stroke={color} stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9.33366 5.33337H6.66699V10.6667H9.33366" stroke={color} stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M9.33366 8H6.66699" stroke={color} stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M2.66699 3.33329C2.66699 3.15648 2.73723 2.98691 2.86225 2.86189C2.98728 2.73686 3.15685 2.66663 3.33366 2.66663H12.667C12.8438 2.66663 13.0134 2.73686 13.1384 2.86189C13.2634 2.98691 13.3337 3.15648 13.3337 3.33329V12.6666C13.3337 12.8434 13.2634 13.013 13.1384 13.138C13.0134 13.2631 12.8438 13.3333 12.667 13.3333H3.33366C3.15685 13.3333 2.98728 13.2631 2.86225 13.138C2.73723 13.013 2.66699 12.8434 2.66699 12.6666V3.33329Z" stroke={color} strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M9.33366 5.33337H6.66699V10.6667H9.33366" stroke={color} strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M9.33366 8H6.66699" stroke={color} strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_486_1191">
|
||||
<rect width={size} height={size} fill="white" />
|
||||
<rect width={size} height={size} fill={color} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
@@ -9,6 +9,14 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.request-pane {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.response-pane {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
div.dragbar-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
|
||||
const RequestTabNotFound = ({ handleCloseClick }) => {
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
@@ -28,9 +28,7 @@ const RequestTabNotFound = ({ handleCloseClick }) => {
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<CloseTabIcon />
|
||||
</div>
|
||||
<GradientCloseButton onClick={handleCloseClick} hasChanges={true} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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 { addResponseExampleHeader, updateResponseExampleHeader, deleteResponseExampleHeader, moveResponseExampleHeader, setResponseExampleHeaders, updateResponseExampleResponse } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveResponseExampleHeader, setResponseExampleHeaders, updateResponseExampleResponse } from 'providers/ReduxStore/slices/collections';
|
||||
import { getBodyType } from 'utils/responseBodyProcessor';
|
||||
import Table from 'components/Table-v2';
|
||||
import ReorderTable from 'components/ReorderTable';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import BulkEditor from 'components/BulkEditor';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
@@ -28,45 +26,17 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
|
||||
return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.response || {} : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.response || {};
|
||||
}, [item, exampleUid]);
|
||||
|
||||
const handleAddHeader = () => {
|
||||
const handleHeadersChange = useCallback((updatedHeaders) => {
|
||||
if (!editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addResponseExampleHeader({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid
|
||||
}));
|
||||
};
|
||||
// Check if content-type header was updated
|
||||
const contentTypeHeader = updatedHeaders.find((h) => h.name?.toLowerCase() === 'content-type');
|
||||
const oldContentTypeHeader = headers.find((h) => h.name?.toLowerCase() === 'content-type');
|
||||
|
||||
const handleHeaderValueChange = (e, header, type) => {
|
||||
if (!editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedHeader = { ...header };
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
updatedHeader.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
updatedHeader.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(updateResponseExampleHeader({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
header: updatedHeader
|
||||
}));
|
||||
|
||||
// If content-type header is being updated, automatically update the body type
|
||||
if (header.name?.toLowerCase() === 'content-type' && type === 'value') {
|
||||
const newContentType = updatedHeader.value?.toLowerCase() || '';
|
||||
if (contentTypeHeader && oldContentTypeHeader && contentTypeHeader.value !== oldContentTypeHeader.value) {
|
||||
const newContentType = contentTypeHeader.value?.toLowerCase() || '';
|
||||
const newBodyType = getBodyType(newContentType);
|
||||
const currentBodyType = response.body?.type || 'text';
|
||||
|
||||
@@ -85,22 +55,16 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveHeader = (header) => {
|
||||
if (!editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(deleteResponseExampleHeader({
|
||||
dispatch(setResponseExampleHeaders({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
exampleUid: exampleUid,
|
||||
headerUid: header.uid
|
||||
headers: updatedHeaders
|
||||
}));
|
||||
};
|
||||
}, [editMode, dispatch, item.uid, collection.uid, exampleUid, headers, response]);
|
||||
|
||||
const handleHeaderDrag = ({ updateReorderedItem }) => {
|
||||
const handleHeaderDrag = useCallback(({ updateReorderedItem }) => {
|
||||
if (!editMode) {
|
||||
return;
|
||||
}
|
||||
@@ -111,7 +75,7 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
|
||||
exampleUid: exampleUid,
|
||||
updateReorderedItem
|
||||
}));
|
||||
};
|
||||
}, [editMode, dispatch, item.uid, collection.uid, exampleUid]);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
@@ -152,79 +116,71 @@ const ResponseExampleResponseHeaders = ({ 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 || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
readOnly={!editMode}
|
||||
placeholder={isLastEmptyRow ? 'Key' : ''}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
name: 'Value',
|
||||
placeholder: 'Value',
|
||||
width: '60%',
|
||||
readOnly: !editMode,
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={onChange}
|
||||
onRun={() => {}}
|
||||
autocomplete={MimeTypes}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
readOnly={!editMode}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const defaultRow = {
|
||||
name: '',
|
||||
value: ''
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full px-4">
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Key', accessor: 'key', width: '40%' },
|
||||
{ name: 'Value', accessor: 'value', width: '60%' }
|
||||
]}
|
||||
>
|
||||
<ReorderTable updateReorderedItem={handleHeaderDrag}>
|
||||
{headers && headers.length
|
||||
? headers.map((header) => (
|
||||
<tr key={header.uid} data-uid={header.uid}>
|
||||
<td className="flex relative">
|
||||
<SingleLineEditor
|
||||
value={header.name || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'name')}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
onRun={() => {}}
|
||||
collection={collection}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<SingleLineEditor
|
||||
value={header.value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={() => {}}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'value')}
|
||||
onRun={() => {}}
|
||||
autocomplete={MimeTypes}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
readOnly={!editMode}
|
||||
/>
|
||||
{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}
|
||||
showCheckbox={false}
|
||||
/>
|
||||
{editMode && (
|
||||
<div className="flex justify-between mt-2 flex-shrink-0">
|
||||
<button
|
||||
className="btn-action text-link pr-2 py-3 select-none"
|
||||
onClick={handleAddHeader}
|
||||
>
|
||||
+ Add Header
|
||||
</button>
|
||||
<div className="flex justify-end mt-2 flex-shrink-0">
|
||||
<button
|
||||
className="btn-action text-link select-none"
|
||||
onClick={toggleBulkEditMode}
|
||||
|
||||
@@ -9,6 +9,14 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.request-pane {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.response-pane {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
div.dragbar-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -5,13 +5,14 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.menu-icon {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
visibility: hidden;
|
||||
|
||||
.dropdown {
|
||||
div[aria-expanded='true'] {
|
||||
visibility: visible;
|
||||
}
|
||||
div[aria-expanded='false'] {
|
||||
visibility: hidden;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,11 +37,7 @@ const StyledWrapper = styled.div`
|
||||
&.item-hovered {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
.menu-icon {
|
||||
.dropdown {
|
||||
div[aria-expanded='false'] {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, forwardRef, useEffect } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import {
|
||||
@@ -8,13 +8,16 @@ import {
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
||||
import { uuid } from 'utils/common';
|
||||
import { IconDots } from '@tabler/icons';
|
||||
import { IconDots, IconEdit, IconCopy, IconTrash, IconCode } from '@tabler/icons';
|
||||
import ExampleIcon from 'components/Icons/ExampleIcon';
|
||||
import range from 'lodash/range';
|
||||
import classnames from 'classnames';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import Modal from 'components/Modal';
|
||||
import DeleteResponseExampleModal from './DeleteResponseExampleModal';
|
||||
import GenerateCodeItem from '../GenerateCodeItem';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ExampleItem = ({ example, item, collection }) => {
|
||||
@@ -25,8 +28,9 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
const [editName, setEditName] = useState(example.name);
|
||||
const [showRenameModal, setShowRenameModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const dropdownTippyRef = useRef(null);
|
||||
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||
const exampleRef = useRef(null);
|
||||
const menuDropdownRef = useRef(null);
|
||||
|
||||
// Calculate indentation: item depth + 1 for examples
|
||||
const indents = range((item.depth || 0) + 1);
|
||||
@@ -48,9 +52,6 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
const handleRename = () => {
|
||||
setEditName(example.name); // Set current name when opening modal
|
||||
setShowRenameModal(true);
|
||||
if (dropdownTippyRef.current) {
|
||||
dropdownTippyRef.current.hide();
|
||||
}
|
||||
};
|
||||
|
||||
// Update editName when example changes
|
||||
@@ -93,26 +94,22 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
itemUid: item.uid,
|
||||
exampleIndex: clonedExampleIndex
|
||||
}));
|
||||
|
||||
if (dropdownTippyRef.current) {
|
||||
dropdownTippyRef.current.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setShowDeleteModal(true);
|
||||
if (dropdownTippyRef.current) {
|
||||
dropdownTippyRef.current.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Show the dropdown menu programmatically
|
||||
if (dropdownTippyRef.current) {
|
||||
dropdownTippyRef.current.show();
|
||||
const handleGenerateCode = () => {
|
||||
// Check if example has a request URL
|
||||
if (
|
||||
(example?.request?.url !== '' && example?.request?.url !== undefined)
|
||||
|| (item?.request?.url !== '' && item?.request?.url !== undefined)
|
||||
|| (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')
|
||||
) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,17 +127,48 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
setShowRenameModal(false);
|
||||
};
|
||||
|
||||
const onDropdownCreate = (instance) => {
|
||||
dropdownTippyRef.current = instance;
|
||||
// Build menu items for MenuDropdown
|
||||
const buildMenuItems = () => {
|
||||
return [
|
||||
{
|
||||
id: 'rename',
|
||||
leftSection: IconEdit,
|
||||
label: 'Rename',
|
||||
onClick: handleRename,
|
||||
testId: 'response-example-rename-option'
|
||||
},
|
||||
{
|
||||
id: 'clone',
|
||||
leftSection: IconCopy,
|
||||
label: 'Clone',
|
||||
onClick: handleClone,
|
||||
testId: 'response-example-clone-option'
|
||||
},
|
||||
{
|
||||
id: 'generate-code',
|
||||
leftSection: IconCode,
|
||||
label: 'Generate Code',
|
||||
onClick: handleGenerateCode,
|
||||
testId: 'response-example-generate-code-option'
|
||||
},
|
||||
{ id: 'separator-1', type: 'divider' },
|
||||
{
|
||||
id: 'delete',
|
||||
leftSection: IconTrash,
|
||||
label: 'Delete',
|
||||
className: 'delete-item',
|
||||
onClick: handleDelete,
|
||||
testId: 'response-example-delete-option'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} data-testid="response-example-menu-icon">
|
||||
<IconDots size={22} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
// Handle right-click context menu
|
||||
const handleContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
menuDropdownRef.current?.open();
|
||||
};
|
||||
|
||||
const itemRowClassName = classnames('flex collection-item-name relative items-center', {
|
||||
'item-focused-in-tab': isExampleActive
|
||||
@@ -152,7 +180,7 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
className={itemRowClassName}
|
||||
onClick={handleExampleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{indents && indents.length
|
||||
? indents.map((i) => (
|
||||
@@ -160,7 +188,6 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
className="indent-block"
|
||||
key={i}
|
||||
style={{ width: 16, minWidth: 16, height: '100%' }}
|
||||
onContextMenu={handleRightClick}
|
||||
>
|
||||
{/* Indent */}
|
||||
</div>
|
||||
@@ -169,45 +196,20 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
<div
|
||||
className="flex flex-grow items-center h-full overflow-hidden"
|
||||
style={{ paddingLeft: 8 }}
|
||||
onContextMenu={handleRightClick}
|
||||
>
|
||||
<div style={{ width: 16, minWidth: 16 }}></div>
|
||||
<ExampleIcon size={16} color="currentColor" className="mr-2 text-gray-400 flex-shrink-0" />
|
||||
<span className="item-name truncate text-gray-700 dark:text-gray-300 ">{example.name}</span>
|
||||
</div>
|
||||
<div className="menu-icon pr-2">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleRename();
|
||||
}}
|
||||
data-testid="response-example-rename-option"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleClone();
|
||||
}}
|
||||
data-testid="response-example-clone-option"
|
||||
>
|
||||
Clone
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item text-red-600"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleDelete();
|
||||
}}
|
||||
data-testid="response-example-delete-option"
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
</Dropdown>
|
||||
<MenuDropdown
|
||||
ref={menuDropdownRef}
|
||||
items={buildMenuItems()}
|
||||
placement="bottom-start"
|
||||
data-testid="response-example-menu"
|
||||
>
|
||||
<IconDots size={22} data-testid="response-example-menu-icon" />
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
|
||||
{showRenameModal && (
|
||||
@@ -250,6 +252,16 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
collection={collection}
|
||||
/>
|
||||
)}
|
||||
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem
|
||||
collectionUid={collection.uid}
|
||||
item={item}
|
||||
onClose={() => setGenerateCodeItemModalOpen(false)}
|
||||
isExample={true}
|
||||
exampleUid={example.uid}
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,13 +4,14 @@ const Wrapper = styled.div`
|
||||
position: relative;
|
||||
.menu-icon {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
visibility: hidden;
|
||||
|
||||
.dropdown {
|
||||
div[aria-expanded='true'] {
|
||||
visibility: visible;
|
||||
}
|
||||
div[aria-expanded='false'] {
|
||||
visibility: hidden;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,11 +98,7 @@ const Wrapper = styled.div`
|
||||
&.item-hovered {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
.menu-icon {
|
||||
.dropdown {
|
||||
div[aria-expanded='false'] {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, forwardRef, useEffect } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import range from 'lodash/range';
|
||||
import filter from 'lodash/filter';
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
IconCopy,
|
||||
IconClipboard,
|
||||
IconCode,
|
||||
IconPhoto,
|
||||
IconFolder,
|
||||
IconTrash,
|
||||
IconSettings,
|
||||
@@ -28,7 +27,7 @@ import { toggleCollectionItem, addResponseExample } from 'providers/ReduxStore/s
|
||||
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
||||
import { uuid } from 'utils/common';
|
||||
import { copyRequest } from 'providers/ReduxStore/slices/app';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
import RenameCollectionItem from './RenameCollectionItem';
|
||||
@@ -46,6 +45,7 @@ import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import CollectionItemInfo from './CollectionItemInfo/index';
|
||||
import CollectionItemIcon from './CollectionItemIcon';
|
||||
import ExampleItem from './ExampleItem';
|
||||
import ExampleIcon from 'components/Icons/ExampleIcon';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';
|
||||
import { isEqual } from 'lodash';
|
||||
@@ -68,6 +68,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
|
||||
// We use a single ref for drag and drop.
|
||||
const ref = useRef(null);
|
||||
const menuDropdownRef = useRef(null);
|
||||
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
||||
@@ -182,15 +183,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
})
|
||||
});
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<IconDots size={22} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const iconClassName = classnames({
|
||||
'rotate-90': !itemIsCollapsed
|
||||
});
|
||||
@@ -289,19 +281,138 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = dropdownTippyRef.current;
|
||||
if (_menuDropdown) {
|
||||
let menuDropdownBehavior = 'show';
|
||||
if (_menuDropdown.state.isShown) {
|
||||
menuDropdownBehavior = 'hide';
|
||||
}
|
||||
_menuDropdown[menuDropdownBehavior]();
|
||||
}
|
||||
// Handle right-click context menu
|
||||
const handleContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
menuDropdownRef.current?.open();
|
||||
};
|
||||
|
||||
let indents = range(item.depth);
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
// Build menu items for MenuDropdown
|
||||
const buildMenuItems = () => {
|
||||
const items = [
|
||||
{
|
||||
id: 'info',
|
||||
leftSection: IconInfoCircle,
|
||||
label: 'Info',
|
||||
onClick: () => setItemInfoModalOpen(true)
|
||||
}
|
||||
];
|
||||
|
||||
if (isFolder) {
|
||||
items.push(
|
||||
{
|
||||
id: 'new-request',
|
||||
leftSection: IconFilePlus,
|
||||
label: 'New Request',
|
||||
onClick: () => setNewRequestModalOpen(true)
|
||||
},
|
||||
{
|
||||
id: 'new-folder',
|
||||
leftSection: IconFolderPlus,
|
||||
label: 'New Folder',
|
||||
onClick: () => setNewFolderModalOpen(true)
|
||||
},
|
||||
{
|
||||
id: 'run',
|
||||
leftSection: IconPlayerPlay,
|
||||
label: 'Run',
|
||||
onClick: () => setRunCollectionModalOpen(true)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: 'clone',
|
||||
leftSection: IconCopy,
|
||||
label: 'Clone',
|
||||
onClick: () => setCloneItemModalOpen(true)
|
||||
},
|
||||
{
|
||||
id: 'copy',
|
||||
leftSection: IconCopy,
|
||||
label: 'Copy',
|
||||
onClick: handleCopyItem
|
||||
}
|
||||
);
|
||||
|
||||
if (isFolder && hasCopiedItems) {
|
||||
items.push({
|
||||
id: 'paste',
|
||||
leftSection: IconClipboard,
|
||||
label: 'Paste',
|
||||
onClick: handlePasteItem
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: 'rename',
|
||||
leftSection: IconEdit,
|
||||
label: 'Rename',
|
||||
onClick: () => setRenameItemModalOpen(true)
|
||||
},
|
||||
{
|
||||
id: 'show-in-folder',
|
||||
leftSection: IconFolder,
|
||||
label: 'Show in Folder',
|
||||
onClick: handleShowInFolder
|
||||
}
|
||||
);
|
||||
|
||||
if (!isFolder && (item.type === 'http-request' || item.type === 'graphql-request')) {
|
||||
items.push({
|
||||
id: 'generate-code',
|
||||
leftSection: IconCode,
|
||||
label: 'Generate Code',
|
||||
onClick: handleGenerateCode
|
||||
});
|
||||
}
|
||||
|
||||
if (!isFolder && isItemARequest(item) && item.type === 'http-request') {
|
||||
items.push({
|
||||
id: 'create-example',
|
||||
leftSection: ExampleIcon,
|
||||
label: 'Create Example',
|
||||
onClick: () => setCreateExampleModalOpen(true)
|
||||
});
|
||||
}
|
||||
|
||||
items.push({ id: 'separator-1', type: 'divider' });
|
||||
|
||||
if (isFolder) {
|
||||
items.push(
|
||||
{
|
||||
id: 'settings',
|
||||
leftSection: IconSettings,
|
||||
label: 'Settings',
|
||||
onClick: viewFolderSettings
|
||||
},
|
||||
{
|
||||
id: 'open-terminal',
|
||||
leftSection: IconTerminal2,
|
||||
label: 'Open in Terminal',
|
||||
onClick: async () => {
|
||||
const folderCwd = item.pathname || collectionPathname;
|
||||
await openDevtoolsAndSwitchToTerminal(dispatch, folderCwd);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: 'delete',
|
||||
leftSection: IconTrash,
|
||||
label: 'Delete',
|
||||
className: 'delete-item',
|
||||
onClick: () => setDeleteItemModalOpen(true)
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const className = classnames('flex flex-col w-full', {
|
||||
'is-sidebar-dragging': isSidebarDragging
|
||||
@@ -382,9 +493,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
const folderItems = sortByNameThenSequence(filter(item.items, (i) => isItemAFolder(i)));
|
||||
const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i)));
|
||||
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
const handleGenerateCode = () => {
|
||||
if (
|
||||
(item?.request?.url !== '')
|
||||
|| (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')
|
||||
@@ -412,15 +521,12 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
};
|
||||
|
||||
const handleCopyItem = () => {
|
||||
dropdownTippyRef.current.hide();
|
||||
dispatch(copyRequest(item));
|
||||
const itemType = isFolder ? 'Folder' : 'Request';
|
||||
toast.success(`${itemType} copied to clipboard`);
|
||||
};
|
||||
|
||||
const handlePasteItem = () => {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
// Only allow paste into folders
|
||||
if (!isFolder) {
|
||||
toast.error('Paste is only available for folders');
|
||||
@@ -504,6 +610,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onContextMenu={handleContextMenu}
|
||||
data-testid="sidebar-collection-item-row"
|
||||
>
|
||||
<div className="flex items-center h-full w-full">
|
||||
@@ -511,7 +618,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
? indents.map((i) => (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="indent-block"
|
||||
key={i}
|
||||
@@ -525,7 +631,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
className="flex flex-grow items-center h-full overflow-hidden"
|
||||
style={{ paddingLeft: 8 }}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<div style={{ width: 16, minWidth: 16 }}>
|
||||
@@ -559,186 +664,14 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
</div>
|
||||
</div>
|
||||
<div className="menu-icon pr-2">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setItemInfoModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconInfoCircle size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Info
|
||||
</div>
|
||||
{isFolder && (
|
||||
<>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setNewRequestModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconFilePlus size={16} strokeWidth={2} />
|
||||
</span>
|
||||
New Request
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setNewFolderModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconFolderPlus size={16} strokeWidth={2} />
|
||||
</span>
|
||||
New Folder
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setRunCollectionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconPlayerPlay size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Run
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setCloneItemModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconCopy size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Clone
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={handleCopyItem}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconCopy size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Copy
|
||||
</div>
|
||||
{isFolder && hasCopiedItems && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={handlePasteItem}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconClipboard size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Paste
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setRenameItemModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconEdit size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Rename
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleShowInFolder();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconFolder size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Show in Folder
|
||||
</div>
|
||||
{!isFolder && (item.type === 'http-request' || item.type === 'graphql-request') && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
handleGenerateCode(e);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconCode size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Generate Code
|
||||
</div>
|
||||
)}
|
||||
{!isFolder && isItemARequest(item) && item.type === 'http-request' && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setCreateExampleModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconPhoto size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Create Example
|
||||
</div>
|
||||
)}
|
||||
<div className="dropdown-separator"></div>
|
||||
{isFolder && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
viewFolderSettings();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconSettings size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Settings
|
||||
</div>
|
||||
)}
|
||||
{isFolder && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={async (e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
// Get folder pathname
|
||||
const folderCwd = item.pathname || collectionPathname;
|
||||
await openDevtoolsAndSwitchToTerminal(dispatch, folderCwd);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconTerminal2 size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Open in Terminal
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="dropdown-item delete-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setDeleteItemModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconTrash size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Delete
|
||||
</div>
|
||||
</Dropdown>
|
||||
<MenuDropdown
|
||||
ref={menuDropdownRef}
|
||||
items={buildMenuItems()}
|
||||
placement="bottom-start"
|
||||
data-testid="collection-item-menu"
|
||||
>
|
||||
<IconDots size={22} />
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -329,8 +329,8 @@ export const setResponseExampleHeaders = (state, action) => {
|
||||
const example = item.draft.examples.find((e) => e.uid === exampleUid);
|
||||
if (!example) return;
|
||||
|
||||
example.response.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
example.response.headers = map(headers, ({ uid, name = '', value = '', enabled = true }) => ({
|
||||
uid: uid || uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
description: '',
|
||||
@@ -921,8 +921,8 @@ export const setResponseExampleRequestHeaders = (state, action) => {
|
||||
const example = item.draft.examples.find((e) => e.uid === exampleUid);
|
||||
if (!example) return;
|
||||
|
||||
example.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
example.request.headers = map(headers, ({ uid, name = '', value = '', enabled = true }) => ({
|
||||
uid: uid || uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
description: '',
|
||||
@@ -950,14 +950,36 @@ export const setResponseExampleParams = (state, action) => {
|
||||
const example = item.draft.examples.find((e) => e.uid === exampleUid);
|
||||
if (!example) return;
|
||||
|
||||
example.request.params = map(params, ({ name = '', value = '', enabled = true, type = 'query' }) => ({
|
||||
uid: uuid(),
|
||||
example.request.params = map(params, ({ uid, name = '', value = '', enabled = true, type = 'query' }) => ({
|
||||
uid: uid || uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
description: '',
|
||||
enabled: enabled,
|
||||
type: type
|
||||
}));
|
||||
|
||||
// Update URL when query parameters change
|
||||
const queryParams = filter(example.request.params, (p) => p.enabled && p.type === 'query');
|
||||
const query = stringifyQueryParams(queryParams);
|
||||
|
||||
if (!example.request.url) {
|
||||
example.request.url = '';
|
||||
}
|
||||
|
||||
const parts = splitOnFirst(example.request.url, '?');
|
||||
|
||||
if (!query || !query.length) {
|
||||
if (parts.length) {
|
||||
example.request.url = parts[0];
|
||||
}
|
||||
} else {
|
||||
if (!parts.length) {
|
||||
example.request.url += '?' + query;
|
||||
} else {
|
||||
example.request.url = parts[0] + '?' + query;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Response Example Body Types
|
||||
|
||||
@@ -17,8 +17,15 @@ const StyledWrapper = styled.div`
|
||||
/* flex container - enforces boundaries */
|
||||
.flex-boundary {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
> * {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
||||
import { useRef, useCallback, useState } from 'react';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
|
||||
@@ -36,8 +36,9 @@ const getNextIndex = (currentIndex, total, key, noFocus) => {
|
||||
* @param {string} props.className - Optional className for the dropdown
|
||||
* @param {string} props.selectedItemId - Optional ID of the selected/active item to focus on open
|
||||
* @param {Object} props.dropdownProps - Other props passed to underlying Dropdown component
|
||||
* @param {React.Ref} ref - Optional ref to expose open/close methods
|
||||
*/
|
||||
const MenuDropdown = ({
|
||||
const MenuDropdown = forwardRef(({
|
||||
items = [],
|
||||
children,
|
||||
placement = 'bottom-end',
|
||||
@@ -45,10 +46,17 @@ const MenuDropdown = ({
|
||||
selectedItemId,
|
||||
'data-testid': testId = 'menu-dropdown',
|
||||
...dropdownProps
|
||||
}) => {
|
||||
}, ref) => {
|
||||
const tippyRef = useRef();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Expose open/close methods via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setIsOpen(true),
|
||||
close: () => setIsOpen(false),
|
||||
toggle: () => setIsOpen((prev) => !prev)
|
||||
}), []);
|
||||
|
||||
// Get all focusable menu items from the menu dropdown
|
||||
const getMenuItems = useCallback(() => {
|
||||
const popper = tippyRef.current?.popper;
|
||||
@@ -274,6 +282,6 @@ const MenuDropdown = ({
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default MenuDropdown;
|
||||
|
||||
@@ -60,7 +60,7 @@ test.describe('Draft values are used in requests', () => {
|
||||
|
||||
// Create a request in the collection
|
||||
// Create a new request via collection menu
|
||||
await folder.locator('.menu-icon').hover();
|
||||
await folder.hover();
|
||||
await folder.locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ test.describe('Copy and Paste Requests', () => {
|
||||
|
||||
// Create a new request
|
||||
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });
|
||||
await collection.locator('.collection-actions').hover();
|
||||
await collection.hover();
|
||||
await collection.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
await page.getByPlaceholder('Request Name').fill('original-request');
|
||||
@@ -23,6 +23,7 @@ test.describe('Copy and Paste Requests', () => {
|
||||
|
||||
// Copy the request
|
||||
const requestItem = page.locator('.collection-item-name').filter({ hasText: 'original-request' });
|
||||
await requestItem.hover();
|
||||
await requestItem.locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();
|
||||
|
||||
@@ -37,7 +38,7 @@ test.describe('Copy and Paste Requests', () => {
|
||||
|
||||
test('should paste request into a folder', async ({ page, createTmpDir }) => {
|
||||
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });
|
||||
await collection.locator('.collection-actions').hover();
|
||||
await collection.hover();
|
||||
await collection.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
|
||||
await page.locator('#folder-name').fill('test-folder');
|
||||
|
||||
@@ -24,19 +24,19 @@ test.describe.serial('Response Example Menu Operations', () => {
|
||||
await page.getByRole('button', { name: 'Create Example' }).click();
|
||||
// Wait for modal to close
|
||||
await page.waitForSelector('text=Save Response as Example', { state: 'detached' });
|
||||
await page.locator('.collection-item-name', { hasText: 'menu-operations' }).getByTestId('request-item-chevron').click();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'menu-operations' }).getByTestId('request-item-chevron').click();
|
||||
|
||||
const exampleItem = page.locator('.collection-item-name').getByText('Example to Clone');
|
||||
await expect(exampleItem).toBeVisible();
|
||||
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone' });
|
||||
await expect(exampleRow).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Clone example', async () => {
|
||||
const exampleItem = page.locator('.collection-item-name').getByText('Example to Clone');
|
||||
await exampleItem.hover();
|
||||
await page.getByTestId('response-example-menu-icon').last().click();
|
||||
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone' });
|
||||
await exampleRow.hover();
|
||||
await exampleRow.locator('.menu-icon').click();
|
||||
|
||||
await page.getByTestId('response-example-clone-option').click();
|
||||
const clonedExampleItem = page.locator('.collection-item-name').getByText('Example to Clone (Copy)');
|
||||
await page.getByTestId('response-example-menu-clone').click();
|
||||
const clonedExampleItem = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone (Copy)' });
|
||||
await expect(clonedExampleItem).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -57,20 +57,20 @@ test.describe.serial('Response Example Menu Operations', () => {
|
||||
// Wait for modal to close
|
||||
await page.waitForSelector('text=Save Response as Example', { state: 'detached' });
|
||||
|
||||
const exampleItem = page.locator('.collection-item-name').getByText('Example to Delete', { exact: true });
|
||||
await expect(exampleItem).toBeVisible();
|
||||
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Delete' });
|
||||
await expect(exampleRow).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Delete example', async () => {
|
||||
const exampleItem = page.locator('.collection-item-name').getByText('Example to Delete', { exact: true });
|
||||
await expect(exampleItem).toBeVisible();
|
||||
await exampleItem.hover();
|
||||
await page.getByTestId('response-example-menu-icon').last().click();
|
||||
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Delete' });
|
||||
await expect(exampleRow).toBeVisible();
|
||||
await exampleRow.hover();
|
||||
await exampleRow.locator('.menu-icon').click();
|
||||
|
||||
await page.getByTestId('response-example-delete-option').click();
|
||||
await page.getByTestId('response-example-menu-delete').click();
|
||||
await expect(page.getByText('Delete Example')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await expect(exampleItem).not.toBeVisible();
|
||||
await expect(exampleRow).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,16 +90,16 @@ test.describe.serial('Response Example Menu Operations', () => {
|
||||
// Wait for modal to close
|
||||
await page.waitForSelector('text=Save Response as Example', { state: 'detached' });
|
||||
|
||||
const exampleItem = page.locator('.collection-item-name').getByText('Example to Rename', { exact: true });
|
||||
await expect(exampleItem).toBeVisible();
|
||||
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Rename' });
|
||||
await expect(exampleRow).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Rename example', async () => {
|
||||
const exampleItem = page.locator('.collection-item-name').getByText('Example to Rename', { exact: true });
|
||||
await expect(exampleItem).toBeVisible();
|
||||
await exampleItem.hover();
|
||||
await page.getByTestId('response-example-menu-icon').last().click();
|
||||
await page.getByTestId('response-example-rename-option').click();
|
||||
const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Rename' });
|
||||
await expect(exampleRow).toBeVisible();
|
||||
await exampleRow.hover();
|
||||
await exampleRow.locator('.menu-icon').click();
|
||||
await page.getByTestId('response-example-menu-rename').click();
|
||||
await expect(page.getByText('Rename Example')).toBeVisible();
|
||||
const renameExampleNameInput = page.getByTestId('rename-example-name-input');
|
||||
await renameExampleNameInput.clear();
|
||||
@@ -107,10 +107,9 @@ test.describe.serial('Response Example Menu Operations', () => {
|
||||
await page.getByRole('button', { name: 'Rename' }).click();
|
||||
// Wait for modal to close
|
||||
await page.waitForSelector('text=Rename Example', { state: 'detached' });
|
||||
const updatedExampleItem = page.locator('.collection-item-name').getByText('Renamed Example', { exact: true });
|
||||
await expect(exampleItem).not.toBeVisible();
|
||||
await expect(updatedExampleItem).toBeVisible();
|
||||
await expect(updatedExampleItem).toHaveText('Renamed Example');
|
||||
const updatedExampleRow = page.locator('.collection-item-name').filter({ hasText: 'Renamed Example' });
|
||||
await expect(exampleRow).not.toBeVisible();
|
||||
await expect(updatedExampleRow).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,6 +217,7 @@ const deleteRequest = async (page, requestName: string, collectionName: string)
|
||||
const collectionWrapper = collectionContainer.locator('..');
|
||||
const request = collectionWrapper.locator('.collection-item-name').filter({ hasText: requestName });
|
||||
|
||||
await request.hover();
|
||||
await request.locator('.menu-icon').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
Reference in New Issue
Block a user