diff --git a/src/App.vue b/src/App.vue index 1a4068a27..5e183c430 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,10 +15,10 @@ import ProgressSpinner from 'primevue/progressspinner' import { computed, onMounted } from 'vue' import GlobalDialog from '@/components/dialog/GlobalDialog.vue' -import { useConflictDetection } from '@/composables/useConflictDetection' import config from '@/config' import { useWorkspaceStore } from '@/stores/workspaceStore' +import { useConflictDetection } from './composables/useConflictDetection' import { electronAPI, isElectron } from './utils/envUtil' const workspaceStore = useWorkspaceStore() diff --git a/src/components/dialog/content/ErrorDialogContent.vue b/src/components/dialog/content/ErrorDialogContent.vue index 4f35511cf..5b732ebbc 100644 --- a/src/components/dialog/content/ErrorDialogContent.vue +++ b/src/components/dialog/content/ErrorDialogContent.vue @@ -105,7 +105,7 @@ const showContactSupport = async () => { onMounted(async () => { if (!systemStatsStore.systemStats) { - await systemStatsStore.fetchSystemStats() + await systemStatsStore.refetchSystemStats() } try { diff --git a/src/components/dialog/content/LoadWorkflowWarning.vue b/src/components/dialog/content/LoadWorkflowWarning.vue index 216e54dd0..3bd571179 100644 --- a/src/components/dialog/content/LoadWorkflowWarning.vue +++ b/src/components/dialog/content/LoadWorkflowWarning.vue @@ -54,19 +54,12 @@ import Button from 'primevue/button' import ListBox from 'primevue/listbox' import { computed } from 'vue' -import { useI18n } from 'vue-i18n' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue' import { useMissingNodes } from '@/composables/nodePack/useMissingNodes' -import { useDialogService } from '@/services/dialogService' +import { useManagerState } from '@/composables/useManagerState' import { useComfyManagerStore } from '@/stores/comfyManagerStore' -import { useCommandStore } from '@/stores/commandStore' -import { - ManagerUIState, - useManagerStateStore -} from '@/stores/managerStateStore' -import { useToastStore } from '@/stores/toastStore' import type { MissingNodeType } from '@/types/comfy' import { ManagerTab } from '@/types/comfyManagerTypes' @@ -81,6 +74,7 @@ const { missingNodePacks, isLoading, error, missingCoreNodes } = useMissingNodes() const comfyManagerStore = useComfyManagerStore() +const managerState = useManagerState() // Check if any of the missing packs are currently being installed const isInstalling = computed(() => { @@ -111,47 +105,21 @@ const uniqueNodes = computed(() => { }) }) -const managerStateStore = useManagerStateStore() - // Show manager buttons unless manager is disabled const showManagerButtons = computed(() => { - return managerStateStore.managerUIState !== ManagerUIState.DISABLED + return managerState.shouldShowManagerButtons.value }) // Only show Install All button for NEW_UI (new manager with v4 support) const showInstallAllButton = computed(() => { - return managerStateStore.managerUIState === ManagerUIState.NEW_UI + return managerState.shouldShowInstallButton.value }) const openManager = async () => { - const state = managerStateStore.managerUIState - - switch (state) { - case ManagerUIState.DISABLED: - useDialogService().showSettingsDialog('extension') - break - - case ManagerUIState.LEGACY_UI: - try { - await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility') - } catch { - // If legacy command doesn't exist, show toast - const { t } = useI18n() - useToastStore().add({ - severity: 'error', - summary: t('g.error'), - detail: t('manager.legacyMenuNotAvailable'), - life: 3000 - }) - } - break - - case ManagerUIState.NEW_UI: - useDialogService().showManagerDialog({ - initialTab: ManagerTab.Missing - }) - break - } + await managerState.openManager({ + initialTab: ManagerTab.Missing, + showToastOnLegacyError: true + }) } diff --git a/src/components/dialog/content/MissingCoreNodesMessage.vue b/src/components/dialog/content/MissingCoreNodesMessage.vue index 036f088b3..cf81441f1 100644 --- a/src/components/dialog/content/MissingCoreNodesMessage.vue +++ b/src/components/dialog/content/MissingCoreNodesMessage.vue @@ -42,9 +42,8 @@ diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue index 852b1b593..c91589407 100644 --- a/src/components/helpcenter/HelpCenterMenuContent.vue +++ b/src/components/helpcenter/HelpCenterMenuContent.vue @@ -142,11 +142,12 @@ import { useI18n } from 'vue-i18n' import PuzzleIcon from '@/components/icons/PuzzleIcon.vue' import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment' -import { useDialogService } from '@/services/dialogService' +import { useManagerState } from '@/composables/useManagerState' import { type ReleaseNote } from '@/services/releaseService' import { useCommandStore } from '@/stores/commandStore' import { useReleaseStore } from '@/stores/releaseStore' import { useSettingStore } from '@/stores/settingStore' +import { ManagerTab } from '@/types/comfyManagerTypes' import { electronAPI, isElectron } from '@/utils/envUtil' import { formatVersionAnchor } from '@/utils/formatUtil' @@ -191,7 +192,6 @@ const { t, locale } = useI18n() const releaseStore = useReleaseStore() const commandStore = useCommandStore() const settingStore = useSettingStore() -const dialogService = useDialogService() // Emits const emit = defineEmits<{ @@ -313,8 +313,11 @@ const menuItems = computed(() => { icon: PuzzleIcon, label: t('helpCenter.managerExtension'), showRedDot: shouldShowManagerRedDot.value, - action: () => { - dialogService.showManagerDialog() + action: async () => { + await useManagerState().openManager({ + initialTab: ManagerTab.All, + showToastOnLegacyError: false + }) emit('close') } }, diff --git a/src/components/topbar/CommandMenubar.vue b/src/components/topbar/CommandMenubar.vue index 4e01965f8..811dbd62c 100644 --- a/src/components/topbar/CommandMenubar.vue +++ b/src/components/topbar/CommandMenubar.vue @@ -83,17 +83,14 @@ import { useI18n } from 'vue-i18n' import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue' import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue' import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue' +import { useManagerState } from '@/composables/useManagerState' import { useColorPaletteService } from '@/services/colorPaletteService' -import { useDialogService } from '@/services/dialogService' import { useCommandStore } from '@/stores/commandStore' import { useDialogStore } from '@/stores/dialogStore' -import { - ManagerUIState, - useManagerStateStore -} from '@/stores/managerStateStore' import { useMenuItemStore } from '@/stores/menuItemStore' import { useSettingStore } from '@/stores/settingStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' +import { ManagerTab } from '@/types/comfyManagerTypes' import { showNativeSystemMenu } from '@/utils/envUtil' import { normalizeI18nKey } from '@/utils/formatUtil' import { whileMouseDown } from '@/utils/mouseDownUtil' @@ -106,6 +103,8 @@ const dialogStore = useDialogStore() const settingStore = useSettingStore() const { t } = useI18n() +const managerState = useManagerState() + const menuRef = ref< ({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null >(null) @@ -138,29 +137,11 @@ const showSettings = (defaultPanel?: string) => { }) } -const managerStateStore = useManagerStateStore() - const showManageExtensions = async () => { - const state = managerStateStore.managerUIState - - switch (state) { - case ManagerUIState.DISABLED: - showSettings('extension') - break - - case ManagerUIState.LEGACY_UI: - try { - await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility') - } catch { - // If legacy command doesn't exist, fall back to extensions panel - showSettings('extension') - } - break - - case ManagerUIState.NEW_UI: - useDialogService().showManagerDialog() - break - } + await managerState.openManager({ + initialTab: ManagerTab.All, + showToastOnLegacyError: false + }) } const themeMenuItems = computed(() => { diff --git a/src/composables/nodePack/useWorkflowPacks.ts b/src/composables/nodePack/useWorkflowPacks.ts index ea7429fda..73ee14dca 100644 --- a/src/composables/nodePack/useWorkflowPacks.ts +++ b/src/composables/nodePack/useWorkflowPacks.ts @@ -61,7 +61,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => { const nodeDef = nodeDefStore.nodeDefsByName[nodeName] if (nodeDef?.nodeSource.type === 'core') { if (!systemStatsStore.systemStats) { - await systemStatsStore.fetchSystemStats() + await systemStatsStore.refetchSystemStats() } return { id: CORE_NODES_PACK_NAME, diff --git a/src/composables/useConflictDetection.ts b/src/composables/useConflictDetection.ts index 6ec8f2cdb..8edcea41c 100644 --- a/src/composables/useConflictDetection.ts +++ b/src/composables/useConflictDetection.ts @@ -1,3 +1,4 @@ +import { until } from '@vueuse/core' import { uniqBy } from 'es-toolkit/compat' import { computed, getCurrentInstance, onUnmounted, readonly, ref } from 'vue' @@ -78,9 +79,8 @@ export function useConflictDetection() { try { // Get system stats from store (primary source of system information) const systemStatsStore = useSystemStatsStore() - if (!systemStatsStore.systemStats) { - await systemStatsStore.fetchSystemStats() - } + // Wait for systemStats to be initialized if not already + await until(systemStatsStore.isInitialized) // Fetch version information from backend (with error resilience) const [frontendVersion] = await Promise.allSettled([ @@ -127,7 +127,7 @@ export function useConflictDetection() { } systemEnvironment.value = environment - console.log( + console.debug( '[ConflictDetection] System environment detection completed:', environment ) @@ -427,7 +427,7 @@ export function useConflictDetection() { Object.entries(bulkResult).forEach(([packageId, failInfo]) => { if (failInfo !== null) { importFailures[packageId] = failInfo - console.log( + console.debug( `[ConflictDetection] Import failure found for ${packageId}:`, failInfo ) @@ -500,7 +500,7 @@ export function useConflictDetection() { */ async function performConflictDetection(): Promise { if (isDetecting.value) { - console.log('[ConflictDetection] Already detecting, skipping') + console.debug('[ConflictDetection] Already detecting, skipping') return { success: false, error_message: 'Already detecting conflicts', @@ -556,7 +556,10 @@ export function useConflictDetection() { detectionSummary.value = summary lastDetectionTime.value = new Date().toISOString() - console.log('[ConflictDetection] Conflict detection completed:', summary) + console.debug( + '[ConflictDetection] Conflict detection completed:', + summary + ) // Store conflict results for later UI display // Dialog will be shown based on specific events, not on app mount @@ -568,7 +571,7 @@ export function useConflictDetection() { // Merge conflicts for packages with the same name const mergedConflicts = mergeConflictsByPackageName(conflictedResults) - console.log( + console.debug( '[ConflictDetection] Conflicts detected (stored for UI):', mergedConflicts ) @@ -632,11 +635,22 @@ export function useConflictDetection() { /** * Error-resilient initialization (called on app mount). * Async function that doesn't block UI setup. - * Ensures proper order: installed -> system_stats -> versions bulk -> import_fail_info_bulk + * Ensures proper order: system_stats -> manager state -> installed -> versions bulk -> import_fail_info_bulk */ async function initializeConflictDetection(): Promise { try { - // Simply perform conflict detection + // Check if manager is new Manager before proceeding + const { useManagerState } = await import('@/composables/useManagerState') + const managerState = useManagerState() + + if (!managerState.isNewManagerUI.value) { + console.debug( + '[ConflictDetection] Manager is not new Manager, skipping conflict detection' + ) + return + } + + // Manager is new Manager, perform conflict detection // The useInstalledPacks will handle fetching installed list if needed await performConflictDetection() } catch (error) { @@ -671,13 +685,13 @@ export function useConflictDetection() { * Check if conflicts should trigger modal display after "What's New" dismissal */ async function shouldShowConflictModalAfterUpdate(): Promise { - console.log( + console.debug( '[ConflictDetection] Checking if conflict modal should show after update...' ) // Ensure conflict detection has run if (detectionResults.value.length === 0) { - console.log( + console.debug( '[ConflictDetection] No detection results, running conflict detection...' ) await performConflictDetection() @@ -689,7 +703,7 @@ export function useConflictDetection() { const hasActualConflicts = hasConflicts.value const canShowModal = acknowledgment.shouldShowConflictModal.value - console.log('[ConflictDetection] Modal check:', { + console.debug('[ConflictDetection] Modal check:', { hasConflicts: hasActualConflicts, canShowModal: canShowModal, conflictedPackagesCount: conflictedPackages.value.length diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 61ed889bb..ce5587104 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -1,5 +1,6 @@ import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems' +import { ManagerUIState, useManagerState } from '@/composables/useManagerState' import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog' import { DEFAULT_DARK_COLOR_PALETTE, @@ -20,15 +21,10 @@ import { useDialogService } from '@/services/dialogService' import { useLitegraphService } from '@/services/litegraphService' import { useWorkflowService } from '@/services/workflowService' import type { ComfyCommand } from '@/stores/commandStore' -import { useCommandStore } from '@/stores/commandStore' 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' @@ -732,34 +728,9 @@ export function useCoreCommands(): ComfyCommand[] { label: 'Custom Nodes Manager', versionAdded: '1.12.10', function: async () => { - const managerState = useManagerStateStore().managerUIState - - switch (managerState) { - case ManagerUIState.DISABLED: - dialogService.showSettingsDialog('extension') - break - - case ManagerUIState.LEGACY_UI: - 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({ - severity: 'error', - summary: t('g.error'), - detail: t('manager.legacyMenuNotAvailable'), - life: 3000 - }) - dialogService.showManagerDialog() - } - break - - case ManagerUIState.NEW_UI: - dialogService.showManagerDialog() - break - } + await useManagerState().openManager({ + showToastOnLegacyError: true + }) } }, { @@ -767,33 +738,25 @@ export function useCoreCommands(): ComfyCommand[] { icon: 'pi pi-sync', label: 'Check for Custom Node Updates', versionAdded: '1.17.0', - function: () => { - const managerStore = useManagerStateStore() - const state = managerStore.managerUIState + function: async () => { + const managerState = useManagerState() + const state = managerState.managerUIState.value - switch (state) { - case ManagerUIState.DISABLED: - toastStore.add({ - severity: 'error', - summary: t('g.error'), - detail: t('manager.notAvailable'), - life: 3000 - }) - break - - case ManagerUIState.LEGACY_UI: - useCommandStore() - .execute('Comfy.Manager.Menu.ToggleVisibility') - .catch(() => { - // If legacy command doesn't exist, fall back to extensions panel - dialogService.showSettingsDialog('extension') - }) - break - - case ManagerUIState.NEW_UI: - dialogService.showManagerDialog() - break + // For DISABLED state, show error toast instead of opening settings + if (state === ManagerUIState.DISABLED) { + toastStore.add({ + severity: 'error', + summary: t('g.error'), + detail: t('manager.notAvailable'), + life: 3000 + }) + return } + + await managerState.openManager({ + initialTab: ManagerTab.UpdateAvailable, + showToastOnLegacyError: false + }) } }, { @@ -802,32 +765,10 @@ export function useCoreCommands(): ComfyCommand[] { label: 'Install Missing Custom Nodes', versionAdded: '1.17.0', function: async () => { - const managerStore = useManagerStateStore() - const state = managerStore.managerUIState - - switch (state) { - case ManagerUIState.DISABLED: - // When manager is disabled, open the extensions panel in settings - dialogService.showSettingsDialog('extension') - break - - case ManagerUIState.LEGACY_UI: - try { - await useCommandStore().execute( - 'Comfy.Manager.Menu.ToggleVisibility' - ) - } catch { - // If legacy command doesn't exist, fall back to extensions panel - dialogService.showSettingsDialog('extension') - } - break - - case ManagerUIState.NEW_UI: - dialogService.showManagerDialog({ - initialTab: ManagerTab.Missing - }) - break - } + await useManagerState().openManager({ + initialTab: ManagerTab.Missing, + showToastOnLegacyError: false + }) } }, { @@ -933,8 +874,11 @@ export function useCoreCommands(): ComfyCommand[] { id: 'Comfy.OpenManagerDialog', icon: 'mdi mdi-puzzle-outline', label: 'Manager', - function: () => { - dialogService.showManagerDialog() + function: async () => { + await useManagerState().openManager({ + initialTab: ManagerTab.All, + showToastOnLegacyError: false + }) } }, { @@ -999,18 +943,11 @@ export function useCoreCommands(): ComfyCommand[] { label: 'Custom Nodes (Legacy)', versionAdded: '1.16.4', function: async () => { - try { - await useCommandStore().execute( - 'Comfy.Manager.CustomNodesManager.ToggleVisibility' - ) - } catch (error) { - useToastStore().add({ - severity: 'error', - summary: t('g.error'), - detail: t('manager.legacyMenuNotAvailable'), - life: 3000 - }) - } + await useManagerState().openManager({ + legacyCommand: 'Comfy.Manager.CustomNodesManager.ToggleVisibility', + showToastOnLegacyError: true, + isLegacyOnly: true + }) } }, { @@ -1019,16 +956,10 @@ export function useCoreCommands(): ComfyCommand[] { label: 'Manager Menu (Legacy)', versionAdded: '1.16.4', function: async () => { - try { - await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility') - } catch (error) { - useToastStore().add({ - severity: 'error', - summary: t('g.error'), - detail: t('manager.legacyMenuNotAvailable'), - life: 3000 - }) - } + await useManagerState().openManager({ + showToastOnLegacyError: true, + isLegacyOnly: true + }) } }, { diff --git a/src/composables/useManagerState.ts b/src/composables/useManagerState.ts new file mode 100644 index 000000000..ae9eb1cde --- /dev/null +++ b/src/composables/useManagerState.ts @@ -0,0 +1,203 @@ +import { storeToRefs } from 'pinia' +import { computed, readonly } from 'vue' + +import { t } from '@/i18n' +import { api } from '@/scripts/api' +import { useDialogService } from '@/services/dialogService' +import { useCommandStore } from '@/stores/commandStore' +import { useSystemStatsStore } from '@/stores/systemStatsStore' +import { useToastStore } from '@/stores/toastStore' +import { ManagerTab } from '@/types/comfyManagerTypes' + +export enum ManagerUIState { + DISABLED = 'disabled', + LEGACY_UI = 'legacy', + NEW_UI = 'new' +} + +export function useManagerState() { + const systemStatsStore = useSystemStatsStore() + const { systemStats, isInitialized: systemInitialized } = + storeToRefs(systemStatsStore) + + /** + * The current manager UI state. + * Computed once and cached until dependencies change (which they don't during runtime). + * This follows Vue's conventions and provides better performance through caching. + */ + const managerUIState = readonly( + computed((): ManagerUIState => { + // Wait for systemStats to be initialized + if (!systemInitialized.value) { + // Default to DISABLED while loading + return ManagerUIState.DISABLED + } + + // Get current values + const clientSupportsV4 = + api.getClientFeatureFlags().supports_manager_v4_ui ?? false + + const serverSupportsV4 = api.getServerFeature( + 'extension.manager.supports_v4' + ) + + // Check command line args first (highest priority) + if (systemStats.value?.system?.argv?.includes('--disable-manager')) { + return ManagerUIState.DISABLED + } + + if ( + systemStats.value?.system?.argv?.includes('--enable-manager-legacy-ui') + ) { + return ManagerUIState.LEGACY_UI + } + + // Both client and server support v4 = NEW_UI + if (clientSupportsV4 && serverSupportsV4 === true) { + return ManagerUIState.NEW_UI + } + + // Server supports v4 but client doesn't = LEGACY_UI + if (serverSupportsV4 === true && !clientSupportsV4) { + return ManagerUIState.LEGACY_UI + } + + // Server explicitly doesn't support v4 = LEGACY_UI + if (serverSupportsV4 === false) { + return ManagerUIState.LEGACY_UI + } + + // If server feature flags haven't loaded yet, default to NEW_UI + // This is a temporary state - feature flags are exchanged immediately on WebSocket connection + // NEW_UI is the safest default since v2 API is the current standard + // If the server doesn't support v2, API calls will fail with 404 and be handled gracefully + if (serverSupportsV4 === undefined) { + return ManagerUIState.NEW_UI + } + + // Should never reach here, but if we do, disable manager + return ManagerUIState.DISABLED + }) + ) + + /** + * Check if manager is enabled (not DISABLED) + */ + const isManagerEnabled = readonly( + computed((): boolean => { + return managerUIState.value !== ManagerUIState.DISABLED + }) + ) + + /** + * Check if manager UI is in NEW_UI mode + */ + const isNewManagerUI = readonly( + computed((): boolean => { + return managerUIState.value === ManagerUIState.NEW_UI + }) + ) + + /** + * Check if manager UI is in LEGACY_UI mode + */ + const isLegacyManagerUI = readonly( + computed((): boolean => { + return managerUIState.value === ManagerUIState.LEGACY_UI + }) + ) + + /** + * Check if install button should be shown (only in NEW_UI mode) + */ + const shouldShowInstallButton = readonly( + computed((): boolean => { + return isNewManagerUI.value + }) + ) + + /** + * Check if manager buttons should be shown (when manager is not disabled) + */ + const shouldShowManagerButtons = readonly( + computed((): boolean => { + return isManagerEnabled.value + }) + ) + + /** + * Opens the manager UI based on current state + * Centralizes the logic for opening manager across the app + * @param options - Optional configuration for opening the manager + * @param options.initialTab - Initial tab to show (for NEW_UI mode) + * @param options.legacyCommand - Legacy command to execute (for LEGACY_UI mode) + * @param options.showToastOnLegacyError - Whether to show toast on legacy command failure + * @param options.isLegacyOnly - If true, shows error in NEW_UI mode instead of opening manager + */ + const openManager = async (options?: { + initialTab?: ManagerTab + legacyCommand?: string + showToastOnLegacyError?: boolean + isLegacyOnly?: boolean + }): Promise => { + const state = managerUIState.value + const dialogService = useDialogService() + const commandStore = useCommandStore() + + switch (state) { + case ManagerUIState.DISABLED: + dialogService.showSettingsDialog('extension') + break + + case ManagerUIState.LEGACY_UI: { + const command = + options?.legacyCommand || 'Comfy.Manager.Menu.ToggleVisibility' + try { + await commandStore.execute(command) + } catch { + // If legacy command doesn't exist + if (options?.showToastOnLegacyError !== false) { + useToastStore().add({ + severity: 'error', + summary: t('g.error'), + detail: t('manager.legacyMenuNotAvailable'), + life: 3000 + }) + } + // Fallback to extensions panel if not showing toast + if (options?.showToastOnLegacyError === false) { + dialogService.showSettingsDialog('extension') + } + } + break + } + + case ManagerUIState.NEW_UI: + if (options?.isLegacyOnly) { + // Legacy command is not available in NEW_UI mode + useToastStore().add({ + severity: 'error', + summary: t('g.error'), + detail: t('manager.legacyMenuNotAvailable'), + life: 3000 + }) + dialogService.showManagerDialog({ initialTab: ManagerTab.All }) + } else { + dialogService.showManagerDialog( + options?.initialTab ? { initialTab: options.initialTab } : undefined + ) + } + break + } + } + + return { + managerUIState, + isManagerEnabled, + isNewManagerUI, + isLegacyManagerUI, + shouldShowInstallButton, + shouldShowManagerButtons, + openManager + } +} diff --git a/src/services/comfyManagerService.ts b/src/services/comfyManagerService.ts index 6bc09b4ae..615ddda86 100644 --- a/src/services/comfyManagerService.ts +++ b/src/services/comfyManagerService.ts @@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosResponse } from 'axios' import { v4 as uuidv4 } from 'uuid' import { ref } from 'vue' +import { useManagerState } from '@/composables/useManagerState' import { api } from '@/scripts/api' import { components } from '@/types/generatedManagerTypes' import { isAbortError } from '@/utils/typeGuardUtil' @@ -44,11 +45,18 @@ const managerApiClient = axios.create({ /** * Service for interacting with the ComfyUI Manager API * Provides methods for managing packs, ComfyUI-Manager queue operations, and system functions + * Note: This service should only be used when Manager state is NEW_UI */ export const useComfyManagerService = () => { const isLoading = ref(false) const error = ref(null) + // Check if manager service should be available + const isManagerServiceAvailable = () => { + const managerState = useManagerState() + return managerState.isNewManagerUI.value + } + const handleRequestError = ( err: unknown, context: string, @@ -87,6 +95,12 @@ export const useComfyManagerService = () => { ): Promise => { const { errorContext, routeSpecificErrors, isQueueOperation } = options + // Block service calls if not in NEW_UI state + if (!isManagerServiceAvailable()) { + error.value = 'Manager service is not available in current mode' + return null + } + isLoading.value = true error.value = null @@ -151,6 +165,10 @@ export const useComfyManagerService = () => { ) => { const errorContext = 'Fetching bulk import failure information' + if (!params.cnr_ids?.length && !params.urls?.length) { + return {} + } + return executeRequest( () => managerApiClient.post(ManagerRoute.IMPORT_FAIL_INFO_BULK, params, { diff --git a/src/stores/managerStateStore.ts b/src/stores/managerStateStore.ts deleted file mode 100644 index 6ba37e80d..000000000 --- a/src/stores/managerStateStore.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { defineStore } from 'pinia' -import { computed, readonly } from 'vue' - -import { api } from '@/scripts/api' -import { useExtensionStore } from '@/stores/extensionStore' -import { useSystemStatsStore } from '@/stores/systemStatsStore' - -export enum ManagerUIState { - DISABLED = 'disabled', - LEGACY_UI = 'legacy', - NEW_UI = 'new' -} - -export const useManagerStateStore = defineStore('managerState', () => { - const systemStatsStore = useSystemStatsStore() - const extensionStore = useExtensionStore() - - // Reactive computed manager state that updates when dependencies change - const managerUIState = computed(() => { - const systemStats = systemStatsStore.systemStats - const clientSupportsV4 = - api.getClientFeatureFlags().supports_manager_v4_ui ?? false - const hasLegacyManager = extensionStore.extensions.some( - (ext) => ext.name === 'Comfy.CustomNodesManager' - ) - - const serverSupportsV4 = api.getServerFeature( - 'extension.manager.supports_v4' - ) - - // Check command line args first - if (systemStats?.system?.argv?.includes('--disable-manager')) { - return ManagerUIState.DISABLED // comfyui_manager package not installed - } - - if (systemStats?.system?.argv?.includes('--enable-manager-legacy-ui')) { - return ManagerUIState.LEGACY_UI // forced legacy - } - - // Both client and server support v4 = NEW_UI - if (clientSupportsV4 && serverSupportsV4 === true) { - return ManagerUIState.NEW_UI - } - - // Server supports v4 but client doesn't = LEGACY_UI - if (serverSupportsV4 === true) { - return ManagerUIState.LEGACY_UI - } - - // No server v4 support but legacy manager extension exists = LEGACY_UI - if (hasLegacyManager) { - return ManagerUIState.LEGACY_UI - } - - // If server feature flags haven't loaded yet, return DISABLED for now - // This will update reactively once feature flags load - if (serverSupportsV4 === undefined) { - return ManagerUIState.DISABLED - } - - // No manager at all = DISABLED - return ManagerUIState.DISABLED - }) - - return { - managerUIState: readonly(managerUIState) - } -}) diff --git a/src/stores/releaseStore.ts b/src/stores/releaseStore.ts index 37faee5a2..a4f87281e 100644 --- a/src/stores/releaseStore.ts +++ b/src/stores/releaseStore.ts @@ -1,3 +1,4 @@ +import { until } from '@vueuse/core' import { defineStore } from 'pinia' import { computed, ref } from 'vue' @@ -240,7 +241,7 @@ export const useReleaseStore = defineStore('release', () => { try { // Ensure system stats are loaded if (!systemStatsStore.systemStats) { - await systemStatsStore.fetchSystemStats() + await until(systemStatsStore.isInitialized) } const fetchedReleases = await releaseService.getReleases({ diff --git a/src/stores/systemStatsStore.ts b/src/stores/systemStatsStore.ts index 07f28c229..06362930b 100644 --- a/src/stores/systemStatsStore.ts +++ b/src/stores/systemStatsStore.ts @@ -1,32 +1,34 @@ +import { useAsyncState } from '@vueuse/core' import { defineStore } from 'pinia' -import { ref } from 'vue' import type { SystemStats } from '@/schemas/apiSchema' import { api } from '@/scripts/api' import { isElectron } from '@/utils/envUtil' export const useSystemStatsStore = defineStore('systemStats', () => { - const systemStats = ref(null) - const isLoading = ref(false) - const error = ref(null) - - async function fetchSystemStats() { - isLoading.value = true - error.value = null - + const fetchSystemStatsData = async () => { try { - systemStats.value = await api.getSystemStats() + return await api.getSystemStats() } catch (err) { - error.value = - err instanceof Error - ? err.message - : 'An error occurred while fetching system stats' console.error('Error fetching system stats:', err) - } finally { - isLoading.value = false + throw err } } + const { + state: systemStats, + isLoading, + error, + isReady: isInitialized, + execute: refetchSystemStats + } = useAsyncState( + fetchSystemStatsData, + null, // initial value + { + immediate: true + } + ) + function getFormFactor(): string { if (!systemStats.value?.system?.os) { return 'other' @@ -62,7 +64,8 @@ export const useSystemStatsStore = defineStore('systemStats', () => { systemStats, isLoading, error, - fetchSystemStats, + isInitialized, + refetchSystemStats, getFormFactor } }) diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts index da483846e..46b25cf33 100644 --- a/src/stores/versionCompatibilityStore.ts +++ b/src/stores/versionCompatibilityStore.ts @@ -1,4 +1,4 @@ -import { useStorage } from '@vueuse/core' +import { until, useStorage } from '@vueuse/core' import { defineStore } from 'pinia' import * as semver from 'semver' import { computed } from 'vue' @@ -103,7 +103,7 @@ export const useVersionCompatibilityStore = defineStore( async function checkVersionCompatibility() { if (!systemStatsStore.systemStats) { - await systemStatsStore.fetchSystemStats() + await until(systemStatsStore.isInitialized) } } diff --git a/tests-ui/tests/components/dialog/content/MissingCoreNodesMessage.spec.ts b/tests-ui/tests/components/dialog/content/MissingCoreNodesMessage.spec.ts index eeecae0a7..afcb90996 100644 --- a/tests-ui/tests/components/dialog/content/MissingCoreNodesMessage.spec.ts +++ b/tests-ui/tests/components/dialog/content/MissingCoreNodesMessage.spec.ts @@ -33,14 +33,14 @@ const createMockNode = (type: string, version?: string): LGraphNode => describe('MissingCoreNodesMessage', () => { const mockSystemStatsStore = { systemStats: null as { system?: { comfyui_version?: string } } | null, - fetchSystemStats: vi.fn() + refetchSystemStats: vi.fn() } beforeEach(() => { vi.clearAllMocks() // Reset the mock store state mockSystemStatsStore.systemStats = null - mockSystemStatsStore.fetchSystemStats = vi.fn() + mockSystemStatsStore.refetchSystemStats = vi.fn() // @ts-expect-error - Mocking the return value of useSystemStatsStore for testing. // The actual store has more properties, but we only need these for our tests. useSystemStatsStore.mockReturnValue(mockSystemStatsStore) @@ -86,15 +86,11 @@ describe('MissingCoreNodesMessage', () => { expect(wrapper.findComponent(Message).exists()).toBe(true) }) - it('fetches and displays current ComfyUI version', async () => { - // Start with no systemStats to trigger fetch - mockSystemStatsStore.fetchSystemStats.mockImplementation(() => { - // Simulate the fetch setting the systemStats - mockSystemStatsStore.systemStats = { - system: { comfyui_version: '1.0.0' } - } - return Promise.resolve() - }) + it('displays current ComfyUI version when available', async () => { + // Set systemStats directly (store auto-fetches with useAsyncState) + mockSystemStatsStore.systemStats = { + system: { comfyui_version: '1.0.0' } + } const missingCoreNodes = { '1.2.0': [createMockNode('TestNode', '1.2.0')] @@ -102,20 +98,18 @@ describe('MissingCoreNodesMessage', () => { const wrapper = mountComponent({ missingCoreNodes }) - // Wait for all async operations - await nextTick() - await new Promise((resolve) => setTimeout(resolve, 0)) + // Wait for component to render await nextTick() - expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled() + // No need to check if fetchSystemStats was called since useAsyncState auto-fetches expect(wrapper.text()).toContain( 'Some nodes require a newer version of ComfyUI (current: 1.0.0)' ) }) it('displays generic message when version is unavailable', async () => { - // Mock fetchSystemStats to resolve without setting systemStats - mockSystemStatsStore.fetchSystemStats.mockResolvedValue(undefined) + // No systemStats set - version unavailable + mockSystemStatsStore.systemStats = null const missingCoreNodes = { '1.2.0': [createMockNode('TestNode', '1.2.0')] diff --git a/tests-ui/tests/composables/useConflictDetection.test.ts b/tests-ui/tests/composables/useConflictDetection.test.ts index 9dfcb844a..52935f65b 100644 --- a/tests-ui/tests/composables/useConflictDetection.test.ts +++ b/tests-ui/tests/composables/useConflictDetection.test.ts @@ -96,7 +96,7 @@ describe.skip('useConflictDetection with Registry Store', () => { } const mockSystemStatsStore = { - fetchSystemStats: vi.fn(), + refetchSystemStats: vi.fn(), systemStats: { system: { comfyui_version: '0.3.41', @@ -133,7 +133,7 @@ describe.skip('useConflictDetection with Registry Store', () => { } as any // Reset mock functions - mockSystemStatsStore.fetchSystemStats.mockResolvedValue(undefined) + mockSystemStatsStore.refetchSystemStats.mockResolvedValue(undefined) mockComfyManagerService.listInstalledPacks.mockReset() mockComfyManagerService.getImportFailInfo.mockReset() mockRegistryService.getPackByVersion.mockReset() @@ -185,7 +185,7 @@ describe.skip('useConflictDetection with Registry Store', () => { it('should return fallback environment information when systemStatsStore fails', async () => { // Mock systemStatsStore failure - mockSystemStatsStore.fetchSystemStats.mockRejectedValue( + mockSystemStatsStore.refetchSystemStats.mockRejectedValue( new Error('Store failure') ) mockSystemStatsStore.systemStats = null @@ -754,7 +754,7 @@ describe.skip('useConflictDetection with Registry Store', () => { describe('error resilience with Registry Store', () => { it('should continue execution even when system environment detection fails', async () => { // Mock system stats store failure - mockSystemStatsStore.fetchSystemStats.mockRejectedValue( + mockSystemStatsStore.refetchSystemStats.mockRejectedValue( new Error('Store error') ) mockSystemStatsStore.systemStats = null @@ -851,7 +851,7 @@ describe.skip('useConflictDetection with Registry Store', () => { it('should handle complete system failure gracefully', async () => { // Mock all stores/services failing - mockSystemStatsStore.fetchSystemStats.mockRejectedValue( + mockSystemStatsStore.refetchSystemStats.mockRejectedValue( new Error('Critical error') ) mockSystemStatsStore.systemStats = null diff --git a/tests-ui/tests/composables/useManagerState.test.ts b/tests-ui/tests/composables/useManagerState.test.ts new file mode 100644 index 000000000..dafb48a6f --- /dev/null +++ b/tests-ui/tests/composables/useManagerState.test.ts @@ -0,0 +1,320 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ref } from 'vue' + +import { useFeatureFlags } from '@/composables/useFeatureFlags' +import { ManagerUIState, useManagerState } from '@/composables/useManagerState' +import { api } from '@/scripts/api' +import { useExtensionStore } from '@/stores/extensionStore' +import { useSystemStatsStore } from '@/stores/systemStatsStore' + +// Mock dependencies +vi.mock('@/scripts/api', () => ({ + api: { + getClientFeatureFlags: vi.fn(), + getServerFeature: vi.fn() + } +})) + +vi.mock('@/composables/useFeatureFlags', () => ({ + useFeatureFlags: vi.fn(() => ({ + flags: { supportsManagerV4: false }, + featureFlag: vi.fn() + })) +})) + +vi.mock('@/stores/extensionStore', () => ({ + useExtensionStore: vi.fn() +})) + +vi.mock('@/stores/systemStatsStore', () => ({ + useSystemStatsStore: vi.fn() +})) + +vi.mock('@/services/dialogService', () => ({ + useDialogService: vi.fn(() => ({ + showManagerPopup: vi.fn(), + showLegacyManagerPopup: vi.fn(), + showSettingsDialog: vi.fn() + })) +})) + +vi.mock('@/stores/commandStore', () => ({ + useCommandStore: vi.fn(() => ({ + execute: vi.fn() + })) +})) + +vi.mock('@/stores/toastStore', () => ({ + useToastStore: vi.fn(() => ({ + add: vi.fn() + })) +})) + +describe('useManagerState', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('managerUIState property', () => { + it('should return DISABLED state when --disable-manager is present', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ + system: { argv: ['python', 'main.py', '--disable-manager'] } + }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + + expect(managerState.managerUIState.value).toBe(ManagerUIState.DISABLED) + }) + + it('should return LEGACY_UI state when --enable-manager-legacy-ui is present', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ + system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] } + }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + + expect(managerState.managerUIState.value).toBe(ManagerUIState.LEGACY_UI) + }) + + it('should return NEW_UI state when client and server both support v4', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(api.getServerFeature).mockReturnValue(true) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: true }, + featureFlag: vi.fn() + } as any) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + + expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI) + }) + + it('should return LEGACY_UI state when server supports v4 but client does not', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: false + }) + vi.mocked(api.getServerFeature).mockReturnValue(true) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: true }, + featureFlag: vi.fn() + } as any) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + + expect(managerState.managerUIState.value).toBe(ManagerUIState.LEGACY_UI) + }) + + it('should return LEGACY_UI state when legacy manager extension exists', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: false }, + featureFlag: vi.fn() + } as any) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [{ name: 'Comfy.CustomNodesManager' }] + } as any) + + const managerState = useManagerState() + + expect(managerState.managerUIState.value).toBe(ManagerUIState.LEGACY_UI) + }) + + it('should return NEW_UI state when server feature flags are undefined', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(api.getServerFeature).mockReturnValue(undefined) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: undefined }, + featureFlag: vi.fn() + } as any) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + + expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI) + }) + + it('should return LEGACY_UI state when server does not support v4', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(api.getServerFeature).mockReturnValue(false) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: false }, + featureFlag: vi.fn() + } as any) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + + expect(managerState.managerUIState.value).toBe(ManagerUIState.LEGACY_UI) + }) + + it('should handle null systemStats gracefully', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref(null), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(api.getServerFeature).mockReturnValue(true) + vi.mocked(useFeatureFlags).mockReturnValue({ + flags: { supportsManagerV4: true }, + featureFlag: vi.fn() + } as any) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + + expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI) + }) + }) + + describe('helper properties', () => { + it('isManagerEnabled should return true when state is not DISABLED', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(api.getServerFeature).mockReturnValue(true) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + expect(managerState.isManagerEnabled.value).toBe(true) + }) + + it('isManagerEnabled should return false when state is DISABLED', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ + system: { argv: ['python', 'main.py', '--disable-manager'] } + }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + expect(managerState.isManagerEnabled.value).toBe(false) + }) + + it('isNewManagerUI should return true when state is NEW_UI', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(api.getServerFeature).mockReturnValue(true) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + expect(managerState.isNewManagerUI.value).toBe(true) + }) + + it('isLegacyManagerUI should return true when state is LEGACY_UI', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ + system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] } + }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + expect(managerState.isLegacyManagerUI.value).toBe(true) + }) + + it('shouldShowInstallButton should return true only for NEW_UI', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(api.getServerFeature).mockReturnValue(true) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + expect(managerState.shouldShowInstallButton.value).toBe(true) + }) + + it('shouldShowManagerButtons should return true when not DISABLED', () => { + vi.mocked(useSystemStatsStore).mockReturnValue({ + systemStats: ref({ system: { argv: ['python', 'main.py'] } }), + isInitialized: ref(true) + } as any) + vi.mocked(api.getClientFeatureFlags).mockReturnValue({ + supports_manager_v4_ui: true + }) + vi.mocked(api.getServerFeature).mockReturnValue(true) + vi.mocked(useExtensionStore).mockReturnValue({ + extensions: [] + } as any) + + const managerState = useManagerState() + expect(managerState.shouldShowManagerButtons.value).toBe(true) + }) + }) +}) diff --git a/tests-ui/tests/store/releaseStore.test.ts b/tests-ui/tests/store/releaseStore.test.ts index 730037f03..93a9d9dc6 100644 --- a/tests-ui/tests/store/releaseStore.test.ts +++ b/tests-ui/tests/store/releaseStore.test.ts @@ -9,6 +9,10 @@ vi.mock('@/utils/envUtil') vi.mock('@/services/releaseService') vi.mock('@/stores/settingStore') vi.mock('@/stores/systemStatsStore') +vi.mock('@vueuse/core', () => ({ + until: vi.fn(() => Promise.resolve()), + useStorage: vi.fn(() => ({ value: {} })) +})) describe('useReleaseStore', () => { let store: ReturnType @@ -49,7 +53,8 @@ describe('useReleaseStore', () => { comfyui_version: '1.0.0' } }, - fetchSystemStats: vi.fn(), + isInitialized: true, + refetchSystemStats: vi.fn(), getFormFactor: vi.fn(() => 'git-windows') } @@ -334,12 +339,15 @@ describe('useReleaseStore', () => { }) it('should fetch system stats if not available', async () => { + const { until } = await import('@vueuse/core') mockSystemStatsStore.systemStats = null + mockSystemStatsStore.isInitialized = false mockReleaseService.getReleases.mockResolvedValue([mockRelease]) await store.initialize() - expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled() + expect(until).toHaveBeenCalled() + expect(mockReleaseService.getReleases).toHaveBeenCalled() }) it('should not set loading state when notifications disabled', async () => { @@ -401,12 +409,14 @@ describe('useReleaseStore', () => { }) it('should proceed with fetchReleases when system stats are not available', async () => { + const { until } = await import('@vueuse/core') mockSystemStatsStore.systemStats = null + mockSystemStatsStore.isInitialized = false mockReleaseService.getReleases.mockResolvedValue([mockRelease]) await store.fetchReleases() - expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled() + expect(until).toHaveBeenCalled() expect(mockReleaseService.getReleases).toHaveBeenCalled() }) }) @@ -530,7 +540,7 @@ describe('useReleaseStore', () => { await store.initialize() // Should not fetch system stats when notifications disabled - expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled() + expect(mockSystemStatsStore.refetchSystemStats).not.toHaveBeenCalled() }) it('should handle concurrent fetchReleases calls', async () => { diff --git a/tests-ui/tests/store/systemStatsStore.test.ts b/tests-ui/tests/store/systemStatsStore.test.ts index 84e84ec44..67d76ee5a 100644 --- a/tests-ui/tests/store/systemStatsStore.test.ts +++ b/tests-ui/tests/store/systemStatsStore.test.ts @@ -21,18 +21,25 @@ describe('useSystemStatsStore', () => { let store: ReturnType beforeEach(() => { + // Mock API to prevent automatic fetch on store creation + vi.mocked(api.getSystemStats).mockResolvedValue(null as any) setActivePinia(createPinia()) store = useSystemStatsStore() vi.clearAllMocks() }) - it('should initialize with null systemStats', () => { - expect(store.systemStats).toBeNull() - expect(store.isLoading).toBe(false) - expect(store.error).toBeNull() + it('should initialize and start fetching immediately', async () => { + // useAsyncState with immediate: true starts loading right away + // In test environment, the mock resolves immediately so loading might be false already + expect(store.systemStats).toBeNull() // Initial value is null + expect(store.error).toBeUndefined() + + // Wait for initial fetch to complete + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(store.isInitialized).toBe(true) // Should be initialized after fetch }) - describe('fetchSystemStats', () => { + describe('refetchSystemStats', () => { it('should fetch system stats successfully', async () => { const mockStats = { system: { @@ -51,11 +58,12 @@ describe('useSystemStatsStore', () => { vi.mocked(api.getSystemStats).mockResolvedValue(mockStats) - await store.fetchSystemStats() + await store.refetchSystemStats() expect(store.systemStats).toEqual(mockStats) expect(store.isLoading).toBe(false) - expect(store.error).toBeNull() + expect(store.error).toBeUndefined() // useAsyncState uses undefined for no error + expect(store.isInitialized).toBe(true) expect(api.getSystemStats).toHaveBeenCalled() }) @@ -63,19 +71,19 @@ describe('useSystemStatsStore', () => { const error = new Error('API Error') vi.mocked(api.getSystemStats).mockRejectedValue(error) - await store.fetchSystemStats() + await store.refetchSystemStats() - expect(store.systemStats).toBeNull() + expect(store.systemStats).toBeNull() // Initial value stays null on error expect(store.isLoading).toBe(false) - expect(store.error).toBe('API Error') + expect(store.error).toEqual(error) // useAsyncState stores the actual error object }) it('should handle non-Error objects', async () => { vi.mocked(api.getSystemStats).mockRejectedValue('String error') - await store.fetchSystemStats() + await store.refetchSystemStats() - expect(store.error).toBe('An error occurred while fetching system stats') + expect(store.error).toBe('String error') // useAsyncState stores the actual error }) it('should set loading state correctly', async () => { @@ -85,7 +93,7 @@ describe('useSystemStatsStore', () => { }) vi.mocked(api.getSystemStats).mockReturnValue(promise) - const fetchPromise = store.fetchSystemStats() + const fetchPromise = store.refetchSystemStats() expect(store.isLoading).toBe(true) resolvePromise({}) @@ -112,11 +120,12 @@ describe('useSystemStatsStore', () => { vi.mocked(api.getSystemStats).mockResolvedValue(updatedStats) - await store.fetchSystemStats() + await store.refetchSystemStats() expect(store.systemStats).toEqual(updatedStats) expect(store.isLoading).toBe(false) - expect(store.error).toBeNull() + expect(store.error).toBeUndefined() + expect(store.isInitialized).toBe(true) expect(api.getSystemStats).toHaveBeenCalled() }) }) diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts index e3d3ceca9..b5ff99ab4 100644 --- a/tests-ui/tests/store/versionCompatibilityStore.test.ts +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -13,10 +13,11 @@ vi.mock('@/config', () => ({ vi.mock('@/stores/systemStatsStore') -// Mock useStorage from VueUse +// Mock useStorage and until from VueUse const mockDismissalStorage = ref({} as Record) vi.mock('@vueuse/core', () => ({ - useStorage: vi.fn(() => mockDismissalStorage) + useStorage: vi.fn(() => mockDismissalStorage), + until: vi.fn(() => Promise.resolve()) })) describe('useVersionCompatibilityStore', () => { @@ -31,7 +32,8 @@ describe('useVersionCompatibilityStore', () => { mockSystemStatsStore = { systemStats: null, - fetchSystemStats: vi.fn() + isInitialized: false, + refetchSystemStats: vi.fn() } vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore) @@ -51,6 +53,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.25.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -68,6 +71,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.23.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -83,6 +87,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.24.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -98,6 +103,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -113,6 +119,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: 'not-a-version' // invalid semver format } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -129,6 +136,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.23.0' // Required is 1.23.0, frontend 1.24.0 meets this } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -148,6 +156,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.25.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -167,6 +176,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.25.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -180,6 +190,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.24.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -195,6 +206,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.25.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -212,6 +224,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.24.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() @@ -230,6 +243,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.25.0' } } + mockSystemStatsStore.isInitialized = true await store.checkVersionCompatibility() store.dismissWarning() @@ -252,6 +266,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.25.0' } } + mockSystemStatsStore.isInitialized = true await store.initialize() @@ -270,6 +285,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.25.0' } } + mockSystemStatsStore.isInitialized = true await store.initialize() @@ -289,6 +305,7 @@ describe('useVersionCompatibilityStore', () => { required_frontend_version: '1.26.0' } } + mockSystemStatsStore.isInitialized = true await store.initialize() @@ -298,24 +315,28 @@ describe('useVersionCompatibilityStore', () => { describe('initialization', () => { it('should fetch system stats if not available', async () => { + const { until } = await import('@vueuse/core') mockSystemStatsStore.systemStats = null + mockSystemStatsStore.isInitialized = false await store.initialize() - expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled() + expect(until).toHaveBeenCalled() }) it('should not fetch system stats if already available', async () => { + const { until } = await import('@vueuse/core') mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.24.0', required_frontend_version: '1.24.0' } } + mockSystemStatsStore.isInitialized = true await store.initialize() - expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled() + expect(until).not.toHaveBeenCalled() }) }) }) diff --git a/tests-ui/tests/stores/managerStateStore.test.ts b/tests-ui/tests/stores/managerStateStore.test.ts deleted file mode 100644 index 28047b12a..000000000 --- a/tests-ui/tests/stores/managerStateStore.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { createPinia, setActivePinia } from 'pinia' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -import { useFeatureFlags } from '@/composables/useFeatureFlags' -import { api } from '@/scripts/api' -import { useExtensionStore } from '@/stores/extensionStore' -import { - ManagerUIState, - useManagerStateStore -} from '@/stores/managerStateStore' -import { useSystemStatsStore } from '@/stores/systemStatsStore' - -// Mock dependencies -vi.mock('@/scripts/api', () => ({ - api: { - getClientFeatureFlags: vi.fn(), - getServerFeature: vi.fn() - } -})) - -vi.mock('@/composables/useFeatureFlags', () => ({ - useFeatureFlags: vi.fn(() => ({ - flags: { supportsManagerV4: false }, - featureFlag: vi.fn() - })) -})) - -vi.mock('@/stores/extensionStore', () => ({ - useExtensionStore: vi.fn() -})) - -vi.mock('@/stores/systemStatsStore', () => ({ - useSystemStatsStore: vi.fn() -})) - -describe('useManagerStateStore', () => { - beforeEach(() => { - setActivePinia(createPinia()) - vi.clearAllMocks() - }) - - describe('managerUIState computed', () => { - it('should return DISABLED state when --disable-manager is present', () => { - vi.mocked(useSystemStatsStore).mockReturnValue({ - systemStats: { - system: { argv: ['python', 'main.py', '--disable-manager'] } - } - } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) - vi.mocked(useExtensionStore).mockReturnValue({ - extensions: [] - } as any) - - const store = useManagerStateStore() - - expect(store.managerUIState).toBe(ManagerUIState.DISABLED) - }) - - it('should return LEGACY_UI state when --enable-manager-legacy-ui is present', () => { - vi.mocked(useSystemStatsStore).mockReturnValue({ - systemStats: { - system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] } - } - } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) - vi.mocked(useExtensionStore).mockReturnValue({ - extensions: [] - } as any) - - const store = useManagerStateStore() - - expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI) - }) - - it('should return NEW_UI state when client and server both support v4', () => { - vi.mocked(useSystemStatsStore).mockReturnValue({ - systemStats: { system: { argv: ['python', 'main.py'] } } - } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({ - supports_manager_v4_ui: true - }) - vi.mocked(api.getServerFeature).mockReturnValue(true) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: true }, - featureFlag: vi.fn() - } as any) - vi.mocked(useExtensionStore).mockReturnValue({ - extensions: [] - } as any) - - const store = useManagerStateStore() - - expect(store.managerUIState).toBe(ManagerUIState.NEW_UI) - }) - - it('should return LEGACY_UI state when server supports v4 but client does not', () => { - vi.mocked(useSystemStatsStore).mockReturnValue({ - systemStats: { system: { argv: ['python', 'main.py'] } } - } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({ - supports_manager_v4_ui: false - }) - vi.mocked(api.getServerFeature).mockReturnValue(true) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: true }, - featureFlag: vi.fn() - } as any) - vi.mocked(useExtensionStore).mockReturnValue({ - extensions: [] - } as any) - - const store = useManagerStateStore() - - expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI) - }) - - it('should return LEGACY_UI state when legacy manager extension exists', () => { - 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(useExtensionStore).mockReturnValue({ - extensions: [{ name: 'Comfy.CustomNodesManager' }] - } as any) - - const store = useManagerStateStore() - - expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI) - }) - - it('should return DISABLED state when feature flags are undefined', () => { - vi.mocked(useSystemStatsStore).mockReturnValue({ - systemStats: { system: { argv: ['python', 'main.py'] } } - } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) - vi.mocked(api.getServerFeature).mockReturnValue(undefined) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: undefined }, - featureFlag: vi.fn() - } as any) - vi.mocked(useExtensionStore).mockReturnValue({ - extensions: [] - } as any) - - const store = useManagerStateStore() - - expect(store.managerUIState).toBe(ManagerUIState.DISABLED) - }) - - it('should return DISABLED state when no manager is available', () => { - vi.mocked(useSystemStatsStore).mockReturnValue({ - systemStats: { system: { argv: ['python', 'main.py'] } } - } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) - vi.mocked(api.getServerFeature).mockReturnValue(false) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: false }, - featureFlag: vi.fn() - } as any) - vi.mocked(useExtensionStore).mockReturnValue({ - extensions: [] - } as any) - - const store = useManagerStateStore() - - expect(store.managerUIState).toBe(ManagerUIState.DISABLED) - }) - - it('should handle null systemStats gracefully', () => { - vi.mocked(useSystemStatsStore).mockReturnValue({ - systemStats: null - } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({ - supports_manager_v4_ui: true - }) - vi.mocked(api.getServerFeature).mockReturnValue(true) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: true }, - featureFlag: vi.fn() - } as any) - vi.mocked(useExtensionStore).mockReturnValue({ - extensions: [] - } as any) - - const store = useManagerStateStore() - - expect(store.managerUIState).toBe(ManagerUIState.NEW_UI) - }) - }) -})