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:
@@ -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`.
|
||||
@@ -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)
|
||||
|
||||
+16
@@ -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>
|
||||
);
|
||||
|
||||
+56
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+37
-30
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user