[feat] Add managerStateStore for three-state manager UI logic

- 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 <noreply@anthropic.com>
This commit is contained in:
bymyself
2025-08-17 21:35:52 -07:00
parent 642686165a
commit 2415b979b9
4 changed files with 291 additions and 16 deletions

View File

@@ -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
}
}
},

View File

@@ -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<ManagerUIState | null>(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
}
})

View File

@@ -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 = () => {

View File

@@ -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)
})
})
})