mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 02:32:18 +00:00
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -105,7 +105,7 @@ const showContactSupport = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
await systemStatsStore.refetchSystemStats()
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -42,9 +42,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Message from 'primevue/message'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
@@ -60,20 +59,11 @@ const hasMissingCoreNodes = computed(() => {
|
||||
return Object.keys(props.missingCoreNodes).length > 0
|
||||
})
|
||||
|
||||
const currentComfyUIVersion = ref<string | null>(null)
|
||||
whenever(
|
||||
hasMissingCoreNodes,
|
||||
async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
currentComfyUIVersion.value =
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ?? null
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
// Use computed for reactive version tracking
|
||||
const currentComfyUIVersion = computed<string | null>(() => {
|
||||
if (!hasMissingCoreNodes.value) return null
|
||||
return systemStatsStore.systemStats?.system?.comfyui_version ?? null
|
||||
})
|
||||
|
||||
const sortedMissingCoreNodes = computed(() => {
|
||||
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import Tag from 'primevue/tag'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import SystemStatsPanel from '@/components/common/SystemStatsPanel.vue'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
@@ -44,10 +43,4 @@ import PanelTemplate from './PanelTemplate.vue'
|
||||
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
|
||||
onMounted(async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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<MenuItem[]>(() => {
|
||||
icon: PuzzleIcon,
|
||||
label: t('helpCenter.managerExtension'),
|
||||
showRedDot: shouldShowManagerRedDot.value,
|
||||
action: () => {
|
||||
dialogService.showManagerDialog()
|
||||
action: async () => {
|
||||
await useManagerState().openManager({
|
||||
initialTab: ManagerTab.All,
|
||||
showToastOnLegacyError: false
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -106,16 +106,13 @@ 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 { useDialogService } from '@/services/dialogService'
|
||||
import { useManagerState } from '@/composables/useManagerState'
|
||||
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'
|
||||
@@ -127,6 +124,8 @@ const dialogStore = useDialogStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const managerState = useManagerState()
|
||||
|
||||
const menuRef = ref<
|
||||
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
|
||||
>(null)
|
||||
@@ -159,29 +158,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 extraMenuItems = computed<MenuItem[]>(() => [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ConflictDetectionResponse> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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
|
||||
|
||||
@@ -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'
|
||||
@@ -720,34 +716,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
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -755,33 +726,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
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -790,32 +753,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
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -921,8 +862,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
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -987,18 +931,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
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1007,16 +944,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
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
203
src/composables/useManagerState.ts
Normal file
203
src/composables/useManagerState.ts
Normal file
@@ -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<void> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<string | null>(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<T | null> => {
|
||||
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<components['schemas']['ImportFailInfoBulkResponse']>(
|
||||
() =>
|
||||
managerApiClient.post(ManagerRoute.IMPORT_FAIL_INFO_BULK, params, {
|
||||
|
||||
@@ -1,76 +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'
|
||||
)
|
||||
|
||||
console.log('[Manager State Debug]', {
|
||||
systemStats: systemStats?.system?.argv,
|
||||
clientSupportsV4,
|
||||
serverSupportsV4,
|
||||
hasLegacyManager,
|
||||
extensions: extensionStore.extensions.map((e) => e.name)
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
@@ -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({
|
||||
|
||||
@@ -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<SystemStats | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(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<SystemStats | null>(
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')]
|
||||
|
||||
@@ -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
|
||||
|
||||
320
tests-ui/tests/composables/useManagerState.test.ts
Normal file
320
tests-ui/tests/composables/useManagerState.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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<typeof useReleaseStore>
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -21,18 +21,25 @@ describe('useSystemStatsStore', () => {
|
||||
let store: ReturnType<typeof useSystemStatsStore>
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, number>)
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user