feat: persist window frames and widths (#7409)

This commit is contained in:
shubh-bruno
2026-03-26 13:11:04 +05:30
committed by GitHub
parent 304f6c8b80
commit 73df422c4e
37 changed files with 766 additions and 133 deletions

237
package-lock.json generated
View File

@@ -30606,6 +30606,7 @@
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.22.0",
"@rsbuild/core": "^1.1.2",
"@rsbuild/plugin-babel": "^1.0.3",
"@rsbuild/plugin-node-polyfill": "^1.2.0",
@@ -30633,6 +30634,21 @@
"webpack-cli": "^4.9.1"
}
},
"packages/bruno-app/node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/compat-data": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz",
@@ -30643,19 +30659,49 @@
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
"integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
"packages/bruno-app/node_modules/@babel/generator": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.1",
"@babel/helper-member-expression-to-functions": "^7.27.1",
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/helper-annotate-as-pure": {
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
"integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.3"
},
"engines": {
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
"integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
"@babel/helper-member-expression-to-functions": "^7.28.5",
"@babel/helper-optimise-call-expression": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1",
"@babel/helper-replace-supers": "^7.28.6",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
"@babel/traverse": "^7.27.1",
"@babel/traverse": "^7.28.6",
"semver": "^6.3.1"
},
"engines": {
@@ -30704,14 +30750,14 @@
}
},
"packages/bruno-app/node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
"integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
"integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1"
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -30730,6 +30776,16 @@
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/helper-plugin-utils": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/helper-remap-async-to-generator": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz",
@@ -30749,15 +30805,15 @@
}
},
"packages/bruno-app/node_modules/@babel/helper-replace-supers": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
"integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
"integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-member-expression-to-functions": "^7.27.1",
"@babel/helper-member-expression-to-functions": "^7.28.5",
"@babel/helper-optimise-call-expression": "^7.27.1",
"@babel/traverse": "^7.27.1"
"@babel/traverse": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -30795,6 +30851,22 @@
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/parser": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"packages/bruno-app/node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz",
@@ -30911,6 +30983,22 @@
"@babel/core": "^7.0.0-0"
}
},
"packages/bruno-app/node_modules/@babel/plugin-syntax-typescript": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
"integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"packages/bruno-app/node_modules/@babel/plugin-transform-arrow-functions": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz",
@@ -31681,6 +31769,26 @@
"@babel/core": "^7.0.0-0"
}
},
"packages/bruno-app/node_modules/@babel/plugin-transform-typescript": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
"integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
"@babel/helper-create-class-features-plugin": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
"@babel/plugin-syntax-typescript": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"packages/bruno-app/node_modules/@babel/plugin-transform-unicode-escapes": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
@@ -31842,6 +31950,74 @@
"semver": "bin/semver.js"
}
},
"packages/bruno-app/node_modules/@babel/preset-typescript": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz",
"integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-syntax-jsx": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
"@babel/plugin-transform-typescript": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"packages/bruno-app/node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/traverse": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"packages/bruno-app/node_modules/@testing-library/react": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
@@ -31980,6 +32156,24 @@
"yarn": ">=1"
}
},
"packages/bruno-app/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"packages/bruno-app/node_modules/electron-to-chromium": {
"version": "1.5.157",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz",
@@ -32049,6 +32243,13 @@
"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",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"packages/bruno-app/node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",

View File

@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
"plugins": [["styled-components", { "ssr": true }]]
}

View File

@@ -100,6 +100,7 @@
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.22.0",
"@rsbuild/core": "^1.1.2",
"@rsbuild/plugin-babel": "^1.0.3",
"@rsbuild/plugin-node-polyfill": "^1.2.0",

View File

