From 745d45907684340ea22ecbb0059a2abbf2dd42dd Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 8 May 2026 15:52:39 -0700 Subject: [PATCH] [backport core/1.43] fix: clarify unsaved-changes modal buttons and fix sign-out 3-state (#12091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of #11669 to core/1.43. Cherry-picked merge commit 0bc951fd121f6a6b7da0343f7f4ef2172878bd32. Conflict in `src/platform/workflow/core/services/workflowService.ts` resolved by accepting the PR's refactored structure (flattened if/else, `return await saveWorkflowAs`). ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12091-backport-core-1-43-fix-clarify-unsaved-changes-modal-buttons-and-fix-sign-out-3-state-35a6d73d3650811389e1fa1f6c9b860a) by [Unito](https://www.unito.io) --------- Co-authored-by: Dante --- .../tests/topbar/workflowTabs.spec.ts | 76 +++++++ .../content/ConfirmationDialogContent.test.ts | 40 ++++ .../content/ConfirmationDialogContent.vue | 7 +- src/components/topbar/WorkflowTab.vue | 1 + src/composables/auth/useAuthActions.test.ts | 195 ++++++++++++++++++ src/composables/auth/useAuthActions.ts | 23 ++- src/locales/en/main.json | 5 +- .../core/services/workflowService.test.ts | 33 ++- .../workflow/core/services/workflowService.ts | 70 +++---- src/services/dialogService.ts | 43 ++-- 10 files changed, 435 insertions(+), 58 deletions(-) create mode 100644 src/composables/auth/useAuthActions.test.ts diff --git a/browser_tests/tests/topbar/workflowTabs.spec.ts b/browser_tests/tests/topbar/workflowTabs.spec.ts index 5f623aaf0c..9d098ced5c 100644 --- a/browser_tests/tests/topbar/workflowTabs.spec.ts +++ b/browser_tests/tests/topbar/workflowTabs.spec.ts @@ -1,4 +1,5 @@ import { expect } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' @@ -151,4 +152,79 @@ test.describe('Workflow tabs', () => { await topbar.closeWorkflowTab('Unsaved Workflow (2)') await expect.poll(() => topbar.getTabNames()).toHaveLength(2) }) + + test.describe('Closing a modified workflow tab (FE-419)', () => { + async function modifyActiveWorkflow(page: Page, activeTab: Locator) { + await page.evaluate(() => { + const graph = window.app?.graph + const node = window.LiteGraph?.createNode('Note') + if (graph && node) graph.add(node) + }) + await expect( + activeTab.getByTestId('workflow-dirty-indicator') + ).toHaveCount(1) + } + + test('shows "Close anyway" label and no Cancel button on dirtyClose dialog', async ({ + comfyPage + }) => { + const topbar = comfyPage.menu.topbar + + await topbar.newWorkflowButton.click() + await expect.poll(() => topbar.getTabNames()).toHaveLength(2) + + await modifyActiveWorkflow(comfyPage.page, topbar.getActiveTab()) + await topbar.closeWorkflowTab('Unsaved Workflow (2)') + + const dialog = comfyPage.page.getByRole('dialog') + await expect(dialog).toBeVisible() + await expect( + dialog.getByRole('button', { name: 'Close anyway' }) + ).toBeVisible() + await expect(dialog.getByRole('button', { name: 'Save' })).toBeVisible() + await expect(dialog.getByRole('button', { name: 'Cancel' })).toHaveCount( + 0 + ) + }) + + test('clicking "Close anyway" closes the tab without saving', async ({ + comfyPage + }) => { + const topbar = comfyPage.menu.topbar + + await topbar.newWorkflowButton.click() + await expect.poll(() => topbar.getTabNames()).toHaveLength(2) + + await modifyActiveWorkflow(comfyPage.page, topbar.getActiveTab()) + await topbar.closeWorkflowTab('Unsaved Workflow (2)') + + await comfyPage.page + .getByRole('dialog') + .getByRole('button', { name: 'Close anyway' }) + .click() + + await expect.poll(() => topbar.getTabNames()).toHaveLength(1) + await expect + .poll(() => topbar.getActiveTabName()) + .toContain('Unsaved Workflow') + }) + + test('dismissing the dialog keeps the modified tab open', async ({ + comfyPage + }) => { + const topbar = comfyPage.menu.topbar + + await topbar.newWorkflowButton.click() + await expect.poll(() => topbar.getTabNames()).toHaveLength(2) + + await modifyActiveWorkflow(comfyPage.page, topbar.getActiveTab()) + await topbar.closeWorkflowTab('Unsaved Workflow (2)') + + await expect(comfyPage.page.getByRole('dialog')).toBeVisible() + await comfyPage.page.keyboard.press('Escape') + await expect(comfyPage.page.getByRole('dialog')).toBeHidden() + + await expect.poll(() => topbar.getTabNames()).toHaveLength(2) + }) + }) }) diff --git a/src/components/dialog/content/ConfirmationDialogContent.test.ts b/src/components/dialog/content/ConfirmationDialogContent.test.ts index fa6c75f4c8..04019e6ca2 100644 --- a/src/components/dialog/content/ConfirmationDialogContent.test.ts +++ b/src/components/dialog/content/ConfirmationDialogContent.test.ts @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/vue' +import userEvent from '@testing-library/user-event' import { createPinia, setActivePinia } from 'pinia' import PrimeVue from 'primevue/config' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -42,4 +43,43 @@ describe('ConfirmationDialogContent', () => { renderComponent({ message: longFilename }) expect(screen.getByText(longFilename)).toBeInTheDocument() }) + + it('omits the Cancel button when type is dirtyClose', () => { + renderComponent({ type: 'dirtyClose' }) + expect(screen.queryByText('g.cancel')).not.toBeInTheDocument() + expect(screen.getByText('g.save')).toBeInTheDocument() + }) + + it('uses the provided denyLabel for the deny button on dirtyClose', () => { + renderComponent({ type: 'dirtyClose', denyLabel: 'Sign out anyway' }) + expect(screen.getByText('Sign out anyway')).toBeInTheDocument() + expect(screen.queryByText('g.no')).not.toBeInTheDocument() + }) + + it('calls onConfirm(false) when deny is clicked on dirtyClose', async () => { + const onConfirm = vi.fn() + renderComponent({ + type: 'dirtyClose', + denyLabel: 'Close anyway', + onConfirm + }) + + await userEvent.click(screen.getByRole('button', { name: 'Close anyway' })) + + expect(onConfirm).toHaveBeenCalledWith(false) + }) + + it('calls onConfirm(true) when save is clicked on dirtyClose', async () => { + const onConfirm = vi.fn() + renderComponent({ type: 'dirtyClose', onConfirm }) + + await userEvent.click(screen.getByRole('button', { name: 'g.save' })) + + expect(onConfirm).toHaveBeenCalledWith(true) + }) + + it('falls back to "no" label when denyLabel is not provided', () => { + renderComponent({ type: 'dirtyClose' }) + expect(screen.getByText('g.no')).toBeInTheDocument() + }) }) diff --git a/src/components/dialog/content/ConfirmationDialogContent.vue b/src/components/dialog/content/ConfirmationDialogContent.vue index cc5c730142..daa9b259b3 100644 --- a/src/components/dialog/content/ConfirmationDialogContent.vue +++ b/src/components/dialog/content/ConfirmationDialogContent.vue @@ -55,7 +55,7 @@ - @@ -131,6 +131,7 @@ const props = defineProps<{ onConfirm: (value?: boolean) => void itemList?: string[] hint?: string + denyLabel?: string }>() const { t } = useI18n() diff --git a/src/components/topbar/WorkflowTab.vue b/src/components/topbar/WorkflowTab.vue index c1036a9b90..a81fc3037a 100644 --- a/src/components/topbar/WorkflowTab.vue +++ b/src/components/topbar/WorkflowTab.vue @@ -23,6 +23,7 @@
diff --git a/src/composables/auth/useAuthActions.test.ts b/src/composables/auth/useAuthActions.test.ts new file mode 100644 index 0000000000..4073434e5d --- /dev/null +++ b/src/composables/auth/useAuthActions.test.ts @@ -0,0 +1,195 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useAuthActions } from '@/composables/auth/useAuthActions' +import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' + +type ModifiedWorkflow = Pick + +const mockAuthStore = vi.hoisted(() => ({ + logout: vi.fn().mockResolvedValue(undefined) +})) + +const mockToastStore = vi.hoisted(() => ({ + add: vi.fn() +})) + +const mockWorkflowStore = vi.hoisted(() => ({ + modifiedWorkflows: [] as ModifiedWorkflow[] +})) + +const mockWorkflowService = vi.hoisted(() => ({ + saveWorkflow: vi.fn().mockResolvedValue(true) +})) + +const mockDialogService = vi.hoisted(() => ({ + confirm: vi.fn() +})) + +vi.mock('@/i18n', () => ({ + t: (key: string, values?: { workflow?: string }) => + values?.workflow ? `${key}:${values.workflow}` : key +})) + +vi.mock('@/platform/distribution/types', () => ({ + isCloud: false +})) + +vi.mock('@/platform/telemetry', () => ({ + useTelemetry: vi.fn(() => undefined) +})) + +vi.mock('@/platform/updates/common/toastStore', () => ({ + useToastStore: vi.fn(() => mockToastStore) +})) + +vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({ + useWorkflowStore: vi.fn(() => mockWorkflowStore) +})) + +vi.mock('@/platform/workflow/core/services/workflowService', () => ({ + useWorkflowService: vi.fn(() => mockWorkflowService) +})) + +vi.mock('@/services/dialogService', () => ({ + useDialogService: vi.fn(() => mockDialogService) +})) + +vi.mock('@/stores/authStore', () => ({ + useAuthStore: vi.fn(() => mockAuthStore) +})) + +vi.mock('@/composables/billing/useBillingContext', () => ({ + useBillingContext: vi.fn(() => ({ + isActiveSubscription: { value: false }, + isFreeTier: { value: true }, + type: { value: 'free' } + })) +})) + +vi.mock('@/composables/useErrorHandling', () => ({ + useErrorHandling: () => ({ + wrapWithErrorHandlingAsync: ( + action: (...args: TArgs) => Promise | TReturn + ) => action, + toastErrorHandler: vi.fn() + }) +})) + +function makeWorkflow(path: string): ModifiedWorkflow { + return { path, isModified: true } satisfies ModifiedWorkflow +} + +describe('useAuthActions.logout', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + mockWorkflowStore.modifiedWorkflows = [] + }) + + it('logs out without prompting when no workflows are modified', async () => { + const { logout } = useAuthActions() + + await logout() + + expect(mockDialogService.confirm).not.toHaveBeenCalled() + expect(mockWorkflowService.saveWorkflow).not.toHaveBeenCalled() + expect(mockAuthStore.logout).toHaveBeenCalledTimes(1) + }) + + it('cancels sign-out when the dialog is dismissed (null)', async () => { + mockWorkflowStore.modifiedWorkflows = [makeWorkflow('a.json')] + mockDialogService.confirm.mockResolvedValueOnce(null) + const { logout } = useAuthActions() + + await logout() + + expect(mockDialogService.confirm).toHaveBeenCalledTimes(1) + expect(mockWorkflowService.saveWorkflow).not.toHaveBeenCalled() + expect(mockAuthStore.logout).not.toHaveBeenCalled() + }) + + it('signs out without saving when the user picks "Sign out anyway" (false)', async () => { + mockWorkflowStore.modifiedWorkflows = [makeWorkflow('a.json')] + mockDialogService.confirm.mockResolvedValueOnce(false) + const { logout } = useAuthActions() + + await logout() + + expect(mockDialogService.confirm).toHaveBeenCalledTimes(1) + expect(mockWorkflowService.saveWorkflow).not.toHaveBeenCalled() + expect(mockAuthStore.logout).toHaveBeenCalledTimes(1) + }) + + it('cancels sign-out when saving a workflow is cancelled', async () => { + mockWorkflowStore.modifiedWorkflows = [makeWorkflow('a.json')] + mockDialogService.confirm.mockResolvedValueOnce(true) + mockWorkflowService.saveWorkflow.mockResolvedValueOnce(false) + const { logout } = useAuthActions() + + await logout() + + expect(mockWorkflowService.saveWorkflow).toHaveBeenCalledTimes(1) + expect(mockAuthStore.logout).not.toHaveBeenCalled() + }) + + it('does not log out if a workflow save fails', async () => { + mockWorkflowStore.modifiedWorkflows = [ + makeWorkflow('a.json'), + makeWorkflow('b.json') + ] + mockDialogService.confirm.mockResolvedValueOnce(true) + mockWorkflowService.saveWorkflow.mockRejectedValueOnce( + new Error('disk full') + ) + const { logout } = useAuthActions() + + await expect(logout()).rejects.toThrow('auth.signOut.saveFailed:a.json') + + expect(mockWorkflowService.saveWorkflow).toHaveBeenCalledTimes(1) + expect(mockAuthStore.logout).not.toHaveBeenCalled() + }) + + it('saves every modified workflow before signing out when user picks Save (true)', async () => { + const workflows = [makeWorkflow('a.json'), makeWorkflow('b.json')] + mockWorkflowStore.modifiedWorkflows = workflows + mockDialogService.confirm.mockResolvedValueOnce(true) + const { logout } = useAuthActions() + + await logout() + + expect(mockWorkflowService.saveWorkflow).toHaveBeenCalledTimes(2) + expect(mockWorkflowService.saveWorkflow).toHaveBeenNthCalledWith( + 1, + workflows[0] + ) + expect(mockWorkflowService.saveWorkflow).toHaveBeenNthCalledWith( + 2, + workflows[1] + ) + expect(mockAuthStore.logout).toHaveBeenCalledTimes(1) + expect( + mockWorkflowService.saveWorkflow.mock.invocationCallOrder[1] + ).toBeLessThan(mockAuthStore.logout.mock.invocationCallOrder[0]) + expect( + mockWorkflowService.saveWorkflow.mock.invocationCallOrder[0] + ).toBeLessThan(mockWorkflowService.saveWorkflow.mock.invocationCallOrder[1]) + }) + + it('passes denyLabel "Sign out anyway" to the dialog', async () => { + mockWorkflowStore.modifiedWorkflows = [makeWorkflow('a.json')] + mockDialogService.confirm.mockResolvedValueOnce(null) + const { logout } = useAuthActions() + + await logout() + + expect(mockDialogService.confirm).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'dirtyClose', + title: 'auth.signOut.unsavedChangesTitle', + message: 'auth.signOut.unsavedChangesMessage', + denyLabel: 'auth.signOut.signOutAnyway' + }) + ) + }) +}) diff --git a/src/composables/auth/useAuthActions.ts b/src/composables/auth/useAuthActions.ts index d721cd0646..216165c4af 100644 --- a/src/composables/auth/useAuthActions.ts +++ b/src/composables/auth/useAuthActions.ts @@ -9,6 +9,7 @@ import { t } from '@/i18n' import { isCloud } from '@/platform/distribution/types' import { useTelemetry } from '@/platform/telemetry' import { useToastStore } from '@/platform/updates/common/toastStore' +import { useWorkflowService } from '@/platform/workflow/core/services/workflowService' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useDialogService } from '@/services/dialogService' import { useAuthStore } from '@/stores/authStore' @@ -53,14 +54,30 @@ export const useAuthActions = () => { const logout = wrapWithErrorHandlingAsync(async () => { const workflowStore = useWorkflowStore() - if (workflowStore.modifiedWorkflows.length > 0) { + const modifiedWorkflows = workflowStore.modifiedWorkflows + if (modifiedWorkflows.length > 0) { const dialogService = useDialogService() const confirmed = await dialogService.confirm({ title: t('auth.signOut.unsavedChangesTitle'), message: t('auth.signOut.unsavedChangesMessage'), - type: 'dirtyClose' + type: 'dirtyClose', + denyLabel: t('auth.signOut.signOutAnyway') }) - if (!confirmed) return + if (confirmed === null) return + + if (confirmed === true) { + const workflowService = useWorkflowService() + for (const workflow of modifiedWorkflows) { + try { + const saved = await workflowService.saveWorkflow(workflow) + if (!saved) return + } catch { + throw new Error( + t('auth.signOut.saveFailed', { workflow: workflow.path }) + ) + } + } + } } await authStore.logout() diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 509b28ec47..56ec246a82 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -973,6 +973,7 @@ "dirtyCloseTitle": "Save Changes?", "dirtyClose": "The files below have been changed. Would you like to save them before closing?", "dirtyCloseHint": "Hold Shift to close without prompt", + "dirtyCloseAnyway": "Close anyway", "confirmOverwriteTitle": "Overwrite existing file?", "confirmOverwrite": "The file below already exists. Would you like to overwrite it?", "workflowTreeType": { @@ -2178,7 +2179,9 @@ "success": "Signed out successfully", "successDetail": "You have been signed out of your account.", "unsavedChangesTitle": "Unsaved Changes", - "unsavedChangesMessage": "You have unsaved changes that will be lost when you sign out. Do you want to continue?" + "unsavedChangesMessage": "You have unsaved changes that will be lost when you sign out. Do you want to continue?", + "signOutAnyway": "Sign out anyway", + "saveFailed": "Sign-out cancelled because saving \"{workflow}\" failed." }, "passwordUpdate": { "success": "Password Updated", diff --git a/src/platform/workflow/core/services/workflowService.test.ts b/src/platform/workflow/core/services/workflowService.test.ts index 708d9ba803..c76f7cb790 100644 --- a/src/platform/workflow/core/services/workflowService.test.ts +++ b/src/platform/workflow/core/services/workflowService.test.ts @@ -417,24 +417,51 @@ describe('useWorkflowService', () => { }) vi.mocked(workflowStore.saveWorkflow).mockResolvedValue() - await useWorkflowService().saveWorkflow(workflow) + const result = await useWorkflowService().saveWorkflow(workflow) + expect(result).toBe(true) expect(workflowStore.saveWorkflow).toHaveBeenCalledWith(workflow) }) - it('should call saveWorkflowAs for temporary workflows', async () => { + it('should return false when temporary workflow save is cancelled', async () => { const workflow = createModeTestWorkflow({ path: 'workflows/Unsaved Workflow.json' }) Object.defineProperty(workflow, 'isTemporary', { get: () => true }) vi.spyOn(workflow, 'promptSave').mockResolvedValue(null) - await useWorkflowService().saveWorkflow(workflow) + const result = await useWorkflowService().saveWorkflow(workflow) + expect(result).toBe(false) expect(workflowStore.saveWorkflow).not.toHaveBeenCalled() }) }) + describe('closeWorkflow', () => { + let workflowStore: ReturnType + let service: ReturnType + + beforeEach(() => { + workflowStore = useWorkflowStore() + service = useWorkflowService() + }) + + it('keeps a temporary workflow open when Save As is cancelled', async () => { + const workflow = createModeTestWorkflow({ + path: 'workflows/Unsaved Workflow.json' + }) + workflow.isModified = true + Object.defineProperty(workflow, 'isTemporary', { get: () => true }) + vi.spyOn(workflow, 'promptSave').mockResolvedValue(null) + mockConfirm.mockResolvedValue(true) + + const closed = await service.closeWorkflow(workflow) + + expect(closed).toBe(false) + expect(workflowStore.closeWorkflow).not.toHaveBeenCalled() + }) + }) + describe('afterLoadNewGraph', () => { let workflowStore: ReturnType let existingWorkflow: LoadedComfyWorkflow diff --git a/src/platform/workflow/core/services/workflowService.ts b/src/platform/workflow/core/services/workflowService.ts index 9b4419c260..fb5ca97bef 100644 --- a/src/platform/workflow/core/services/workflowService.ts +++ b/src/platform/workflow/core/services/workflowService.ts @@ -174,41 +174,39 @@ export const useWorkflowService = () => { * Save a workflow * @param workflow The workflow to save */ - const saveWorkflow = async (workflow: ComfyWorkflow) => { + const saveWorkflow = async (workflow: ComfyWorkflow): Promise => { if (workflow.isTemporary) { - await saveWorkflowAs(workflow) - } else { - if (workflowStore.isActive(workflow)) workflow.changeTracker?.checkState() - - const isApp = workflow.initialMode === 'app' - const expectedPath = - workflow.directory + - '/' + - appendWorkflowJsonExt(workflow.filename, isApp) - if (workflow.path !== expectedPath) { - const existing = workflowStore.getWorkflowByPath(expectedPath) - if (existing && !existing.isTemporary) { - if ((await confirmOverwrite(expectedPath)) !== true) { - await workflowStore.saveWorkflow(workflow) - return - } - await deleteWorkflow(existing, true) - } - await renameWorkflow(workflow, expectedPath) - toastStore.add({ - severity: 'info', - summary: t( - isApp - ? 'workflowService.savedAsApp' - : 'workflowService.savedAsWorkflow' - ), - life: 3000 - }) - } - - await workflowStore.saveWorkflow(workflow) - useTelemetry()?.trackWorkflowSaved({ is_app: isApp, is_new: false }) + return await saveWorkflowAs(workflow) } + + if (workflowStore.isActive(workflow)) workflow.changeTracker?.checkState() + const isApp = workflow.initialMode === 'app' + const expectedPath = + workflow.directory + '/' + appendWorkflowJsonExt(workflow.filename, isApp) + if (workflow.path !== expectedPath) { + const existing = workflowStore.getWorkflowByPath(expectedPath) + if (existing && !existing.isTemporary) { + if ((await confirmOverwrite(expectedPath)) !== true) { + await workflowStore.saveWorkflow(workflow) + return true + } + await deleteWorkflow(existing, true) + } + await renameWorkflow(workflow, expectedPath) + toastStore.add({ + severity: 'info', + summary: t( + isApp + ? 'workflowService.savedAsApp' + : 'workflowService.savedAsWorkflow' + ), + life: 3000 + }) + } + + await workflowStore.saveWorkflow(workflow) + useTelemetry()?.trackWorkflowSaved({ is_app: isApp, is_new: false }) + return true } /** @@ -285,13 +283,15 @@ export const useWorkflowService = () => { type: 'dirtyClose', message: t('sideToolbar.workflowTab.dirtyClose'), itemList: [workflow.path], - hint: options.hint + hint: options.hint, + denyLabel: t('sideToolbar.workflowTab.dirtyCloseAnyway') }) // Cancel if (confirmed === null) return false if (confirmed === true) { - await saveWorkflow(workflow) + const saved = await saveWorkflow(workflow) + if (!saved) return false } } diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 92185d563d..354a3346e7 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -40,6 +40,31 @@ export type ConfirmationDialogType = | 'reinstall' | 'info' +interface BaseConfirmOptions { + /** Dialog heading */ + title: string + /** The main message body */ + message: string + /** Displayed as an unordered list immediately below the message body */ + itemList?: string[] + hint?: string +} + +type ConfirmOptions = BaseConfirmOptions & + ( + | { + /** Pre-configured dialog type */ + type: 'dirtyClose' + /** Override the deny button label. Defaults to `g.no`. */ + denyLabel?: string + } + | { + /** Pre-configured dialog type */ + type?: Exclude + denyLabel?: never + } + ) + /** * Minimal interface for execution error dialogs. * Satisfied by both ExecutionErrorWsMessage (WebSocket) and ExecutionError (Jobs API). @@ -241,18 +266,9 @@ export const useDialogService = () => { message, type = 'default', itemList = [], - hint - }: { - /** Dialog heading */ - title: string - /** The main message body */ - message: string - /** Pre-configured dialog type */ - type?: ConfirmationDialogType - /** Displayed as an unordered list immediately below the message body */ - itemList?: string[] - hint?: string - }): Promise { + hint, + denyLabel + }: ConfirmOptions): Promise { return new Promise((resolve) => { const options: ShowDialogOptions = { key: 'global-prompt', @@ -263,7 +279,8 @@ export const useDialogService = () => { type, itemList, onConfirm: resolve, - hint + hint, + denyLabel }, dialogComponentProps: { onClose: () => resolve(null)