scaffolder: fix actions page list and schema rendering issues

Always show descriptions in the action list regardless of selection
state, use MarkdownContent for the detail description, fix the
isRowHeader error by always marking the first column, and move schema
expansion content outside of the table to avoid broken nesting and
scroll-jump issues. Subschemas (anyOf/oneOf) now render at full width
inside a Flex column layout.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-28 17:42:02 +01:00
parent 338f7e01f4
commit 16a15f276e
2 changed files with 102 additions and 132 deletions
@@ -23,9 +23,10 @@ import {
EmptyState,
ErrorPanel,
Header,
MarkdownContent,
Page,
} from '@backstage/core-components';
import { Box, Flex, List, ListRow, SearchField, Text } from '@backstage/ui';
import { Flex, List, ListRow, SearchField, Text } from '@backstage/ui';
import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/alpha';
import { useNavigate } from 'react-router-dom';
import {
@@ -185,9 +186,7 @@ export const ActionPageContent = () => {
key={action.id}
id={action.id}
textValue={action.id}
description={
selectedAction ? undefined : action.description ?? undefined
}
description={action.description ?? undefined}
>
{action.id}
</ListRow>
@@ -195,9 +194,13 @@ export const ActionPageContent = () => {
</List>
);
if (!selectedAction) {
return (
<Flex direction="column" gap="4">
return (
<Flex gap="6">
<Flex
direction="column"
gap="4"
style={selectedAction ? { minWidth: 320, maxWidth: 320 } : undefined}
>
<SearchField
aria-label={t('actionsPage.content.searchFieldPlaceholder')}
placeholder={t('actionsPage.content.searchFieldPlaceholder')}
@@ -206,35 +209,19 @@ export const ActionPageContent = () => {
/>
{listElement}
</Flex>
);
}
return (
<Flex gap="6">
<Box minWidth="320px" maxWidth="320px">
<Flex direction="column" gap="4">
<SearchField
aria-label={t('actionsPage.content.searchFieldPlaceholder')}
placeholder={t('actionsPage.content.searchFieldPlaceholder')}
value={searchQuery}
onChange={setSearchQuery}
/>
{listElement}
</Flex>
</Box>
<Flex direction="column" gap="3" style={{ flex: 1, minWidth: 0 }}>
<Flex direction="column" gap="1">
<Text as="h2" variant="title-medium" weight="bold">
{selectedAction.id}
</Text>
{selectedAction.description && (
<Text as="p" variant="body-medium" color="secondary">
{selectedAction.description}
{selectedAction && (
<Flex direction="column" gap="3" style={{ flex: 1, minWidth: 0 }}>
<Flex direction="column" gap="1">
<Text as="h2" variant="title-medium" weight="bold">
{selectedAction.id}
</Text>
)}
{selectedAction.description && (
<MarkdownContent content={selectedAction.description} />
)}
</Flex>
<ActionDetail action={selectedAction} />
</Flex>
<ActionDetail action={selectedAction} />
</Flex>
)}
</Flex>
);
};
@@ -34,7 +34,7 @@ import {
JSONSchema7Definition,
JSONSchema7Type,
} from 'json-schema';
import { FC, Fragment, JSX } from 'react';
import { FC, JSX } from 'react';
import { scaffolderTranslationRef } from '../../translation';
import { SchemaRenderContext, SchemaRenderStrategy } from './types';
@@ -313,111 +313,94 @@ export const RenderSchema = ({
const [isExpanded] = context.expanded;
return (
<>
<Flex direction="column" gap="2">
{columns && elements && (
<TableRoot
data-testid={`${strategy}_${context.parentId}`}
aria-label={`${strategy} schema`}
>
<TableHeader>
{columns.map(col => (
<Column
key={col.key}
id={col.key}
isRowHeader={col.key === 'name'}
defaultWidth={col.width ?? undefined}
>
{col.title}
</Column>
))}
</TableHeader>
<TableBody>
{elements.map(el => {
const id = generateId(el, context);
const info = inspectSchema(el.schema);
const hasDetails =
typeof el.schema !== 'boolean' &&
(info.canSubschema || info.hasEnum);
return (
<Fragment key={id}>
<>
<TableRoot
data-testid={`${strategy}_${context.parentId}`}
aria-label={`${strategy} schema`}
>
<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 => {
const id = generateId(el, context);
return (
<Row
key={id}
id={`${strategy}-row_${id}`}
data-testid={`${strategy}-row_${id}`}
columns={columns}
>
{col => col.render(el, context)}
</Row>
{hasDetails &&
typeof el.schema !== 'boolean' &&
(() => {
const s = el.schema as JSONSchema7;
const isOpen = !getTypes(s) || isExpanded[id];
if (!isOpen) {
return null;
}
return (
<Row
id={`${strategy}-detail_${id}`}
columns={columns}
>
{(col: ColumnDef) =>
col.key === columns![0].key ? (
<Cell
data-testid={`expansion_${id}`}
style={{
gridColumn: `1 / -1`,
padding: '8px 16px',
}}
>
{info.canSubschema && (
<RenderSchema
strategy="properties"
context={{
...context,
parentId: id,
parent: context,
}}
schema={
s.type === 'array'
? (s.items as
| JSONSchema7
| undefined)
: s
}
/>
)}
{info.hasEnum && (
<>
<Text
as="h4"
variant="title-small"
weight="bold"
>
Valid values:
</Text>
<RenderEnum
data-testid={`enum_${id}`}
e={enumFrom(s)!}
/>
</>
)}
</Cell>
) : (
<Cell style={{ display: 'none' }} />
)
}
</Row>
);
})()}
</Fragment>
);
})}
</TableBody>
</TableRoot>
);
})}
</TableBody>
</TableRoot>
{elements.map(el => {
const id = generateId(el, context);
const info = inspectSchema(el.schema);
const hasDetails =
typeof el.schema !== 'boolean' &&
(info.canSubschema || info.hasEnum);
if (!hasDetails || typeof el.schema === 'boolean') {
return null;
}
const s = el.schema as JSONSchema7;
const isOpen = !getTypes(s) || isExpanded[id];
if (!isOpen) {
return null;
}
return (
<div
key={id}
data-testid={`expansion_${id}`}
style={{ paddingLeft: 16 }}
>
{info.canSubschema && (
<RenderSchema
strategy="properties"
context={{
...context,
parentId: id,
parent: context,
}}
schema={
s.type === 'array'
? (s.items as JSONSchema7 | undefined)
: s
}
/>
)}
{info.hasEnum && (
<>
<Text as="h4" variant="title-small" weight="bold">
Valid values:
</Text>
<RenderEnum
data-testid={`enum_${id}`}
e={enumFrom(s)!}
/>
</>
)}
</div>
);
})}
</>
)}
{(Object.keys(subschemas) as Array<keyof subSchemasType>).map(sk => (
<Fragment key={sk}>
<Flex key={sk} direction="column" gap="2">
<Text as="h4" variant="title-small" weight="bold">
{sk}
</Text>
@@ -436,9 +419,9 @@ export const RenderSchema = ({
schema={sub}
/>
))}
</Fragment>
</Flex>
))}
</>
</Flex>
);
}
return undefined;