Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(<KeyValueTable data={valueOnlyRow} onChange={noop} showEnabled />);

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(<KeyValueTable data={[]} onChange={noop} showEnabled />);

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);
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -263,7 +263,7 @@ const KeyValueTable: React.FC<KeyValueTableProps> = ({
<tbody>
{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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,7 +37,7 @@ const EnvVarCards: React.FC<EnvVarCardsProps> = ({
return (
<StyledWrapper className="env-card-list" data-testid={testId}>
{rows.map((row, index) => {
const isBlankRow = index === rows.length - 1 && (!row.name || row.name.trim() === '');
const isBlankRow = index === rows.length - 1 && isRowBlank(row);
return (
<div
key={row.id}
Expand Down
55 changes: 55 additions & 0 deletions packages/bruno-api-docs/src/hooks/useEditableRows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,58 @@ describe('removeRowAt', () => {
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']);
});
});
18 changes: 10 additions & 8 deletions packages/bruno-api-docs/src/hooks/useEditableRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -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[],
Expand All @@ -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));
}
Expand All @@ -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));
Expand Down
Loading