From 2415b979b98af2fe2c2142a779e1b5bfa76633f2 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sun, 17 Aug 2025 21:35:52 -0700 Subject: [PATCH] [feat] Add managerStateStore for three-state manager UI logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create managerStateStore to determine manager UI state (disabled, legacy, new) - Check command line args, feature flags, and legacy API endpoints - Update useCoreCommands to use the new store instead of async API calls - Initialize manager state after system stats are loaded in GraphView - Add comprehensive tests for all manager state scenarios 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/composables/useCoreCommands.ts | 44 ++-- src/stores/managerStateStore.ts | 59 ++++++ src/views/GraphView.vue | 8 + .../tests/stores/managerStateStore.test.ts | 196 ++++++++++++++++++ 4 files changed, 291 insertions(+), 16 deletions(-) create mode 100644 src/stores/managerStateStore.ts create mode 100644 tests-ui/tests/stores/managerStateStore.test.ts diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index fc39efb60..2ad3e5653 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -16,7 +16,6 @@ import { import { Point } from '@/lib/litegraph/src/litegraph' import { api } from '@/scripts/api' import { app } from '@/scripts/app' -import { useComfyManagerService } from '@/services/comfyManagerService' import { useDialogService } from '@/services/dialogService' import { useLitegraphService } from '@/services/litegraphService' import { useWorkflowService } from '@/services/workflowService' @@ -26,6 +25,10 @@ import { useExecutionStore } from '@/stores/executionStore' import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore' import { useHelpCenterStore } from '@/stores/helpCenterStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore' +import { + ManagerUIState, + useManagerStateStore +} from '@/stores/managerStateStore' import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore' import { useSettingStore } from '@/stores/settingStore' import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' @@ -739,27 +742,36 @@ export function useCoreCommands(): ComfyCommand[] { icon: 'pi pi-objects-column', label: 'Custom Nodes Manager', versionAdded: '1.12.10', - function: async () => { - const { is_legacy_manager_ui } = - (await useComfyManagerService().isLegacyManagerUI()) ?? {} + function: () => { + const managerStore = useManagerStateStore() + const state = managerStore.managerUIState - if (is_legacy_manager_ui === true) { - try { - await useCommandStore().execute( - 'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension - ) - } catch (error) { - console.error('error', error) - useToastStore().add({ + switch (state) { + case ManagerUIState.DISABLED: + toastStore.add({ severity: 'error', summary: t('g.error'), - detail: t('manager.legacyMenuNotAvailable'), + detail: t('manager.notAvailable'), life: 3000 }) + break + + case ManagerUIState.LEGACY_UI: + useCommandStore() + .execute('Comfy.Manager.Menu.ToggleVisibility') + .catch(() => { + toastStore.add({ + severity: 'error', + summary: t('g.error'), + detail: t('manager.legacyMenuNotAvailable'), + life: 3000 + }) + }) + break + + case ManagerUIState.NEW_UI: dialogService.showManagerDialog() - } - } else { - dialogService.showManagerDialog() + break } } }, diff --git a/src/stores/managerStateStore.ts b/src/stores/managerStateStore.ts new file mode 100644 index 000000000..9fb7d7be8 --- /dev/null +++ b/src/stores/managerStateStore.ts @@ -0,0 +1,59 @@ +import { defineStore } from 'pinia' +import { readonly, ref } from 'vue' + +import { useFeatureFlags } from '@/composables/useFeatureFlags' +import { api } from '@/scripts/api' +import { useComfyManagerService } from '@/services/comfyManagerService' +import { useSystemStatsStore } from '@/stores/systemStatsStore' + +export enum ManagerUIState { + DISABLED = 'disabled', + LEGACY_UI = 'legacy', + NEW_UI = 'new' +} + +export const useManagerStateStore = defineStore('managerState', () => { + const managerUIState = ref(null) + const isInitialized = ref(false) + + const initializeManagerState = async () => { + if (isInitialized.value) return + + const systemStats = useSystemStatsStore().systemStats + const { flags } = useFeatureFlags() + const clientSupportsV4 = + api.getClientFeatureFlags().supports_manager_v4_ui ?? false + + // Check command line args first + if (systemStats?.system?.argv?.includes('--disable-manager')) { + managerUIState.value = ManagerUIState.DISABLED + } else if ( + systemStats?.system?.argv?.includes('--enable-manager-legacy-ui') + ) { + managerUIState.value = ManagerUIState.LEGACY_UI + } else { + // Check if we can use new UI + if (clientSupportsV4 && flags.supportsManagerV4) { + managerUIState.value = ManagerUIState.NEW_UI + } else { + // For old frontend, we need to check if legacy manager exists + try { + await useComfyManagerService().isLegacyManagerUI() + // Route exists but we can't use v4 + managerUIState.value = ManagerUIState.LEGACY_UI + } catch { + // Route doesn't exist = old manager OR no manager + // Old frontend will handle this itself + managerUIState.value = ManagerUIState.LEGACY_UI + } + } + } + + isInitialized.value = true + } + + return { + managerUIState: readonly(managerUIState), + initializeManagerState + } +}) diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index ef38ec569..98c841f63 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -53,6 +53,7 @@ import { setupAutoQueueHandler } from '@/services/autoQueueService' import { useKeybindingService } from '@/services/keybindingService' import { useCommandStore } from '@/stores/commandStore' import { useExecutionStore } from '@/stores/executionStore' +import { useManagerStateStore } from '@/stores/managerStateStore' import { useMenuItemStore } from '@/stores/menuItemStore' import { useModelStore } from '@/stores/modelStore' import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore' @@ -250,6 +251,13 @@ void nextTick(() => { versionCompatibilityStore.initialize().catch((error) => { console.warn('Version compatibility check failed:', error) }) + + // Initialize manager state after system stats are loaded + useManagerStateStore() + .initializeManagerState() + .catch((error) => { + console.warn('Manager state initialization failed:', error) + }) }) const onGraphReady = () => { diff --git a/tests-ui/tests/stores/managerStateStore.test.ts b/tests-ui/tests/stores/managerStateStore.test.ts new file mode 100644 index 000000000..c3eb758a8 --- /dev/null +++ b/tests-ui/tests/stores/managerStateStore.test.ts @@ -0,0 +1,196 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useFeatureFlags } from '@/composables/useFeatureFlags' +import { api } from '@/scripts/api' +import { useComfyManagerService } from '@/services/comfyManagerService' +import { + ManagerUIState, + useManagerStateStore +} from '@/stores/managerStateStore' +import { useSystemStatsStore } from '@/stores/systemStatsStore' + +// Mock dependencies +vi.mock('@/scripts/api', () => ({ + api: { + getClientFeatureFlags: vi.fn() + } +})) + +vi.mock('@/composables/useFeatureFlags', () => ({ + useFeatureFlags: vi.fn(() => ({ + flags: { supportsManagerV4: false }, + featureFlag: vi.fn() + })) +})) + +vi.mock('@/services/comfyManagerService', () => ({ + useComfyManagerService: vi.fn() +})) + +vi.mock('@/stores/systemStatsStore', () => ({ + useSystemStatsStore: vi.fn() +})) + +describe('useManagerStateStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + describe('initializeManagerState', () => { + it('should set DISABLED state when --disable-manager is present', async () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: { + system: { argv: ['python', 'main.py', '--disable-manager'] } + } + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + + const store = useManagerStateStore() + await store.initializeManagerState() + + expect(store.managerUIState).toBe(ManagerUIState.DISABLED) + }) + + it('should set LEGACY_UI state when --enable-manager-legacy-ui is present', async () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: { + system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] } + } + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + + const store = useManagerStateStore() + await store.initializeManagerState() + + expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI) + }) + + it('should set NEW_UI state when client and server both support v4', async () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: { system: { argv: ['python', 'main.py'] } } + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: true }, + featureFlag: vi.fn() + } as any) + vi.mocked(useComfyManagerService).mockReturnValue({ + isLegacyManagerUI: vi.fn().mockResolvedValue({}) + } as any) + + const store = useManagerStateStore() + await store.initializeManagerState() + + expect(store.managerUIState).toBe(ManagerUIState.NEW_UI) + }) + + it('should set LEGACY_UI state when client does not support v4', async () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: { system: { argv: ['python', 'main.py'] } } + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: false + }) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: true }, + featureFlag: vi.fn() + } as any) + vi.mocked(useComfyManagerService).mockReturnValue({ + isLegacyManagerUI: vi.fn().mockResolvedValue({}) + } as any) + + const store = useManagerStateStore() + await store.initializeManagerState() + + expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI) + }) + + it('should set LEGACY_UI state when server does not support v4', async () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: { system: { argv: ['python', 'main.py'] } } + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: false }, + featureFlag: vi.fn() + } as any) + vi.mocked(useComfyManagerService).mockReturnValue({ + isLegacyManagerUI: vi.fn().mockResolvedValue({}) + } as any) + + const store = useManagerStateStore() + await store.initializeManagerState() + + expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI) + }) + + it('should set LEGACY_UI state when isLegacyManagerUI route does not exist', async () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: { system: { argv: ['python', 'main.py'] } } + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: false }, + featureFlag: vi.fn() + } as any) + vi.mocked(useComfyManagerService).mockReturnValue({ + isLegacyManagerUI: vi.fn().mockRejectedValue(new Error('404')) + } as any) + + const store = useManagerStateStore() + await store.initializeManagerState() + + expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI) + }) + + it('should not re-initialize if already initialized', async () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: { + system: { argv: ['python', 'main.py', '--disable-manager'] } + } + } as any) + + const store = useManagerStateStore() + await store.initializeManagerState() + expect(store.managerUIState).toBe(ManagerUIState.DISABLED) + + // Change the mock to return different value + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: { system: { argv: ['python', 'main.py'] } } + } as any) + + // Try to initialize again + await store.initializeManagerState() + + // Should still be DISABLED from first initialization + expect(store.managerUIState).toBe(ManagerUIState.DISABLED) + }) + + it('should handle null systemStats gracefully', async () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: null + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: true }, + featureFlag: vi.fn() + } as any) + vi.mocked(useComfyManagerService).mockReturnValue({ + isLegacyManagerUI: vi.fn().mockResolvedValue({}) + } as any) + + const store = useManagerStateStore() + await store.initializeManagerState() + + expect(store.managerUIState).toBe(ManagerUIState.NEW_UI) + }) + }) +})