Skip to content

feat: add resizable columns to table#6843

Merged
sid-bruno merged 4 commits intousebruno:mainfrom
pooja-bruno:feat/table-column-resize
Jan 30, 2026
Merged

feat: add resizable columns to table#6843
sid-bruno merged 4 commits intousebruno:mainfrom
pooja-bruno:feat/table-column-resize

Conversation

@pooja-bruno
Copy link
Collaborator

@pooja-bruno pooja-bruno commented Jan 19, 2026

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:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.
Screen.Recording.2026-01-19.at.2.43.58.PM.mov

Summary by CodeRabbit

  • New Features

    • Draggable column resize handles with live width adjustments.
    • New Environment Variables table with per-row editing, validation, draft support, save/reset flows, and trailing empty-row behavior.
  • Style

    • Improved resize cursor and selection locking while resizing.
    • Column label truncation, overflow/ellipsis handling, and editor content constraints.
    • Increased Content-Type column width in multipart form params.
  • Refactor

    • Replaced legacy environment-variables UI with the centralized table and removed its old styled wrapper.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Walkthrough

Extracts 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

Cohort / File(s) Summary
EditableTable (resize UI + logic)
packages/bruno-app/src/components/EditableTable/index.js, packages/bruno-app/src/components/EditableTable/StyledWrapper.js
Added drag-to-resize state and handlers (MIN_COLUMN_WIDTH, mousemove/mouseup flow, pixel→% conversion), ResizeObserver for alignment, dynamic getColumnWidth, header DOM for column-name and resize-handle, and is-resizing wrapper class plus cursor/selection styling.
EnvironmentVariablesTable (new component)
packages/bruno-app/src/components/EnvironmentVariablesTable/index.js, packages/bruno-app/src/components/EnvironmentVariablesTable/StyledWrapper.js
New virtualized editable table (~523 lines): per-row Formik/Yup validation, trailing empty row, secret/enable toggles, draft sync/restore via callbacks, save/reset flows, optional extra-value rendering, and draggable column resizing; plus styling for resize handles and CodeMirror overflow constraints.
Delegation: EnvironmentVariables usage sites
packages/bruno-app/src/components/Environments/.../EnvironmentVariables/index.js, packages/bruno-app/src/components/WorkspaceHome/.../EnvironmentVariables/index.js
Replaced inline Formik-driven EnvironmentVariables implementations with EnvironmentVariablesTable; added handleSave/handleDraftChange/handleDraftClear callbacks and sensitive-usage rendering; removed large legacy local form logic and the old styled wrapper import.
Removed legacy styling
packages/bruno-app/src/components/WorkspaceHome/.../EnvironmentDetails/EnvironmentVariables/StyledWrapper.js
Deleted previous component-specific StyledWrapper (old styles removed; styles consolidated into new table's StyledWrapper).
Minor UI tweak
packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
Adjusted Content-Type column width from 20%30%.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #6330: Modifies EditableTable implementation and its StyledWrapper—likely overlaps on resize logic and CSS.
  • PR #6790: Also refactors EnvironmentVariables to use virtualization and changes row handling—strong overlap with the new table extraction.
  • PR #6463: Touches similar UI styling files (EditableTable / EnvironmentVariables wrappers) and may conflict on CSS selectors and overflow behavior.

Suggested labels

size/XL

Suggested reviewers

  • lohit-bruno
  • naman-bruno
  • bijin-bruno
  • helloanoop

Poem

✨ Columns stretch and rows align,
A trailing row saves every line.
Drafts tucked safe, validations bright,
Resize, save, reset — the table’s right.
🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature added across the PR: interactive column resizing for table components.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 varName contains 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 with formik.values in useCallback dependency.

Using formik.values directly in the dependency array means the callback recreates on every value change, but there's still a subtle timing issue where formik.values inside the callback could be stale if the callback is invoked during a render cycle.

Consider using formik.setValues with 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: Missing formik in effect dependencies.

The effect calls formik.setValues but doesn't include formik in the dependency array. While formik is typically stable across renders, this could cause linter warnings.

- }, [environment.uid, hasDraftForThisEnv, draft?.variables]);
+ }, [environment.uid, hasDraftForThisEnv, draft?.variables, formik]);

236-257: Consider extracting ErrorMessage outside the component.

Defining ErrorMessage inside 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: prefer const for _collection.

The variable is reassigned via property assignment (_collection.globalEnvironmentVariables = ...), but the binding itself doesn't change. Use const for 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 when columns changes.
State is only initialized once; if columns are added/removed or re-keyed later, widths can drift or become stale. Consider syncing on columns updates 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]);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]);

Comment on lines +38 to +101
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]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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).

Comment on lines +103 to +118
// 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]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ResizeObserver references, polyfills, and stubs
rg -n 'ResizeObserver' --type js --type json

Repository: 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 -20

Repository: 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/null

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Read the setup.js file
cat ./scripts/setup.js

Repository: 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/null

Repository: 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/null

Repository: 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/null

Repository: 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 ts

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check the jest.config.js for bruno-app
cat ./packages/bruno-app/jest.config.js

Repository: 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 -10

Repository: 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/null

Repository: usebruno/bruno

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Read the jest.setup.js file
cat ./packages/bruno-app/jest.setup.js

Repository: 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 tsx

Repository: 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 -20

Repository: usebruno/bruno

Length of output: 1460


🏁 Script executed:

#!/bin/bash
# Search for EditableTable in all test files
rg -n 'EditableTable' --type js --type ts

Repository: 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 1

Repository: 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 component ErrorMessage recreated 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 useCallback or extracting it outside and passing formik as 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 setTimeout with 0ms to avoid updating during render works but is fragile. Consider using a ref to track pending additions and applying them in a useEffect:

♻️ 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;
    }
  };

Comment on lines +52 to +87
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);
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +120 to +140
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()
})
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +499 to +501
<button onClick={() => handleRemoveVar(variable.uid)}>
<IconTrash strokeWidth={1.5} size={18} />
</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

@pooja-bruno pooja-bruno force-pushed the feat/table-column-resize branch from 6ff11b3 to 36edf6a Compare January 30, 2026 10:46
@sid-bruno sid-bruno merged commit e1c01eb into usebruno:main Jan 30, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants