From 7af6150f425bf71c286126d4cfc9fa23b4e3bc37 Mon Sep 17 00:00:00 2001 From: sachin-thakur-bruno Date: Wed, 8 Jul 2026 12:43:53 +0530 Subject: [PATCH] fix(request-panel)/fixed overflow size column issue on first load (#8525) * fix(request-panel)/fixed overflow size column issue on first load * added tests * refactor the total width calculation based on available space --- .../src/hooks/useResizableColumns/index.js | 81 ++++++++++++++++--- .../hooks/useResizableColumns/index.spec.js | 49 +++++++++++ 2 files changed, 120 insertions(+), 10 deletions(-) diff --git a/packages/bruno-app/src/hooks/useResizableColumns/index.js b/packages/bruno-app/src/hooks/useResizableColumns/index.js index e30d0aa7a..eb3c0ca9f 100644 --- a/packages/bruno-app/src/hooks/useResizableColumns/index.js +++ b/packages/bruno-app/src/hooks/useResizableColumns/index.js @@ -35,20 +35,81 @@ const getSeparatorPositions = (widths) => { return positions; }; +// Container too narrow for everyone to get minColWidth — split evenly instead +// (columns clip instead of overflow). +const distributeEqually = (count, total) => { + const each = Math.floor(total / count); + return [ + ...Array(count - 1).fill(each), + total - each * (count - 1) + ]; +}; + +// Pins any column that would shrink below minColWidth, then re-checks the +// rest (pinning one column shifts the math for the others). Returns the +// pinned widths plus what's left for the caller to hand out. +const pinColumnsAtMinimum = (widths, targetTotal, minColWidth) => { + const result = Array(widths.length).fill(null); + let remainingIdx = widths.map((_, i) => i); + let remainingTotal = targetTotal; + + while (true) { + const remainingWidth = remainingIdx.reduce((sum, i) => sum + widths[i], 0); + const scale = remainingTotal / remainingWidth; + + const nextRemaining = []; + for (const i of remainingIdx) { + if (widths[i] * scale < minColWidth) { + result[i] = minColWidth; + remainingTotal -= minColWidth; + } else { + nextRemaining.push(i); + } + } + + if (nextRemaining.length === remainingIdx.length) { + return { result, remainingIdx, remainingTotal }; + } + remainingIdx = nextRemaining; + } +}; + +// Largest remainder method: scales proportionally, floors each share, then +// gives the leftover whole pixels to whoever rounded down the most, so the +// sum always hits `total` exactly, with no single column eating the error. +const scaleAndRound = (widths, total) => { + const widthSum = widths.reduce((a, b) => a + b, 0); + const exact = widths.map((w) => (w / widthSum) * total); + const rounded = exact.map(Math.floor); + const leftover = total - rounded.reduce((a, b) => a + b, 0); + + exact + .map((value, i) => ({ i, remainder: value - rounded[i] })) + .sort((a, b) => b.remainder - a.remainder) + .slice(0, leftover) + .forEach(({ i }) => rounded[i]++); + + return rounded; +}; + const scaleWidthsToTotal = (widths, targetTotal, minColWidth) => { - // When the container is too narrow to satisfy minColWidth for every column, - // fall back to equal distribution so the total stays exact (columns clip instead of overflow). if (targetTotal < minColWidth * widths.length) { - const each = Math.floor(targetTotal / widths.length); - const last = targetTotal - each * (widths.length - 1); - return [...Array(widths.length - 1).fill(each), last]; + return distributeEqually(widths.length, targetTotal); } - const currentTotal = widths.reduce((s, w) => s + w, 0); - const factor = targetTotal / currentTotal; - const next = widths.slice(0, -1).map((w) => Math.max(minColWidth, Math.round(w * factor))); - const last = Math.max(minColWidth, targetTotal - next.reduce((s, w) => s + w, 0)); - return [...next, last]; + const { result, remainingIdx, remainingTotal } + = pinColumnsAtMinimum(widths, targetTotal, minColWidth); + + const scaled = scaleAndRound( + remainingIdx.map((i) => widths[i]), + remainingTotal + ); + + remainingIdx.forEach((colIndex, i) => { + result[colIndex] = scaled[i]; + }); + + return result; }; export function useResizableColumns({ defaultWidths, initialWidths = null, minColWidth = 60, onResizeEnd = null }) { diff --git a/packages/bruno-app/src/hooks/useResizableColumns/index.spec.js b/packages/bruno-app/src/hooks/useResizableColumns/index.spec.js index ebb163341..c62bdf23e 100644 --- a/packages/bruno-app/src/hooks/useResizableColumns/index.spec.js +++ b/packages/bruno-app/src/hooks/useResizableColumns/index.spec.js @@ -138,6 +138,55 @@ describe('useResizableColumns', () => { }); }); + describe('scaling invariants (regression: Devtools network tab "Size" column overflow)', () => { + const NETWORK_TAB_WIDTHS = [80, 70, 180, 300, 110, 100, 80]; + + const assertNoOverflowAndRespectsMin = (widths, minColWidth) => { + setup({ defaultWidths: widths, minColWidth }); + + const from = minColWidth * widths.length; + const to = Math.ceil(widths.reduce((s, w) => s + w, 0) * 1.5); + // Step by at least 2px: the hook itself treats a <=1px delta as noise + // and skips recomputing (see the ResizeObserver callback in index.js), + // which is unrelated to the scaling algorithm under test here. + const step = Math.max(2, Math.floor((to - from) / 200)); + + for (let target = from; target <= to; target += step) { + act(() => { triggerResize(target); }); + + const total = hookValue.colWidths.reduce((s, w) => s + w, 0); + const minVal = Math.min(...hookValue.colWidths); + + expect(total).toBe(target); + expect(minVal).toBeGreaterThanOrEqual(minColWidth); + } + }; + + it('never overflows or dips under minColWidth across a sweep of container widths (real network-tab columns)', () => { + assertNoOverflowAndRespectsMin(NETWORK_TAB_WIDTHS, 60); + }); + + it('never overflows or dips under minColWidth across a sweep of container widths (generic columns)', () => { + assertNoOverflowAndRespectsMin(DEFAULT_WIDTHS, MIN_COL_WIDTH); + }); + + it('exact reproduction: shrinking to 380px (below minColWidth*7=420) still clips instead of overflowing', () => { + setup({ defaultWidths: NETWORK_TAB_WIDTHS, minColWidth: 60 }); + + act(() => { triggerResize(380); }); + + const total = hookValue.colWidths.reduce((s, w) => s + w, 0); + expect(total).toBe(380); + }); + + it('handles evenly-weighted columns without rounding pile-up pushing a column under minColWidth', () => { + // With naive per-column Math.round(), 4 equal-weight columns at this + // exact target used to round 3 columns up to minColWidth and leave the + // 4th short of it. + assertNoOverflowAndRespectsMin([20, 20, 20, 20], 10); + }); + }); + describe('drag resize', () => { beforeEach(() => { setup();