From dfb3eb12e1b5aa5d33b957ade5118e20c90a4ac6 Mon Sep 17 00:00:00 2001 From: Sachin Venugopalan Date: Tue, 15 Sep 2026 09:19:31 +0530 Subject: [PATCH] fix: variables count shows the total count of selected values --- .../KeyValueTable/KeyValueTable.spec.tsx | 20 +++++++ .../KeyValueTable/KeyValueTable.tsx | 4 +- .../EnvVarCards/EnvVarCards.tsx | 4 +- .../src/hooks/useEditableRows.spec.ts | 55 +++++++++++++++++++ .../src/hooks/useEditableRows.ts | 18 +++--- 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/packages/bruno-api-docs/src/components/KeyValueTable/KeyValueTable.spec.tsx b/packages/bruno-api-docs/src/components/KeyValueTable/KeyValueTable.spec.tsx index 75698412..32917d07 100644 --- a/packages/bruno-api-docs/src/components/KeyValueTable/KeyValueTable.spec.tsx +++ b/packages/bruno-api-docs/src/components/KeyValueTable/KeyValueTable.spec.tsx @@ -57,3 +57,23 @@ describe('KeyValueTable — resizable columns', () => { expect(root.querySelector('.col-resize-handle')).toBeFalsy(); }); }); + +describe('KeyValueTable — a row that has a value but no name', () => { + const valueOnlyRow: KeyValueRow[] = [{ id: 'r1', name: '', value: 'orphan', enabled: true }]; + + it('treats it as a real row: it gets a checkbox and a blank row appears underneath', () => { + const root = useRenderToDom(); + + expect(root.querySelectorAll('tbody tr').length).toBe(2); + expect(root.querySelectorAll('tbody tr.empty-row').length).toBe(1); + expect(root.querySelectorAll('tbody input[type="checkbox"]').length).toBe(1); + }); + + it('leaves a row that is empty in both fields as the trailing blank, with no checkbox', () => { + const root = useRenderToDom(); + + expect(root.querySelectorAll('tbody tr').length).toBe(1); + expect(root.querySelectorAll('tbody tr.empty-row').length).toBe(1); + expect(root.querySelectorAll('tbody input[type="checkbox"]').length).toBe(0); + }); +}); diff --git a/packages/bruno-api-docs/src/components/KeyValueTable/KeyValueTable.tsx b/packages/bruno-api-docs/src/components/KeyValueTable/KeyValueTable.tsx index 93742554..1988fda5 100644 --- a/packages/bruno-api-docs/src/components/KeyValueTable/KeyValueTable.tsx +++ b/packages/bruno-api-docs/src/components/KeyValueTable/KeyValueTable.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { useResolvedVariables } from '@/hooks/useVariableResolver'; -import { useEditableRows } from '@/hooks/useEditableRows'; +import { isBlankRow, useEditableRows } from '@/hooks/useEditableRows'; import { Tooltip } from '@/ui/Tooltip/Tooltip'; import { WarningIcon } from '@/assets/icons'; import HighlightedInput from '../HighlightedInput/HighlightedInput'; @@ -263,7 +263,7 @@ const KeyValueTable: React.FC = ({ {rows.map((row, index) => { const isLastRow = index === rows.length - 1; - const isEmptyRow = !row.name || row.name.trim() === ''; + const isEmptyRow = isBlankRow(row); const isLastEmptyRow = isLastRow && isEmptyRow; const updateCell = (field: string, value: unknown) => updateField(index, field, value); diff --git a/packages/bruno-api-docs/src/components/Playground/Content/Views/EnvironmentsView/EnvVarCards/EnvVarCards.tsx b/packages/bruno-api-docs/src/components/Playground/Content/Views/EnvironmentsView/EnvVarCards/EnvVarCards.tsx index f2604024..d7159f8d 100644 --- a/packages/bruno-api-docs/src/components/Playground/Content/Views/EnvironmentsView/EnvVarCards/EnvVarCards.tsx +++ b/packages/bruno-api-docs/src/components/Playground/Content/Views/EnvironmentsView/EnvVarCards/EnvVarCards.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { KeyValueRow } from '@/components/KeyValueTable/KeyValueTable'; import { SecretValue } from '@/ui/SecretValue/SecretValue'; import { TrashIcon } from '@/assets/icons'; -import { useEditableRows } from '@/hooks/useEditableRows'; +import { isBlankRow as isRowBlank, useEditableRows } from '@/hooks/useEditableRows'; import { cx } from '@/utils/cx'; import { toDataType } from '@/utils/variableDataType'; import { VariableTypeControl } from '../../Common/VariableTypeControl/VariableTypeControl'; @@ -37,7 +37,7 @@ const EnvVarCards: React.FC = ({ return ( {rows.map((row, index) => { - const isBlankRow = index === rows.length - 1 && (!row.name || row.name.trim() === ''); + const isBlankRow = index === rows.length - 1 && isRowBlank(row); return (
{ expect(names(out)).toEqual(['b', '']); }); }); + +describe('a row that has a value but no name', () => { + it('counts as a real row, so a fresh blank is added below it', () => { + const rows = [row({ name: 'a', value: '1' }), row({})]; + const out = applyRowPatch(rows, 1, { value: 'orphan' }, false); + + expect(out).toHaveLength(3); + expect(out[1]).toMatchObject({ name: '', value: 'orphan' }); + expect(out[2]).toMatchObject({ name: '', value: '' }); + }); + + it('is handed back to the caller, so it is saved and counted like any other row', () => { + const out = committableRows([ + row({ name: 'a', value: '1' }), + row({ name: '', value: 'orphan' }), + row({}) + ]); + + expect(out).toHaveLength(2); + expect(out[1]).toMatchObject({ name: '', value: 'orphan' }); + }); + + it('gets a trailing blank of its own when the rows are laid out', () => { + const out = withTrailingBlank([row({ name: '', value: 'orphan' })], false); + + expect(out).toHaveLength(2); + expect(out[1]).toMatchObject({ name: '', value: '' }); + }); + + it('can be deleted, leaving a blank row to type into', () => { + const rows = [row({ name: '', value: 'orphan' }), row({})]; + const out = removeRowAt(rows, 0, false); + + expect(out).toHaveLength(1); + expect(out[0]).toMatchObject({ name: '', value: '' }); + }); + + it('is not enough for the environments table, which waits for both fields', () => { + const out = applyRowPatch([row({})], 0, { value: 'orphan' }, false, undefined, true); + + expect(out).toHaveLength(1); + }); +}); + +describe('a row that is empty in both fields', () => { + it('is still treated as the trailing blank, so no second blank is added', () => { + const out = withTrailingBlank([row({ name: 'a', value: '1' }), row({})], false); + expect(out).toHaveLength(2); + }); + + it('is still dropped when the rows are handed back', () => { + const out = committableRows([row({ name: 'a', value: '1' }), row({ name: ' ', value: ' ' })]); + expect(names(out)).toEqual(['a']); + }); +}); diff --git a/packages/bruno-api-docs/src/hooks/useEditableRows.ts b/packages/bruno-api-docs/src/hooks/useEditableRows.ts index b886ac4a..a7c12a88 100644 --- a/packages/bruno-api-docs/src/hooks/useEditableRows.ts +++ b/packages/bruno-api-docs/src/hooks/useEditableRows.ts @@ -13,14 +13,16 @@ const blankRow = (makeNewRow?: NewRow): KeyValueRow => ({ ...makeNewRow?.() }); -const isBlank = (row?: KeyValueRow): boolean => !row?.name || row.name.trim() === ''; -const isComplete = (row?: KeyValueRow): boolean => !isBlank(row) && (row?.value ?? '').trim() !== ''; +const hasText = (value: unknown): boolean => typeof value === 'string' && value.trim() !== ''; + +export const isBlankRow = (row?: KeyValueRow): boolean => !hasText(row?.name) && !hasText(row?.value); +const isComplete = (row?: KeyValueRow): boolean => hasText(row?.name) && hasText(row?.value); // Whether the last row already serves as the trailing "type here" row, so we // don't append another. In addWhenComplete mode the trailing row lasts until it -// has both a name and a value; otherwise until it has a name. +// has both a name and a value; otherwise until either field has something in it. const hasTrailing = (last: KeyValueRow | undefined, addWhenComplete: boolean): boolean => - addWhenComplete ? !isComplete(last) : isBlank(last); + addWhenComplete ? !isComplete(last) : isBlankRow(last); export const withTrailingBlank = ( data: KeyValueRow[], @@ -33,7 +35,7 @@ export const withTrailingBlank = ( return [...rows, blankRow(makeNewRow)]; }; -export const committableRows = (rows: KeyValueRow[]): KeyValueRow[] => rows.filter((row) => !isBlank(row)); +export const committableRows = (rows: KeyValueRow[]): KeyValueRow[] => rows.filter((row) => !isBlankRow(row)); export const applyRowPatch = ( rows: KeyValueRow[], @@ -44,9 +46,9 @@ export const applyRowPatch = ( addWhenComplete = false ): KeyValueRow[] => { const next = [...rows]; - const wasBlank = isBlank(next[index]); + const wasBlank = isBlankRow(next[index]); next[index] = { ...next[index], ...patch }; - const spawn = addWhenComplete ? isComplete(next[index]) : wasBlank && !isBlank(next[index]); + const spawn = addWhenComplete ? isComplete(next[index]) : wasBlank && !isBlankRow(next[index]); if (!disableNewRow && index === rows.length - 1 && spawn) { next.push(blankRow(makeNewRow)); } @@ -60,7 +62,7 @@ export const removeRowAt = ( makeNewRow?: NewRow, addWhenComplete = false ): KeyValueRow[] => { - if (index === rows.length - 1 && isBlank(rows[index])) return rows; + if (index === rows.length - 1 && isBlankRow(rows[index])) return rows; const next = rows.filter((_, i) => i !== index); if (!disableNewRow && (next.length === 0 || !hasTrailing(next[next.length - 1], addWhenComplete))) { next.push(blankRow(makeNewRow));