diff --git a/.changeset/catalog-dialog-api-open.md b/.changeset/catalog-dialog-api-open.md new file mode 100644 index 0000000000..d27828e57c --- /dev/null +++ b/.changeset/catalog-dialog-api-open.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Migrated the unregister entity context menu item from the deprecated `DialogApi.showModal` to the new `DialogApi.open` method. diff --git a/.changeset/dialog-api-open-implementation.md b/.changeset/dialog-api-open-implementation.md new file mode 100644 index 0000000000..1d0ee4f38b --- /dev/null +++ b/.changeset/dialog-api-open-implementation.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': patch +--- + +Updated the default `DialogApi` implementation to support the new `open` method. The dialog display layer no longer renders any dialog chrome — callers provide their own dialog component. The deprecated `show` and `showModal` methods now use `open` internally with a Material UI dialog wrapper for backward compatibility. diff --git a/.changeset/dialog-api-open-method.md b/.changeset/dialog-api-open-method.md new file mode 100644 index 0000000000..43caede3de --- /dev/null +++ b/.changeset/dialog-api-open-method.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added `open` method to `DialogApi` that renders dialogs without any built-in dialog chrome, giving the caller full control over the dialog presentation. This avoids focus trap conflicts that occur when mixing components from different design libraries. The existing `show` and `showModal` methods are now deprecated in favor of `open`. diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 9752eaa6c8..4b29101634 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -907,6 +907,12 @@ export function createTranslationResource< // @public export interface DialogApi { + open( + elementOrComponent: + | JSX.Element + | ((props: { dialog: DialogApiDialog }) => JSX.Element), + ): DialogApiDialog; + // @deprecated show( elementOrComponent: | JSX.Element @@ -914,6 +920,7 @@ export interface DialogApi { dialog: DialogApiDialog; }) => JSX.Element), ): DialogApiDialog; + // @deprecated showModal( elementOrComponent: | JSX.Element diff --git a/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts b/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts index bcb92f528a..aa8d4da9fc 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/DialogApi.ts @@ -21,24 +21,31 @@ import { createApiRef } from '../system'; * * @remarks * - * Dialogs can be opened using either {@link DialogApi.show} or {@link DialogApi.showModal}. + * Dialogs are opened using {@link DialogApi.open}. * * @public */ export interface DialogApiDialog { /** - * Closes the dialog with that provided result. + * Closes the dialog with the provided result. * * @remarks * - * If the dialog is a modal dialog a result must always be provided. If it's a regular dialog then passing a result is optional. + * Whether a result is required depends on the `TResult` type parameter + * chosen when the dialog was opened. If the type includes `undefined`, + * calling `close()` without a result is allowed. */ close( ...args: undefined extends TResult ? [result?: TResult] : [result: TResult] ): void; /** - * Replaces the content of the dialog with the provided element or component, causing it to be rerenedered. + * Replaces the rendered dialog with the provided element or component. + * + * @remarks + * + * Just like the element or component passed to {@link DialogApi.open}, the + * caller is responsible for providing the full dialog including all chrome. */ update( elementOrComponent: @@ -48,10 +55,6 @@ export interface DialogApiDialog { /** * Wait until the dialog is closed and return the result. - * - * @remarks - * - * If the dialog is a modal dialog a result will always be returned. If it's a regular dialog then the result may be `undefined`. */ result(): Promise; } @@ -63,51 +66,67 @@ export interface DialogApiDialog { */ export interface DialogApi { /** - * Opens a modal dialog and returns a handle to it. + * Opens a dialog and returns a handle to it. * * @remarks * - * This dialog can be closed by calling the `close` method on the returned handle, optionally providing a result. - * The dialog can also be closed by the user by clicking the backdrop or pressing the escape key. - * - * If the dialog is closed without a result, the result will be `undefined`. + * The provided element or component is rendered as-is in the app's React tree. + * It is the caller's responsibility to provide all dialog chrome, such as an + * overlay, backdrop, and dialog surface. This makes the method agnostic to the + * design library used for the dialog. * * @example * - * ### Example with inline dialog content + * ### Example with inline dialog element * ```tsx - * const dialog = dialogApi.show( - * - * Are you sure? - * - * - * - * - * + * const dialog = dialogApi.open( + * !isOpen && dialog.close()}> + * Are you sure? + * This action cannot be undone. + * + * + * + * + * * ); * const result = await dialog.result(); * ``` * * @example * - * ### Example with separate dialog component + * ### Example with a dialog component * ```tsx - * function CustomDialog({ dialog }: { dialog: DialogApiDialog }) { + * function ConfirmDialog({ dialog }: { dialog: DialogApiDialog }) { * return ( - * - * Are you sure? - * - * - * - * - * - * ) + * !isOpen && dialog.close()}> + * Are you sure? + * This action cannot be undone. + * + * + * + * + * + * ); * } - * const result = await dialogApi.show(CustomDialog).result(); + * const result = await dialogApi.open(ConfirmDialog).result(); * ``` * + * @param elementOrComponent - The element or component to render. If a component is provided, it will be provided with a `dialog` prop that contains the dialog handle. + */ + open( + elementOrComponent: + | JSX.Element + | ((props: { dialog: DialogApiDialog }) => JSX.Element), + ): DialogApiDialog; + + /** + * Opens a dialog with built-in dialog chrome and returns a handle to it. + * + * @deprecated Use {@link DialogApi.open} instead. The `open` method does not + * render any dialog chrome, giving the caller full control over the dialog + * presentation. This avoids focus trap conflicts across design libraries. + * * @param elementOrComponent - The element or component to render in the dialog. If a component is provided, it will be provided with a `dialog` prop that contains the dialog handle. - * @public */ show( elementOrComponent: @@ -118,48 +137,13 @@ export interface DialogApi { ): DialogApiDialog; /** - * Opens a modal dialog and returns a handle to it. + * Opens a modal dialog with built-in dialog chrome and returns a handle to it. * - * @remarks - * - * This dialog can not be closed in any other way than calling the `close` method on the returned handle and providing a result. - * - * @example - * - * ### Example with inline dialog content - * ```tsx - * const dialog = dialogApi.showModal( - * - * Are you sure? - * - * - * - * - * - * ); - * const result = await dialog.result(); - * ``` - * - * @example - * - * ### Example with separate dialog component - * ```tsx - * function CustomDialog({ dialog }: { dialog: DialogApiDialog }) { - * return ( - * - * Are you sure? - * - * - * - * - * - * ) - * } - * const result = await dialogApi.showModal(CustomDialog).result(); - * ``` + * @deprecated Use {@link DialogApi.open} instead. The `open` method does not + * render any dialog chrome, giving the caller full control over the dialog + * presentation. This avoids focus trap conflicts across design libraries. * * @param elementOrComponent - The element or component to render in the dialog. If a component is provided, it will be provided with a `dialog` prop that contains the dialog handle. - * @public */ showModal( elementOrComponent: diff --git a/plugins/app/src/apis/DefaultDialogApi.ts b/plugins/app/src/apis/DefaultDialogApi.ts deleted file mode 100644 index 49fbf1c5e6..0000000000 --- a/plugins/app/src/apis/DefaultDialogApi.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 { DialogApi, DialogApiDialog } from '@backstage/frontend-plugin-api'; - -export type OnShowDialog = (options: { - component: (props: { dialog: DialogApiDialog }) => React.JSX.Element; - modal: boolean; -}) => DialogApiDialog; - -/** - * Default implementation for the {@link DialogApi}. - * @internal - */ -export class DefaultDialogApi implements DialogApi { - #onShow?: OnShowDialog; - - show( - elementOrComponent: - | JSX.Element - | ((props: { - dialog: DialogApiDialog; - }) => JSX.Element), - ): DialogApiDialog { - if (!this.#onShow) { - throw new Error('Dialog API has not been connected'); - } - return this.#onShow({ - component: - typeof elementOrComponent === 'function' - ? elementOrComponent - : () => elementOrComponent, - modal: false, - }) as DialogApiDialog; - } - - showModal( - elementOrComponent: - | JSX.Element - | ((props: { dialog: DialogApiDialog }) => JSX.Element), - ): DialogApiDialog { - if (!this.#onShow) { - throw new Error('Dialog API has not been connected'); - } - return this.#onShow({ - component: - typeof elementOrComponent === 'function' - ? elementOrComponent - : () => elementOrComponent, - modal: true, - }) as DialogApiDialog; - } - - connect(onShow: OnShowDialog): void { - this.#onShow = onShow; - } -} diff --git a/plugins/app/src/apis/DefaultDialogApi.tsx b/plugins/app/src/apis/DefaultDialogApi.tsx new file mode 100644 index 0000000000..c683cc5293 --- /dev/null +++ b/plugins/app/src/apis/DefaultDialogApi.tsx @@ -0,0 +1,141 @@ +/* + * 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 { DialogApi, DialogApiDialog } from '@backstage/frontend-plugin-api'; +import Dialog from '@material-ui/core/Dialog'; + +export type OnOpenDialog = (options: { + component: (props: { dialog: DialogApiDialog }) => JSX.Element; +}) => DialogApiDialog; + +/** + * Default implementation for the {@link DialogApi}. + * @internal + */ +export class DefaultDialogApi implements DialogApi { + #onOpen?: OnOpenDialog; + + open( + elementOrComponent: + | JSX.Element + | ((props: { dialog: DialogApiDialog }) => JSX.Element), + ): DialogApiDialog { + if (!this.#onOpen) { + throw new Error('Dialog API has not been connected'); + } + return this.#onOpen({ + component: + typeof elementOrComponent === 'function' + ? elementOrComponent + : () => elementOrComponent, + }) as DialogApiDialog; + } + + /** @deprecated Use {@link DefaultDialogApi.open} instead */ + show( + elementOrComponent: + | JSX.Element + | ((props: { + dialog: DialogApiDialog; + }) => JSX.Element), + ): DialogApiDialog { + // eslint-disable-next-line no-console + console.warn( + 'DialogApi.show() is deprecated and will be removed in a future release. Use DialogApi.open() instead.', + ); + const innerDialog = this.open(({ dialog }) => ( + + )); + return wrapDialogHandle(innerDialog, false); + } + + /** @deprecated Use {@link DefaultDialogApi.open} instead */ + showModal( + elementOrComponent: + | JSX.Element + | ((props: { dialog: DialogApiDialog }) => JSX.Element), + ): DialogApiDialog { + // eslint-disable-next-line no-console + console.warn( + 'DialogApi.showModal() is deprecated and will be removed in a future release. Use DialogApi.open() instead.', + ); + const innerDialog = this.open(({ dialog }) => ( + + )); + return wrapDialogHandle(innerDialog, true); + } + + connect(onOpen: OnOpenDialog): void { + this.#onOpen = onOpen; + } +} + +function DeprecatedMuiDialogWrapper({ + dialog, + content, + modal, +}: { + dialog: DialogApiDialog; + content: + | JSX.Element + | ((props: { dialog: DialogApiDialog }) => JSX.Element); + modal: boolean; +}) { + if (typeof content === 'function') { + const Content = content; + return ( + dialog.close()}> + + + ); + } + return ( + dialog.close()}> + {content} + + ); +} + +function wrapDialogHandle( + innerDialog: DialogApiDialog, + modal: boolean, +): DialogApiDialog { + return { + close(...args: any[]) { + (innerDialog.close as any)(...args); + }, + result() { + return innerDialog.result(); + }, + update(newContent: any) { + innerDialog.update(({ dialog }: { dialog: DialogApiDialog }) => ( + + )); + }, + }; +} diff --git a/plugins/app/src/extensions/DialogDisplay.test.tsx b/plugins/app/src/extensions/DialogDisplay.test.tsx index dbad551fcb..4c0ca73f5d 100644 --- a/plugins/app/src/extensions/DialogDisplay.test.tsx +++ b/plugins/app/src/extensions/DialogDisplay.test.tsx @@ -16,6 +16,7 @@ import { renderTestApp } from '@backstage/frontend-test-utils'; import { act, useEffect } from 'react'; +import { screen } from '@testing-library/react'; import { AppRootElementBlueprint, DialogApi, @@ -49,11 +50,11 @@ async function withDialogApi( } describe('DialogDisplay', () => { - function AutoDialog({ + function AutoCloseDialog({ dialog, result, }: { - dialog: DialogApiDialog; + dialog: DialogApiDialog; result?: string; }) { useEffect(() => { @@ -64,112 +65,219 @@ describe('DialogDisplay', () => { return
; } - it('should render a simple dialog', async () => { - const result = await withDialogApi(async dialogApi => { - const dialog = await act(() => dialogApi.show(
Test
)); - dialog.close('test'); - return dialog.result(); - }); - expect(result).toBe('test'); - }); - - it('should allow dialog to be updated', async () => { - const result = await withDialogApi(async dialogApi => { - const dialog = await act(() => dialogApi.show(AutoDialog)); - - setTimeout(async () => { - await act(async () => { - dialog.update(props => ); - }); - }, 100); - - return dialog.result(); + describe('open', () => { + it('should render a dialog and return a result', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.open(
Test
)); + dialog.close('test'); + return dialog.result(); + }); + expect(result).toBe('test'); }); - expect(result).toBe('test2'); - }); - - it('should allow dialog to be closed by pressing escape', async () => { - const result = await withDialogApi(async dialogApi => { - const dialog = await act(() => dialogApi.show(AutoDialog)); - - setTimeout(async () => { - await userEvent.keyboard('{Escape}'); - }, 100); - - return dialog.result(); + it('should render dialog content directly without wrapper chrome', async () => { + await withDialogApi(async dialogApi => { + await act(() => + dialogApi.open(
Hello
), + ); + expect(await screen.findByTestId('bare-content')).toBeInTheDocument(); + return undefined; + }); }); - expect(result).toBe(undefined); - }); - - it('should allow a stack of dialogs', async () => { - const result = await withDialogApi(async dialogApi => { - const dialog1 = await act(() => dialogApi.show(AutoDialog)); - const dialog2 = await act(() => dialogApi.show(AutoDialog)); - const dialog3 = await act(() => dialogApi.show(AutoDialog)); - - setTimeout(async () => { - await act(async () => { - dialog3.close('test3'); - dialog1.close('test1'); - dialog2.close('test2'); - }); - }, 100); - - return Promise.all([ - dialog1.result(), - dialog2.result(), - dialog3.result(), - ]); - }); - - expect(result).toEqual(['test1', 'test2', 'test3']); - }); - - it('should only cancel one dialog at a time', async () => { - const result = await withDialogApi(async dialogApi => { - const dialog1 = await act(() => dialogApi.show(AutoDialog)); - const dialog2 = await act(() => dialogApi.show(AutoDialog)); - const dialog3 = await act(() => dialogApi.show(AutoDialog)); - - setTimeout(async () => { - await userEvent.keyboard('{Escape}'); - - await act(async () => { - dialog1.close('test1'); - dialog2.close('test2'); - dialog3.close('test3'); - }); - }, 100); - - return Promise.all([ - dialog1.result(), - dialog2.result(), - dialog3.result(), - ]); - }); - - expect(result).toEqual(['test1', 'test2', undefined]); - }); - - it('should not allow modal dialog to be closed by pressing escape', async () => { - const result = await withDialogApi(async dialogApi => { - const dialog = await act(() => dialogApi.showModal(AutoDialog)); - - setTimeout(async () => { - await userEvent.keyboard('{Escape}'); + it('should allow dialog to be updated', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.open(AutoCloseDialog)); setTimeout(async () => { await act(async () => { - dialog.close('test'); + dialog.update(props => ( + + )); }); }, 100); - }, 100); - return dialog.result(); + return dialog.result(); + }); + + expect(result).toBe('test2'); }); - expect(result).toBe('test'); + it('should allow a stack of dialogs, rendering only the most recent', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog1 = await act(() => + dialogApi.open(AutoCloseDialog), + ); + const dialog2 = await act(() => + dialogApi.open(AutoCloseDialog), + ); + const dialog3 = await act(() => + dialogApi.open(AutoCloseDialog), + ); + + setTimeout(async () => { + await act(async () => { + dialog3.close('test3'); + dialog1.close('test1'); + dialog2.close('test2'); + }); + }, 100); + + return Promise.all([ + dialog1.result(), + dialog2.result(), + dialog3.result(), + ]); + }); + + expect(result).toEqual(['test1', 'test2', 'test3']); + }); + + it('should accept a component that receives the dialog handle', async () => { + function TestDialog({ dialog }: { dialog: DialogApiDialog }) { + return ( + + ); + } + + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.open(TestDialog)); + + setTimeout(async () => { + const button = await screen.findByRole('button', { name: 'Close' }); + await userEvent.click(button); + }, 100); + + return dialog.result(); + }); + + expect(result).toBe('from-component'); + }); + }); + + describe('deprecated show', () => { + function AutoDialog({ + dialog, + result, + }: { + dialog: DialogApiDialog; + result?: string; + }) { + useEffect(() => { + if (result) { + dialog.close(result); + } + }, [dialog, result]); + return
; + } + + it('should render a simple dialog', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.show(
Test
)); + dialog.close('test'); + return dialog.result(); + }); + expect(result).toBe('test'); + }); + + it('should allow dialog to be updated', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.show(AutoDialog)); + + setTimeout(async () => { + await act(async () => { + dialog.update(props => ); + }); + }, 100); + + return dialog.result(); + }); + + expect(result).toBe('test2'); + }); + + it('should allow dialog to be closed by pressing escape', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.show(AutoDialog)); + + setTimeout(async () => { + await userEvent.keyboard('{Escape}'); + }, 100); + + return dialog.result(); + }); + + expect(result).toBe(undefined); + }); + + it('should allow a stack of dialogs', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog1 = await act(() => dialogApi.show(AutoDialog)); + const dialog2 = await act(() => dialogApi.show(AutoDialog)); + const dialog3 = await act(() => dialogApi.show(AutoDialog)); + + setTimeout(async () => { + await act(async () => { + dialog3.close('test3'); + dialog1.close('test1'); + dialog2.close('test2'); + }); + }, 100); + + return Promise.all([ + dialog1.result(), + dialog2.result(), + dialog3.result(), + ]); + }); + + expect(result).toEqual(['test1', 'test2', 'test3']); + }); + + it('should only cancel one dialog at a time', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog1 = await act(() => dialogApi.show(AutoDialog)); + const dialog2 = await act(() => dialogApi.show(AutoDialog)); + const dialog3 = await act(() => dialogApi.show(AutoDialog)); + + setTimeout(async () => { + await userEvent.keyboard('{Escape}'); + + await act(async () => { + dialog1.close('test1'); + dialog2.close('test2'); + dialog3.close('test3'); + }); + }, 100); + + return Promise.all([ + dialog1.result(), + dialog2.result(), + dialog3.result(), + ]); + }); + + expect(result).toEqual(['test1', 'test2', undefined]); + }); + + it('should not allow modal dialog to be closed by pressing escape', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.showModal(AutoDialog)); + + setTimeout(async () => { + await userEvent.keyboard('{Escape}'); + + setTimeout(async () => { + await act(async () => { + dialog.close('test'); + }); + }, 100); + }, 100); + + return dialog.result(); + }); + + expect(result).toBe('test'); + }); }); }); diff --git a/plugins/app/src/extensions/DialogDisplay.tsx b/plugins/app/src/extensions/DialogDisplay.tsx index 688641650e..10f1371ba3 100644 --- a/plugins/app/src/extensions/DialogDisplay.tsx +++ b/plugins/app/src/extensions/DialogDisplay.tsx @@ -22,8 +22,7 @@ import { dialogApiRef, } from '@backstage/frontend-plugin-api'; import { createDeferred } from '@backstage/types'; -import { OnShowDialog } from '../apis/DefaultDialogApi'; -import Dialog from '@material-ui/core/Dialog'; +import { OnOpenDialog } from '../apis/DefaultDialogApi'; let dialogId = 0; function getDialogId() { @@ -33,20 +32,25 @@ function getDialogId() { type DialogState = DialogApiDialog & { id: string; - modal: boolean; }; /** * The other half of the default implementation of the {@link DialogApi}. * - * This component is responsible for rendering the dialogs in the React tree and managing a stack of dialogs. - * It expects the implementation of the {@link DialogApi} to be the `DefaultDialogApi`. If one is replaced the other must be too. + * This component is responsible for rendering the dialogs in the React tree + * and managing a stack of dialogs. It renders only the most recently opened + * dialog, without any dialog chrome — the caller is expected to provide their + * own dialog component (overlay, backdrop, surface, etc.). + * + * It expects the implementation of the {@link DialogApi} to be the + * `DefaultDialogApi`. If one is replaced the other must be too. + * * @internal */ function DialogDisplay({ dialogApi, }: { - dialogApi: DialogApi & { connect(onShow: OnShowDialog): void }; + dialogApi: DialogApi & { connect(onOpen: OnOpenDialog): void }; }) { const [dialogs, setDialogs] = useState< { dialog: DialogState; element: React.JSX.Element }[] @@ -58,7 +62,6 @@ function DialogDisplay({ const deferred = createDeferred(); const dialog: DialogState = { id, - modal: options.modal, close(result) { deferred.resolve(result); setDialogs(ds => ds.filter(d => d.dialog.id !== id)); @@ -85,19 +88,7 @@ function DialogDisplay({ }, [dialogApi]); if (dialogs.length > 0) { - const lastDialog = dialogs[dialogs.length - 1]; - return ( - { - if (!lastDialog.dialog.modal) { - lastDialog.dialog.close(); - } - }} - > - {lastDialog.element} - - ); + return dialogs[dialogs.length - 1].element; } return null; @@ -121,7 +112,7 @@ export const dialogDisplayAppRootElement = function isInternalDialogApi( dialogApi?: DialogApi, -): dialogApi is DialogApi & { connect(onShow: OnShowDialog): void } { +): dialogApi is DialogApi & { connect(onOpen: OnOpenDialog): void } { if (!dialogApi) { return false; } diff --git a/plugins/catalog/src/alpha/contextMenuItems.tsx b/plugins/catalog/src/alpha/contextMenuItems.tsx index 4b37321c6f..24dfe037ab 100644 --- a/plugins/catalog/src/alpha/contextMenuItems.tsx +++ b/plugins/catalog/src/alpha/contextMenuItems.tsx @@ -26,7 +26,6 @@ import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { dialogApiRef, useTranslationRef, - type DialogApiDialog, } from '@backstage/frontend-plugin-api'; import { catalogTranslationRef } from './translation'; import { useNavigate, useSearchParams } from 'react-router-dom'; @@ -110,7 +109,7 @@ export const unregisterEntityContextMenuItem = title: t('entityContextMenu.unregisterMenuTitle'), disabled: !unregisterPermission.allowed, onClick: async () => { - dialogApi.showModal(({ dialog }: { dialog: DialogApiDialog }) => ( + dialogApi.open(({ dialog }) => (