Skip to content
Merged
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
31 changes: 31 additions & 0 deletions packages/cli/src/ui/utils/TableRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,35 @@ describe('TableRenderer', () => {
});
expect(output).toMatchSnapshot();
});

it.each([
{
name: 'renders correctly when headers are empty but rows have data',
headers: [] as string[],
rows: [['Data 1', 'Data 2']],
expected: ['Data 1', 'Data 2'],
},
{
name: 'renders correctly when there are more headers than columns in rows',
headers: ['Header 1', 'Header 2', 'Header 3'],
rows: [['Data 1', 'Data 2']],
expected: ['Header 1', 'Header 2', 'Header 3', 'Data 1', 'Data 2'],
},
])('$name', ({ headers, rows, expected }) => {
const terminalWidth = 50;

const { lastFrame } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);

const output = lastFrame();
expected.forEach((text) => {
expect(output).toContain(text);
});
expect(output).toMatchSnapshot();
});
});
76 changes: 46 additions & 30 deletions packages/cli/src/ui/utils/TableRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ const MIN_COLUMN_WIDTH = 5;
const COLUMN_PADDING = 2;
const TABLE_MARGIN = 2;

const calculateWidths = (text: string) => {
const styledChars = toStyledCharacters(text);
const calculateWidths = (styledChars: StyledChar[]) => {
const contentWidth = styledCharsWidth(styledChars);

const words: StyledChar[][] = wordBreakStyledChars(styledChars);
Expand Down Expand Up @@ -60,31 +59,48 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
[headers],
);

const { wrappedHeaders, wrappedRows, adjustedWidths } = useMemo(() => {
// --- Define Constraints per Column ---
const constraints = cleanedHeaders.map((header, colIndex) => {
let { contentWidth: maxContentWidth, maxWordWidth } =
calculateWidths(header);
const styledHeaders = useMemo(
() => cleanedHeaders.map((header) => toStyledCharacters(header)),
[cleanedHeaders],
);

rows.forEach((row) => {
const cell = row[colIndex] || '';
const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } =
calculateWidths(cell);
const styledRows = useMemo(
() => rows.map((row) => row.map((cell) => toStyledCharacters(cell))),
[rows],
);

maxContentWidth = Math.max(maxContentWidth, cellWidth);
maxWordWidth = Math.max(maxWordWidth, cellWordWidth);
});
const { wrappedHeaders, wrappedRows, adjustedWidths } = useMemo(() => {
const numColumns = styledRows.reduce(
(max, row) => Math.max(max, row.length),
styledHeaders.length,
);

const minWidth = maxWordWidth;
const maxWidth = Math.max(minWidth, maxContentWidth);
// --- Define Constraints per Column ---
const constraints = Array.from({ length: numColumns }).map(
(_, colIndex) => {
const headerStyledChars = styledHeaders[colIndex] || [];
let { contentWidth: maxContentWidth, maxWordWidth } =
calculateWidths(headerStyledChars);

styledRows.forEach((row) => {
const cellStyledChars = row[colIndex] || [];
const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } =
calculateWidths(cellStyledChars);

maxContentWidth = Math.max(maxContentWidth, cellWidth);
maxWordWidth = Math.max(maxWordWidth, cellWordWidth);
});

return { minWidth, maxWidth };
});
const minWidth = maxWordWidth;
const maxWidth = Math.max(minWidth, maxContentWidth);

return { minWidth, maxWidth };
},
);

// --- Calculate Available Space ---
// Fixed overhead: borders (n+1) + padding (2n)
const fixedOverhead =
cleanedHeaders.length + 1 + cleanedHeaders.length * COLUMN_PADDING;
const fixedOverhead = numColumns + 1 + numColumns * COLUMN_PADDING;
const availableWidth = Math.max(
0,
terminalWidth - fixedOverhead - TABLE_MARGIN,
Expand Down Expand Up @@ -136,17 +152,18 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
}

// --- Pre-wrap and Optimize Widths ---
const actualColumnWidths = new Array(cleanedHeaders.length).fill(0);
const actualColumnWidths = new Array(numColumns).fill(0);

const wrapAndProcessRow = (row: string[]) => {
const wrapAndProcessRow = (row: StyledChar[][]) => {
const rowResult: ProcessedLine[][] = [];
row.forEach((cell, colIndex) => {
// Ensure we iterate up to numColumns, filling with empty cells if needed
for (let colIndex = 0; colIndex < numColumns; colIndex++) {
const cellStyledChars = row[colIndex] || [];
const allocatedWidth = finalContentWidths[colIndex];
const contentWidth = Math.max(1, allocatedWidth);

const contentStyledChars = toStyledCharacters(cell);
const wrappedStyledLines = wrapStyledChars(
contentStyledChars,
cellStyledChars,
contentWidth,
);

Expand All @@ -161,19 +178,18 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
width: styledCharsWidth(line),
}));
rowResult.push(lines);
});
}
return rowResult;
};

const wrappedHeaders = wrapAndProcessRow(cleanedHeaders);
const wrappedRows = rows.map((row) => wrapAndProcessRow(row));
const wrappedHeaders = wrapAndProcessRow(styledHeaders);
const wrappedRows = styledRows.map((row) => wrapAndProcessRow(row));

// Use the TIGHTEST widths that fit the wrapped content + padding
const adjustedWidths = actualColumnWidths.map((w) => w + COLUMN_PADDING);

return { wrappedHeaders, wrappedRows, adjustedWidths };
}, [cleanedHeaders, rows, terminalWidth]);

}, [styledHeaders, styledRows, terminalWidth]);
// Helper function to render a cell with proper width
const renderCell = (
content: ProcessedLine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ exports[`TableRenderer > 'renders a table with only emojis and …' 1`] = `
"
`;

exports[`TableRenderer > 'renders correctly when headers are em…' 1`] = `
"
┌────────┬────────┐
│ │ │
├────────┼────────┤
│ Data 1 │ Data 2 │
└────────┴────────┘
"
`;

exports[`TableRenderer > 'renders correctly when there are more…' 1`] = `
"
┌──────────┬──────────┬──────────┐
│ Header 1 │ Header 2 │ Header 3 │
├──────────┼──────────┼──────────┤
│ Data 1 │ Data 2 │ │
└──────────┴──────────┴──────────┘
"
`;

exports[`TableRenderer > handles wrapped bold headers without showing markers 1`] = `
"
┌─────────────┬───────┬─────────┐
Expand Down
Loading