From cc773582342c0ee30b4f17a4c81ea2a82fd0285d Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Tue, 8 Apr 2025 09:19:51 -0400 Subject: [PATCH] use context provider for handling context menu close Signed-off-by: Mark Dunphy --- plugins/catalog-react/report-alpha.api.md | 9 +-- plugins/catalog-react/report.api.md | 21 ++++++ .../EntityContextMenuItemBlueprint.tsx | 17 ++--- .../src/alpha/blueprints/index.ts | 1 - plugins/catalog-react/src/hooks/index.ts | 9 +++ .../src/hooks/useEntityContextMenu.test.tsx | 44 ++++++++++++ .../src/hooks/useEntityContextMenu.tsx | 72 +++++++++++++++++++ .../EntityContextMenu/EntityContextMenu.tsx | 12 ++-- 8 files changed, 159 insertions(+), 26 deletions(-) create mode 100644 plugins/catalog-react/src/hooks/useEntityContextMenu.test.tsx create mode 100644 plugins/catalog-react/src/hooks/useEntityContextMenu.tsx diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 242aa82fa0..6245c7c5c8 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -89,9 +89,7 @@ export const catalogReactTranslationRef: TranslationRef< >; // @alpha (undocumented) -export type ContextMenuItemComponent = ( - props: ContextMenuItemProps, -) => React_2.JSX.Element; +export type ContextMenuItemComponent = (props: {}) => React_2.JSX.Element; // @alpha (undocumented) export const contextMenuItemComponentDataRef: ConfigurableExtensionDataRef< @@ -100,11 +98,6 @@ export const contextMenuItemComponentDataRef: ConfigurableExtensionDataRef< {} >; -// @alpha (undocumented) -export type ContextMenuItemProps = { - onClose: () => void; -}; - // @alpha (undocumented) export function convertLegacyEntityCardExtension( LegacyExtension: ComponentType<{}>, diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index 1e5735bd86..ed7e1e4971 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -243,6 +243,24 @@ export type EntityAutocompletePickerProps< hidden?: boolean; }; +// @public (undocumented) +export type EntityContextMenuContextValue = { + onMenuClose: () => void; +}; + +// @public (undocumented) +export const EntityContextMenuProvider: ( + props: EntityContextMenuProviderProps, +) => React_2.JSX.Element; + +// @public (undocumented) +export interface EntityContextMenuProviderProps { + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + onMenuClose: () => void; +} + // @public export const EntityDisplayName: (props: EntityDisplayNameProps) => JSX.Element; @@ -778,6 +796,9 @@ export function useEntity(): { entity: TEntity; }; +// @public (undocumented) +export function useEntityContextMenu(): EntityContextMenuContextValue; + // @public export function useEntityList< EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx index e76830eaf6..c273fa9f4c 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -23,6 +23,7 @@ import { import MenuItem from '@material-ui/core/MenuItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; +import { useEntityContextMenu } from '../../hooks/useEntityContextMenu'; /** @alpha */ export type UseProps = () => @@ -44,14 +45,7 @@ export type EntityContextMenuItemParams = { }; /** @alpha */ -export type ContextMenuItemProps = { - onClose: () => void; -}; - -/** @alpha */ -export type ContextMenuItemComponent = ( - props: ContextMenuItemProps, -) => React.JSX.Element; +export type ContextMenuItemComponent = (props: {}) => React.JSX.Element; /** @alpha */ export const contextMenuItemComponentDataRef = @@ -66,7 +60,8 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ output: [contextMenuItemComponentDataRef], *factory(params: EntityContextMenuItemParams, { node }) { const loader = async (): Promise => { - return ({ onClose }) => { + return () => { + const { onMenuClose } = useEntityContextMenu(); const { title, ...menuItemProps } = params.useProps(); let handleClick = undefined; @@ -74,9 +69,9 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ handleClick = () => { const result = menuItemProps.onClick(); if (result && 'finally' in result) { - result.finally(onClose); + result.finally(onMenuClose); } else { - onClose(); + onMenuClose(); } }; } diff --git a/plugins/catalog-react/src/alpha/blueprints/index.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts index 89ee947c25..65cc45c946 100644 --- a/plugins/catalog-react/src/alpha/blueprints/index.ts +++ b/plugins/catalog-react/src/alpha/blueprints/index.ts @@ -26,7 +26,6 @@ export type { EntityCardType } from './extensionData'; export { contextMenuItemComponentDataRef, EntityContextMenuItemBlueprint, - type ContextMenuItemProps, type ContextMenuItemComponent, type EntityContextMenuItemParams, type UseProps, diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index befcce520d..96a7315ca5 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -35,6 +35,15 @@ export type { EntityListProviderProps, PaginationMode, } from './useEntityListProvider'; +export type { + EntityContextMenuContextValue, + EntityContextMenuProviderProps, +} from './useEntityContextMenu'; +export { + EntityContextMenuProvider, + useEntityContextMenu, +} from './useEntityContextMenu'; + export { useEntityTypeFilter } from './useEntityTypeFilter'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; diff --git a/plugins/catalog-react/src/hooks/useEntityContextMenu.test.tsx b/plugins/catalog-react/src/hooks/useEntityContextMenu.test.tsx new file mode 100644 index 0000000000..a28c41caa9 --- /dev/null +++ b/plugins/catalog-react/src/hooks/useEntityContextMenu.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + EntityContextMenuProvider, + useEntityContextMenu, +} from './useEntityContextMenu'; +import { renderHook } from '@testing-library/react'; + +describe('useEntityContextMenu', () => { + it('should throw error when used outside of provider', () => { + expect(() => { + renderHook(() => useEntityContextMenu()); + }).toThrow( + 'useEntityContextMenu must be used within an EntityContextMenuProvider', + ); + }); + + it('should return the context value', () => { + const mockOnMenuClose = jest.fn(); + const { result } = renderHook(() => useEntityContextMenu(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(result.current.onMenuClose).toBe(mockOnMenuClose); + }); +}); diff --git a/plugins/catalog-react/src/hooks/useEntityContextMenu.tsx b/plugins/catalog-react/src/hooks/useEntityContextMenu.tsx new file mode 100644 index 0000000000..46db699abd --- /dev/null +++ b/plugins/catalog-react/src/hooks/useEntityContextMenu.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + createVersionedContext, + createVersionedValueMap, + useVersionedContext, +} from '@backstage/version-bridge'; + +/** @public */ +export type EntityContextMenuContextValue = { + onMenuClose: () => void; +}; + +const EntityContextMenuContext = createVersionedContext<{ + 1: EntityContextMenuContextValue; +}>('entity-context-menu-context'); + +/** @public */ +export interface EntityContextMenuProviderProps { + children: React.ReactNode; + onMenuClose: () => void; +} + +/** @public */ +export const EntityContextMenuProvider = ( + props: EntityContextMenuProviderProps, +) => { + const { children, onMenuClose } = props; + const value = { onMenuClose }; + + return ( + + {children} + + ); +}; + +/** @public */ +export function useEntityContextMenu() { + const versionedHolder = useVersionedContext<{ + 1: EntityContextMenuContextValue; + }>('entity-context-menu-context'); + + if (!versionedHolder) { + throw new Error( + 'useEntityContextMenu must be used within an EntityContextMenuProvider', + ); + } + + const value = versionedHolder.atVersion(1); + if (!value) { + throw new Error('EntityContextMenu v1 is not available'); + } + + return value; +} diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index 6bde89c957..a5a4f8d8cc 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -36,6 +36,7 @@ import useCopyToClipboard from 'react-use/esm/useCopyToClipboard'; import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import type { ContextMenuItemComponent } from '@backstage/plugin-catalog-react/alpha'; +import { EntityContextMenuProvider } from '@backstage/plugin-catalog-react'; /** @public */ export type EntityContextMenuClassKey = 'button'; @@ -187,12 +188,11 @@ export function EntityContextMenu(props: EntityContextMenuProps) { ) : ( - contextMenuItems.map((ContextMenuItem, idx) => ( - - )) + + {contextMenuItems.map((ContextMenuItem, idx) => ( + + ))} + )}