From 50ce1645abecb81b5765613371cc7db7173d48d1 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Thu, 27 Mar 2025 10:20:37 -0400 Subject: [PATCH] implement useProps api Signed-off-by: Mark Dunphy --- plugins/catalog-react/report-alpha.api.md | 11 ++- .../EntityContextMenuItemBlueprint.test.tsx | 24 +++--- .../EntityContextMenuItemBlueprint.tsx | 54 ++++++------- .../catalog/src/alpha/contextMenuItems.tsx | 80 +++++++++---------- plugins/catalog/src/alpha/pages.test.tsx | 50 ++++++++---- 5 files changed, 114 insertions(+), 105 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index c10c982dcc..e67bbb537c 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -11,6 +11,8 @@ import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; +import { MenuItemProps } from '@material-ui/core/MenuItem'; +import { default as React_2 } from 'react'; import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; @@ -359,9 +361,12 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ }>; // @alpha (undocumented) -export type EntityContextMenuItemParams = - | FactoryHrefParams - | FactoryDialogParams; +export type EntityContextMenuItemParams = { + useProps: () => Omit & { + onClick?: () => Promise; + }; + icon: React_2.JSX.Element; +}; // @alpha (undocumented) export const EntityHeaderBlueprint: ExtensionBlueprint<{ diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index 9a56c08b94..29d6cc11d8 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx @@ -16,28 +16,24 @@ import React from 'react'; import { EntityContextMenuItemBlueprint } from './EntityContextMenuItemBlueprint'; -import { createRouteRef, useRouteRef } from '@backstage/frontend-plugin-api'; describe('EntityContextMenuItemBlueprint', () => { - const routeRef = createRouteRef(); const data = [ { - useTitle: () => 'Test', - href: '/somewhere', icon: Test, + useProps: () => ({ + title: 'Test', + href: '/somewhere', + component: 'a', + disabled: true, + }), }, { - useTitle: () => 'Test', - useHref: () => { - const r = useRouteRef(routeRef) ?? (() => '/somewhere'); - return r(); - }, - icon: Test, - }, - { - useTitle: () => 'TestDialog', - useOnClick: () => () => true, icon: Test, + useProps: () => ({ + title: 'Test', + onClick: async () => {}, + }), }, ]; diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx index 89448680a2..02e9428c87 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -20,7 +20,7 @@ import { createExtensionDataRef, ExtensionBoundary, } from '@backstage/frontend-plugin-api'; -import MenuItem from '@material-ui/core/MenuItem'; +import MenuItem, { MenuItemProps } from '@material-ui/core/MenuItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; @@ -48,9 +48,12 @@ export type FactoryDialogParams = { }; /** @alpha */ -export type EntityContextMenuItemParams = - | FactoryHrefParams - | FactoryDialogParams; +export type EntityContextMenuItemParams = { + useProps: () => Omit & { + onClick?: () => Promise; + }; + icon: React.JSX.Element; +}; /** @alpha */ export type ContextMenuItemProps = { @@ -75,36 +78,25 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ output: [contextMenuItemComponentDataRef], *factory(params: EntityContextMenuItemParams, { node }) { const loader = async (): Promise => { - if ('useOnClick' in params) { - return ({ onClose }) => { - const onClick = params.useOnClick(); - const title = params.useTitle(); - const disabled = params.useIsDisabled?.() ?? false; + return ({ onClose }) => { + const { + children, + button, + title, + onClick: onClickProp, + ...menuItemProps + } = params.useProps(); + let onClick; - return ( - { - onClose(); - onClick(e); - }} - > - {params.icon} - - - ); - }; - } - - const useHref = 'useHref' in params ? params.useHref : () => params.href; - - return () => { - const href = useHref(); - const title = params.useTitle(); - const disabled = params.useIsDisabled?.() ?? false; + if (onClickProp !== undefined) { + onClick = async () => { + await onClickProp(); + onClose(); + }; + } return ( - + {params.icon} diff --git a/plugins/catalog/src/alpha/contextMenuItems.tsx b/plugins/catalog/src/alpha/contextMenuItems.tsx index a5148dbfc6..5c503313cf 100644 --- a/plugins/catalog/src/alpha/contextMenuItems.tsx +++ b/plugins/catalog/src/alpha/contextMenuItems.tsx @@ -43,11 +43,7 @@ export const copyEntityUrlContextMenuItem = EntityContextMenuItemBlueprint.make( name: 'copy-entity-url', params: { icon: , - useTitle: () => { - const { t } = useTranslationRef(catalogTranslationRef); - return t('entityContextMenu.copyURLMenuTitle'); - }, - useOnClick: () => { + useProps: () => { const [copyState, copyToClipboard] = useCopyToClipboard(); const alertApi = useApi(alertApiRef); const { t } = useTranslationRef(catalogTranslationRef); @@ -62,8 +58,11 @@ export const copyEntityUrlContextMenuItem = EntityContextMenuItemBlueprint.make( } }, [copyState, alertApi, t]); - return async () => { - copyToClipboard(window.location.toString()); + return { + title: t('entityContextMenu.copyURLMenuTitle'), + onClick: async () => { + copyToClipboard(window.location.toString()); + }, }; }, }, @@ -75,15 +74,15 @@ export const inspectEntityContextMenuItem = EntityContextMenuItemBlueprint.make( name: 'inspect-entity', params: { icon: , - useTitle: () => { - const { t } = useTranslationRef(catalogTranslationRef); - return t('entityContextMenu.inspectMenuTitle'); - }, - useOnClick: () => { + useProps: () => { const [_, setSearchParams] = useSearchParams(); + const { t } = useTranslationRef(catalogTranslationRef); - return () => { - setSearchParams('inspect'); + return { + title: t('entityContextMenu.inspectMenuTitle'), + onClick: async () => { + setSearchParams('inspect'); + }, }; }, }, @@ -95,40 +94,37 @@ export const unregisterEntityContextMenuItem = name: 'unregister-entity', params: { icon: , - useTitle: () => { - const { t } = useTranslationRef(catalogTranslationRef); - return t('entityContextMenu.unregisterMenuTitle'); - }, - useIsDisabled: () => { - const unregisterPermission = useEntityPermission( - catalogEntityDeletePermission, - ); - - return !unregisterPermission.allowed; - }, - useOnClick: () => { + useProps: () => { const { entity } = useEntity(); const dialogApi = useApi(dialogApiRef); const navigate = useNavigate(); const catalogRoute = useRouteRef(rootRouteRef); + const { t } = useTranslationRef(catalogTranslationRef); const unregisterRedirectRoute = useRouteRef(unregisterRedirectRouteRef); + const unregisterPermission = useEntityPermission( + catalogEntityDeletePermission, + ); - return async () => { - dialogApi.showModal(({ dialog }: { dialog: DialogApiDialog }) => ( - dialog.close()} - onConfirm={() => { - dialog.close(); - navigate( - unregisterRedirectRoute - ? unregisterRedirectRoute() - : catalogRoute(), - ); - }} - /> - )); + return { + title: t('entityContextMenu.unregisterMenuTitle'), + disabled: !unregisterPermission.allowed, + onClick: async () => { + dialogApi.showModal(({ dialog }: { dialog: DialogApiDialog }) => ( + dialog.close()} + onConfirm={() => { + dialog.close(); + navigate( + unregisterRedirectRoute + ? unregisterRedirectRoute() + : catalogRoute(), + ); + }} + /> + )); + }, }; }, }, diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 0d18dd9dfa..f5aeb7d196 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -605,15 +605,26 @@ describe('Entity page', () => { }); it.each([ - { useIsDisabled: () => true, href: '/somewhere' }, - { useIsDisabled: () => false, href: '/somewhere' }, - { useIsDisabled: () => true, useHref: () => '/somewhere' }, - { useIsDisabled: () => false, useHref: () => '/somewhere' }, + { + useProps: () => ({ + title: 'Test Title', + href: '/somewhere', + disabled: true, + component: 'a', + }), + }, + { + useProps: () => ({ + title: 'Test Title', + href: '/somewhere', + disabled: false, + component: 'a', + }), + }, ])('should render an href based context menu item', async params => { const menuItem = EntityContextMenuItemBlueprint.make({ name: 'test-href', params: { - useTitle: () => 'Test Title', icon: Test Icon, ...params, }, @@ -645,6 +656,7 @@ describe('Entity page', () => { }, }, ); + const { disabled } = params.useProps(); await waitFor(async () => { await userEvent.click(screen.getByTestId('menu-button')); @@ -652,21 +664,29 @@ describe('Entity page', () => { expect(screen.getByText('Test Icon')).toBeInTheDocument(); const anchor = screen.getByText('Test Title').closest('a'); expect(anchor).toHaveAttribute('href', '/somewhere'); - expect(anchor).toHaveAttribute( - 'aria-disabled', - params.useIsDisabled().toString(), - ); + expect(anchor).toHaveAttribute('aria-disabled', disabled.toString()); }); }); it.each([ - { useIsDisabled: () => true, useOnClick: () => onClickMock }, - { useIsDisabled: () => false, useOnClick: () => onClickMock }, - ])('should render a useOnClick based context menu item', async params => { + { + useProps: () => ({ + title: 'Test Title', + onClick: onClickMock, + disabled: true, + }), + }, + { + useProps: () => ({ + title: 'Test Title', + onClick: onClickMock, + disabled: false, + }), + }, + ])('should render an onClick based context menu item', async params => { const menuItem = EntityContextMenuItemBlueprint.make({ - name: 'test-href', + name: 'test-click', params: { - useTitle: () => 'Test Title', icon: Test Icon, ...params, }, @@ -674,7 +694,6 @@ describe('Entity page', () => { const tester = createExtensionTester( Object.assign({ namespace: 'catalog' }, catalogEntityPage), ).add(menuItem); - const disabled = params.useIsDisabled(); renderInTestApp( { }, ); + const { disabled } = params.useProps(); await waitFor(async () => { await userEvent.click(screen.getByTestId('menu-button')); expect(screen.getByText('Test Title')).toBeInTheDocument();