From 2ef760c59934dac93bb9f83499540a8a07330b5e Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 20 Jun 2025 15:22:42 -0700 Subject: [PATCH] [Manager] Keep progress dialog on top using priority system (#4225) --- src/services/dialogService.ts | 1 + src/stores/dialogStore.ts | 35 ++++- tests-ui/tests/store/dialogStore.test.ts | 175 +++++++++++++++++++++++ 3 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 tests-ui/tests/store/dialogStore.test.ts diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 258d50867..08bda9822 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -154,6 +154,7 @@ export const useDialogService = () => { headerComponent: ManagerProgressHeader, footerComponent: ManagerProgressFooter, props: options?.props, + priority: 2, dialogComponentProps: { closable: false, modal: false, diff --git a/src/stores/dialogStore.ts b/src/stores/dialogStore.ts index 1b80f156a..ae8988a0d 100644 --- a/src/stores/dialogStore.ts +++ b/src/stores/dialogStore.ts @@ -40,6 +40,7 @@ interface DialogInstance { contentProps: Record footerComponent?: Component dialogComponentProps: DialogComponentProps + priority: number } export interface ShowDialogOptions { @@ -50,6 +51,12 @@ export interface ShowDialogOptions { component: Component props?: Record dialogComponentProps?: DialogComponentProps + /** + * Optional priority for dialog stacking. + * A dialog will never be shown above a dialog with a higher priority. + * @default 1 + */ + priority?: number } export const useDialogStore = defineStore('dialog', () => { @@ -57,13 +64,29 @@ export const useDialogStore = defineStore('dialog', () => { const genDialogKey = () => `dialog-${Math.random().toString(36).slice(2, 9)}` + /** + * Inserts a dialog into the stack at the correct position based on priority. + * Higher priority dialogs are placed before lower priority ones. + */ + function insertDialogByPriority(dialog: DialogInstance) { + const insertIndex = dialogStack.value.findIndex( + (d) => d.priority <= dialog.priority + ) + + dialogStack.value.splice( + insertIndex === -1 ? dialogStack.value.length : insertIndex, + 0, + dialog + ) + } + function riseDialog(options: { key: string }) { const dialogKey = options.key const index = dialogStack.value.findIndex((d) => d.key === dialogKey) if (index !== -1) { - const dialogs = dialogStack.value.splice(index, 1) - dialogStack.value.push(...dialogs) + const [dialog] = dialogStack.value.splice(index, 1) + insertDialogByPriority(dialog) } } @@ -85,12 +108,13 @@ export const useDialogStore = defineStore('dialog', () => { component: Component props?: Record dialogComponentProps?: DialogComponentProps + priority?: number }) { if (dialogStack.value.length >= 10) { dialogStack.value.shift() } - const dialog = { + const dialog: DialogInstance = { key: options.key, visible: true, title: options.title, @@ -102,6 +126,7 @@ export const useDialogStore = defineStore('dialog', () => { : undefined, component: markRaw(options.component), contentProps: { ...options.props }, + priority: options.priority ?? 1, dialogComponentProps: { maximizable: false, modal: true, @@ -110,6 +135,7 @@ export const useDialogStore = defineStore('dialog', () => { dismissableMask: true, ...options.dialogComponentProps, maximized: false, + // @ts-expect-error TODO: fix this onMaximize: () => { dialog.dialogComponentProps.maximized = true }, @@ -128,7 +154,8 @@ export const useDialogStore = defineStore('dialog', () => { }) } } - dialogStack.value.push(dialog) + + insertDialogByPriority(dialog) return dialog } diff --git a/tests-ui/tests/store/dialogStore.test.ts b/tests-ui/tests/store/dialogStore.test.ts new file mode 100644 index 000000000..6d76d9bb1 --- /dev/null +++ b/tests-ui/tests/store/dialogStore.test.ts @@ -0,0 +1,175 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it } from 'vitest' +import { defineComponent } from 'vue' + +import { useDialogStore } from '@/stores/dialogStore' + +const MockComponent = defineComponent({ + name: 'MockComponent', + template: '
Mock
' +}) + +describe('dialogStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + describe('priority system', () => { + it('should create dialogs in correct priority order', () => { + const store = useDialogStore() + + // Create dialogs with different priorities + store.showDialog({ + key: 'low-priority', + component: MockComponent, + priority: 0 + }) + + store.showDialog({ + key: 'high-priority', + component: MockComponent, + priority: 10 + }) + + store.showDialog({ + key: 'medium-priority', + component: MockComponent, + priority: 5 + }) + + store.showDialog({ + key: 'no-priority', + component: MockComponent + }) + + // Check order: high (2) -> medium (1) -> low (0) + expect(store.dialogStack.map((d) => d.key)).toEqual([ + 'high-priority', + 'medium-priority', + 'no-priority', + 'low-priority' + ]) + }) + + it('should maintain priority order when rising dialogs', () => { + const store = useDialogStore() + + // Create dialogs with different priorities + store.showDialog({ + key: 'priority-2', + component: MockComponent, + priority: 2 + }) + + store.showDialog({ + key: 'priority-1', + component: MockComponent, + priority: 1 + }) + + store.showDialog({ + key: 'priority-0', + component: MockComponent, + priority: 0 + }) + + // Try to rise the lowest priority dialog + store.riseDialog({ key: 'priority-0' }) + + // Should still be at the bottom because of its priority + expect(store.dialogStack.map((d) => d.key)).toEqual([ + 'priority-2', + 'priority-1', + 'priority-0' + ]) + + // Rise the medium priority dialog + store.riseDialog({ key: 'priority-1' }) + + // Should be above priority-0 but below priority-2 + expect(store.dialogStack.map((d) => d.key)).toEqual([ + 'priority-2', + 'priority-1', + 'priority-0' + ]) + }) + + it('should keep high priority dialogs on top when creating new lower priority dialogs', () => { + const store = useDialogStore() + + // Create a high priority dialog (like manager progress) + store.showDialog({ + key: 'manager-progress', + component: MockComponent, + priority: 10 + }) + + store.showDialog({ + key: 'dialog-2', + component: MockComponent, + priority: 0 + }) + + store.showDialog({ + key: 'dialog-3', + component: MockComponent + // Default priority is 1 + }) + + // Manager progress should still be on top + expect(store.dialogStack[0].key).toBe('manager-progress') + + // Check full order + expect(store.dialogStack.map((d) => d.key)).toEqual([ + 'manager-progress', // priority 2 + 'dialog-3', // priority 1 (default) + 'dialog-2' // priority 0 + ]) + }) + }) + + describe('basic dialog operations', () => { + it('should show and close dialogs', () => { + const store = useDialogStore() + + store.showDialog({ + key: 'test-dialog', + component: MockComponent + }) + + expect(store.dialogStack).toHaveLength(1) + expect(store.isDialogOpen('test-dialog')).toBe(true) + + store.closeDialog({ key: 'test-dialog' }) + + expect(store.dialogStack).toHaveLength(0) + expect(store.isDialogOpen('test-dialog')).toBe(false) + }) + + it('should reuse existing dialog when showing with same key', () => { + const store = useDialogStore() + + store.showDialog({ + key: 'reusable-dialog', + component: MockComponent, + title: 'Original Title' + }) + + // First call should create the dialog + expect(store.dialogStack).toHaveLength(1) + expect(store.dialogStack[0].title).toBe('Original Title') + + // Second call with same key should reuse the dialog + store.showDialog({ + key: 'reusable-dialog', + component: MockComponent, + title: 'New Title' // This should be ignored + }) + + // Should still have only one dialog with original title + expect(store.dialogStack).toHaveLength(1) + expect(store.dialogStack[0].key).toBe('reusable-dialog') + expect(store.dialogStack[0].title).toBe('Original Title') + }) + }) +})