Merge pull request #20979 from dotboris/fix-structured-metadata-table

Fix `<StructuredMetadataTable>`'s key formatting applying to only root keys and not nested keys
This commit is contained in:
Ben Lambert
2023-11-08 14:24:09 -06:00
committed by GitHub
6 changed files with 151 additions and 49 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/core-components': patch
---
Make the `options.titleFormat` prop of `<StructuredMetadataTable />` apply to all keys including nested ones. Previously, this option would only apply to the root keys of the `metadata` prop.
Document and improve the props of `<StructuredMetadataTable />`. Previously, the `options` prop was `any`.
+31 -18
View File
@@ -120,10 +120,10 @@ export type BottomLinkProps = {
onClick?: (event: React_2.MouseEvent<HTMLAnchorElement>) => void;
};
// Warning: (ae-forgotten-export) The symbol "Props_19" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Props_18" needs to be exported by the entry point index.d.ts
//
// @public
export function Breadcrumbs(props: Props_19): React_2.JSX.Element;
export function Breadcrumbs(props: Props_18): React_2.JSX.Element;
// @public (undocumented)
export type BreadcrumbsClickableTextClassKey = 'root';
@@ -177,11 +177,11 @@ export interface CodeSnippetProps {
text: string;
}
// Warning: (ae-forgotten-export) The symbol "Props_13" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Props_12" needs to be exported by the entry point index.d.ts
//
// @public
export function Content(
props: PropsWithChildren<Props_13>,
props: PropsWithChildren<Props_12>,
): React_2.JSX.Element;
// Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts
@@ -455,10 +455,10 @@ export function GitHubIcon(props: IconComponentProps): React_2.JSX.Element;
// @public (undocumented)
export function GroupIcon(props: IconComponentProps): React_2.JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props_14" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Props_13" needs to be exported by the entry point index.d.ts
//
// @public
export function Header(props: PropsWithChildren<Props_14>): React_2.JSX.Element;
export function Header(props: PropsWithChildren<Props_13>): React_2.JSX.Element;
// @public (undocumented)
export function HeaderActionMenu(
@@ -565,10 +565,10 @@ export type IconLinkVerticalProps = {
// @public (undocumented)
export type IdentityProviders = ('guest' | 'custom' | SignInProviderConfig)[];
// Warning: (ae-forgotten-export) The symbol "Props_15" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Props_14" needs to be exported by the entry point index.d.ts
//
// @public
export function InfoCard(props: Props_15): JSX.Element;
export function InfoCard(props: Props_14): JSX.Element;
// @public (undocumented)
export type InfoCardClassKey =
@@ -796,11 +796,11 @@ export function OverflowTooltip(props: Props_9): React_2.JSX.Element;
// @public (undocumented)
export type OverflowTooltipClassKey = 'container';
// Warning: (ae-forgotten-export) The symbol "Props_16" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Props_15" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "Page" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function Page(props: Props_16): React_2.JSX.Element;
export function Page(props: Props_15): React_2.JSX.Element;
// Warning: (ae-missing-release-tag) "PageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -1126,11 +1126,11 @@ export type SidebarSubmenuProps = {
children: ReactNode;
};
// Warning: (ae-forgotten-export) The symbol "Props_17" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Props_16" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "SignInPage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function SignInPage(props: Props_17): React_2.JSX.Element;
export function SignInPage(props: Props_16): React_2.JSX.Element;
// Warning: (ae-missing-release-tag) "SignInPageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -1223,11 +1223,10 @@ export function StatusWarning(
props: PropsWithChildren<{}>,
): React_2.JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props_12" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "StructuredMetadataTable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function StructuredMetadataTable(props: Props_12): React_2.JSX.Element;
export function StructuredMetadataTable(
props: StructuredMetadataTableProps,
): React_2.JSX.Element;
// Warning: (ae-missing-release-tag) "StructuredMetadataTableListClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -1239,6 +1238,20 @@ export type StructuredMetadataTableListClassKey = 'root';
// @public (undocumented)
export type StructuredMetadataTableNestedListClassKey = 'root';
// @public (undocumented)
export interface StructuredMetadataTableProps {
// (undocumented)
dense?: boolean;
// (undocumented)
metadata: {
[key: string]: any;
};
// (undocumented)
options?: {
titleFormat?: (key: string) => string;
};
}
// @public (undocumented)
export type SubmenuOptions = {
drawerWidthClosed?: number;
@@ -1306,12 +1319,12 @@ export type Tab = {
>;
};
// Warning: (ae-forgotten-export) The symbol "Props_18" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Props_17" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "TabbedCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function TabbedCard(
props: PropsWithChildren<Props_18>,
props: PropsWithChildren<Props_17>,
): React_2.JSX.Element;
// Warning: (ae-missing-release-tag) "TabbedCardClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -71,3 +71,19 @@ export const NotDenseTable = () => (
</InfoCard>
</Wrapper>
);
export const WithoutKeyFormatting = () => (
<Wrapper>
<InfoCard
title="Structured Metadata Table without key formatting"
subheader="Wrapped in InfoCard"
>
<div style={cardContentStyle}>
<StructuredMetadataTable
metadata={metadata}
options={{ titleFormat: key => key }}
/>
</div>
</InfoCard>
</Wrapper>
);
@@ -115,4 +115,60 @@ describe('<StructuredMetadataTable />', () => {
}
});
});
describe('Title formatting', () => {
const metadata = {
testA: 'stuff',
testB: { testC: 'stuff' },
testD: [{ testE: 'stuff' }],
};
it('should make keys human readable', async () => {
const rendered = render(<StructuredMetadataTable metadata={metadata} />);
expect(rendered.queryByText(/^Test A/)).toBeInTheDocument();
expect(rendered.queryByText(/^Test B/)).toBeInTheDocument();
expect(rendered.queryByText(/^Test C/)).toBeInTheDocument();
expect(rendered.queryByText(/^Test D/)).toBeInTheDocument();
expect(rendered.queryByText(/^Test E/)).toBeInTheDocument();
});
it('should be possible to disable it', async () => {
const rendered = render(
<StructuredMetadataTable
metadata={metadata}
options={{ titleFormat: key => key }}
/>,
);
expect(rendered.queryByText(/^testA/)).toBeInTheDocument();
expect(rendered.queryByText(/^testB/)).toBeInTheDocument();
expect(rendered.queryByText(/^testC/)).toBeInTheDocument();
expect(rendered.queryByText(/^testD/)).toBeInTheDocument();
expect(rendered.queryByText(/^testE/)).toBeInTheDocument();
});
it('should be customizable', async () => {
const spongeBobCase = (key: string) =>
key
.split('')
.map((letter, index) => {
if (index % 2 === 0) {
return letter.toLocaleLowerCase('en-US');
}
return letter.toLocaleUpperCase('en-US');
})
.join('');
const rendered = render(
<StructuredMetadataTable
metadata={metadata}
options={{ titleFormat: spongeBobCase }}
/>,
);
expect(rendered.queryByText(/^tEsTa/)).toBeInTheDocument();
expect(rendered.queryByText(/^tEsTb/)).toBeInTheDocument();
expect(rendered.queryByText(/^tEsTc/)).toBeInTheDocument();
expect(rendered.queryByText(/^tEsTd/)).toBeInTheDocument();
expect(rendered.queryByText(/^tEsTe/)).toBeInTheDocument();
});
});
});
@@ -65,9 +65,11 @@ const StyledNestedList = withStyles(nestedListStyle, {
<MetadataList classes={classes}>{children}</MetadataList>
));
function renderList(list: Array<any>, nested?: boolean) {
function renderList(list: Array<any>, options: Options, nested: boolean) {
const values = list.map((item: any, index: number) => (
<MetadataListItem key={index}>{toValue(item)}</MetadataListItem>
<MetadataListItem key={index}>
{toValue(item, options, nested)}
</MetadataListItem>
));
return nested ? (
<StyledNestedList>{values}</StyledNestedList>
@@ -78,19 +80,15 @@ function renderList(list: Array<any>, nested?: boolean) {
function renderMap(
map: { [key: string]: any },
nested?: boolean,
options?: any,
options: Options,
nested: boolean,
) {
const values = Object.keys(map).map(key => {
const value = toValue(map[key], true);
const fmtKey =
options && options.titleFormat
? options.titleFormat(key)
: startCase(key);
const value = toValue(map[key], options, true);
return (
<MetadataListItem key={key}>
<Typography variant="body2" component="span">
{`${fmtKey}: `}
{`${options.titleFormat(key)}: `}
</Typography>
{value}
</MetadataListItem>
@@ -106,8 +104,8 @@ function renderMap(
function toValue(
value: ReactElement | object | Array<any> | boolean,
options?: any,
nested?: boolean,
options: Options,
nested: boolean,
) {
if (React.isValidElement(value)) {
return <Fragment>{value}</Fragment>;
@@ -118,7 +116,7 @@ function toValue(
}
if (Array.isArray(value)) {
return renderList(value, nested);
return renderList(value, options, nested);
}
if (typeof value === 'boolean') {
@@ -131,8 +129,8 @@ function toValue(
</Typography>
);
}
const ItemValue = ({ value, options }: { value: any; options: any }) => (
<Fragment>{toValue(value, options)}</Fragment>
const ItemValue = ({ value, options }: { value: any; options: Options }) => (
<Fragment>{toValue(value, options, false)}</Fragment>
);
const TableItem = ({
@@ -142,35 +140,44 @@ const TableItem = ({
}: {
title: string;
value: any;
options: any;
options: Options;
}) => {
return (
<MetadataTableItem
title={
options && options.titleFormat
? options.titleFormat(title)
: startCase(title)
}
>
<MetadataTableItem title={options.titleFormat(title)}>
<ItemValue value={value} options={options} />
</MetadataTableItem>
);
};
function mapToItems(info: { [key: string]: string }, options: any) {
function mapToItems(info: { [key: string]: string }, options: Options) {
return Object.keys(info).map(key => (
<TableItem key={key} title={key} value={info[key]} options={options} />
));
}
type Props = {
/** @public */
export interface StructuredMetadataTableProps {
metadata: { [key: string]: any };
dense?: boolean;
options?: any;
};
options?: {
/**
* Function to format the keys from the `metadata` object. Defaults to
* startCase from the lodash library.
* @param key - A key within the `metadata`
* @returns Formatted key
*/
titleFormat?: (key: string) => string;
};
}
export function StructuredMetadataTable(props: Props) {
const { metadata, dense = true, options } = props;
const metadataItems = mapToItems(metadata, options || {});
type Options = Required<NonNullable<StructuredMetadataTableProps['options']>>;
/** @public */
export function StructuredMetadataTable(props: StructuredMetadataTableProps) {
const { metadata, dense = true, options = {} } = props;
const metadataItems = mapToItems(metadata, {
titleFormat: startCase,
...options,
});
return <MetadataTable dense={dense}>{metadataItems}</MetadataTable>;
}
@@ -20,7 +20,10 @@ export type {
MetadataTableListClassKey,
MetadataTableListItemClassKey,
} from './MetadataTable';
export { StructuredMetadataTable } from './StructuredMetadataTable';
export {
StructuredMetadataTable,
type StructuredMetadataTableProps,
} from './StructuredMetadataTable';
export type {
StructuredMetadataTableListClassKey,
StructuredMetadataTableNestedListClassKey,