scaffolder: use high-level Table component with column configs
Replace manual composition of TableRoot/TableHeader/TableBody/Row/Column primitives with the top-level Table component and ColumnConfig-based column definitions. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -128,16 +128,39 @@ describe('JSON schema UI rendering', () => {
|
||||
},
|
||||
};
|
||||
type qt = typeof queries;
|
||||
|
||||
const getRowById = (container: HTMLElement, rowId: string): HTMLElement => {
|
||||
const el = container.querySelector(`[data-key="${rowId}"]`);
|
||||
if (!el) {
|
||||
throw new Error(`Could not find row with data-key="${rowId}"`);
|
||||
}
|
||||
return el as HTMLElement;
|
||||
};
|
||||
|
||||
const queryRowById = (
|
||||
container: HTMLElement,
|
||||
rowId: string,
|
||||
): HTMLElement | null => {
|
||||
return container.querySelector(`[data-key="${rowId}"]`);
|
||||
};
|
||||
|
||||
const findRowById = (
|
||||
container: HTMLElement,
|
||||
rowId: string,
|
||||
): Promise<HTMLElement> => {
|
||||
return waitFor(() => getRowById(container, rowId));
|
||||
};
|
||||
|
||||
const assertBasicSchemaProperties = (
|
||||
q: {
|
||||
[P in keyof qt]: BoundFunction<qt[P]>;
|
||||
},
|
||||
} & { container: HTMLElement },
|
||||
id: string,
|
||||
) => {
|
||||
const { getByTestId, queryByTestId } = q;
|
||||
const { container, queryByTestId } = q;
|
||||
|
||||
for (const p of Object.keys(basic.properties!)) {
|
||||
const tr = getByTestId(`properties-row_${id}.${p}`);
|
||||
const tr = getRowById(container, `properties-row_${id}.${p}`);
|
||||
expect(tr).toBeInTheDocument();
|
||||
|
||||
const pt = (basic.properties![p] as JSONSchema7).type;
|
||||
@@ -197,17 +220,17 @@ describe('JSON schema UI rendering', () => {
|
||||
const assertDeepSchemaProperties = async (
|
||||
q: {
|
||||
[P in keyof qt]: BoundFunction<qt[P]>;
|
||||
},
|
||||
} & { container: HTMLElement },
|
||||
id: string,
|
||||
) => {
|
||||
const { findByTestId, getByTestId, queryByTestId } = q;
|
||||
const { container, findByTestId, getByTestId, queryByTestId } = q;
|
||||
|
||||
const xp: (k: string) => Promise<{
|
||||
expander: HTMLElement;
|
||||
expansionId: string;
|
||||
expansion: HTMLElement;
|
||||
}> = async (k: string) => {
|
||||
const r = getByTestId(`properties-row_${id}.${k}`);
|
||||
const r = getRowById(container, `properties-row_${id}.${k}`);
|
||||
expect(r).toBeInTheDocument();
|
||||
|
||||
const expansionId = `expansion_${id}.${k}`;
|
||||
@@ -226,7 +249,10 @@ describe('JSON schema UI rendering', () => {
|
||||
const b = await xp('basic');
|
||||
const m = await xp('msvs');
|
||||
|
||||
assertBasicSchemaProperties(within(b.expansion), `${id}.basic`);
|
||||
assertBasicSchemaProperties(
|
||||
{ ...within(b.expansion), container: b.expansion },
|
||||
`${id}.basic`,
|
||||
);
|
||||
assertMsv(within(m.expansion), `${id}.msvs`);
|
||||
|
||||
await userEvent.click(b.expander);
|
||||
@@ -276,7 +302,10 @@ describe('JSON schema UI rendering', () => {
|
||||
const expanded = await findByTestId('expansion_test');
|
||||
expect(expanded).toBeInTheDocument();
|
||||
|
||||
assertBasicSchemaProperties(within(expanded), 'test');
|
||||
assertBasicSchemaProperties(
|
||||
{ ...within(expanded), container: expanded },
|
||||
'test',
|
||||
);
|
||||
|
||||
await userEvent.click(expand);
|
||||
|
||||
@@ -325,7 +354,10 @@ describe('JSON schema UI rendering', () => {
|
||||
const expansion = await findByTestId('expansion_test');
|
||||
expect(expansion).toBeInTheDocument();
|
||||
|
||||
await assertDeepSchemaProperties(within(expansion), 'test');
|
||||
await assertDeepSchemaProperties(
|
||||
{ ...within(expansion), container: expansion },
|
||||
'test',
|
||||
);
|
||||
|
||||
await userEvent.click(expand);
|
||||
|
||||
@@ -387,22 +419,25 @@ describe('JSON schema UI rendering', () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<LocalRenderSchema strategy="properties" {...{ schema }} />,
|
||||
);
|
||||
const { findByTestId, getByTestId, queryByTestId } = rendered;
|
||||
const { container, getByTestId } = rendered;
|
||||
|
||||
for (const k of Object.keys(schema.properties!)) {
|
||||
const tr = getByTestId(`properties-row_test.${k}`);
|
||||
const tr = getRowById(container, `properties-row_test.${k}`);
|
||||
expect(tr).toBeInTheDocument();
|
||||
}
|
||||
|
||||
expect(
|
||||
queryByTestId('properties-row_test_oneOf0.foo'),
|
||||
queryRowById(container, 'properties-row_test_oneOf0.foo'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const expandOneOf = getByTestId('expand_test_oneOf');
|
||||
await userEvent.click(expandOneOf);
|
||||
|
||||
for (const [i, k] of Object.keys(schema.properties!).entries()) {
|
||||
const sub = await findByTestId(`properties-row_test_oneOf${i}.${k}`);
|
||||
const sub = await findRowById(
|
||||
container,
|
||||
`properties-row_test_oneOf${i}.${k}`,
|
||||
);
|
||||
expect(sub).toBeInTheDocument();
|
||||
|
||||
const nameCell =
|
||||
@@ -411,7 +446,7 @@ describe('JSON schema UI rendering', () => {
|
||||
expect(nameCell.textContent).toContain('*');
|
||||
|
||||
expect(
|
||||
getByTestId(`properties-row_test_oneOf${i}.${k}Flag`),
|
||||
getRowById(container, `properties-row_test_oneOf${i}.${k}Flag`),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
|
||||
@@ -419,7 +454,7 @@ describe('JSON schema UI rendering', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
queryByTestId('properties-row_test_oneOf0.foo'),
|
||||
queryRowById(container, 'properties-row_test_oneOf0.foo'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -466,7 +501,10 @@ describe('JSON schema UI rendering', () => {
|
||||
for (const i of subs.keys()) {
|
||||
for (const k of Object.keys(subs[i].properties!)) {
|
||||
expect(
|
||||
await rendered.findByTestId(`properties-row_test_oneOf${i}.${k}`),
|
||||
await findRowById(
|
||||
rendered.container,
|
||||
`properties-row_test_oneOf${i}.${k}`,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
}
|
||||
@@ -491,17 +529,17 @@ describe('JSON schema UI rendering', () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.queryByTestId('root-row_test.bs_anyOf0'),
|
||||
queryRowById(rendered.container, 'root-row_test.bs_anyOf0'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const expandAnyOf = rendered.getByTestId('expand_test.bs_anyOf');
|
||||
await userEvent.click(expandAnyOf);
|
||||
|
||||
expect(
|
||||
await rendered.findByTestId('root-row_test.bs_anyOf0'),
|
||||
await findRowById(rendered.container, 'root-row_test.bs_anyOf0'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
rendered.getByTestId('root-row_test.bs_anyOf1'),
|
||||
getRowById(rendered.container, 'root-row_test.bs_anyOf1'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,12 +19,9 @@ import {
|
||||
Button,
|
||||
Cell,
|
||||
CellText,
|
||||
Column,
|
||||
ColumnConfig,
|
||||
Flex,
|
||||
Row,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TableRoot,
|
||||
Table,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
@@ -34,7 +31,7 @@ import {
|
||||
JSONSchema7Definition,
|
||||
JSONSchema7Type,
|
||||
} from 'json-schema';
|
||||
import { FC, JSX } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { scaffolderTranslationRef } from '../../translation';
|
||||
import { SchemaRenderContext, SchemaRenderStrategy } from './types';
|
||||
|
||||
@@ -123,29 +120,20 @@ const getSubschemas = (schema: JSONSchema7Definition): subSchemasType => {
|
||||
);
|
||||
};
|
||||
|
||||
type SchemaRenderElement = {
|
||||
interface SchemaTableItem {
|
||||
id: string;
|
||||
schema: JSONSchema7Definition;
|
||||
key?: string;
|
||||
propKey?: string;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
type RenderColumn = (
|
||||
element: SchemaRenderElement,
|
||||
context: SchemaRenderContext,
|
||||
) => JSX.Element;
|
||||
|
||||
type ColumnDef = {
|
||||
key: string;
|
||||
title: string;
|
||||
render: RenderColumn;
|
||||
width?: number | `${number}fr`;
|
||||
};
|
||||
}
|
||||
|
||||
const generateId = (
|
||||
element: SchemaRenderElement,
|
||||
item: { propKey?: string },
|
||||
context: SchemaRenderContext,
|
||||
) => {
|
||||
return element.key ? `${context.parentId}.${element.key}` : context.parentId;
|
||||
return item.propKey
|
||||
? `${context.parentId}.${item.propKey}`
|
||||
: context.parentId;
|
||||
};
|
||||
|
||||
const enumFrom = (schema: JSONSchema7) => {
|
||||
@@ -247,41 +235,48 @@ export const RenderSchema = ({
|
||||
}) => {
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
const columnConfig: ColumnConfig<SchemaTableItem>[] =
|
||||
strategy === 'root'
|
||||
? [
|
||||
{
|
||||
id: 'value',
|
||||
label: t('renderSchema.tableCell.value'),
|
||||
defaultWidth: '3fr' as any,
|
||||
cell: item => <ValueCell item={item} context={context} />,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'name',
|
||||
label: t('renderSchema.tableCell.name'),
|
||||
isRowHeader: true,
|
||||
defaultWidth: 200,
|
||||
cell: item => {
|
||||
const name = item.propKey ?? '';
|
||||
return <CellText title={item.required ? `${name} *` : name} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'value',
|
||||
label: t('renderSchema.tableCell.value'),
|
||||
defaultWidth: '1fr' as any,
|
||||
cell: item => <ValueCell item={item} context={context} />,
|
||||
},
|
||||
];
|
||||
|
||||
const result = (() => {
|
||||
if (typeof schema === 'object') {
|
||||
const subschemas = getSubschemas(schema);
|
||||
let columns: ColumnDef[] | undefined;
|
||||
let elements: SchemaRenderElement[] | undefined;
|
||||
let data: SchemaTableItem[] | undefined;
|
||||
if (strategy === 'root') {
|
||||
if ('type' in schema || !Object.keys(subschemas).length) {
|
||||
elements = [{ schema }];
|
||||
columns = [
|
||||
{
|
||||
key: 'value',
|
||||
title: t('renderSchema.tableCell.value'),
|
||||
render: renderValueCell,
|
||||
width: '3fr',
|
||||
},
|
||||
];
|
||||
data = [{ id: `root-row_${context.parentId}`, schema }];
|
||||
}
|
||||
} else if (schema.properties) {
|
||||
columns = [
|
||||
{
|
||||
key: 'name',
|
||||
title: t('renderSchema.tableCell.name'),
|
||||
render: renderNameCell,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
title: t('renderSchema.tableCell.value'),
|
||||
render: renderValueCell,
|
||||
width: '1fr',
|
||||
},
|
||||
];
|
||||
elements = Object.entries(schema.properties!).map(([key, v]) => ({
|
||||
data = Object.entries(schema.properties).map(([key, v]) => ({
|
||||
id: `${strategy}-row_${context.parentId}.${key}`,
|
||||
schema: v,
|
||||
key,
|
||||
propKey: key,
|
||||
required: schema.required?.includes(key),
|
||||
}));
|
||||
} else if (!Object.keys(subschemas).length) {
|
||||
@@ -291,36 +286,14 @@ export const RenderSchema = ({
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
{columns && elements && (
|
||||
<TableRoot
|
||||
data-testid={`${strategy}_${context.parentId}`}
|
||||
aria-label={`${strategy} schema for ${context.parentId}`}
|
||||
>
|
||||
<TableHeader>
|
||||
{columns.map((col, index) => (
|
||||
<Column
|
||||
key={col.key}
|
||||
id={col.key}
|
||||
isRowHeader={index === 0}
|
||||
defaultWidth={col.width ?? undefined}
|
||||
>
|
||||
{col.title}
|
||||
</Column>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{elements.map(el => (
|
||||
<Row
|
||||
key={generateId(el, context)}
|
||||
id={`${strategy}-row_${generateId(el, context)}`}
|
||||
data-testid={`${strategy}-row_${generateId(el, context)}`}
|
||||
columns={columns}
|
||||
>
|
||||
{col => col.render(el, context)}
|
||||
</Row>
|
||||
))}
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
{data && (
|
||||
<div data-testid={`${strategy}_${context.parentId}`}>
|
||||
<Table
|
||||
columnConfig={columnConfig}
|
||||
data={data}
|
||||
pagination={{ type: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(Object.keys(subschemas) as Array<keyof subSchemasType>).map(sk => {
|
||||
const subId = `${context.parentId}_${sk}`;
|
||||
@@ -369,20 +342,18 @@ export const RenderSchema = ({
|
||||
};
|
||||
|
||||
function RenderExpansion({
|
||||
element,
|
||||
item,
|
||||
context,
|
||||
}: {
|
||||
element: SchemaRenderElement;
|
||||
item: SchemaTableItem;
|
||||
context: SchemaRenderContext;
|
||||
}) {
|
||||
const id = generateId(element, context);
|
||||
const info = inspectSchema(element.schema);
|
||||
const id = generateId(item, context);
|
||||
const info = inspectSchema(item.schema);
|
||||
const hasDetails =
|
||||
typeof element.schema !== 'boolean' && (info.canSubschema || info.hasEnum);
|
||||
typeof item.schema !== 'boolean' && (info.canSubschema || info.hasEnum);
|
||||
const s =
|
||||
typeof element.schema !== 'boolean'
|
||||
? (element.schema as JSONSchema7)
|
||||
: undefined;
|
||||
typeof item.schema !== 'boolean' ? (item.schema as JSONSchema7) : undefined;
|
||||
const [isExpanded] = context.expanded;
|
||||
const isOpen = hasDetails && s && (!getTypes(s) || isExpanded[id]);
|
||||
|
||||
@@ -417,26 +388,21 @@ function RenderExpansion({
|
||||
);
|
||||
}
|
||||
|
||||
function renderNameCell(
|
||||
element: SchemaRenderElement,
|
||||
_context: SchemaRenderContext,
|
||||
) {
|
||||
const name = element.key ?? '';
|
||||
return <CellText title={element.required ? `${name} *` : name} />;
|
||||
}
|
||||
|
||||
function renderValueCell(
|
||||
element: SchemaRenderElement,
|
||||
context: SchemaRenderContext,
|
||||
) {
|
||||
if (typeof element.schema === 'boolean') {
|
||||
return <CellText title={element.schema ? 'any' : 'none'} />;
|
||||
function ValueCell({
|
||||
item,
|
||||
context,
|
||||
}: {
|
||||
item: SchemaTableItem;
|
||||
context: SchemaRenderContext;
|
||||
}) {
|
||||
if (typeof item.schema === 'boolean') {
|
||||
return <CellText title={item.schema ? 'any' : 'none'} />;
|
||||
}
|
||||
const types = getTypes(element.schema);
|
||||
const types = getTypes(item.schema);
|
||||
const [isExpanded, setIsExpanded] = context.expanded;
|
||||
const id = generateId(element, context);
|
||||
const info = inspectSchema(element.schema);
|
||||
const description = element.schema.description;
|
||||
const id = generateId(item, context);
|
||||
const info = inspectSchema(item.schema);
|
||||
const description = item.schema.description;
|
||||
return (
|
||||
<Cell>
|
||||
<Flex direction="column" gap="1">
|
||||
@@ -470,7 +436,7 @@ function renderValueCell(
|
||||
)}
|
||||
</Flex>
|
||||
{description && <MarkdownContent content={description} />}
|
||||
<RenderExpansion element={element} context={context} />
|
||||
<RenderExpansion item={item} context={context} />
|
||||
</Flex>
|
||||
</Cell>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user