feat: add resizable columns to table#6843
Conversation
WalkthroughExtracts a new virtualized EnvironmentVariablesTable component with per-row editing, draft/save/reset flows and validation, replaces inline EnvironmentVariables implementations to delegate to the new table, and adds column-resize UI/logic and supporting styling to EditableTable and related wrappers. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Table as EnvironmentVariablesTable
participant Validator as Formik/Yup
participant API as onSave handler
participant Store as Parent / Draft Store
User->>Table: edit cell / drag resize
Table->>Table: update local state (drafts, columnWidths)
Table->>Store: onDraftChange (persist draft)
Note right of Table: ResizeObserver aligns handles and height
User->>Table: click Save
Table->>Validator: validate rows
Validator-->>Table: validation result
alt valid
Table->>API: onSave(changes)
API-->>Table: success / error
Table->>Store: onDraftClear
Table-->>User: show toast / reset trailing row
else invalid
Table-->>User: show validation errors
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js (1)
30-38: Potential regex injection if variable names contain special characters.If
varNamecontains regex special characters (e.g.,.,*,+,$), the dynamically constructed RegExp could behave unexpectedly or throw errors.Proposed fix: escape regex special characters
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const checkSensitiveField = (obj, fieldPath) => { const value = get(obj, fieldPath); if (typeof value === 'string') { varNames.forEach((varName) => { - if (new RegExp(`\\{\\{\\s*${varName}\\s*\\}\\}`).test(value)) { + if (new RegExp(`\\{\\{\\s*${escapeRegExp(varName)}\\s*\\}\\}`).test(value)) { result[varName] = true; } }); } };
🧹 Nitpick comments (5)
packages/bruno-app/src/components/EnvironmentVariablesTable/index.js (4)
259-298: Stale closure risk withformik.valuesin useCallback dependency.Using
formik.valuesdirectly in the dependency array means the callback recreates on every value change, but there's still a subtle timing issue whereformik.valuesinside the callback could be stale if the callback is invoked during a render cycle.Consider using
formik.setValueswith a functional updater or accessing values via ref for more predictable behavior.Alternative approach using functional update
const handleRemoveVar = useCallback( (id) => { - const currentValues = formik.values; - - if (!currentValues || currentValues.length === 0) { - return; - } - - const lastRow = currentValues[currentValues.length - 1]; + formik.setValues((currentValues) => { + if (!currentValues || currentValues.length === 0) { + return currentValues; + } + + const lastRow = currentValues[currentValues.length - 1]; // ... rest of logic, returning newValues + }); }, - [formik.values] + [formik] );
179-200: Missingformikin effect dependencies.The effect calls
formik.setValuesbut doesn't includeformikin the dependency array. Whileformikis typically stable across renders, this could cause linter warnings.- }, [environment.uid, hasDraftForThisEnv, draft?.variables]); + }, [environment.uid, hasDraftForThisEnv, draft?.variables, formik]);
236-257: Consider extractingErrorMessageoutside the component.Defining
ErrorMessageinside the component means it's recreated on every render. While functional, extracting it or memoizing it would improve performance slightly for large tables.
110-114: Minor: preferconstfor_collection.The variable is reassigned via property assignment (
_collection.globalEnvironmentVariables = ...), but the binding itself doesn't change. Useconstfor consistency.- let _collection = collection ? cloneDeep(collection) : {}; + const _collection = collection ? cloneDeep(collection) : {};packages/bruno-app/src/components/EditableTable/index.js (1)
28-36: Sync column widths whencolumnschanges.
State is only initialized once; if columns are added/removed or re-keyed later, widths can drift or become stale. Consider syncing oncolumnsupdates while preserving existing widths.♻️ Suggested sync on columns change
const [columnWidths, setColumnWidths] = useState(() => { const initialWidths = {}; columns.forEach((col) => { initialWidths[col.key] = col.width || 'auto'; }); return initialWidths; }); + useEffect(() => { + setColumnWidths((prev) => { + const next = {}; + columns.forEach((col) => { + next[col.key] = prev[col.key] ?? col.width ?? 'auto'; + }); + return next; + }); + }, [columns]);
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/EditableTable/index.js`:
- Around line 38-101: The document-level mousemove and mouseup listeners added
in handleResizeStart can persist if the component unmounts during a drag; add
cleanup logic to remove them on unmount. Implement a useEffect that tracks the
current handlers (or a ref to whether resizing is active and the handler
functions handleMouseMove/handleMouseUp created by handleResizeStart) and
removes document.removeEventListener('mousemove', handleMouseMove) and
document.removeEventListener('mouseup', handleMouseUp) in its cleanup; also
ensure handleResizeStart sets/clears a ref (or state) for the active handlers so
the effect can remove the exact functions, and keep existing calls to
setResizing and setColumnWidths intact (use symbols: handleResizeStart,
handleMouseMove, handleMouseUp, setResizing, setColumnWidths, tableRef).
- Around line 103-118: Add a Jest-friendly ResizeObserver mock to the test setup
so components using the useEffect that references ResizeObserver (in
EditableTable's useEffect that reads tableRef and calls new ResizeObserver(...))
don't fail under jsdom; update jest.setup.js to set global.ResizeObserver to a
jest.fn() mock implementation with observe, unobserve and disconnect methods
(each a jest.fn()) so ResizeObserver.observe/unobserve/disconnect calls in
components like EditableTable, WorkspaceTabs, RequestTabs, and
Console/TerminalTab are no-ops during tests.
🧹 Nitpick comments (1)
packages/bruno-app/src/components/EditableTable/index.js (1)
120-122: Add JSDoc for the resize helper.This helper is a new abstraction and would benefit from a brief JSDoc.
As per coding guidelines: Add JSDoc comments to abstractions for additional details.
📝 Example JSDoc
+ /** + * Resolves a column width from resized state or column config. + * `@param` {{ key: string, width?: string }} column + * `@returns` {string} + */ const getColumnWidth = useCallback((column) => { return columnWidths[column.key] || column.width || 'auto'; }, [columnWidths]);
| const handleResizeStart = useCallback((e, columnKey) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
|
|
||
| const currentCell = e.target.closest('td'); | ||
| const nextCell = currentCell?.nextElementSibling; | ||
| if (!currentCell || !nextCell) return; | ||
|
|
||
| const columnIndex = columns.findIndex((col) => col.key === columnKey); | ||
| if (columnIndex >= columns.length - 1) return; | ||
|
|
||
| const startX = e.clientX; | ||
| const startWidth = currentCell.offsetWidth; | ||
| const nextColumnKey = columns[columnIndex + 1].key; | ||
| const nextColumnStartWidth = nextCell.offsetWidth; | ||
|
|
||
| setResizing(columnKey); | ||
|
|
||
| const handleMouseMove = (moveEvent) => { | ||
| const diff = moveEvent.clientX - startX; | ||
| const maxGrow = nextColumnStartWidth - MIN_COLUMN_WIDTH; | ||
| const maxShrink = startWidth - MIN_COLUMN_WIDTH; | ||
| const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff)); | ||
|
|
||
| setColumnWidths((prev) => ({ | ||
| ...prev, | ||
| [columnKey]: `${startWidth + clampedDiff}px`, | ||
| [nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px` | ||
| })); | ||
| }; | ||
|
|
||
| const handleMouseUp = () => { | ||
| // Convert pixel widths to percentages for responsive scaling | ||
| const table = tableRef.current?.querySelector('table'); | ||
| if (table) { | ||
| const tableWidth = table.offsetWidth; | ||
| const headerCells = table.querySelectorAll('thead td'); | ||
| const newWidths = {}; | ||
|
|
||
| headerCells.forEach((cell, cellIndex) => { | ||
| const checkboxOffset = showCheckbox ? 1 : 0; | ||
| const colIndex = cellIndex - checkboxOffset; | ||
|
|
||
| if (colIndex >= 0 && colIndex < columns.length) { | ||
| const colKey = columns[colIndex]?.key; | ||
| if (colKey) { | ||
| const percentage = (cell.offsetWidth / tableWidth) * 100; | ||
| newWidths[colKey] = `${percentage}%`; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| if (Object.keys(newWidths).length > 0) { | ||
| setColumnWidths((prev) => ({ ...prev, ...newWidths })); | ||
| } | ||
| } | ||
| setResizing(null); | ||
| document.removeEventListener('mousemove', handleMouseMove); | ||
| document.removeEventListener('mouseup', handleMouseUp); | ||
| }; | ||
|
|
||
| document.addEventListener('mousemove', handleMouseMove); | ||
| document.addEventListener('mouseup', handleMouseUp); | ||
| }, [columns, showCheckbox]); |
There was a problem hiding this comment.
Clean up document-level mouse listeners on unmount.
If the component unmounts mid-resize, the document listeners can linger and keep firing state updates. Track and clean them up on unmount.
🧹 Suggested cleanup for resize listeners
@@
const tableRef = useRef(null);
const emptyRowUidRef = useRef(null);
+ const resizeListenersRef = useRef(null);
@@
const handleResizeStart = useCallback((e, columnKey) => {
e.preventDefault();
e.stopPropagation();
+ if (resizeListenersRef.current) {
+ resizeListenersRef.current();
+ }
+
const currentCell = e.target.closest('td');
@@
const handleMouseUp = () => {
// Convert pixel widths to percentages for responsive scaling
@@
setResizing(null);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
+ resizeListenersRef.current = null;
};
+ resizeListenersRef.current = () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ resizeListenersRef.current = null;
+ };
+
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, [columns, showCheckbox]);
+
+ useEffect(() => {
+ return () => {
+ if (resizeListenersRef.current) {
+ resizeListenersRef.current();
+ }
+ };
+ }, []);🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/EditableTable/index.js` around lines 38 -
101, The document-level mousemove and mouseup listeners added in
handleResizeStart can persist if the component unmounts during a drag; add
cleanup logic to remove them on unmount. Implement a useEffect that tracks the
current handlers (or a ref to whether resizing is active and the handler
functions handleMouseMove/handleMouseUp created by handleResizeStart) and
removes document.removeEventListener('mousemove', handleMouseMove) and
document.removeEventListener('mouseup', handleMouseUp) in its cleanup; also
ensure handleResizeStart sets/clears a ref (or state) for the active handlers so
the effect can remove the exact functions, and keep existing calls to
setResizing and setColumnWidths intact (use symbols: handleResizeStart,
handleMouseMove, handleMouseUp, setResizing, setColumnWidths, tableRef).
| // Track table height for resize handles | ||
| useEffect(() => { | ||
| const table = tableRef.current?.querySelector('table'); | ||
| if (!table) return; | ||
|
|
||
| const updateHeight = () => { | ||
| setTableHeight(table.offsetHeight); | ||
| }; | ||
|
|
||
| updateHeight(); | ||
|
|
||
| const resizeObserver = new ResizeObserver(updateHeight); | ||
| resizeObserver.observe(table); | ||
|
|
||
| return () => resizeObserver.disconnect(); | ||
| }, [rows.length]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for ResizeObserver references, polyfills, and stubs
rg -n 'ResizeObserver' --type js --type jsonRepository: usebruno/bruno
Length of output: 801
🏁 Script executed:
#!/bin/bash
# Check for jest/vitest configuration and test setup files
find . -maxdepth 2 -type f \( -name 'jest.config.*' -o -name 'vitest.config.*' -o -name 'setupTests.*' -o -name 'setup.js' \) 2>/dev/null | head -20Repository: usebruno/bruno
Length of output: 76
🏁 Script executed:
#!/bin/bash
# Check package.json for ResizeObserver polyfills
cat package.json | jq '.dependencies, .devDependencies' 2>/dev/null | grep -i 'resize\|polyfill'Repository: usebruno/bruno
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Look for test files related to EditableTable
find . -path '*/test*' -o -path '*/spec*' | grep -i 'editabletable' 2>/dev/nullRepository: usebruno/bruno
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Read the setup.js file
cat ./scripts/setup.jsRepository: usebruno/bruno
Length of output: 3708
🏁 Script executed:
#!/bin/bash
# Find all jest/vitest config files more broadly
find . -name 'jest.config*' -o -name 'vitest.config*' -o -name '.jestrc*' 2>/dev/nullRepository: usebruno/bruno
Length of output: 296
🏁 Script executed:
#!/bin/bash
# Check for test setup or setupFilesAfterEnv in package.json
cat package.json | jq '.jest, .vitest' 2>/dev/nullRepository: usebruno/bruno
Length of output: 67
🏁 Script executed:
#!/bin/bash
# Look for EditableTable in test files
find . -type f \( -name '*.test.*' -o -name '*.spec.*' \) -exec grep -l 'EditableTable' {} \; 2>/dev/nullRepository: usebruno/bruno
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Search for ResizeObserver mocks or stubs throughout the codebase
rg -n -i 'mock.*resizeobserver\|resizeobserver.*mock\|resizeobserver.*stub' --type js --type tsRepository: usebruno/bruno
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Check the jest.config.js for bruno-app
cat ./packages/bruno-app/jest.config.jsRepository: usebruno/bruno
Length of output: 993
🏁 Script executed:
#!/bin/bash
# Check if there are any setupFiles configured and read them
find ./packages/bruno-app -name 'setupTests*' -o -name 'setup.js' -o -name 'setup.ts' 2>/dev/null | head -10Repository: usebruno/bruno
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Search for jest configuration inline in package.json for bruno-app
cat ./packages/bruno-app/package.json | jq '.jest' 2>/dev/nullRepository: usebruno/bruno
Length of output: 62
🏁 Script executed:
#!/bin/bash
# Read the jest.setup.js file
cat ./packages/bruno-app/jest.setup.jsRepository: usebruno/bruno
Length of output: 224
🏁 Script executed:
#!/bin/bash
# Search more broadly for tests that might use EditableTable component
rg -n 'EditableTable' --type js --type ts --type jsx --type tsxRepository: usebruno/bruno
Length of output: 85
🏁 Script executed:
#!/bin/bash
# Check if there are any test directories in bruno-app
find ./packages/bruno-app -type d -name '__tests__' -o -name 'tests' -o -type f -name '*.test.*' -o -name '*.spec.*' | head -20Repository: usebruno/bruno
Length of output: 1460
🏁 Script executed:
#!/bin/bash
# Search for EditableTable in all test files
rg -n 'EditableTable' --type js --type tsRepository: usebruno/bruno
Length of output: 5313
🏁 Script executed:
#!/bin/bash
# Check if ResizeObserver is used elsewhere and search for any polyfills
rg -n 'ResizeObserver' --type js --type ts -A 3 -B 1Repository: usebruno/bruno
Length of output: 2930
Add ResizeObserver polyfill to jest.setup.js.
ResizeObserver is not available in jsdom by default. Tests mounting EditableTable (and other components using ResizeObserver like WorkspaceTabs, RequestTabs, or Console/TerminalTab) will fail without a mock. Add a polyfill to jest.setup.js:
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}));🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/EditableTable/index.js` around lines 103 -
118, Add a Jest-friendly ResizeObserver mock to the test setup so components
using the useEffect that references ResizeObserver (in EditableTable's useEffect
that reads tableRef and calls new ResizeObserver(...)) don't fail under jsdom;
update jest.setup.js to set global.ResizeObserver to a jest.fn() mock
implementation with observe, unobserve and disconnect methods (each a jest.fn())
so ResizeObserver.observe/unobserve/disconnect calls in components like
EditableTable, WorkspaceTabs, RequestTabs, and Console/TerminalTab are no-ops
during tests.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/EnvironmentVariablesTable/index.js`:
- Around line 120-140: The Yup schema is using a non-existent Formik context
variable `$isLastRow`, so remove that conditional and let your existing custom
validate function handle last-row logic; update the `validationSchema` in
EnvironmentVariablesTable (the `name` field) to a straightforward validator like
`Yup.string().required('Name cannot be empty').matches(variableNameRegex, 'Name
contains invalid characters...').trim()` (i.e., delete the `.when('$isLastRow',
...)` block referencing `$isLastRow` and keep the `variableNameRegex` check),
leaving the custom `validate` function as the place that permits an optional
last empty row.
- Around line 499-501: The delete button using IconTrash lacks an accessible
label; update the button element (the one calling handleRemoveVar(variable.uid)
and rendering IconTrash) to include an accessible name such as aria-label or
title (for example aria-label={`Delete ${variable.key || 'variable'}`}), and
ensure the element has type="button" so it’s announced properly by assistive
tech; alternatively include visually hidden text inside the button that
describes the action (e.g., "Delete [variable name]") so screen readers can
convey its purpose.
- Around line 52-87: The handleResizeStart listener pair
(handleMouseMove/handleMouseUp) can remain attached if the component unmounts
during a drag; modify handleResizeStart to register those handlers onto a ref
(e.g., resizeListenersRef) and add a useEffect with a cleanup that, on unmount,
removes any registered document 'mousemove' and 'mouseup' listeners and clears
the ref; ensure handleMouseUp also clears the ref and calls setResizing(null)
and that setColumnWidths is used as before so no leftover listeners reference
stale state.
🧹 Nitpick comments (2)
packages/bruno-app/src/components/EnvironmentVariablesTable/index.js (2)
222-243: Inner componentErrorMessagerecreated on every render.Defining a component inside the render body creates a new function reference each render, causing React to unmount/remount the component unnecessarily. Consider memoizing with
useCallbackor extracting it outside and passingformikas a prop.♻️ Proposed fix using useCallback
- const ErrorMessage = ({ name, index }) => { - const meta = formik.getFieldMeta(name); - const id = `error-${name}-${index}`; - - const isLastRow = index === formik.values.length - 1; - const variable = formik.values[index]; - const isEmptyRow = !variable?.name || variable.name.trim() === ''; - - if (isLastRow && isEmptyRow) { - return null; - } - - if (!meta.error || !meta.touched) { - return null; - } - return ( - <span> - <IconAlertCircle id={id} className="text-red-600 cursor-pointer" size={20} /> - <Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} /> - </span> - ); - }; + const renderErrorMessage = useCallback((name, index) => { + const meta = formik.getFieldMeta(name); + const id = `error-${name}-${index}`; + + const isLastRow = index === formik.values.length - 1; + const variable = formik.values[index]; + const isEmptyRow = !variable?.name || variable.name.trim() === ''; + + if ((isLastRow && isEmptyRow) || !meta.error || !meta.touched) { + return null; + } + return ( + <span> + <IconAlertCircle id={id} className="text-red-600 cursor-pointer" size={20} /> + <Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} /> + </span> + ); + }, [formik]);Then in JSX:
{renderErrorMessage(${index}.name, index)}
299-301:setTimeout(..., 0)workaround to defer state update.Using
setTimeoutwith 0ms to avoid updating during render works but is fragile. Consider using a ref to track pending additions and applying them in auseEffect:♻️ Alternative approach
+ const pendingNewRowRef = useRef(false); + + useEffect(() => { + if (pendingNewRowRef.current) { + pendingNewRowRef.current = false; + const newVariable = { + uid: uuid(), + name: '', + value: '', + type: 'text', + secret: false, + enabled: true + }; + formik.setFieldValue(formik.values.length, newVariable, false); + } + }, [formik.values.length]); const handleNameChange = (index, e) => { formik.handleChange(e); const isLastRow = index === formik.values.length - 1; if (isLastRow) { - const newVariable = { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - }; - setTimeout(() => { - formik.setFieldValue(formik.values.length, newVariable, false); - }, 0); + pendingNewRowRef.current = true; } };
| const handleResizeStart = useCallback((e, columnKey) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
|
|
||
| const currentCell = e.target.closest('td'); | ||
| const nextCell = currentCell?.nextElementSibling; | ||
| if (!currentCell || !nextCell) return; | ||
|
|
||
| const startX = e.clientX; | ||
| const startWidth = currentCell.offsetWidth; | ||
| const nextColumnKey = 'value'; | ||
| const nextColumnStartWidth = nextCell.offsetWidth; | ||
|
|
||
| setResizing(columnKey); | ||
|
|
||
| const handleMouseMove = (moveEvent) => { | ||
| const diff = moveEvent.clientX - startX; | ||
| const maxGrow = nextColumnStartWidth - MIN_COLUMN_WIDTH; | ||
| const maxShrink = startWidth - MIN_COLUMN_WIDTH; | ||
| const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff)); | ||
|
|
||
| setColumnWidths({ | ||
| [columnKey]: `${startWidth + clampedDiff}px`, | ||
| [nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px` | ||
| }); | ||
| }; | ||
|
|
||
| const handleMouseUp = () => { | ||
| setResizing(null); | ||
| document.removeEventListener('mousemove', handleMouseMove); | ||
| document.removeEventListener('mouseup', handleMouseUp); | ||
| }; | ||
|
|
||
| document.addEventListener('mousemove', handleMouseMove); | ||
| document.addEventListener('mouseup', handleMouseUp); | ||
| }, []); |
There was a problem hiding this comment.
Potential memory leak: event listeners not cleaned up on unmount.
If the component unmounts while a resize is in progress, the mousemove and mouseup listeners remain attached to document. Store a cleanup ref and clear listeners in a useEffect cleanup.
🛠️ Proposed fix
+ const cleanupResizeRef = useRef(null);
+
+ useEffect(() => {
+ return () => {
+ if (cleanupResizeRef.current) {
+ cleanupResizeRef.current();
+ }
+ };
+ }, []);
const handleResizeStart = useCallback((e, columnKey) => {
// ... existing setup code ...
const handleMouseUp = () => {
setResizing(null);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
+ cleanupResizeRef.current = null;
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
+ cleanupResizeRef.current = handleMouseUp;
}, []);🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/EnvironmentVariablesTable/index.js` around
lines 52 - 87, The handleResizeStart listener pair
(handleMouseMove/handleMouseUp) can remain attached if the component unmounts
during a drag; modify handleResizeStart to register those handlers onto a ref
(e.g., resizeListenersRef) and add a useEffect with a cleanup that, on unmount,
removes any registered document 'mousemove' and 'mouseup' listeners and clears
the ref; ensure handleMouseUp also clears the ref and calls setResizing(null)
and that setColumnWidths is used as before so no leftover listeners reference
stale state.
| validationSchema: Yup.array().of( | ||
| Yup.object({ | ||
| enabled: Yup.boolean(), | ||
| name: Yup.string().when('$isLastRow', { | ||
| is: true, | ||
| then: (schema) => schema.optional(), | ||
| otherwise: (schema) => | ||
| schema | ||
| .required('Name cannot be empty') | ||
| .matches( | ||
| variableNameRegex, | ||
| 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.' | ||
| ) | ||
| .trim() | ||
| }), | ||
| secret: Yup.boolean(), | ||
| type: Yup.string(), | ||
| uid: Yup.string(), | ||
| value: Yup.mixed().nullable() | ||
| }) | ||
| ), |
There was a problem hiding this comment.
Yup $isLastRow context variable is never provided.
The schema references $isLastRow in the .when() condition, but Formik doesn't pass this context. The conditional validation won't trigger as expected. Your custom validate function (lines 141-161) already handles last-row logic correctly.
Consider simplifying the Yup schema since the custom validator covers this:
♻️ Proposed simplification
validationSchema: Yup.array().of(
Yup.object({
enabled: Yup.boolean(),
- name: Yup.string().when('$isLastRow', {
- is: true,
- then: (schema) => schema.optional(),
- otherwise: (schema) =>
- schema
- .required('Name cannot be empty')
- .matches(
- variableNameRegex,
- 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'
- )
- .trim()
- }),
+ name: Yup.string(),
secret: Yup.boolean(),
type: Yup.string(),
uid: Yup.string(),
value: Yup.mixed().nullable()
})
),Note: The Biome static analysis warning about noThenProperty on line 125 is a false positive—this is Yup's conditional .then() method, not a Promise.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| validationSchema: Yup.array().of( | |
| Yup.object({ | |
| enabled: Yup.boolean(), | |
| name: Yup.string().when('$isLastRow', { | |
| is: true, | |
| then: (schema) => schema.optional(), | |
| otherwise: (schema) => | |
| schema | |
| .required('Name cannot be empty') | |
| .matches( | |
| variableNameRegex, | |
| 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.' | |
| ) | |
| .trim() | |
| }), | |
| secret: Yup.boolean(), | |
| type: Yup.string(), | |
| uid: Yup.string(), | |
| value: Yup.mixed().nullable() | |
| }) | |
| ), | |
| validationSchema: Yup.array().of( | |
| Yup.object({ | |
| enabled: Yup.boolean(), | |
| name: Yup.string(), | |
| secret: Yup.boolean(), | |
| type: Yup.string(), | |
| uid: Yup.string(), | |
| value: Yup.mixed().nullable() | |
| }) | |
| ), |
🧰 Tools
🪛 Biome (2.3.13)
[error] 125-125: Do not add then to an object.
(lint/suspicious/noThenProperty)
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/EnvironmentVariablesTable/index.js` around
lines 120 - 140, The Yup schema is using a non-existent Formik context variable
`$isLastRow`, so remove that conditional and let your existing custom validate
function handle last-row logic; update the `validationSchema` in
EnvironmentVariablesTable (the `name` field) to a straightforward validator like
`Yup.string().required('Name cannot be empty').matches(variableNameRegex, 'Name
contains invalid characters...').trim()` (i.e., delete the `.when('$isLastRow',
...)` block referencing `$isLastRow` and keep the `variableNameRegex` check),
leaving the custom `validate` function as the place that permits an optional
last empty row.
| <button onClick={() => handleRemoveVar(variable.uid)}> | ||
| <IconTrash strokeWidth={1.5} size={18} /> | ||
| </button> |
There was a problem hiding this comment.
Delete button lacks accessible label.
The trash icon button has no accessible text. Screen readers won't convey its purpose.
♿ Proposed fix
- <button onClick={() => handleRemoveVar(variable.uid)}>
+ <button
+ onClick={() => handleRemoveVar(variable.uid)}
+ aria-label={`Remove variable ${variable.name}`}
+ title="Remove variable"
+ >
<IconTrash strokeWidth={1.5} size={18} />
</button>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button onClick={() => handleRemoveVar(variable.uid)}> | |
| <IconTrash strokeWidth={1.5} size={18} /> | |
| </button> | |
| <button | |
| onClick={() => handleRemoveVar(variable.uid)} | |
| aria-label={`Remove variable ${variable.name}`} | |
| title="Remove variable" | |
| > | |
| <IconTrash strokeWidth={1.5} size={18} /> | |
| </button> |
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/EnvironmentVariablesTable/index.js` around
lines 499 - 501, The delete button using IconTrash lacks an accessible label;
update the button element (the one calling handleRemoveVar(variable.uid) and
rendering IconTrash) to include an accessible name such as aria-label or title
(for example aria-label={`Delete ${variable.key || 'variable'}`}), and ensure
the element has type="button" so it’s announced properly by assistive tech;
alternatively include visually hidden text inside the button that describes the
action (e.g., "Delete [variable name]") so screen readers can convey its
purpose.
6ff11b3 to
36edf6a
Compare
Description
This PR adds column resizing functionality, allowing users to interactively adjust column widths by dragging column borders. This enhancement improves usability for tables with varying content lengths.
JIRA
Issue: #6818
Contribution Checklist:
Screen.Recording.2026-01-19.at.2.43.58.PM.mov
Summary by CodeRabbit
New Features
Style
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.