mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
11 Commits
fix/codera
...
backport-5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ae2d8b8d | ||
|
|
959ede2529 | ||
|
|
132e98b85e | ||
|
|
5d1cbd5612 | ||
|
|
5befd00dfc | ||
|
|
75e5089546 | ||
|
|
b9881fac29 | ||
|
|
a51e228e44 | ||
|
|
4f01333e74 | ||
|
|
2bb158c51c | ||
|
|
fdbf476179 |
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 96 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.9",
|
||||
"version": "1.26.10",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<Button
|
||||
v-bind="$attrs"
|
||||
unstyled
|
||||
:class="buttonStyle"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -20,6 +26,10 @@ interface IconButtonProps extends BaseButtonProps {
|
||||
onClick: (event: Event) => void
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'secondary',
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<Button
|
||||
v-bind="$attrs"
|
||||
unstyled
|
||||
:class="buttonStyle"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
|
||||
<span>{{ label }}</span>
|
||||
<slot v-if="iconPosition === 'right'" name="icon"></slot>
|
||||
@@ -18,6 +24,10 @@ import {
|
||||
getButtonTypeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
interface IconTextButtonProps extends BaseButtonProps {
|
||||
iconPosition?: 'left' | 'right'
|
||||
label: string
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
|
||||
<Button
|
||||
v-bind="$attrs"
|
||||
unstyled
|
||||
:class="buttonStyle"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
>
|
||||
<span>{{ label }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -21,6 +27,10 @@ interface TextButtonProps extends BaseButtonProps {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'primary',
|
||||
|
||||
@@ -105,7 +105,7 @@ const showContactSupport = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
await systemStatsStore.refetchSystemStats()
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<NoResultsPlaceholder
|
||||
class="pb-0"
|
||||
icon="pi pi-exclamation-circle"
|
||||
title="Some Nodes Are Missing"
|
||||
message="When loading the graph, the following node types were not found"
|
||||
:title="$t('loadWorkflowWarning.missingNodesTitle')"
|
||||
:message="$t('loadWorkflowWarning.missingNodesDescription')"
|
||||
/>
|
||||
<MissingCoreNodesMessage :missing-core-nodes="missingCoreNodes" />
|
||||
<ListBox
|
||||
@@ -53,19 +53,15 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed } from 'vue'
|
||||
import { computed, nextTick, watch } 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 { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
@@ -81,6 +77,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,48 +108,51 @@ 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
|
||||
})
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
// Computed to check if all missing nodes have been installed
|
||||
const allMissingNodesInstalled = computed(() => {
|
||||
return (
|
||||
!isLoading.value &&
|
||||
!isInstalling.value &&
|
||||
missingNodePacks.value?.length === 0
|
||||
)
|
||||
})
|
||||
// Watch for completion and close dialog
|
||||
watch(allMissingNodesInstalled, async (allInstalled) => {
|
||||
if (allInstalled) {
|
||||
// Use nextTick to ensure state updates are complete
|
||||
await nextTick()
|
||||
|
||||
dialogStore.closeDialog({ key: 'global-load-workflow-warning' })
|
||||
|
||||
// Show success toast
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: t('g.success'),
|
||||
detail: t('manager.allMissingNodesInstalled'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- Conflict Warning Banner -->
|
||||
<div
|
||||
v-if="shouldShowManagerBanner"
|
||||
class="bg-yellow-600 bg-opacity-20 border border-yellow-400 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6 relative"
|
||||
class="bg-yellow-500/20 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6 relative"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-600 text-lg"></i>
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
@@ -46,14 +46,15 @@
|
||||
{{ $t('manager.conflicts.warningBanner.button') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-2 right-2 w-6 h-6 border-none outline-none bg-transparent flex items-center justify-center text-yellow-600 rounded transition-colors"
|
||||
:aria-label="$t('g.close')"
|
||||
<IconButton
|
||||
class="absolute top-0 right-0"
|
||||
type="transparent"
|
||||
@click="dismissWarningBanner"
|
||||
>
|
||||
<i class="pi pi-times text-sm"></i>
|
||||
</button>
|
||||
<i
|
||||
class="pi pi-times text-neutral-900 dark-theme:text-white text-xs"
|
||||
></i>
|
||||
</IconButton>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:searchQuery="searchQuery"
|
||||
@@ -138,6 +139,7 @@ import {
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
@@ -31,11 +32,14 @@ const mockInstalledPacks = {
|
||||
'installed-pack': { ver: '2.0.0' }
|
||||
}
|
||||
|
||||
const mockIsPackEnabled = vi.fn(() => true)
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
installedPacks: mockInstalledPacks,
|
||||
isPackInstalled: (id: string) =>
|
||||
!!mockInstalledPacks[id as keyof typeof mockInstalledPacks]
|
||||
!!mockInstalledPacks[id as keyof typeof mockInstalledPacks],
|
||||
isPackEnabled: mockIsPackEnabled
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -60,6 +64,7 @@ describe('PackVersionBadge', () => {
|
||||
beforeEach(() => {
|
||||
mockToggle.mockReset()
|
||||
mockHide.mockReset()
|
||||
mockIsPackEnabled.mockReturnValue(true) // Reset to default enabled state
|
||||
})
|
||||
|
||||
const mountComponent = ({
|
||||
@@ -79,6 +84,9 @@ describe('PackVersionBadge', () => {
|
||||
},
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
},
|
||||
stubs: {
|
||||
Popover: PopoverStub,
|
||||
PackVersionSelectorPopover: true
|
||||
@@ -229,4 +237,63 @@ describe('PackVersionBadge', () => {
|
||||
expect(mockHide).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('disabled state', () => {
|
||||
beforeEach(() => {
|
||||
mockIsPackEnabled.mockReturnValue(false) // Set all packs as disabled for these tests
|
||||
})
|
||||
|
||||
it('adds disabled styles when pack is disabled', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const badge = wrapper.find('[role="text"]') // role changes to "text" when disabled
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.classes()).toContain('cursor-not-allowed')
|
||||
expect(badge.classes()).toContain('opacity-60')
|
||||
})
|
||||
|
||||
it('does not show chevron icon when disabled', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const chevronIcon = wrapper.find('.pi-chevron-right')
|
||||
expect(chevronIcon.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not show update arrow when disabled', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const updateIcon = wrapper.find('.pi-arrow-circle-up')
|
||||
expect(updateIcon.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not toggle popover when clicked while disabled', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const badge = wrapper.find('[role="text"]') // role changes to "text" when disabled
|
||||
expect(badge.exists()).toBe(true)
|
||||
await badge.trigger('click')
|
||||
|
||||
// Since it's disabled, the popover should not be toggled
|
||||
expect(mockToggle).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('has correct tabindex when disabled', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const badge = wrapper.find('[role="text"]') // role changes to "text" when disabled
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.attributes('tabindex')).toBe('-1')
|
||||
})
|
||||
|
||||
it('does not respond to keyboard events when disabled', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const badge = wrapper.find('[role="text"]') // role changes to "text" when disabled
|
||||
expect(badge.exists()).toBe(true)
|
||||
await badge.trigger('keydown.enter')
|
||||
await badge.trigger('keydown.space')
|
||||
|
||||
expect(mockToggle).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer py-1"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700 px-1.5': fill }"
|
||||
aria-haspopup="true"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="toggleVersionSelector"
|
||||
@keydown.enter="toggleVersionSelector"
|
||||
@keydown.space="toggleVersionSelector"
|
||||
v-tooltip.top="
|
||||
isDisabled ? $t('manager.enablePackToChangeVersion') : null
|
||||
"
|
||||
class="inline-flex items-center gap-1 rounded-2xl text-xs py-1"
|
||||
:class="{
|
||||
'bg-gray-100 dark-theme:bg-neutral-700 px-1.5': fill,
|
||||
'cursor-pointer': !isDisabled,
|
||||
'cursor-not-allowed opacity-60': isDisabled
|
||||
}"
|
||||
:aria-haspopup="!isDisabled"
|
||||
:role="isDisabled ? 'text' : 'button'"
|
||||
:tabindex="isDisabled ? -1 : 0"
|
||||
@click="!isDisabled && toggleVersionSelector($event)"
|
||||
@keydown.enter="!isDisabled && toggleVersionSelector($event)"
|
||||
@keydown.space="!isDisabled && toggleVersionSelector($event)"
|
||||
>
|
||||
<i
|
||||
v-if="isUpdateAvailable"
|
||||
class="pi pi-arrow-circle-up text-blue-600 text-xs"
|
||||
/>
|
||||
<span>{{ installedVersion }}</span>
|
||||
<i class="pi pi-chevron-right text-xxs" />
|
||||
<i v-if="!isDisabled" class="pi pi-chevron-right text-xxs" />
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
@@ -61,6 +68,11 @@ const popoverRef = ref()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const isInstalled = computed(() => managerStore.isPackInstalled(nodePack?.id))
|
||||
const isDisabled = computed(
|
||||
() => isInstalled.value && !managerStore.isPackEnabled(nodePack?.id)
|
||||
)
|
||||
|
||||
const installedVersion = computed(() => {
|
||||
if (!nodePack.id) return 'nightly'
|
||||
const version =
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<IconTextButton
|
||||
v-tooltip.top="
|
||||
hasDisabledUpdatePacks ? $t('manager.disabledNodesWontUpdate') : null
|
||||
"
|
||||
v-bind="$attrs"
|
||||
type="transparent"
|
||||
:label="$t('manager.updateAll')"
|
||||
@@ -24,8 +27,9 @@ import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
const { nodePacks, hasDisabledUpdatePacks } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
hasDisabledUpdatePacks?: boolean
|
||||
}>()
|
||||
|
||||
const isUpdating = ref<boolean>(false)
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
:has-conflict="hasConflicts"
|
||||
:conflict-info="conflictInfo"
|
||||
/>
|
||||
<PackEnableToggle v-else :node-pack="nodePack" />
|
||||
<PackEnableToggle
|
||||
v-else
|
||||
:has-conflict="hasConflicts"
|
||||
:node-pack="nodePack"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
/>
|
||||
<PackUpdateButton
|
||||
v-if="isUpdateAvailableTab && hasUpdateAvailable"
|
||||
:node-packs="updateAvailableNodePacks"
|
||||
:node-packs="enabledUpdateAvailableNodePacks"
|
||||
:has-disabled-update-packs="hasDisabledUpdatePacks"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-3 text-sm">
|
||||
@@ -103,8 +104,11 @@ const { t } = useI18n()
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
|
||||
// Use the composable to get update available nodes
|
||||
const { hasUpdateAvailable, updateAvailableNodePacks } =
|
||||
useUpdateAvailableNodes()
|
||||
const {
|
||||
hasUpdateAvailable,
|
||||
enabledUpdateAvailableNodePacks,
|
||||
hasDisabledUpdatePacks
|
||||
} = useUpdateAvailableNodes()
|
||||
|
||||
const hasResults = computed(
|
||||
() => searchQuery.value?.trim() && searchResults?.length
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<div
|
||||
ref="toolboxRef"
|
||||
style="transform: translate(var(--tb-x), var(--tb-y))"
|
||||
class="fixed left-0 top-0 z-40"
|
||||
class="fixed left-0 top-0 z-40 pointer-events-none"
|
||||
>
|
||||
<Transition name="slide-up">
|
||||
<Panel
|
||||
v-if="visible"
|
||||
class="rounded-lg selection-toolbox"
|
||||
class="rounded-lg selection-toolbox pointer-events-auto"
|
||||
:pt="{
|
||||
header: 'hidden',
|
||||
content: 'p-0 flex flex-row'
|
||||
@@ -83,7 +83,6 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
<style scoped>
|
||||
.selection-toolbox {
|
||||
transform: translateX(-50%) translateY(-120%);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
|
||||
@@ -147,7 +147,8 @@ watch(
|
||||
showColorPicker.value = false
|
||||
selectedColorOption.value = null
|
||||
currentColorOption.value = getItemsColorOption(newSelectedItems)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</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')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -88,8 +88,8 @@ const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
// Use either release red dot or conflict red dot
|
||||
const shouldShowRedDot = computed(() => {
|
||||
const releaseRedDot = showReleaseRedDot
|
||||
const shouldShowRedDot = computed((): boolean => {
|
||||
const releaseRedDot = showReleaseRedDot.value
|
||||
return releaseRedDot || shouldShowConflictRedDot.value
|
||||
})
|
||||
|
||||
|
||||
@@ -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[]>(() => [
|
||||
|
||||
@@ -1053,7 +1053,7 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
) as IComboWidget
|
||||
|
||||
if (!modelWidget || !generateAudioWidget) {
|
||||
return '$2.00-6.00/Run (varies with model & audio generation)'
|
||||
return '$0.80-3.20/Run (varies with model & audio generation)'
|
||||
}
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
@@ -1061,13 +1061,13 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
String(generateAudioWidget.value).toLowerCase() === 'true'
|
||||
|
||||
if (model.includes('veo-3.0-fast-generate-001')) {
|
||||
return generateAudio ? '$3.20/Run' : '$2.00/Run'
|
||||
return generateAudio ? '$1.20/Run' : '$0.80/Run'
|
||||
} else if (model.includes('veo-3.0-generate-001')) {
|
||||
return generateAudio ? '$6.00/Run' : '$4.00/Run'
|
||||
return generateAudio ? '$3.20/Run' : '$1.60/Run'
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return '$2.00-6.00/Run'
|
||||
return '$0.80-3.20/Run'
|
||||
}
|
||||
},
|
||||
LumaImageNode: {
|
||||
@@ -1502,6 +1502,32 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
return 'Token-based'
|
||||
}
|
||||
},
|
||||
ByteDanceSeedreamNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const sequentialGenerationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'sequential_image_generation'
|
||||
) as IComboWidget
|
||||
const maxImagesWidget = node.widgets?.find(
|
||||
(w) => w.name === 'max_images'
|
||||
) as IComboWidget
|
||||
|
||||
if (!sequentialGenerationWidget || !maxImagesWidget)
|
||||
return '$0.03/Run ($0.03 for one output image)'
|
||||
|
||||
if (
|
||||
String(sequentialGenerationWidget.value).toLowerCase() === 'disabled'
|
||||
) {
|
||||
return '$0.03/Run'
|
||||
}
|
||||
|
||||
const maxImages = Number(maxImagesWidget.value)
|
||||
if (maxImages === 1) {
|
||||
return '$0.03/Run'
|
||||
}
|
||||
const cost = (0.03 * maxImages).toFixed(2)
|
||||
return `$${cost}/Run ($0.03 for one output image)`
|
||||
}
|
||||
},
|
||||
ByteDanceTextToVideoNode: {
|
||||
displayPrice: byteDanceVideoPricingCalculator
|
||||
},
|
||||
@@ -1604,6 +1630,11 @@ export const useNodePricing = () => {
|
||||
// ByteDance
|
||||
ByteDanceImageNode: ['model'],
|
||||
ByteDanceImageEditNode: ['model'],
|
||||
ByteDanceSeedreamNode: [
|
||||
'model',
|
||||
'sequential_image_generation',
|
||||
'max_images'
|
||||
],
|
||||
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
|
||||
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
|
||||
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
|
||||
|
||||
@@ -44,9 +44,24 @@ export const useUpdateAvailableNodes = () => {
|
||||
return filterOutdatedPacks(installedPacks.value)
|
||||
})
|
||||
|
||||
// Check if there are any outdated packs
|
||||
// Filter only enabled outdated packs
|
||||
const enabledUpdateAvailableNodePacks = computed(() => {
|
||||
return updateAvailableNodePacks.value.filter((pack) =>
|
||||
comfyManagerStore.isPackEnabled(pack.id)
|
||||
)
|
||||
})
|
||||
|
||||
// Check if there are any enabled outdated packs
|
||||
const hasUpdateAvailable = computed(() => {
|
||||
return updateAvailableNodePacks.value.length > 0
|
||||
return enabledUpdateAvailableNodePacks.value.length > 0
|
||||
})
|
||||
|
||||
// Check if there are disabled packs with updates
|
||||
const hasDisabledUpdatePacks = computed(() => {
|
||||
return (
|
||||
updateAvailableNodePacks.value.length >
|
||||
enabledUpdateAvailableNodePacks.value.length
|
||||
)
|
||||
})
|
||||
|
||||
// Automatically fetch installed pack data when composable is used
|
||||
@@ -58,7 +73,9 @@ export const useUpdateAvailableNodes = () => {
|
||||
|
||||
return {
|
||||
updateAvailableNodePacks,
|
||||
enabledUpdateAvailableNodePacks,
|
||||
hasUpdateAvailable,
|
||||
hasDisabledUpdatePacks,
|
||||
isLoading,
|
||||
error
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -21,6 +22,7 @@ import type {
|
||||
NodePackRequirements,
|
||||
SystemEnvironment
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
import { normalizePackId } from '@/utils/packUtils'
|
||||
import {
|
||||
cleanVersion,
|
||||
satisfiesVersion,
|
||||
@@ -78,9 +80,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 +128,7 @@ export function useConflictDetection() {
|
||||
}
|
||||
|
||||
systemEnvironment.value = environment
|
||||
console.log(
|
||||
console.debug(
|
||||
'[ConflictDetection] System environment detection completed:',
|
||||
environment
|
||||
)
|
||||
@@ -427,7 +428,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 +501,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 +557,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 +572,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 +636,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 +686,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 +704,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
|
||||
@@ -860,9 +875,7 @@ function mergeConflictsByPackageName(
|
||||
|
||||
conflicts.forEach((conflict) => {
|
||||
// Normalize package name by removing version suffix (@1_0_3) for consistent merging
|
||||
const normalizedPackageName = conflict.package_name.includes('@')
|
||||
? conflict.package_name.substring(0, conflict.package_name.indexOf('@'))
|
||||
: conflict.package_name
|
||||
const normalizedPackageName = normalizePackId(conflict.package_name)
|
||||
|
||||
if (mergedMap.has(normalizedPackageName)) {
|
||||
// Package already exists, merge conflicts
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Ref, computed, ref } from 'vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
import { normalizePackKeys } from '@/utils/packUtils'
|
||||
|
||||
type ManagerTaskHistory = Record<
|
||||
string,
|
||||
@@ -98,7 +99,8 @@ export const useManagerQueue = (
|
||||
taskHistory.value = filterHistoryByClientId(state.history)
|
||||
|
||||
if (state.installed_packs) {
|
||||
installedPacks.value = state.installed_packs
|
||||
// Normalize pack keys to ensure consistent access
|
||||
installedPacks.value = normalizePackKeys(state.installed_packs)
|
||||
}
|
||||
updateProcessingState()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -187,6 +187,8 @@
|
||||
"updateSelected": "Update Selected",
|
||||
"updateAll": "Update All",
|
||||
"updatingAllPacks": "Updating all packages",
|
||||
"disabledNodesWontUpdate": "Disabled nodes will not be updated",
|
||||
"enablePackToChangeVersion": "Enable this pack to change versions",
|
||||
"license": "License",
|
||||
"nightlyVersion": "Nightly",
|
||||
"latestVersion": "Latest",
|
||||
@@ -205,6 +207,7 @@
|
||||
"noDescription": "No description available",
|
||||
"installSelected": "Install Selected",
|
||||
"installAllMissingNodes": "Install All Missing Nodes",
|
||||
"allMissingNodesInstalled": "All missing nodes have been successfully installed",
|
||||
"packsSelected": "packs selected",
|
||||
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
|
||||
"notAvailable": "Not Available",
|
||||
@@ -1436,6 +1439,8 @@
|
||||
"missingModelsMessage": "When loading the graph, the following models were not found"
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"missingNodesTitle": "Some Nodes Are Missing",
|
||||
"missingNodesDescription": "When loading the graph, the following node types were not found.\nThis may also happen if your installed version is lower and that node type can’t be found.",
|
||||
"outdatedVersion": "Some nodes require a newer version of ComfyUI (current: {version}). Please update to use all nodes.",
|
||||
"outdatedVersionGeneric": "Some nodes require a newer version of ComfyUI. Please update to use all nodes.",
|
||||
"coreNodesFromVersion": "Requires ComfyUI {version}:"
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { mapKeys } from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ref, watch } from 'vue'
|
||||
@@ -14,6 +13,7 @@ import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { TaskLog } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
import { normalizePackKeys } from '@/utils/packUtils'
|
||||
|
||||
type InstallPackParams = components['schemas']['InstallPackParams']
|
||||
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
|
||||
@@ -185,12 +185,8 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const refreshInstalledList = async () => {
|
||||
const packs = await managerService.listInstalledPacks()
|
||||
if (packs) {
|
||||
// The keys are 'cleaned' by stripping the version suffix.
|
||||
// The pack object itself (the value) still contains the version info.
|
||||
const packsWithCleanedKeys = mapKeys(packs, (_value, key) => {
|
||||
return key.split('@')[0]
|
||||
})
|
||||
installedPacks.value = packsWithCleanedKeys
|
||||
// Normalize pack keys to ensure consistent access
|
||||
installedPacks.value = normalizePackKeys(packs)
|
||||
}
|
||||
isStale.value = false
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
|
||||
import { normalizePackId } from '@/utils/packUtils'
|
||||
|
||||
export function extractCustomNodeName(
|
||||
pythonModule: string | undefined
|
||||
): string | null {
|
||||
const modules = pythonModule?.split('.') || []
|
||||
if (modules.length >= 2 && modules[0] === 'custom_nodes') {
|
||||
return modules[1].split('@')[0]
|
||||
// Use normalizePackId to remove version suffix
|
||||
return normalizePackId(modules[1])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
35
src/utils/packUtils.ts
Normal file
35
src/utils/packUtils.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { mapKeys } from 'es-toolkit/compat'
|
||||
|
||||
/**
|
||||
* Normalizes a pack ID by removing the version suffix.
|
||||
*
|
||||
* ComfyUI-Manager returns pack IDs in different formats:
|
||||
* - Enabled packs: "packname" (without version)
|
||||
* - Disabled packs: "packname@1_0_3" (with version suffix)
|
||||
* - Latest versions from registry: "packname" (without version)
|
||||
*
|
||||
* Since the pack object itself contains the version info (ver field),
|
||||
* we normalize all pack IDs to just the base name for consistent access.
|
||||
* This ensures we can always find a pack by its base name (nodePack.id)
|
||||
* regardless of its enabled/disabled state.
|
||||
*
|
||||
* @param packId - The pack ID that may contain a version suffix
|
||||
* @returns The normalized pack ID without version suffix
|
||||
*/
|
||||
export function normalizePackId(packId: string): string {
|
||||
return packId.split('@')[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes all keys in a pack record by removing version suffixes.
|
||||
* This is used when receiving pack data from the server to ensure
|
||||
* consistent key format across the application.
|
||||
*
|
||||
* @param packs - Record of packs with potentially versioned keys
|
||||
* @returns Record with normalized keys
|
||||
*/
|
||||
export function normalizePackKeys<T>(
|
||||
packs: Record<string, T>
|
||||
): Record<string, T> {
|
||||
return mapKeys(packs, (_value, key) => normalizePackId(key))
|
||||
}
|
||||
@@ -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')]
|
||||
|
||||
@@ -505,7 +505,7 @@ describe('useNodePricing', () => {
|
||||
})
|
||||
|
||||
describe('dynamic pricing - Veo3VideoGenerationNode', () => {
|
||||
it('should return $2.00 for veo-3.0-fast-generate-001 without audio', () => {
|
||||
it('should return $0.80 for veo-3.0-fast-generate-001 without audio', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Veo3VideoGenerationNode', [
|
||||
{ name: 'model', value: 'veo-3.0-fast-generate-001' },
|
||||
@@ -513,49 +513,49 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$2.00/Run')
|
||||
expect(price).toBe('$0.80/Run')
|
||||
})
|
||||
|
||||
it('should return $3.20 for veo-3.0-fast-generate-001 with audio', () => {
|
||||
it('should return $1.20 for veo-3.0-fast-generate-001 with audio', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Veo3VideoGenerationNode', [
|
||||
{ name: 'model', value: 'veo-3.0-fast-generate-001' },
|
||||
{ name: 'generate_audio', value: true }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.20/Run')
|
||||
})
|
||||
|
||||
it('should return $1.60 for veo-3.0-generate-001 without audio', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Veo3VideoGenerationNode', [
|
||||
{ name: 'model', value: 'veo-3.0-generate-001' },
|
||||
{ name: 'generate_audio', value: false }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.60/Run')
|
||||
})
|
||||
|
||||
it('should return $3.20 for veo-3.0-generate-001 with audio', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Veo3VideoGenerationNode', [
|
||||
{ name: 'model', value: 'veo-3.0-generate-001' },
|
||||
{ name: 'generate_audio', value: true }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$3.20/Run')
|
||||
})
|
||||
|
||||
it('should return $4.00 for veo-3.0-generate-001 without audio', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Veo3VideoGenerationNode', [
|
||||
{ name: 'model', value: 'veo-3.0-generate-001' },
|
||||
{ name: 'generate_audio', value: false }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$4.00/Run')
|
||||
})
|
||||
|
||||
it('should return $6.00 for veo-3.0-generate-001 with audio', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Veo3VideoGenerationNode', [
|
||||
{ name: 'model', value: 'veo-3.0-generate-001' },
|
||||
{ name: 'generate_audio', value: true }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$6.00/Run')
|
||||
})
|
||||
|
||||
it('should return range when widgets are missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Veo3VideoGenerationNode', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe(
|
||||
'$2.00-6.00/Run (varies with model & audio generation)'
|
||||
'$0.80-3.20/Run (varies with model & audio generation)'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -567,7 +567,7 @@ describe('useNodePricing', () => {
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe(
|
||||
'$2.00-6.00/Run (varies with model & audio generation)'
|
||||
'$0.80-3.20/Run (varies with model & audio generation)'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -579,7 +579,7 @@ describe('useNodePricing', () => {
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe(
|
||||
'$2.00-6.00/Run (varies with model & audio generation)'
|
||||
'$0.80-3.20/Run (varies with model & audio generation)'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1781,6 +1781,38 @@ describe('useNodePricing', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - ByteDanceSeedreamNode', () => {
|
||||
it('should return fallback when widgets are missing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('ByteDanceSeedreamNode', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.03/Run ($0.03 for one output image)')
|
||||
})
|
||||
|
||||
it('should return $0.03/Run when sequential generation is disabled', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('ByteDanceSeedreamNode', [
|
||||
{ name: 'sequential_image_generation', value: 'disabled' },
|
||||
{ name: 'max_images', value: 5 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.03/Run')
|
||||
})
|
||||
|
||||
it('should multiply by max_images when sequential generation is enabled', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('ByteDanceSeedreamNode', [
|
||||
{ name: 'sequential_image_generation', value: 'enabled' },
|
||||
{ name: 'max_images', value: 4 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.12/Run ($0.03 for one output image)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic pricing - ByteDance Seedance video nodes', () => {
|
||||
it('should return base 10s range for PRO 1080p on ByteDanceTextToVideoNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -63,12 +63,14 @@ describe('useUpdateAvailableNodes', () => {
|
||||
const mockStartFetchInstalled = vi.fn()
|
||||
const mockIsPackInstalled = vi.fn()
|
||||
const mockGetInstalledPackVersion = vi.fn()
|
||||
const mockIsPackEnabled = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Default setup
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
mockIsPackEnabled.mockReturnValue(true) // Default: all packs are enabled
|
||||
mockGetInstalledPackVersion.mockImplementation((id: string) => {
|
||||
switch (id) {
|
||||
case 'pack-1':
|
||||
@@ -100,7 +102,8 @@ describe('useUpdateAvailableNodes', () => {
|
||||
|
||||
mockUseComfyManagerStore.mockReturnValue({
|
||||
isPackInstalled: mockIsPackInstalled,
|
||||
getInstalledPackVersion: mockGetInstalledPackVersion
|
||||
getInstalledPackVersion: mockGetInstalledPackVersion,
|
||||
isPackEnabled: mockIsPackEnabled
|
||||
} as any)
|
||||
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
@@ -357,4 +360,127 @@ describe('useUpdateAvailableNodes', () => {
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('enabledUpdateAvailableNodePacks', () => {
|
||||
it('returns only enabled packs with updates', () => {
|
||||
mockIsPackEnabled.mockImplementation((id: string) => {
|
||||
// pack-1 is disabled
|
||||
return id !== 'pack-1'
|
||||
})
|
||||
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0], mockInstalledPacks[1]]),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks, enabledUpdateAvailableNodePacks } =
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
// pack-1 has updates but is disabled
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(1)
|
||||
expect(updateAvailableNodePacks.value[0].id).toBe('pack-1')
|
||||
|
||||
// enabledUpdateAvailableNodePacks should be empty
|
||||
expect(enabledUpdateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns all packs when all are enabled', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks, enabledUpdateAvailableNodePacks } =
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(1)
|
||||
expect(enabledUpdateAvailableNodePacks.value).toHaveLength(1)
|
||||
expect(enabledUpdateAvailableNodePacks.value[0].id).toBe('pack-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasDisabledUpdatePacks', () => {
|
||||
it('returns true when there are disabled packs with updates', () => {
|
||||
mockIsPackEnabled.mockImplementation((id: string) => {
|
||||
// pack-1 is disabled
|
||||
return id !== 'pack-1'
|
||||
})
|
||||
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasDisabledUpdatePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasDisabledUpdatePacks.value).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when all packs with updates are enabled', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasDisabledUpdatePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasDisabledUpdatePacks.value).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when no packs have updates', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[1]]), // pack-2: up to date
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasDisabledUpdatePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasDisabledUpdatePacks.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasUpdateAvailable with disabled packs', () => {
|
||||
it('returns false when only disabled packs have updates', () => {
|
||||
mockIsPackEnabled.mockReturnValue(false) // All packs disabled
|
||||
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasUpdateAvailable } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasUpdateAvailable.value).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true when at least one enabled pack has updates', () => {
|
||||
mockIsPackEnabled.mockImplementation((id: string) => {
|
||||
// Only pack-1 is enabled
|
||||
return id === 'pack-1'
|
||||
})
|
||||
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasUpdateAvailable } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasUpdateAvailable.value).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -161,5 +161,62 @@ describe('useManagerQueue', () => {
|
||||
expect(taskHistory.value).toHaveProperty('task1')
|
||||
expect(taskHistory.value).not.toHaveProperty('task2')
|
||||
})
|
||||
|
||||
it('normalizes pack IDs when updating installed packs', () => {
|
||||
const queue = createManagerQueue()
|
||||
|
||||
const mockState = {
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {
|
||||
'ComfyUI-GGUF@1_1_4': {
|
||||
enabled: false,
|
||||
cnr_id: 'ComfyUI-GGUF',
|
||||
ver: '1.1.4'
|
||||
},
|
||||
'test-pack': {
|
||||
enabled: true,
|
||||
cnr_id: 'test-pack',
|
||||
ver: '2.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queue.updateTaskState(mockState)
|
||||
|
||||
// Packs should be accessible by normalized keys
|
||||
expect(installedPacks.value['ComfyUI-GGUF']).toEqual({
|
||||
enabled: false,
|
||||
cnr_id: 'ComfyUI-GGUF',
|
||||
ver: '1.1.4'
|
||||
})
|
||||
expect(installedPacks.value['test-pack']).toEqual({
|
||||
enabled: true,
|
||||
cnr_id: 'test-pack',
|
||||
ver: '2.0.0'
|
||||
})
|
||||
|
||||
// Version suffixed keys should not exist after normalization
|
||||
// The pack should be accessible by its base name only (without @version)
|
||||
expect(installedPacks.value['ComfyUI-GGUF@1_1_4']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles empty installed_packs gracefully', () => {
|
||||
const queue = createManagerQueue()
|
||||
|
||||
const mockState: any = {
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: undefined
|
||||
}
|
||||
|
||||
// Just call the function - if it throws, the test will fail automatically
|
||||
queue.updateTaskState(mockState)
|
||||
|
||||
// installedPacks should remain unchanged
|
||||
expect(installedPacks.value).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -439,4 +439,97 @@ describe('useComfyManagerStore', () => {
|
||||
expect(store.isPackInstalling('pack-3')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('refreshInstalledList with pack ID normalization', () => {
|
||||
it('normalizes pack IDs by removing version suffixes', async () => {
|
||||
const mockPacks = {
|
||||
'ComfyUI-GGUF@1_1_4': {
|
||||
enabled: false,
|
||||
cnr_id: 'ComfyUI-GGUF',
|
||||
ver: '1.1.4',
|
||||
aux_id: undefined
|
||||
},
|
||||
'ComfyUI-Manager': {
|
||||
enabled: true,
|
||||
cnr_id: 'ComfyUI-Manager',
|
||||
ver: '2.0.0',
|
||||
aux_id: undefined
|
||||
}
|
||||
}
|
||||
|
||||
vi.mocked(mockManagerService.listInstalledPacks).mockResolvedValue(
|
||||
mockPacks
|
||||
)
|
||||
|
||||
const store = useComfyManagerStore()
|
||||
await store.refreshInstalledList()
|
||||
|
||||
// Both packs should be accessible by their base name
|
||||
expect(store.installedPacks['ComfyUI-GGUF']).toEqual({
|
||||
enabled: false,
|
||||
cnr_id: 'ComfyUI-GGUF',
|
||||
ver: '1.1.4',
|
||||
aux_id: undefined
|
||||
})
|
||||
expect(store.installedPacks['ComfyUI-Manager']).toEqual({
|
||||
enabled: true,
|
||||
cnr_id: 'ComfyUI-Manager',
|
||||
ver: '2.0.0',
|
||||
aux_id: undefined
|
||||
})
|
||||
|
||||
// Version suffixed keys should not exist
|
||||
expect(store.installedPacks['ComfyUI-GGUF@1_1_4']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles duplicate keys after normalization', async () => {
|
||||
const mockPacks = {
|
||||
'test-pack': {
|
||||
enabled: true,
|
||||
cnr_id: 'test-pack',
|
||||
ver: '1.0.0',
|
||||
aux_id: undefined
|
||||
},
|
||||
'test-pack@1_1_0': {
|
||||
enabled: false,
|
||||
cnr_id: 'test-pack',
|
||||
ver: '1.1.0',
|
||||
aux_id: undefined
|
||||
}
|
||||
}
|
||||
|
||||
vi.mocked(mockManagerService.listInstalledPacks).mockResolvedValue(
|
||||
mockPacks
|
||||
)
|
||||
|
||||
const store = useComfyManagerStore()
|
||||
await store.refreshInstalledList()
|
||||
|
||||
// The normalized key should exist (last one wins with mapKeys)
|
||||
expect(store.installedPacks['test-pack']).toBeDefined()
|
||||
expect(store.installedPacks['test-pack'].ver).toBe('1.1.0')
|
||||
})
|
||||
|
||||
it('preserves version information for disabled packs', async () => {
|
||||
const mockPacks = {
|
||||
'disabled-pack@2_0_0': {
|
||||
enabled: false,
|
||||
cnr_id: 'disabled-pack',
|
||||
ver: '2.0.0',
|
||||
aux_id: undefined
|
||||
}
|
||||
}
|
||||
|
||||
vi.mocked(mockManagerService.listInstalledPacks).mockResolvedValue(
|
||||
mockPacks
|
||||
)
|
||||
|
||||
const store = useComfyManagerStore()
|
||||
await store.refreshInstalledList()
|
||||
|
||||
// Pack should be accessible by base name with version preserved
|
||||
expect(store.getInstalledPackVersion('disabled-pack')).toBe('2.0.0')
|
||||
expect(store.isPackInstalled('disabled-pack')).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)
|
||||
})
|
||||
})
|
||||
})
|
||||
254
tests-ui/tests/utils/packUtils.test.ts
Normal file
254
tests-ui/tests/utils/packUtils.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { normalizePackId, normalizePackKeys } from '@/utils/packUtils'
|
||||
|
||||
describe('packUtils', () => {
|
||||
describe('normalizePackId', () => {
|
||||
it('should return pack ID unchanged when no version suffix exists', () => {
|
||||
expect(normalizePackId('ComfyUI-GGUF')).toBe('ComfyUI-GGUF')
|
||||
expect(normalizePackId('ComfyUI-Manager')).toBe('ComfyUI-Manager')
|
||||
expect(normalizePackId('simple-pack')).toBe('simple-pack')
|
||||
})
|
||||
|
||||
it('should remove version suffix with underscores', () => {
|
||||
expect(normalizePackId('ComfyUI-GGUF@1_1_4')).toBe('ComfyUI-GGUF')
|
||||
expect(normalizePackId('ComfyUI-Manager@2_0_0')).toBe('ComfyUI-Manager')
|
||||
expect(normalizePackId('pack@1_0_0_beta')).toBe('pack')
|
||||
})
|
||||
|
||||
it('should remove version suffix with dots', () => {
|
||||
expect(normalizePackId('ComfyUI-GGUF@1.1.4')).toBe('ComfyUI-GGUF')
|
||||
expect(normalizePackId('pack@2.0.0')).toBe('pack')
|
||||
})
|
||||
|
||||
it('should handle multiple @ symbols by only removing after first @', () => {
|
||||
expect(normalizePackId('pack@1_0_0@extra')).toBe('pack')
|
||||
expect(normalizePackId('my@pack@1_0_0')).toBe('my')
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(normalizePackId('')).toBe('')
|
||||
})
|
||||
|
||||
it('should handle pack ID with @ but no version', () => {
|
||||
expect(normalizePackId('pack@')).toBe('pack')
|
||||
})
|
||||
|
||||
it('should handle special characters in pack name', () => {
|
||||
expect(normalizePackId('my-pack_v2@1_0_0')).toBe('my-pack_v2')
|
||||
expect(normalizePackId('pack.with.dots@2_0_0')).toBe('pack.with.dots')
|
||||
expect(normalizePackId('UPPERCASE-Pack@1_0_0')).toBe('UPPERCASE-Pack')
|
||||
})
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
// Only @ symbol
|
||||
expect(normalizePackId('@')).toBe('')
|
||||
expect(normalizePackId('@1_0_0')).toBe('')
|
||||
|
||||
// Whitespace
|
||||
expect(normalizePackId(' pack @1_0_0')).toBe(' pack ')
|
||||
expect(normalizePackId('pack @1_0_0')).toBe('pack ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizePackKeys', () => {
|
||||
it('should normalize all keys with version suffixes', () => {
|
||||
const input = {
|
||||
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
|
||||
'ComfyUI-Manager@2_0_0': { ver: '2.0.0', enabled: false },
|
||||
'another-pack@1_0_0': { ver: '1.0.0', enabled: true }
|
||||
}
|
||||
|
||||
const expected = {
|
||||
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
|
||||
'ComfyUI-Manager': { ver: '2.0.0', enabled: false },
|
||||
'another-pack': { ver: '1.0.0', enabled: true }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should handle empty object', () => {
|
||||
expect(normalizePackKeys({})).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle keys without version suffixes', () => {
|
||||
const input = {
|
||||
pack1: { data: 'value1' },
|
||||
pack2: { data: 'value2' }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(input)
|
||||
})
|
||||
|
||||
it('should handle mixed keys (with and without versions)', () => {
|
||||
const input = {
|
||||
'normal-pack': { ver: '1.0.0' },
|
||||
'versioned-pack@2_0_0': { ver: '2.0.0' },
|
||||
'another-normal': { ver: '3.0.0' },
|
||||
'another-versioned@4_0_0': { ver: '4.0.0' }
|
||||
}
|
||||
|
||||
const expected = {
|
||||
'normal-pack': { ver: '1.0.0' },
|
||||
'versioned-pack': { ver: '2.0.0' },
|
||||
'another-normal': { ver: '3.0.0' },
|
||||
'another-versioned': { ver: '4.0.0' }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should handle duplicate keys after normalization (last one wins)', () => {
|
||||
const input = {
|
||||
'pack@1_0_0': { ver: '1.0.0', data: 'first' },
|
||||
'pack@2_0_0': { ver: '2.0.0', data: 'second' },
|
||||
pack: { ver: '3.0.0', data: 'third' }
|
||||
}
|
||||
|
||||
const result = normalizePackKeys(input)
|
||||
|
||||
// The exact behavior depends on object iteration order,
|
||||
// but there should only be one 'pack' key in the result
|
||||
expect(Object.keys(result)).toEqual(['pack'])
|
||||
expect(result.pack).toBeDefined()
|
||||
expect(result.pack.ver).toBeDefined()
|
||||
})
|
||||
|
||||
it('should preserve value references', () => {
|
||||
const value1 = { ver: '1.0.0', complex: { nested: 'data' } }
|
||||
const value2 = { ver: '2.0.0', complex: { nested: 'data2' } }
|
||||
|
||||
const input = {
|
||||
'pack1@1_0_0': value1,
|
||||
'pack2@2_0_0': value2
|
||||
}
|
||||
|
||||
const result = normalizePackKeys(input)
|
||||
|
||||
// Values should be the same references, not cloned
|
||||
expect(result.pack1).toBe(value1)
|
||||
expect(result.pack2).toBe(value2)
|
||||
})
|
||||
|
||||
it('should handle special characters in keys', () => {
|
||||
const input = {
|
||||
'@1_0_0': { ver: '1.0.0' },
|
||||
'my-pack.v2@2_0_0': { ver: '2.0.0' },
|
||||
'UPPERCASE@3_0_0': { ver: '3.0.0' }
|
||||
}
|
||||
|
||||
const expected = {
|
||||
'': { ver: '1.0.0' },
|
||||
'my-pack.v2': { ver: '2.0.0' },
|
||||
UPPERCASE: { ver: '3.0.0' }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should work with different value types', () => {
|
||||
const input = {
|
||||
'pack1@1_0_0': 'string value',
|
||||
'pack2@2_0_0': 123,
|
||||
'pack3@3_0_0': null,
|
||||
'pack4@4_0_0': undefined,
|
||||
'pack5@5_0_0': true,
|
||||
pack6: []
|
||||
}
|
||||
|
||||
const expected = {
|
||||
pack1: 'string value',
|
||||
pack2: 123,
|
||||
pack3: null,
|
||||
pack4: undefined,
|
||||
pack5: true,
|
||||
pack6: []
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration scenarios from JSDoc examples', () => {
|
||||
it('should handle the examples from normalizePackId JSDoc', () => {
|
||||
expect(normalizePackId('ComfyUI-GGUF')).toBe('ComfyUI-GGUF')
|
||||
expect(normalizePackId('ComfyUI-GGUF@1_1_4')).toBe('ComfyUI-GGUF')
|
||||
})
|
||||
|
||||
it('should handle the examples from normalizePackKeys JSDoc', () => {
|
||||
const input = {
|
||||
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
|
||||
'ComfyUI-Manager@2_0_0': { ver: '2.0.0', enabled: false }
|
||||
}
|
||||
|
||||
const expected = {
|
||||
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
|
||||
'ComfyUI-Manager': { ver: '2.0.0', enabled: false }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Real-world scenarios', () => {
|
||||
it('should handle typical ComfyUI-Manager response with mixed enabled/disabled packs', () => {
|
||||
// Simulating actual server response pattern
|
||||
const serverResponse = {
|
||||
// Enabled packs come without version suffix
|
||||
'ComfyUI-Essential': { ver: '1.2.3', enabled: true, aux_id: undefined },
|
||||
'ComfyUI-Impact': { ver: '2.0.0', enabled: true, aux_id: undefined },
|
||||
// Disabled packs come with version suffix
|
||||
'ComfyUI-GGUF@1_1_4': {
|
||||
ver: '1.1.4',
|
||||
enabled: false,
|
||||
aux_id: undefined
|
||||
},
|
||||
'ComfyUI-Manager@2_5_0': {
|
||||
ver: '2.5.0',
|
||||
enabled: false,
|
||||
aux_id: undefined
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = normalizePackKeys(serverResponse)
|
||||
|
||||
// All keys should be normalized (no version suffixes)
|
||||
expect(Object.keys(normalized)).toEqual([
|
||||
'ComfyUI-Essential',
|
||||
'ComfyUI-Impact',
|
||||
'ComfyUI-GGUF',
|
||||
'ComfyUI-Manager'
|
||||
])
|
||||
|
||||
// Values should be preserved
|
||||
expect(normalized['ComfyUI-GGUF']).toEqual({
|
||||
ver: '1.1.4',
|
||||
enabled: false,
|
||||
aux_id: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow consistent access by pack ID regardless of enabled state', () => {
|
||||
const packsBeforeToggle = {
|
||||
'my-pack': { ver: '1.0.0', enabled: true }
|
||||
}
|
||||
|
||||
const packsAfterToggle = {
|
||||
'my-pack@1_0_0': { ver: '1.0.0', enabled: false }
|
||||
}
|
||||
|
||||
const normalizedBefore = normalizePackKeys(packsBeforeToggle)
|
||||
const normalizedAfter = normalizePackKeys(packsAfterToggle)
|
||||
|
||||
// Both should have the same key after normalization
|
||||
expect(normalizedBefore['my-pack']).toBeDefined()
|
||||
expect(normalizedAfter['my-pack']).toBeDefined()
|
||||
|
||||
// Can access by the same key regardless of the original format
|
||||
expect(Object.keys(normalizedBefore)).toEqual(
|
||||
Object.keys(normalizedAfter)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user