@@ -1,9 +1,10 @@
import React, { useState, useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { setCollectionHeaders } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import SingleLineEditor from 'components/SingleLineEditor';
import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
@@ -18,11 +19,21 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const headers = collection.draft?.root
? get(collection, 'draft.root.request.headers', [])
: get(collection, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const collectionHeadersWidths = focusedTab?.tableColumnWidths?.['collection-headers'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
};
@@ -114,11 +125,14 @@ const Headers = ({ collection }) => {
Add request headers that will be sent with every request in this collection.
</div>
<EditableTable
tableId="collection-headers"
columns={columns}
rows={headers}
onChange={handleHeadersChange}
defaultRow={defaultRow}
getRowError={getRowError}
columnWidths={collectionHeadersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('collection-headers', widths)}
/>
<div className="flex justify-end mt-2">
<button className="text-link select-none" onClick={toggleBulkEditMode}>

View File

@@ -1,7 +1,8 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import EditableTable from 'components/EditableTable';
@@ -13,6 +14,16 @@ import { setCollectionVars } from 'providers/ReduxStore/slices/collections/index
const VarsTable = ({ collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const collectionVarsWidths = focusedTab?.tableColumnWidths?.['collection-vars'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveCollectionSettings(collection.uid));
@@ -68,11 +79,14 @@ const VarsTable = ({ collection, vars, varType }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="collection-vars"
columns={columns}
rows={vars}
onChange={handleVarsChange}
defaultRow={defaultRow}
getRowError={getRowError}
columnWidths={collectionVarsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('collection-vars', widths)}
/>
</StyledWrapper>
);

View File

@@ -1,6 +1,8 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import find from 'lodash/find';
import { updateRequestDocs } from 'providers/ReduxStore/slices/collections';
import { updateDocsEditing } from 'providers/ReduxStore/slices/tabs';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -12,12 +14,15 @@ import StyledWrapper from './StyledWrapper';
const Documentation = ({ item, collection }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const isEditing = focusedTab?.docsEditing || false;
const docs = item.draft ? get(item, 'draft.request.docs') : get(item, 'request.docs');
const preferences = useSelector((state) => state.app.preferences);
const toggleViewMode = () => {
setIsEditing((prev) => !prev);
dispatch(updateDocsEditing({ uid: activeTabUid, docsEditing: !isEditing }));
};
const onEdit = (value) => {

View File

@@ -7,6 +7,7 @@ import StyledWrapper from './StyledWrapper';
const MIN_COLUMN_WIDTH = 80;
const EditableTable = ({
tableId, // Not being used kept to maintain uniqueness & pass similar in onColumnWidthsChange
columns,
rows,
onChange,
@@ -20,20 +21,20 @@ const EditableTable = ({
reorderable = false,
onReorder,
showAddRow = true,
testId = 'editable-table'
testId = 'editable-table',
columnWidths,
onColumnWidthsChange
}) => {
const tableRef = useRef(null);
const emptyRowUidRef = useRef(null);
const [hoveredRow, setHoveredRow] = useState(null);
const [resizing, setResizing] = useState(null);
const [tableHeight, setTableHeight] = useState(0);
const [columnWidths, setColumnWidths] = useState(() => {
const initialWidths = {};
columns.forEach((col) => {
initialWidths[col.key] = col.width || 'auto';
});
return initialWidths;
});
const widths = columnWidths || {};
const handleColumnWidthsChange = useCallback((newWidths) => {
onColumnWidthsChange?.(newWidths);
}, [onColumnWidthsChange]);
const handleResizeStart = useCallback((e, columnKey) => {
e.preventDefault();
@@ -59,11 +60,13 @@ const EditableTable = ({
const maxShrink = startWidth - MIN_COLUMN_WIDTH;
const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff));
setColumnWidths((prev) => ({
...prev,
const newWidths = {
...widths,
[columnKey]: `${startWidth + clampedDiff}px`,
[nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px`
}));
};
handleColumnWidthsChange(newWidths);
};
const handleMouseUp = () => {
@@ -88,7 +91,7 @@ const EditableTable = ({
});
if (Object.keys(newWidths).length > 0) {
setColumnWidths((prev) => ({ ...prev, ...newWidths }));
handleColumnWidthsChange({ ...widths, ...newWidths });
}
}
setResizing(null);
@@ -98,7 +101,7 @@ const EditableTable = ({
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, [columns, showCheckbox]);
}, [columns, showCheckbox, widths, handleColumnWidthsChange]);
// Track table height for resize handles
useEffect(() => {
@@ -118,8 +121,8 @@ const EditableTable = ({
}, [rows.length]);
const getColumnWidth = useCallback((column) => {
return columnWidths[column.key] || column.width || 'auto';
}, [columnWidths]);
return widths[column.key] || column.width || 'auto';
}, [widths]);
const createEmptyRow = useCallback(() => {
const newUid = uuid();

View File

@@ -3,7 +3,8 @@ import { TableVirtuoso } from 'react-virtuoso';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useSelector } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import MultiLineEditor from 'components/MultiLineEditor/index';
import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common';
@@ -45,13 +46,37 @@ const EnvironmentVariablesTable = ({
const { storedTheme } = useTheme();
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const hasDraftForThisEnv = draft?.environmentUid === environment.uid;
const [tableHeight, setTableHeight] = useState(MIN_H);
const [columnWidths, setColumnWidths] = useState({ name: '30%', value: 'auto' });
// Use environment UID as part of tableId so each environment has its own column widths
const tableId = `env-vars-table-${environment.uid}`;
// Get column widths from Redux - derived value (not state)
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const storedColumnWidths = focusedTab?.tableColumnWidths?.[tableId];
// Local state initialized from Redux (computed once on mount/environment change via key)
const [columnWidths, setColumnWidths] = useState(() => {
return storedColumnWidths || { name: '30%', value: 'auto' };
});
const [resizing, setResizing] = useState(null);
const [pinnedData, setPinnedData] = useState({ query: '', uids: new Set() });
const handleColumnWidthsChange = (id, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId: id, widths }));
};
// Store column widths in ref for access in event handlers
const columnWidthsRef = useRef(columnWidths);
columnWidthsRef.current = columnWidths;
const handleResizeStart = useCallback((e, columnKey) => {
e.preventDefault();
e.stopPropagation();
@@ -73,21 +98,24 @@ const EnvironmentVariablesTable = ({
const maxShrink = startWidth - MIN_COLUMN_WIDTH;
const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff));
setColumnWidths({
const newWidths = {
[columnKey]: `${startWidth + clampedDiff}px`,
[nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px`
});
};
setColumnWidths(newWidths);
};
const handleMouseUp = () => {
setResizing(null);
// Save to Redux after resize ends using ref for latest values
handleColumnWidthsChange(tableId, columnWidthsRef.current);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, []);
}, [handleColumnWidthsChange]);
const handleTotalHeightChanged = useCallback((h) => {
setTableHeight(h);

View File

@@ -103,6 +103,7 @@ const EnvironmentVariables = ({ environment, setIsModified, collection, searchQu
return (
<EnvironmentVariablesTable
key={environment?.uid}
environment={environment}
collection={collection}
onSave={handleSave}

View File

@@ -1,9 +1,10 @@
import React, { useState, useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { setFolderHeaders } from 'providers/ReduxStore/slices/collections';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import SingleLineEditor from 'components/SingleLineEditor';
import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
@@ -18,11 +19,21 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection, folder }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const headers = folder.draft
? get(folder, 'draft.request.headers', [])
: get(folder, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const folderHeadersWidths = focusedTab?.tableColumnWidths?.['folder-headers'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
};
@@ -119,11 +130,14 @@ const Headers = ({ collection, folder }) => {
Request headers that will be sent with every request inside this folder.
</div>
<EditableTable
tableId="folder-headers"
columns={columns}
rows={headers}
onChange={handleHeadersChange}
defaultRow={defaultRow}
getRowError={getRowError}
columnWidths={folderHeadersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('folder-headers', widths)}
/>
<div className="flex justify-end mt-2">
<button className="text-link select-none" onClick={toggleBulkEditMode}>

View File

@@ -1,7 +1,8 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import EditableTable from 'components/EditableTable';
@@ -13,6 +14,16 @@ import { setFolderVars } from 'providers/ReduxStore/slices/collections/index';
const VarsTable = ({ folder, collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const folderVarsWidths = focusedTab?.tableColumnWidths?.['folder-vars'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
@@ -74,11 +85,14 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="folder-vars"
columns={columns}
rows={vars}
onChange={handleVarsChange}
defaultRow={defaultRow}
getRowError={getRowError}
columnWidths={folderVarsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('folder-vars', widths)}
/>
</StyledWrapper>
);

View File

@@ -1,9 +1,10 @@
import React, { useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { moveAssertion, setRequestAssertions } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import SingleLineEditor from 'components/SingleLineEditor';
import AssertionOperator from './AssertionOperator';
import EditableTable from 'components/EditableTable';
@@ -54,8 +55,18 @@ const isUnaryOperator = (operator) => unaryOperators.includes(operator);
const Assertions = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const assertionsWidths = focusedTab?.tableColumnWidths?.['assertions'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
@@ -157,6 +168,7 @@ const Assertions = ({ item, collection }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="assertions"
columns={columns}
rows={assertions || []}
onChange={handleAssertionsChange}
@@ -164,6 +176,8 @@ const Assertions = ({ item, collection }) => {
reorderable={true}
onReorder={handleAssertionDrag}
testId="assertions-table"
columnWidths={assertionsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('assertions', widths)}
/>
</StyledWrapper>
);

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import {
moveFormUrlEncodedParam,
@@ -8,14 +8,25 @@ import {
} from 'providers/ReduxStore/slices/collections';
import MultiLineEditor from 'components/MultiLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
const FormUrlEncodedParams = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const formUrlEncodedWidths = focusedTab?.tableColumnWidths?.['form-url-encoded'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
@@ -72,12 +83,15 @@ const FormUrlEncodedParams = ({ item, collection }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="form-url-encoded"
columns={columns}
rows={params || []}
onChange={handleParamsChange}
defaultRow={defaultRow}
reorderable={true}
onReorder={handleParamDrag}
columnWidths={formUrlEncodedWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('form-url-encoded', widths)}
/>
</StyledWrapper>
);

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { IconUpload, IconX, IconFile } from '@tabler/icons';
import {
@@ -11,6 +11,7 @@ import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
import MultiLineEditor from 'components/MultiLineEditor';
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
import path from 'utils/common/path';
@@ -19,8 +20,18 @@ import { isWindowsOS } from 'utils/common/platform';
const MultipartFormParams = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const multipartFormWidths = focusedTab?.tableColumnWidths?.['multipart-form'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
@@ -202,12 +213,15 @@ const MultipartFormParams = ({ item, collection }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="multipart-form"
columns={columns}
rows={params || []}
onChange={handleParamsChange}
defaultRow={defaultRow}
reorderable={true}
onReorder={handleParamDrag}
columnWidths={multipartFormWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('multipart-form', widths)}
/>
</StyledWrapper>
);

View File

@@ -1,15 +1,16 @@
import React, { useState, useCallback } from 'react';
import get from 'lodash/get';
import InfoTip from 'components/InfoTip';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import {
moveQueryParam,
updatePathParam,
setQueryParams
} from 'providers/ReduxStore/slices/collections';
import MultiLineEditor from 'components/MultiLineEditor';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import MultiLineEditor from 'components/MultiLineEditor';
import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
import BulkEditor from '../../BulkEditor';
@@ -17,12 +18,23 @@ import BulkEditor from '../../BulkEditor';
const QueryParams = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
const queryParams = params.filter((param) => param.type === 'query');
const pathParams = params.filter((param) => param.type === 'path');
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const queryParamsWidths = focusedTab?.tableColumnWidths?.['query-params'] || {};
const pathParamsWidths = focusedTab?.tableColumnWidths?.['path-params'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
@@ -138,12 +150,15 @@ const QueryParams = ({ item, collection }) => {
<div className="flex-1">
<div className="mb-3 title text-xs">Query</div>
<EditableTable
tableId="query-params"
columns={queryColumns}
rows={queryParams || []}
onChange={handleQueryParamsChange}
defaultRow={defaultQueryRow}
reorderable={true}
onReorder={handleQueryParamDrag}
columnWidths={queryParamsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('query-params', widths)}
/>
<div className="flex justify-end mt-2">
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
@@ -166,6 +181,7 @@ const QueryParams = ({ item, collection }) => {
</div>
{pathParams && pathParams.length > 0 ? (
<EditableTable
tableId="path-params"
columns={pathColumns}
rows={pathParams}
onChange={() => {}}
@@ -173,6 +189,8 @@ const QueryParams = ({ item, collection }) => {
showCheckbox={false}
showDelete={false}
showAddRow={false}
columnWidths={pathParamsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('path-params', widths)}
/>
) : (
<div className="title pr-2 py-3 mt-2 text-xs"></div>

View File

@@ -1,9 +1,10 @@
import React, { useState, useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { moveRequestHeader, setRequestHeaders } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import SingleLineEditor from 'components/SingleLineEditor';
import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
@@ -17,9 +18,19 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const RequestHeaders = ({ item, collection, addHeaderText }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const headersWidths = focusedTab?.tableColumnWidths?.['request-headers'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
@@ -123,6 +134,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="request-headers"
columns={columns}
rows={headers || []}
onChange={handleHeadersChange}
@@ -130,6 +142,8 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
getRowError={getRowError}
reorderable={true}
onReorder={handleHeaderDrag}
columnWidths={headersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('request-headers', widths)}
/>
<div className="flex justify-end mt-2">
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>

View File

@@ -1,8 +1,9 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { moveVar, setRequestVars } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import EditableTable from 'components/EditableTable';
@@ -13,6 +14,16 @@ import { variableNameRegex } from 'utils/common/regex';
const VarsTable = ({ item, collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const varsWidths = focusedTab?.tableColumnWidths?.['request-vars'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
@@ -85,6 +96,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="request-vars"
columns={columns}
rows={vars || []}
onChange={handleVarsChange}
@@ -92,6 +104,8 @@ const VarsTable = ({ item, collection, vars, varType }) => {
getRowError={getRowError}
reorderable={true}
onReorder={handleVarDrag}
columnWidths={varsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('request-vars', widths)}
/>
</StyledWrapper>
);

View File

@@ -9,6 +9,7 @@ import ResponsePane from 'components/ResponsePane';
import GrpcResponsePane from 'components/ResponsePane/GrpcResponsePane';
import { findItemInCollection } from 'utils/collections';
import { cancelRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateGqlDocsOpen } from 'providers/ReduxStore/slices/tabs';
import RequestNotFound from './RequestNotFound';
import QueryUrl from 'components/RequestPane/QueryUrl/index';
import GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index';
@@ -31,6 +32,7 @@ import WsQueryUrl from 'components/RequestPane/WsQueryUrl';
import WSRequestPane from 'components/RequestPane/WSRequestPane';
import WSResponsePane from 'components/ResponsePane/WsResponsePane';
import { useTabPaneBoundaries } from 'hooks/useTabPaneBoundaries/index';
import { ScopedPersistenceProvider } from 'hooks/usePersistedState/PersistedScopeProvider';
import ResponseExample from 'components/ResponseExample';
import WorkspaceOverview from 'components/WorkspaceHome/WorkspaceOverview';
import Preferences from 'components/Preferences';
@@ -92,18 +94,23 @@ const RequestTabPanel = () => {
const mainSectionRef = useRef(null);
const [schema, setSchema] = useState(null);
const [showGqlDocs, setShowGqlDocs] = useState(false);
// Get gqlDocsOpen from Redux for persistence across tab switches
const showGqlDocs = focusedTab?.gqlDocsOpen || false;
const onSchemaLoad = useCallback((schema) => setSchema(schema), []);
const toggleDocs = useCallback(() => setShowGqlDocs((prev) => !prev), []);
const toggleDocs = useCallback(() => {
dispatch(updateGqlDocsOpen({ uid: activeTabUid, gqlDocsOpen: !showGqlDocs }));
}, [dispatch, activeTabUid, showGqlDocs]);
const handleGqlClickReference = useCallback((reference) => {
if (docExplorerRef.current) {
docExplorerRef.current.showDocForReference(reference);
}
if (!showGqlDocs) {
setShowGqlDocs(true);
dispatch(updateGqlDocsOpen({ uid: activeTabUid, gqlDocsOpen: true }));
}
}, []);
}, [dispatch, activeTabUid, showGqlDocs]);
const handleMouseMove = useCallback((e) => {
if (!draggingRef.current || !mainSectionRef.current) return;
@@ -353,50 +360,52 @@ const RequestTabPanel = () => {
};
return (
<StyledWrapper
className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${
isVerticalLayout ? 'vertical-layout' : ''
}`}
>
<div className="pt-3 pb-3 px-4">
{renderQueryUrl()}
</div>
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative overflow-auto`}>
<section className="request-pane">
<ScopedPersistenceProvider scope={focusedTab.uid}>
<StyledWrapper
className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${
isVerticalLayout ? 'vertical-layout' : ''
}`}
>
<div className="pt-3 pb-3 px-4">
{renderQueryUrl()}
</div>
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative overflow-auto`}>
<section className="request-pane">
<div
className="px-4 h-full"
style={requestPaneStyle}
>
{renderRequestPane()}
</div>
</section>
<div
className="px-4 h-full"
style={requestPaneStyle}
className="dragbar-wrapper"
onDoubleClick={(e) => {
e.preventDefault();
resetPaneBoundaries();
}}
onMouseDown={handleDragbarMouseDown}
>
{renderRequestPane()}
<div className="dragbar-handle" />
</div>
<section className="response-pane flex-grow overflow-x-auto">
{renderResponsePane()}
</section>
</section>
<div
className="dragbar-wrapper"
onDoubleClick={(e) => {
e.preventDefault();
resetPaneBoundaries();
}}
onMouseDown={handleDragbarMouseDown}
>
<div className="dragbar-handle" />
</div>
<section className="response-pane flex-grow overflow-x-auto">
{renderResponsePane()}
</section>
</section>
{item.type === 'graphql-request' ? (
<div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>
<DocExplorer schema={schema} ref={(r) => (docExplorerRef.current = r)}>
<button className="mr-2" onClick={toggleDocs} aria-label="Close Documentation Explorer">
{'\u2715'}
</button>
</DocExplorer>
</div>
) : null}
</StyledWrapper>
{item.type === 'graphql-request' ? (
<div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>
<DocExplorer schema={schema} ref={(r) => (docExplorerRef.current = r)}>
<button className="mr-2" onClick={toggleDocs} aria-label="Close Documentation Explorer">
{'\u2715'}
</button>
</DocExplorer>
</div>
) : null}
</StyledWrapper>
</ScopedPersistenceProvider>
);
};

View File

@@ -1,8 +1,9 @@
import React, { useState, useMemo, useCallback } from 'react';
import { get } from 'lodash';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { updateResponseExampleFileBodyParams } from 'providers/ReduxStore/slices/collections';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import mime from 'mime-types';
import path from 'utils/common/path';
import EditableTable from 'components/EditableTable';
@@ -14,6 +15,16 @@ import RadioButton from 'components/RadioButton';
const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = false }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const fileBodyWidths = focusedTab?.tableColumnWidths?.['example-file-body'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
// Get file data from the specific example
const params = useMemo(() => {
@@ -180,6 +191,9 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
return (
<StyledWrapper className="w-full mt-4">
<EditableTable
tableId="example-file-body"
columnWidths={fileBodyWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('example-file-body', widths)}
columns={columns}
rows={params || []}
onChange={handleParamsChange}

View File

@@ -1,8 +1,9 @@
import React, { useMemo, useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { updateResponseExampleFormUrlEncodedParams } from 'providers/ReduxStore/slices/collections';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import EditableTable from 'components/EditableTable';
import MultiLineEditor from 'components/MultiLineEditor';
import StyledWrapper from './StyledWrapper';
@@ -10,6 +11,16 @@ import StyledWrapper from './StyledWrapper';
const ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, editMode = false }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const formUrlEncodedWidths = focusedTab?.tableColumnWidths?.['example-form-url-encoded'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const params = useMemo(() => {
return item.draft
@@ -87,6 +98,9 @@ const ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, edi
return (
<StyledWrapper className="w-full mt-4">
<EditableTable
tableId="example-form-url-encoded"
columnWidths={formUrlEncodedWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('example-form-url-encoded', widths)}
columns={columns}
rows={params || []}
onChange={handleParamsChange}

View File

@@ -1,8 +1,9 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import get from 'lodash/get';
import { moveResponseExampleRequestHeader, setResponseExampleRequestHeaders } from 'providers/ReduxStore/slices/collections';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import EditableTable from 'components/EditableTable';
import SingleLineEditor from 'components/SingleLineEditor';
import BulkEditor from 'components/BulkEditor';
@@ -15,8 +16,18 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const exampleHeadersWidths = focusedTab?.tableColumnWidths?.['example-headers'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const headers = useMemo(() => {
return item.draft
? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.request?.headers || []
@@ -132,6 +143,9 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
<StyledWrapper className="w-full mt-4">
<div className="mb-3 title text-xs font-bold">Headers</div>
<EditableTable
tableId="example-headers"
columnWidths={exampleHeadersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('example-headers', widths)}
columns={columns}
rows={headers || []}
onChange={handleHeadersChange}

View File

@@ -1,10 +1,11 @@
import React, { useMemo, useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } 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 { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import mime from 'mime-types';
import path from 'utils/common/path';
import EditableTable from 'components/EditableTable';
@@ -16,6 +17,16 @@ import { isWindowsOS } from 'utils/common/platform';
const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, editMode = false }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const multipartFormWidths = focusedTab?.tableColumnWidths?.['example-multipart-form'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const params = useMemo(() => {
return item.draft
@@ -258,6 +269,9 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
return (
<StyledWrapper className="w-full mt-4">
<EditableTable
tableId="example-multipart-form"
columnWidths={multipartFormWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('example-multipart-form', widths)}
columns={columns}
rows={params || []}
onChange={handleParamsChange}

View File

@@ -1,8 +1,9 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import get from 'lodash/get';
import { moveResponseExampleParam, setResponseExampleParams } from 'providers/ReduxStore/slices/collections';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import EditableTable from 'components/EditableTable';
import SingleLineEditor from 'components/SingleLineEditor';
import BulkEditor from 'components/BulkEditor';
@@ -12,8 +13,19 @@ import StyledWrapper from './StyledWrapper';
const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const exampleQueryParamsWidths = focusedTab?.tableColumnWidths?.['example-query-params'] || {};
const examplePathParamsWidths = focusedTab?.tableColumnWidths?.['example-path-params'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const params = useMemo(() => {
return item.draft
? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.request?.params || []
@@ -185,6 +197,7 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
<StyledWrapper className="w-full mt-4">
<div className="mb-3 title text-xs font-bold">Query parameters</div>
<EditableTable
tableId="example-query-params"
columns={queryColumns}
rows={queryParams || []}
onChange={handleQueryParamsChange}
@@ -194,6 +207,8 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
showAddRow={editMode}
showDelete={editMode}
disableCheckbox={!editMode}
columnWidths={exampleQueryParamsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('example-query-params', widths)}
/>
{editMode && (
<div className="flex justify-end mt-2">
@@ -221,6 +236,7 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
</InfoTip>
</div>
<EditableTable
tableId="example-path-params"
columns={pathColumns}
rows={pathParams}
onChange={handlePathParamsChange}
@@ -229,6 +245,8 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
showDelete={false}
showAddRow={false}
reorderable={false}
columnWidths={examplePathParamsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('example-path-params', widths)}
/>
</>
)}

View File

@@ -1,8 +1,9 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import get from 'lodash/get';
import { moveResponseExampleHeader, setResponseExampleHeaders, updateResponseExampleResponse } from 'providers/ReduxStore/slices/collections';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import { getBodyType } from 'utils/responseBodyProcessor';
import EditableTable from 'components/EditableTable';
import SingleLineEditor from 'components/SingleLineEditor';
@@ -16,8 +17,18 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const responseHeadersWidths = focusedTab?.tableColumnWidths?.['example-response-headers'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const headers = useMemo(() => {
return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.response?.headers || [] : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.response?.headers || [];
}, [item, exampleUid]);
@@ -170,6 +181,9 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
return (
<StyledWrapper className="w-full px-4">
<EditableTable
tableId="example-response-headers"
columnWidths={responseHeadersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('example-response-headers', widths)}
columns={columns}
rows={headers || []}
onChange={handleHeadersChange}

View File

@@ -82,7 +82,7 @@ const GrpcResponsePane = ({ item, collection }) => {
return <ResponseTrailers trailers={response.trailers} />;
}
case 'timeline': {
return <Timeline collection={collection} item={item} />;
return <Timeline collection={collection} item={item} activeTabUid={activeTabUid} />;
}
default: {
return <div>404 | Not found</div>;
@@ -152,7 +152,7 @@ const GrpcResponsePane = ({ item, collection }) => {
{isLoading ? <Overlay item={item} collection={collection} /> : null}
{!item?.response ? (
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
<Timeline collection={collection} item={item} />
<Timeline collection={collection} item={item} activeTabUid={activeTabUid} />
) : null
) : (
<>{getTabPanel(focusedTab.responsePaneTab)}</>

View File

@@ -1,21 +1,29 @@
import { IconFilter, IconX } from '@tabler/icons';
import React, { useMemo } from 'react';
import { useRef } from 'react';
import { useState } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import { Tooltip as ReactInfotip } from 'react-tooltip';
const QueryResultFilter = ({ filter, onChange, mode }) => {
const QueryResultFilter = ({ filter, filterExpanded, onChange, onExpandChange, mode }) => {
const inputRef = useRef(null);
const [isExpanded, toggleExpand] = useState(false);
const [isExpanded, setIsExpanded] = useState(filterExpanded || false);
const handleFilterClick = () => {
// Toggle filter search bar
toggleExpand(!isExpanded);
// Reset filter search input
onChange({ target: { value: '' } });
// Reset input value
if (inputRef?.current) {
inputRef.current.value = '';
const newExpanded = !isExpanded;
setIsExpanded(newExpanded);
// Reset filter search input when closing
if (!newExpanded) {
onChange('');
if (inputRef?.current) {
inputRef.current.value = '';
}
}
if (onExpandChange) {
onExpandChange(newExpanded);
}
};
const handleInputChange = (e) => {
if (onChange) {
onChange(e.target.value);
}
};
@@ -53,6 +61,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
type="text"
name="response-filter"
id="response-filter"
value={filter || ''}
placeholder={placeholderText}
autoComplete="off"
autoCorrect="off"
@@ -61,7 +70,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
className={`block ml-14 p-2 py-1 transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
isExpanded ? 'w-full opacity-100 pointer-events-auto' : 'w-[0] opacity-0'
}`}
onChange={onChange}
onChange={handleInputChange}
/>
<div className="text-gray-500 cursor-pointer pointer-events-auto" id="request-filter-icon" onClick={handleFilterClick}>
{isExpanded ? <IconX size={20} strokeWidth={1.5} /> : <IconFilter size={20} strokeWidth={1.5} />}

View File

@@ -98,10 +98,13 @@ const QueryResult = ({
headers,
error,
selectedFormat, // one of the options in PREVIEW_FORMAT_OPTIONS
selectedTab // 'editor' or 'preview'
selectedTab, // 'editor' or 'preview'
filter,
filterExpanded,
onFilterChange,
onFilterExpandChange
}) => {
const contentType = getContentType(headers);
const [filter, setFilter] = useState(null);
const [showLargeResponse, setShowLargeResponse] = useState(false);
const { displayedTheme } = useTheme();
@@ -134,9 +137,11 @@ const QueryResult = ({
[data, dataBuffer, selectedFormat, filter, isLargeResponse, showLargeResponse]
);
const debouncedResultFilterOnChange = debounce((e) => {
setFilter(e.target.value);
}, 250);
const handleFilterChange = (value) => {
if (onFilterChange) {
onFilterChange(value);
}
};
const previewMode = useMemo(() => {
// Derive preview mode based on selected format
@@ -213,7 +218,13 @@ const QueryResult = ({
/>
</div>
{queryFilterEnabled && (
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={codeMirrorMode} />
<QueryResultFilter
filter={filter}
filterExpanded={filterExpanded}
onChange={handleFilterChange}
onExpandChange={onFilterExpandChange}
mode={codeMirrorMode}
/>
)}
</div>
</div>

View File

@@ -13,6 +13,7 @@ import {
IconSend
} from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import { usePersistedState } from 'hooks/usePersistedState/index';
// Event type display names
const EventTypeNames = {
@@ -26,9 +27,12 @@ const EventTypeNames = {
cancel: 'Cancelled'
};
const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData, item }) => {
const [isCollapsed, setIsCollapsed] = useState(true);
const toggleCollapse = () => setIsCollapsed((prev) => !prev);
const GrpcTimelineItem = ({ timestamp, request, response, eventType, collection, eventData, item }) => {
const [isExpanded, onToggleExpand] = usePersistedState({
key: `grpc-timeline-${timestamp}`,
default: false
});
const toggleCollapse = () => onToggleExpand(!isExpanded);
// Use requestSent if available, otherwise fall back to request
const effectiveRequest = item.requestSent || request || item.request || {};
@@ -247,7 +251,7 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
return (
<StyledWrapper className={`${eventClass} pl-1 mb-2`}>
<div className="event-header" onClick={toggleCollapse}>
{isCollapsed ? <IconChevronRight size={16} strokeWidth={1.5} /> : <IconChevronDown size={16} strokeWidth={1.5} />}
{!isExpanded ? <IconChevronRight size={16} strokeWidth={1.5} /> : <IconChevronDown size={16} strokeWidth={1.5} />}
<div className="event-icon-container">
{eventIcon}
</div>
@@ -272,7 +276,7 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
<div className="url-text">{url}</div>
{/* Expanded content - only show for non-status items */}
{!isCollapsed && renderEventContent()}
{isExpanded && renderEventContent()}
</StyledWrapper>
);
};

View File

@@ -7,10 +7,14 @@ import Method from './Common/Method/index';
import Status from './Common/Status/index';
import { RelativeTime } from './Common/Time/index';
import StyledWrapper from './StyledWrapper';
import { usePersistedState } from 'hooks/usePersistedState/index';
const TimelineItem = ({ timestamp, request, response, item, collection, isOauth2, hideTimestamp = false }) => {
const { theme } = useTheme();
const [isCollapsed, _toggleCollapse] = useState(false);
const [isCollapsed, _toggleCollapse] = usePersistedState({
key: `timeline-${timestamp}`,
default: false
});
const [activeTab, setActiveTab] = useState('request');
const toggleCollapse = () => _toggleCollapse((prev) => !prev);
const { method, status, statusCode, statusText, url = '' } = request || {};

View File

@@ -43,7 +43,7 @@ const getEffectiveAuthSource = (collection, item) => {
return effectiveSource;
};
const Timeline = ({ collection, item }) => {
const Timeline = ({ collection, item, activeTabUid }) => {
// Get the effective auth source if auth mode is inherit
const authSource = getEffectiveAuthSource(collection, item);
const isGrpcRequest = item.type === 'grpc-request' || item.type === 'ws-request';

View File

@@ -71,7 +71,7 @@ const WSResponsePane = ({ item, collection }) => {
return <WSResponseHeaders response={response} />;
}
case 'timeline': {
return <Timeline collection={collection} item={item} />;
return <Timeline collection={collection} item={item} activeTabUid={activeTabUid} />;
}
default: {
return <div>404 | Not found</div>;
@@ -141,7 +141,7 @@ const WSResponsePane = ({ item, collection }) => {
{isLoading ? <Overlay item={item} collection={collection} /> : null}
{!item?.response ? (
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
<Timeline collection={collection} item={item} />
<Timeline collection={collection} item={item} activeTabUid={activeTabUid} />
) : null
) : (
<>{getTabPanel(focusedTab.responsePaneTab)}</>

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import find from 'lodash/find';
import { useDispatch, useSelector } from 'react-redux';
import { updateResponsePaneTab, updateResponseFormat, updateResponseViewTab } from 'providers/ReduxStore/slices/tabs';
import { updateResponsePaneTab, updateResponseFormat, updateResponseViewTab, updateResponseFilter, updateResponseFilterExpanded } from 'providers/ReduxStore/slices/tabs';
import QueryResult from './QueryResult';
import Overlay from './Overlay';
import Placeholder from './Placeholder';
@@ -168,6 +168,10 @@ const ResponsePane = ({ item, collection }) => {
key={item.filename}
selectedFormat={selectedFormat}
selectedTab={selectedViewTab}
filter={focusedTab?.responseFilter}
filterExpanded={focusedTab?.responseFilterExpanded}
onFilterChange={(value) => dispatch(updateResponseFilter({ uid: activeTabUid, responseFilter: value }))}
onFilterExpandChange={(expanded) => dispatch(updateResponseFilterExpanded({ uid: activeTabUid, responseFilterExpanded: expanded }))}
/>
);
}
@@ -175,7 +179,7 @@ const ResponsePane = ({ item, collection }) => {
return <ResponseHeaders headers={response.headers} />;
}
case 'timeline': {
return <Timeline collection={collection} item={item} />;
return <Timeline collection={collection} item={item} activeTabUid={activeTabUid} />;
}
case 'tests': {
return (
@@ -313,6 +317,7 @@ const ResponsePane = ({ item, collection }) => {
<Timeline
collection={collection}
item={item}
activeTabUid={activeTabUid}
/>
) : null
) : (

View File

@@ -40,6 +40,7 @@ const EnvironmentVariables = ({ environment, setIsModified, collection, searchQu
return (
<EnvironmentVariablesTable
key={environment?.uid}
environment={environment}
collection={collection}
onSave={handleSave}

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { ReactNode } from 'react';
import { createContext, useContext } from 'react';
export const ScopedPersistedContext = createContext<string>('');
export function usePersistenceScope(): string {
return useContext(ScopedPersistedContext);
}
export function ScopedPersistenceProvider({ scope, children }: { scope: string; children: ReactNode }) {
return <ScopedPersistedContext.Provider value={scope}>{children}</ScopedPersistedContext.Provider>;
}
export function clearPersistedScope(scope: string) {
const prefix = `persisted::${scope}::`;
Object.keys(localStorage)
.filter((k) => k.startsWith(prefix))
.forEach((k) => localStorage.removeItem(k));
}

View File

@@ -0,0 +1,48 @@
import type { Dispatch, SetStateAction } from 'react';
import { useCallback, useState, useEffect } from 'react';
import { usePersistenceScope } from './PersistedScopeProvider';
type Options<T> = {
key: string;
default: T;
};
export { ScopedPersistenceProvider as PersistedScopeProvider, clearPersistedScope } from './PersistedScopeProvider';
export function usePersistedState<T>(options: Options<T>): [T, Dispatch<SetStateAction<T>>] {
const scope = usePersistenceScope();
const storageKey = scope ? `persisted::${scope}::${options.key}` : options.key;
const [state, setState] = useState<T>(options.default ?? undefined);
useEffect(() => {
const raw = localStorage.getItem(storageKey);
const existingState = JSON.parse(raw);
if (existingState !== undefined) {
setState(existingState);
}
return;
}, [storageKey]);
const onSet = useCallback(
(value: T | ((prev: T) => T)) => {
let _next: T;
if (typeof value === 'function') {
setState((prev) => {
_next = (value as (prev: T) => T)(prev);
localStorage.setItem(storageKey, JSON.stringify(_next));
return _next;
});
} else {
_next = value;
setState(_next);
localStorage.setItem(storageKey, JSON.stringify(_next));
}
},
[storageKey]
);
return [state, onSet];
}

View File

@@ -90,6 +90,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import { updateSettingsSelectedTab } from './index';
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { getTabToFocusForCurrentWorkspace } from 'providers/ReduxStore/slices/workspaces/getTabToFocusForCurrentWorkspace';
import { clearPersistedScope } from 'hooks/usePersistedState/PersistedScopeProvider';
// generate a unique names
const generateUniqueName = (originalName, existingItems, isFolder) => {
@@ -3146,6 +3147,7 @@ export const closeTabs = ({ tabUids }) => async (dispatch, getState) => {
// Find transient items and group by temp directory before closing tabs
const transientByTempDir = {};
each(tabUids, (tabUid) => {
clearPersistedScope(tabUid);
for (const collection of collections) {
const item = findItemInCollection(collection, tabUid);
if (item?.isTransient && item.pathname) {

View File

@@ -88,7 +88,12 @@ export const tabsSlice = createSlice({
responsePaneScrollPosition: null,
responseFormat: null,
responseViewTab: null,
responseFilter: null,
responseFilterExpanded: false,
gqlDocsOpen: false,
tableColumnWidths: {},
scriptPaneTab: null,
docsEditing: false,
type: type || 'request',
...(uid ? { folderUid: uid } : {}),
preview: preview !== undefined
@@ -182,6 +187,44 @@ export const tabsSlice = createSlice({
tab.responseViewTab = action.payload.responseViewTab;
}
},
updateResponseFilter: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if (tab) {
tab.responseFilter = action.payload.responseFilter;
}
},
updateResponseFilterExpanded: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if (tab) {
tab.responseFilterExpanded = action.payload.responseFilterExpanded;
}
},
updateDocsEditing: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if (tab) {
tab.docsEditing = action.payload.docsEditing;
}
},
updateGqlDocsOpen: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if (tab) {
tab.gqlDocsOpen = action.payload.gqlDocsOpen;
}
},
updateTableColumnWidths: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
if (tab) {
if (!tab.tableColumnWidths) {
tab.tableColumnWidths = {};
}
tab.tableColumnWidths[action.payload.tableId] = action.payload.widths;
}
},
updateScriptPaneTab: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
@@ -283,6 +326,11 @@ export const {
updateRequestBodyScrollPosition,
updateResponseFormat,
updateResponseViewTab,
updateResponseFilter,
updateResponseFilterExpanded,
updateDocsEditing,
updateGqlDocsOpen,
updateTableColumnWidths,
updateScriptPaneTab,
closeTabs,
closeAllCollectionTabs,