mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 13:59:28 +00:00
Compare commits
12 Commits
fix/litegr
...
drjkl/debu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f2736095c | ||
|
|
16b285a79c | ||
|
|
35eb0286e5 | ||
|
|
c171227d28 | ||
|
|
51bce9239e | ||
|
|
52483b34ed | ||
|
|
80d58ddc8b | ||
|
|
3c0b227f94 | ||
|
|
0a73c5a131 | ||
|
|
326154f2b2 | ||
|
|
33c457aca0 | ||
|
|
8afb45db9c |
@@ -300,6 +300,12 @@ When referencing Comfy-Org repos:
|
|||||||
|
|
||||||
Rules for agent-based coding tasks.
|
Rules for agent-based coding tasks.
|
||||||
|
|
||||||
|
### Chrome DevTools MCP
|
||||||
|
|
||||||
|
When using `take_snapshot` to inspect dropdowns, listboxes, or other components with dynamic options:
|
||||||
|
- Use `verbose: true` to see the full accessibility tree including list items
|
||||||
|
- Non-verbose snapshots often omit nested options in comboboxes/listboxes
|
||||||
|
|
||||||
### Temporary Files
|
### Temporary Files
|
||||||
|
|
||||||
- Put planning documents under `/temp/plans/`
|
- Put planning documents under `/temp/plans/`
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
- Clear public interfaces
|
- Clear public interfaces
|
||||||
- Restrict extension access
|
- Restrict extension access
|
||||||
- Clean up subscriptions
|
- Clean up subscriptions
|
||||||
|
- Only expose state/actions that are used externally; keep internal state private
|
||||||
|
|
||||||
## General Guidelines
|
## General Guidelines
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { captureException } from '@sentry/vue'
|
import { captureException } from '@sentry/vue'
|
||||||
import { useEventListener } from '@vueuse/core'
|
|
||||||
import BlockUI from 'primevue/blockui'
|
import BlockUI from 'primevue/blockui'
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import ProgressSpinner from 'primevue/progressspinner'
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
@@ -21,15 +20,13 @@ import { useWorkspaceStore } from '@/stores/workspaceStore'
|
|||||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||||
|
|
||||||
import { electronAPI, isElectron } from './utils/envUtil'
|
import { electronAPI, isElectron } from './utils/envUtil'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
app.extensionManager = useWorkspaceStore()
|
||||||
|
|
||||||
const conflictDetection = useConflictDetection()
|
const conflictDetection = useConflictDetection()
|
||||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||||
const handleKey = (e: KeyboardEvent) => {
|
|
||||||
workspaceStore.shiftDown = e.shiftKey
|
|
||||||
}
|
|
||||||
useEventListener(window, 'keydown', handleKey)
|
|
||||||
useEventListener(window, 'keyup', handleKey)
|
|
||||||
|
|
||||||
const showContextMenu = (event: MouseEvent) => {
|
const showContextMenu = (event: MouseEvent) => {
|
||||||
const { target } = event
|
const { target } = event
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventListener, whenever } from '@vueuse/core'
|
import { until, useEventListener } from '@vueuse/core'
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
@@ -129,7 +129,7 @@ import { useCopy } from '@/composables/useCopy'
|
|||||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||||
import { usePaste } from '@/composables/usePaste'
|
import { usePaste } from '@/composables/usePaste'
|
||||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
import { mergeCustomNodesI18n, t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
|
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
|
||||||
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
||||||
@@ -144,12 +144,15 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
|
|||||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||||
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
|
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
|
||||||
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
||||||
import { UnauthorizedError, api } from '@/scripts/api'
|
import { UnauthorizedError } from '@/scripts/api'
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||||
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
||||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||||
import { newUserService } from '@/services/newUserService'
|
import { newUserService } from '@/services/newUserService'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
import { useBootstrapStore } from '@/stores/bootstrapStore'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
@@ -175,11 +178,16 @@ const settingStore = useSettingStore()
|
|||||||
const nodeDefStore = useNodeDefStore()
|
const nodeDefStore = useNodeDefStore()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
const colorPaletteStore = useColorPaletteStore()
|
const colorPaletteStore = useColorPaletteStore()
|
||||||
const colorPaletteService = useColorPaletteService()
|
const colorPaletteService = useColorPaletteService()
|
||||||
const canvasInteractions = useCanvasInteractions()
|
const canvasInteractions = useCanvasInteractions()
|
||||||
|
const bootstrapStore = useBootstrapStore()
|
||||||
|
const { isI18nReady, i18nError } = storeToRefs(bootstrapStore)
|
||||||
|
const { isReady: isSettingsReady, error: settingsError } =
|
||||||
|
storeToRefs(settingStore)
|
||||||
|
|
||||||
const betaMenuEnabled = computed(
|
const betaMenuEnabled = computed(
|
||||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||||
@@ -386,15 +394,6 @@ useEventListener(
|
|||||||
{ passive: true }
|
{ passive: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadCustomNodesI18n = async () => {
|
|
||||||
try {
|
|
||||||
const i18nData = await api.getCustomNodesI18n()
|
|
||||||
mergeCustomNodesI18n(i18nData)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load custom nodes i18n', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const comfyAppReady = ref(false)
|
const comfyAppReady = ref(false)
|
||||||
const workflowPersistence = useWorkflowPersistence()
|
const workflowPersistence = useWorkflowPersistence()
|
||||||
const { flags } = useFeatureFlags()
|
const { flags } = useFeatureFlags()
|
||||||
@@ -404,35 +403,72 @@ useCanvasDrop(canvasRef)
|
|||||||
useLitegraphSettings()
|
useLitegraphSettings()
|
||||||
useNodeBadge()
|
useNodeBadge()
|
||||||
|
|
||||||
|
useGlobalLitegraph()
|
||||||
|
useContextMenuTranslation()
|
||||||
|
useCopy()
|
||||||
|
usePaste()
|
||||||
|
useWorkflowAutoSave()
|
||||||
|
|
||||||
|
// Start watching for locale change after the initial value is loaded.
|
||||||
|
watch(
|
||||||
|
() => settingStore.get('Comfy.Locale'),
|
||||||
|
async (_newLocale, oldLocale) => {
|
||||||
|
if (!oldLocale) return
|
||||||
|
await Promise.all([
|
||||||
|
until(() => isSettingsReady.value || !!settingsError.value).toBe(true),
|
||||||
|
until(() => isI18nReady.value || !!i18nError.value).toBe(true)
|
||||||
|
])
|
||||||
|
if (settingsError.value || i18nError.value) {
|
||||||
|
console.warn(
|
||||||
|
'Somehow the Locale setting was changed while the settings or i18n had a setup error'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||||
|
await useWorkflowService().reloadCurrentWorkflow()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
useEventListener(
|
||||||
|
() => canvasStore.canvas?.canvas,
|
||||||
|
'litegraph:set-graph',
|
||||||
|
() => {
|
||||||
|
workflowStore.updateActiveGraph()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
useGlobalLitegraph()
|
|
||||||
useContextMenuTranslation()
|
|
||||||
useCopy()
|
|
||||||
usePaste()
|
|
||||||
useWorkflowAutoSave()
|
|
||||||
useVueFeatureFlags()
|
|
||||||
|
|
||||||
comfyApp.vueAppReady = true
|
comfyApp.vueAppReady = true
|
||||||
|
|
||||||
workspaceStore.spinner = true
|
workspaceStore.spinner = true
|
||||||
// ChangeTracker needs to be initialized before setup, as it will overwrite
|
// ChangeTracker needs to be initialized before setup, as it will overwrite
|
||||||
// some listeners of litegraph canvas.
|
// some listeners of litegraph canvas.
|
||||||
ChangeTracker.init()
|
ChangeTracker.init()
|
||||||
await loadCustomNodesI18n()
|
|
||||||
try {
|
await until(() => isSettingsReady.value || !!settingsError.value).toBe(true)
|
||||||
await settingStore.loadSettingValues()
|
|
||||||
} catch (error) {
|
if (settingsError.value) {
|
||||||
if (error instanceof UnauthorizedError) {
|
if (settingsError.value instanceof UnauthorizedError) {
|
||||||
localStorage.removeItem('Comfy.userId')
|
localStorage.removeItem('Comfy.userId')
|
||||||
localStorage.removeItem('Comfy.userName')
|
localStorage.removeItem('Comfy.userName')
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} else {
|
return
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
throw settingsError.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register core settings immediately after settings are ready
|
||||||
CORE_SETTINGS.forEach(settingStore.addSetting)
|
CORE_SETTINGS.forEach(settingStore.addSetting)
|
||||||
|
|
||||||
await newUserService().initializeIfNewUser(settingStore)
|
// Wait for both i18n and newUserService in parallel
|
||||||
|
// (newUserService only needs settings, not i18n)
|
||||||
|
await Promise.all([
|
||||||
|
until(() => isI18nReady.value || !!i18nError.value).toBe(true),
|
||||||
|
newUserService().initializeIfNewUser(settingStore)
|
||||||
|
])
|
||||||
|
if (i18nError.value) {
|
||||||
|
console.warn(
|
||||||
|
'[GraphCanvas] Failed to load custom nodes i18n:',
|
||||||
|
i18nError.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
await comfyApp.setup(canvasRef.value)
|
await comfyApp.setup(canvasRef.value)
|
||||||
@@ -487,25 +523,6 @@ onMounted(async () => {
|
|||||||
const releaseStore = useReleaseStore()
|
const releaseStore = useReleaseStore()
|
||||||
void releaseStore.initialize()
|
void releaseStore.initialize()
|
||||||
|
|
||||||
// Start watching for locale change after the initial value is loaded.
|
|
||||||
watch(
|
|
||||||
() => settingStore.get('Comfy.Locale'),
|
|
||||||
async () => {
|
|
||||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
|
||||||
await useWorkflowService().reloadCurrentWorkflow()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
whenever(
|
|
||||||
() => useCanvasStore().canvas,
|
|
||||||
(canvas) => {
|
|
||||||
useEventListener(canvas.canvas, 'litegraph:set-graph', () => {
|
|
||||||
useWorkflowStore().updateActiveGraph()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
emit('ready')
|
emit('ready')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ export const useNodeBadge = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (extensionStore.isExtensionInstalled('Comfy.NodeBadge')) return
|
||||||
|
|
||||||
|
// TODO: Fix the composables and watchers being setup in onMounted
|
||||||
const nodePricing = useNodePricing()
|
const nodePricing = useNodePricing()
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -167,25 +167,30 @@ describe('useCoreCommands', () => {
|
|||||||
return {
|
return {
|
||||||
get: vi.fn().mockReturnValue(getReturnValue),
|
get: vi.fn().mockReturnValue(getReturnValue),
|
||||||
addSetting: vi.fn(),
|
addSetting: vi.fn(),
|
||||||
loadSettingValues: vi.fn(),
|
load: vi.fn(),
|
||||||
set: vi.fn(),
|
set: vi.fn(),
|
||||||
exists: vi.fn(),
|
exists: vi.fn(),
|
||||||
getDefaultValue: vi.fn(),
|
getDefaultValue: vi.fn(),
|
||||||
|
isReady: true,
|
||||||
|
isLoading: false,
|
||||||
|
error: undefined,
|
||||||
settingValues: {},
|
settingValues: {},
|
||||||
settingsById: {},
|
settingsById: {},
|
||||||
$id: 'setting',
|
$id: 'setting',
|
||||||
$state: {
|
$state: {
|
||||||
settingValues: {},
|
settingValues: {},
|
||||||
settingsById: {}
|
settingsById: {},
|
||||||
|
isReady: true,
|
||||||
|
isLoading: false,
|
||||||
|
error: undefined
|
||||||
},
|
},
|
||||||
$patch: vi.fn(),
|
$patch: vi.fn(),
|
||||||
$reset: vi.fn(),
|
$reset: vi.fn(),
|
||||||
$subscribe: vi.fn(),
|
$subscribe: vi.fn(),
|
||||||
$onAction: vi.fn(),
|
$onAction: vi.fn(),
|
||||||
$dispose: vi.fn(),
|
$dispose: vi.fn(),
|
||||||
_customProperties: new Set(),
|
_customProperties: new Set()
|
||||||
_p: {}
|
} satisfies ReturnType<typeof useSettingStore>
|
||||||
} as ReturnType<typeof useSettingStore>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -67,10 +67,9 @@ import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelector
|
|||||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
|
||||||
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
|
|
||||||
|
|
||||||
const moveSelectedNodesVersionAdded = '1.22.2'
|
const moveSelectedNodesVersionAdded = '1.22.2'
|
||||||
export function useCoreCommands(): ComfyCommand[] {
|
export function useCoreCommands(): ComfyCommand[] {
|
||||||
|
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
|
||||||
const workflowService = useWorkflowService()
|
const workflowService = useWorkflowService()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const dialogService = useDialogService()
|
const dialogService = useDialogService()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { VueFire, VueFireAuth } from 'vuefire'
|
|||||||
import { getFirebaseConfig } from '@/config/firebase'
|
import { getFirebaseConfig } from '@/config/firebase'
|
||||||
import '@/lib/litegraph/public/css/litegraph.css'
|
import '@/lib/litegraph/public/css/litegraph.css'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
import { useBootstrapStore } from '@/stores/bootstrapStore'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
// Intentionally relative import to ensure the CSS is loaded in the right order (after litegraph.css)
|
// Intentionally relative import to ensure the CSS is loaded in the right order (after litegraph.css)
|
||||||
@@ -43,6 +44,10 @@ const firebaseApp = initializeApp(getFirebaseConfig())
|
|||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
const bootstrapStore = useBootstrapStore(pinia)
|
||||||
|
bootstrapStore.startEarlyBootstrap()
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
app,
|
app,
|
||||||
dsn: __SENTRY_DSN__,
|
dsn: __SENTRY_DSN__,
|
||||||
@@ -88,4 +93,6 @@ app
|
|||||||
modules: [VueFireAuth()]
|
modules: [VueFireAuth()]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
void bootstrapStore.startStoreBootstrap()
|
||||||
|
|
||||||
app.mount('#vue-app')
|
app.mount('#vue-app')
|
||||||
|
|||||||
@@ -203,6 +203,10 @@ function useSubscriptionInternal() {
|
|||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
try {
|
try {
|
||||||
await fetchSubscriptionStatus()
|
await fetchSubscriptionStatus()
|
||||||
|
} catch (error) {
|
||||||
|
// Network errors are expected during navigation/component unmount
|
||||||
|
// and when offline - log for debugging but don't surface to user
|
||||||
|
console.error('Failed to fetch subscription status:', error)
|
||||||
} finally {
|
} finally {
|
||||||
isInitialized.value = true
|
isInitialized.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
|
import { setActivePinia } from 'pinia'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -32,7 +33,7 @@ describe('useSettingStore', () => {
|
|||||||
let store: ReturnType<typeof useSettingStore>
|
let store: ReturnType<typeof useSettingStore>
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia())
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||||
store = useSettingStore()
|
store = useSettingStore()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
@@ -42,18 +43,18 @@ describe('useSettingStore', () => {
|
|||||||
expect(store.settingsById).toEqual({})
|
expect(store.settingsById).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('loadSettingValues', () => {
|
describe('load', () => {
|
||||||
it('should load settings from API', async () => {
|
it('should load settings from API', async () => {
|
||||||
const mockSettings = { 'test.setting': 'value' }
|
const mockSettings = { 'test.setting': 'value' }
|
||||||
vi.mocked(api.getSettings).mockResolvedValue(mockSettings as any)
|
vi.mocked(api.getSettings).mockResolvedValue(mockSettings as any)
|
||||||
|
|
||||||
await store.loadSettingValues()
|
await store.load()
|
||||||
|
|
||||||
expect(store.settingValues).toEqual(mockSettings)
|
expect(store.settingValues).toEqual(mockSettings)
|
||||||
expect(api.getSettings).toHaveBeenCalled()
|
expect(api.getSettings).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error if settings are loaded after registration', async () => {
|
it('should set error if settings are loaded after registration', async () => {
|
||||||
const setting: SettingParams = {
|
const setting: SettingParams = {
|
||||||
id: 'test.setting',
|
id: 'test.setting',
|
||||||
name: 'test.setting',
|
name: 'test.setting',
|
||||||
@@ -62,9 +63,14 @@ describe('useSettingStore', () => {
|
|||||||
}
|
}
|
||||||
store.addSetting(setting)
|
store.addSetting(setting)
|
||||||
|
|
||||||
await expect(store.loadSettingValues()).rejects.toThrow(
|
await store.load()
|
||||||
'Setting values must be loaded before any setting is registered.'
|
|
||||||
)
|
expect(store.error).toBeInstanceOf(Error)
|
||||||
|
if (store.error instanceof Error) {
|
||||||
|
expect(store.error.message).toBe(
|
||||||
|
'Setting values must be loaded before any setting is registered.'
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -82,18 +88,24 @@ describe('useSettingStore', () => {
|
|||||||
expect(store.settingsById['test.setting']).toEqual(setting)
|
expect(store.settingsById['test.setting']).toEqual(setting)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error for duplicate setting ID', () => {
|
it('should warn and skip for duplicate setting ID', () => {
|
||||||
const setting: SettingParams = {
|
const setting: SettingParams = {
|
||||||
id: 'test.setting',
|
id: 'test.setting',
|
||||||
name: 'test.setting',
|
name: 'test.setting',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
defaultValue: 'default'
|
defaultValue: 'default'
|
||||||
}
|
}
|
||||||
|
const consoleWarnSpy = vi
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
|
||||||
store.addSetting(setting)
|
store.addSetting(setting)
|
||||||
expect(() => store.addSetting(setting)).toThrow(
|
store.addSetting(setting)
|
||||||
'Setting test.setting must have a unique ID.'
|
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||||
|
'Setting already registered: test.setting'
|
||||||
)
|
)
|
||||||
|
consoleWarnSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should migrate deprecated values', () => {
|
it('should migrate deprecated values', () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import _ from 'es-toolkit/compat'
|
import _ from 'es-toolkit/compat'
|
||||||
|
import { useAsyncState } from '@vueuse/core'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { compare, valid } from 'semver'
|
import { compare, valid } from 'semver'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
@@ -47,6 +48,31 @@ export const useSettingStore = defineStore('setting', () => {
|
|||||||
const settingValues = ref<Record<string, any>>({})
|
const settingValues = ref<Record<string, any>>({})
|
||||||
const settingsById = ref<Record<string, SettingParams>>({})
|
const settingsById = ref<Record<string, SettingParams>>({})
|
||||||
|
|
||||||
|
const {
|
||||||
|
isReady,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
execute: loadSettingValues
|
||||||
|
} = useAsyncState(
|
||||||
|
async () => {
|
||||||
|
if (Object.keys(settingsById.value).length) {
|
||||||
|
throw new Error(
|
||||||
|
'Setting values must be loaded before any setting is registered.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
settingValues.value = await api.getSettings()
|
||||||
|
await migrateZoomThresholdToFontSize()
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (!isReady.value && !isLoading.value) {
|
||||||
|
return loadSettingValues()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a setting's value exists, i.e. if the user has set it manually.
|
* Check if a setting's value exists, i.e. if the user has set it manually.
|
||||||
* @param key - The key of the setting to check.
|
* @param key - The key of the setting to check.
|
||||||
@@ -170,7 +196,11 @@ export const useSettingStore = defineStore('setting', () => {
|
|||||||
throw new Error('Settings must have an ID')
|
throw new Error('Settings must have an ID')
|
||||||
}
|
}
|
||||||
if (setting.id in settingsById.value) {
|
if (setting.id in settingsById.value) {
|
||||||
throw new Error(`Setting ${setting.id} must have a unique ID.`)
|
// Setting already registered - skip to allow component remounting
|
||||||
|
// TODO: Add store reset methods to bootstrapStore and settingStore, then
|
||||||
|
// replace window.location.reload() with router.push() in SidebarLogoutIcon.vue
|
||||||
|
console.warn(`Setting already registered: ${setting.id}`)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsById.value[setting.id] = setting
|
settingsById.value[setting.id] = setting
|
||||||
@@ -184,22 +214,6 @@ export const useSettingStore = defineStore('setting', () => {
|
|||||||
onChange(setting, get(setting.id), undefined)
|
onChange(setting, get(setting.id), undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Load setting values from server.
|
|
||||||
* This needs to be called before any setting is registered.
|
|
||||||
*/
|
|
||||||
async function loadSettingValues() {
|
|
||||||
if (Object.keys(settingsById.value).length) {
|
|
||||||
throw new Error(
|
|
||||||
'Setting values must be loaded before any setting is registered.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
settingValues.value = await api.getSettings()
|
|
||||||
|
|
||||||
// Migrate old zoom threshold setting to new font size setting
|
|
||||||
await migrateZoomThresholdToFontSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate the old zoom threshold setting to the new font size setting.
|
* Migrate the old zoom threshold setting to the new font size setting.
|
||||||
* Preserves the exact zoom threshold behavior by converting it to equivalent font size.
|
* Preserves the exact zoom threshold behavior by converting it to equivalent font size.
|
||||||
@@ -242,8 +256,11 @@ export const useSettingStore = defineStore('setting', () => {
|
|||||||
return {
|
return {
|
||||||
settingValues,
|
settingValues,
|
||||||
settingsById,
|
settingsById,
|
||||||
|
isReady,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
load,
|
||||||
addSetting,
|
addSetting,
|
||||||
loadSettingValues,
|
|
||||||
set,
|
set,
|
||||||
get,
|
get,
|
||||||
exists,
|
exists,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import _ from 'es-toolkit/compat'
|
import _ from 'es-toolkit/compat'
|
||||||
|
import { useAsyncState } from '@vueuse/core'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, markRaw, ref, shallowRef, watch } from 'vue'
|
import { computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||||
import type { Raw } from 'vue'
|
import type { Raw } from 'vue'
|
||||||
@@ -500,48 +501,67 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
|||||||
workflow.isPersisted && !workflow.path.startsWith('subgraphs/')
|
workflow.isPersisted && !workflow.path.startsWith('subgraphs/')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const syncWorkflows = async (dir: string = '') => {
|
|
||||||
await syncEntities(
|
|
||||||
dir ? 'workflows/' + dir : 'workflows',
|
|
||||||
workflowLookup.value,
|
|
||||||
(file) =>
|
|
||||||
new ComfyWorkflow({
|
|
||||||
path: file.path,
|
|
||||||
modified: file.modified,
|
|
||||||
size: file.size
|
|
||||||
}),
|
|
||||||
(existingWorkflow, file) => {
|
|
||||||
const isActiveWorkflow =
|
|
||||||
activeWorkflow.value?.path === existingWorkflow.path
|
|
||||||
|
|
||||||
const nextLastModified = Math.max(
|
const {
|
||||||
existingWorkflow.lastModified,
|
isReady: isSyncReady,
|
||||||
file.modified
|
isLoading: isSyncLoading,
|
||||||
)
|
execute: executeSyncWorkflows
|
||||||
|
} = useAsyncState(
|
||||||
|
async (dir: string = '') => {
|
||||||
|
await syncEntities(
|
||||||
|
dir ? 'workflows/' + dir : 'workflows',
|
||||||
|
workflowLookup.value,
|
||||||
|
(file) =>
|
||||||
|
new ComfyWorkflow({
|
||||||
|
path: file.path,
|
||||||
|
modified: file.modified,
|
||||||
|
size: file.size
|
||||||
|
}),
|
||||||
|
(existingWorkflow, file) => {
|
||||||
|
const isActiveWorkflow =
|
||||||
|
activeWorkflow.value?.path === existingWorkflow.path
|
||||||
|
|
||||||
const isMetadataUnchanged =
|
const nextLastModified = Math.max(
|
||||||
nextLastModified === existingWorkflow.lastModified &&
|
existingWorkflow.lastModified,
|
||||||
file.size === existingWorkflow.size
|
file.modified
|
||||||
|
)
|
||||||
|
|
||||||
if (!isMetadataUnchanged) {
|
const isMetadataUnchanged =
|
||||||
existingWorkflow.lastModified = nextLastModified
|
nextLastModified === existingWorkflow.lastModified &&
|
||||||
existingWorkflow.size = file.size
|
file.size === existingWorkflow.size
|
||||||
}
|
|
||||||
|
|
||||||
// Never unload the active workflow - it may contain unsaved in-memory edits.
|
if (!isMetadataUnchanged) {
|
||||||
if (isActiveWorkflow) {
|
existingWorkflow.lastModified = nextLastModified
|
||||||
return
|
existingWorkflow.size = file.size
|
||||||
}
|
}
|
||||||
|
|
||||||
// If nothing changed, keep any loaded content cached.
|
// Never unload the active workflow - it may contain unsaved in-memory edits.
|
||||||
if (isMetadataUnchanged) {
|
if (isActiveWorkflow) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
existingWorkflow.unload()
|
// If nothing changed, keep any loaded content cached.
|
||||||
},
|
if (isMetadataUnchanged) {
|
||||||
/* exclude */ (workflow) => workflow.isTemporary
|
return
|
||||||
)
|
}
|
||||||
|
|
||||||
|
existingWorkflow.unload()
|
||||||
|
},
|
||||||
|
/* exclude */ (workflow) => workflow.isTemporary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function syncWorkflows(dir: string = '') {
|
||||||
|
return executeSyncWorkflows(0, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkflows() {
|
||||||
|
if (!isSyncReady.value && !isSyncLoading.value) {
|
||||||
|
return syncWorkflows()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookmarkStore = useWorkflowBookmarkStore()
|
const bookmarkStore = useWorkflowBookmarkStore()
|
||||||
@@ -849,6 +869,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
|||||||
modifiedWorkflows,
|
modifiedWorkflows,
|
||||||
getWorkflowByPath,
|
getWorkflowByPath,
|
||||||
syncWorkflows,
|
syncWorkflows,
|
||||||
|
loadWorkflows,
|
||||||
|
|
||||||
isSubgraphActive,
|
isSubgraphActive,
|
||||||
activeSubgraph,
|
activeSubgraph,
|
||||||
|
|||||||
@@ -139,21 +139,11 @@ describe('useMinimapGraph', () => {
|
|||||||
expect(mockGraph.onConnectionChange).toBe(originalOnConnectionChange)
|
expect(mockGraph.onConnectionChange).toBe(originalOnConnectionChange)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle cleanup for never-setup graph', () => {
|
it('should handle cleanup for never-setup graph silently', () => {
|
||||||
const consoleErrorSpy = vi
|
|
||||||
.spyOn(console, 'error')
|
|
||||||
.mockImplementation(() => {})
|
|
||||||
|
|
||||||
const graphRef = ref(mockGraph as any)
|
const graphRef = ref(mockGraph as any)
|
||||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||||
|
|
||||||
graphManager.cleanupEventListeners()
|
expect(() => graphManager.cleanupEventListeners()).not.toThrow()
|
||||||
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
||||||
'Attempted to cleanup event listeners for graph that was never set up'
|
|
||||||
)
|
|
||||||
|
|
||||||
consoleErrorSpy.mockRestore()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should detect node position changes', () => {
|
it('should detect node position changes', () => {
|
||||||
|
|||||||
@@ -102,9 +102,7 @@ export function useMinimapGraph(
|
|||||||
|
|
||||||
const originalCallbacks = originalCallbacksMap.get(g.id)
|
const originalCallbacks = originalCallbacksMap.get(g.id)
|
||||||
if (!originalCallbacks) {
|
if (!originalCallbacks) {
|
||||||
console.error(
|
// Graph was never set up (e.g., minimap destroyed before init) - nothing to clean up
|
||||||
'Attempted to cleanup event listeners for graph that was never set up'
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { shallowRef } from 'vue'
|
|||||||
|
|
||||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||||
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
|
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
|
||||||
import { st, t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||||
import {
|
import {
|
||||||
LGraph,
|
LGraph,
|
||||||
@@ -61,7 +61,7 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
|||||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||||
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||||
import { useModelStore } from '@/stores/modelStore'
|
import { useModelStore } from '@/stores/modelStore'
|
||||||
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||||
import { useWidgetStore } from '@/stores/widgetStore'
|
import { useWidgetStore } from '@/stores/widgetStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
@@ -881,21 +881,18 @@ export class ComfyApp {
|
|||||||
this.canvas?.draw(true, true)
|
this.canvas?.draw(true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateVueAppNodeDefs(defs: Record<string, ComfyNodeDefV1>) {
|
private buildFrontendOnlyDefs(
|
||||||
// Frontend only nodes registered by custom nodes.
|
backendDefs: Record<string, ComfyNodeDefV1>
|
||||||
// Example: https://github.com/rgthree/rgthree-comfy/blob/dd534e5384be8cf0c0fa35865afe2126ba75ac55/src_web/comfyui/fast_groups_bypasser.ts#L10
|
): ComfyNodeDefV1[] {
|
||||||
|
const frontendOnlyDefs: ComfyNodeDefV1[] = []
|
||||||
// Only create frontend_only definitions for nodes that don't have backend definitions
|
|
||||||
const frontendOnlyDefs: Record<string, ComfyNodeDefV1> = {}
|
|
||||||
for (const [name, node] of Object.entries(
|
for (const [name, node] of Object.entries(
|
||||||
LiteGraph.registered_node_types
|
LiteGraph.registered_node_types
|
||||||
)) {
|
)) {
|
||||||
// Skip if we already have a backend definition or system definition
|
if (name in backendDefs || node.skip_list) {
|
||||||
if (name in defs || name in SYSTEM_NODE_DEFS || node.skip_list) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
frontendOnlyDefs[name] = {
|
frontendOnlyDefs.push({
|
||||||
name,
|
name,
|
||||||
display_name: name,
|
display_name: name,
|
||||||
category: node.category || '__frontend_only__',
|
category: node.category || '__frontend_only__',
|
||||||
@@ -906,54 +903,33 @@ export class ComfyApp {
|
|||||||
output_node: false,
|
output_node: false,
|
||||||
python_module: 'custom_nodes.frontend_only',
|
python_module: 'custom_nodes.frontend_only',
|
||||||
description: node.description ?? `Frontend only node for ${name}`
|
description: node.description ?? `Frontend only node for ${name}`
|
||||||
} as ComfyNodeDefV1
|
})
|
||||||
}
|
}
|
||||||
|
return frontendOnlyDefs
|
||||||
const allNodeDefs = {
|
|
||||||
...frontendOnlyDefs,
|
|
||||||
...defs,
|
|
||||||
...SYSTEM_NODE_DEFS
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeDefStore = useNodeDefStore()
|
|
||||||
const nodeDefArray: ComfyNodeDefV1[] = Object.values(allNodeDefs)
|
|
||||||
useExtensionService().invokeExtensions(
|
|
||||||
'beforeRegisterVueAppNodeDefs',
|
|
||||||
nodeDefArray,
|
|
||||||
this
|
|
||||||
)
|
|
||||||
nodeDefStore.updateNodeDefs(nodeDefArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNodeDefs(): Promise<Record<string, ComfyNodeDefV1>> {
|
|
||||||
const translateNodeDef = (def: ComfyNodeDefV1): ComfyNodeDefV1 => ({
|
|
||||||
...def,
|
|
||||||
display_name: st(
|
|
||||||
`nodeDefs.${def.name}.display_name`,
|
|
||||||
def.display_name ?? def.name
|
|
||||||
),
|
|
||||||
description: def.description
|
|
||||||
? st(`nodeDefs.${def.name}.description`, def.description)
|
|
||||||
: '',
|
|
||||||
category: def.category
|
|
||||||
.split('/')
|
|
||||||
.map((category: string) => st(`nodeCategories.${category}`, category))
|
|
||||||
.join('/')
|
|
||||||
})
|
|
||||||
|
|
||||||
return _.mapValues(await api.getNodeDefs(), (def) => translateNodeDef(def))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers nodes with the graph
|
* Registers nodes with the graph
|
||||||
*/
|
*/
|
||||||
async registerNodes() {
|
async registerNodes() {
|
||||||
// Load node definitions from the backend
|
const nodeDefStore = useNodeDefStore()
|
||||||
const defs = await this.getNodeDefs()
|
|
||||||
|
while (!nodeDefStore.isReady) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeDefStore.error) {
|
||||||
|
throw nodeDefStore.error
|
||||||
|
}
|
||||||
|
|
||||||
|
const defs = nodeDefStore.rawNodeDefs
|
||||||
await this.registerNodesFromDefs(defs)
|
await this.registerNodesFromDefs(defs)
|
||||||
await useExtensionService().invokeExtensionsAsync('registerCustomNodes')
|
await useExtensionService().invokeExtensionsAsync('registerCustomNodes')
|
||||||
|
|
||||||
if (this.vueAppReady) {
|
if (this.vueAppReady) {
|
||||||
this.updateVueAppNodeDefs(defs)
|
const frontendOnlyDefs = this.buildFrontendOnlyDefs(defs)
|
||||||
|
const allDefs = [...frontendOnlyDefs, ...Object.values(defs)]
|
||||||
|
useNodeDefStore().updateNodeDefs(allDefs, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1653,7 +1629,7 @@ export class ComfyApp {
|
|||||||
useToastStore().add(requestToastMessage)
|
useToastStore().add(requestToastMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
const defs = await this.getNodeDefs()
|
const defs = await api.getNodeDefs()
|
||||||
for (const nodeId in defs) {
|
for (const nodeId in defs) {
|
||||||
this.registerNodeDef(nodeId, defs[nodeId])
|
this.registerNodeDef(nodeId, defs[nodeId])
|
||||||
}
|
}
|
||||||
@@ -1698,7 +1674,9 @@ export class ComfyApp {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (this.vueAppReady) {
|
if (this.vueAppReady) {
|
||||||
this.updateVueAppNodeDefs(defs)
|
const frontendOnlyDefs = this.buildFrontendOnlyDefs(defs)
|
||||||
|
const allDefs = [...frontendOnlyDefs, ...Object.values(defs)]
|
||||||
|
useNodeDefStore().updateNodeDefs(allDefs, this)
|
||||||
useToastStore().remove(requestToastMessage)
|
useToastStore().remove(requestToastMessage)
|
||||||
useToastStore().add({
|
useToastStore().add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
|
|||||||
98
src/stores/bootstrapStore.test.ts
Normal file
98
src/stores/bootstrapStore.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
|
import { setActivePinia } from 'pinia'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { useBootstrapStore } from './bootstrapStore'
|
||||||
|
|
||||||
|
vi.mock('@/scripts/api', () => ({
|
||||||
|
api: {
|
||||||
|
init: vi.fn().mockResolvedValue(undefined),
|
||||||
|
getNodeDefs: vi.fn().mockResolvedValue({ TestNode: { name: 'TestNode' } }),
|
||||||
|
getCustomNodesI18n: vi.fn().mockResolvedValue({}),
|
||||||
|
getUserConfig: vi.fn().mockResolvedValue({})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/i18n', () => ({
|
||||||
|
mergeCustomNodesI18n: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockIsSettingsReady = ref(false)
|
||||||
|
const mockIsNodeDefsReady = ref(false)
|
||||||
|
const mockNodeDefStoreLoad = vi.fn(() => {
|
||||||
|
mockIsNodeDefsReady.value = true
|
||||||
|
})
|
||||||
|
const mockSettingStoreLoad = vi.fn(() => {
|
||||||
|
mockIsSettingsReady.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/platform/settings/settingStore', () => ({
|
||||||
|
useSettingStore: vi.fn(() => ({
|
||||||
|
load: mockSettingStoreLoad,
|
||||||
|
isReady: mockIsSettingsReady,
|
||||||
|
isLoading: { value: false },
|
||||||
|
error: { value: undefined }
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/nodeDefStore', () => ({
|
||||||
|
useNodeDefStore: vi.fn(() => ({
|
||||||
|
load: mockNodeDefStoreLoad,
|
||||||
|
isReady: mockIsNodeDefsReady,
|
||||||
|
isLoading: { value: false },
|
||||||
|
error: { value: undefined },
|
||||||
|
rawNodeDefs: { value: {} }
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||||
|
useWorkflowStore: vi.fn(() => ({
|
||||||
|
loadWorkflows: vi.fn(),
|
||||||
|
syncWorkflows: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/userStore', () => ({
|
||||||
|
useUserStore: vi.fn(() => ({
|
||||||
|
initialize: vi.fn().mockResolvedValue(undefined),
|
||||||
|
needsLogin: false
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('bootstrapStore', () => {
|
||||||
|
let store: ReturnType<typeof useBootstrapStore>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockIsSettingsReady.value = false
|
||||||
|
mockIsNodeDefsReady.value = false
|
||||||
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||||
|
store = useBootstrapStore()
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes with all flags false', () => {
|
||||||
|
expect(mockIsNodeDefsReady.value).toBe(false)
|
||||||
|
expect(mockIsSettingsReady.value).toBe(false)
|
||||||
|
expect(store.isI18nReady).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('starts early bootstrap (node defs)', async () => {
|
||||||
|
store.startEarlyBootstrap()
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(mockIsNodeDefsReady.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockNodeDefStoreLoad).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('starts store bootstrap (settings, i18n)', async () => {
|
||||||
|
void store.startStoreBootstrap()
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(mockIsSettingsReady.value).toBe(true)
|
||||||
|
expect(store.isI18nReady).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
51
src/stores/bootstrapStore.ts
Normal file
51
src/stores/bootstrapStore.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useAsyncState } from '@vueuse/core'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
|
import { useUserStore } from '@/stores/userStore'
|
||||||
|
|
||||||
|
export const useBootstrapStore = defineStore('bootstrap', () => {
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const nodeDefStore = useNodeDefStore()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
isReady: isI18nReady,
|
||||||
|
error: i18nError,
|
||||||
|
execute: loadI18n
|
||||||
|
} = useAsyncState(
|
||||||
|
async () => {
|
||||||
|
const { mergeCustomNodesI18n } = await import('@/i18n')
|
||||||
|
const i18nData = await api.getCustomNodesI18n()
|
||||||
|
mergeCustomNodesI18n(i18nData)
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
function startEarlyBootstrap() {
|
||||||
|
void nodeDefStore.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startStoreBootstrap() {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
await userStore.initialize()
|
||||||
|
|
||||||
|
void loadI18n()
|
||||||
|
|
||||||
|
if (!userStore.needsLogin) {
|
||||||
|
await settingStore.load()
|
||||||
|
await workflowStore.loadWorkflows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isI18nReady,
|
||||||
|
i18nError,
|
||||||
|
startEarlyBootstrap,
|
||||||
|
startStoreBootstrap
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,16 +1,28 @@
|
|||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
import { setActivePinia } from 'pinia'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import type { NodeDefFilter } from '@/stores/nodeDefStore'
|
import type { NodeDefFilter } from '@/stores/nodeDefStore'
|
||||||
|
|
||||||
describe('useNodeDefStore', () => {
|
vi.mock('@/scripts/api', () => ({
|
||||||
let store: ReturnType<typeof useNodeDefStore>
|
api: {
|
||||||
|
getNodeDefs: vi.fn().mockResolvedValue({ TestNode: { name: 'TestNode' } }),
|
||||||
|
apiURL: vi.fn((path: string) => `/api${path}`),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
getUserData: vi.fn(),
|
||||||
|
storeUserData: vi.fn(),
|
||||||
|
listUserDataFullInfo: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const SYSTEM_NODE_COUNT = Object.keys(SYSTEM_NODE_DEFS).length
|
||||||
|
|
||||||
|
describe('useNodeDefStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia())
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||||
store = useNodeDefStore()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
const createMockNodeDef = (
|
const createMockNodeDef = (
|
||||||
@@ -31,8 +43,42 @@ describe('useNodeDefStore', () => {
|
|||||||
...overrides
|
...overrides
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('load', () => {
|
||||||
|
it('initializes with isReady false', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
|
expect(store.isReady).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads node definitions from API', async () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
|
const { api } = await import('@/scripts/api')
|
||||||
|
|
||||||
|
await store.load()
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(store.isReady).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(api.getNodeDefs).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not reload if already ready', async () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
|
const { api } = await import('@/scripts/api')
|
||||||
|
|
||||||
|
await store.load()
|
||||||
|
await vi.waitFor(() => expect(store.isReady).toBe(true))
|
||||||
|
|
||||||
|
vi.clearAllMocks()
|
||||||
|
await store.load()
|
||||||
|
|
||||||
|
expect(api.getNodeDefs).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('filter registry', () => {
|
describe('filter registry', () => {
|
||||||
it('should register a new filter', () => {
|
it('should register a new filter', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const filter: NodeDefFilter = {
|
const filter: NodeDefFilter = {
|
||||||
id: 'test.filter',
|
id: 'test.filter',
|
||||||
name: 'Test Filter',
|
name: 'Test Filter',
|
||||||
@@ -44,6 +90,7 @@ describe('useNodeDefStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should unregister a filter by id', () => {
|
it('should unregister a filter by id', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const filter: NodeDefFilter = {
|
const filter: NodeDefFilter = {
|
||||||
id: 'test.filter',
|
id: 'test.filter',
|
||||||
name: 'Test Filter',
|
name: 'Test Filter',
|
||||||
@@ -56,6 +103,7 @@ describe('useNodeDefStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should register core filters on initialization', () => {
|
it('should register core filters on initialization', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const deprecatedFilter = store.nodeDefFilters.find(
|
const deprecatedFilter = store.nodeDefFilters.find(
|
||||||
(f) => f.id === 'core.deprecated'
|
(f) => f.id === 'core.deprecated'
|
||||||
)
|
)
|
||||||
@@ -69,12 +117,23 @@ describe('useNodeDefStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('filter application', () => {
|
describe('filter application', () => {
|
||||||
beforeEach(() => {
|
const systemNodeFilter: NodeDefFilter = {
|
||||||
|
id: 'test.no-system',
|
||||||
|
name: 'Hide System Nodes',
|
||||||
|
predicate: (node) => !(node.name in SYSTEM_NODE_DEFS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterTestStore() {
|
||||||
|
const store = useNodeDefStore()
|
||||||
// Clear existing filters for isolated tests
|
// Clear existing filters for isolated tests
|
||||||
store.nodeDefFilters.splice(0)
|
store.nodeDefFilters.splice(0)
|
||||||
})
|
// Exclude system nodes from filter tests for cleaner assertions
|
||||||
|
store.registerNodeDefFilter(systemNodeFilter)
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
it('should apply single filter to visible nodes', () => {
|
it('should apply single filter to visible nodes', () => {
|
||||||
|
const store = createFilterTestStore()
|
||||||
const normalNode = createMockNodeDef({
|
const normalNode = createMockNodeDef({
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
deprecated: false
|
deprecated: false
|
||||||
@@ -98,6 +157,7 @@ describe('useNodeDefStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should apply multiple filters with AND logic', () => {
|
it('should apply multiple filters with AND logic', () => {
|
||||||
|
const store = createFilterTestStore()
|
||||||
const node1 = createMockNodeDef({
|
const node1 = createMockNodeDef({
|
||||||
name: 'node1',
|
name: 'node1',
|
||||||
deprecated: false,
|
deprecated: false,
|
||||||
@@ -140,6 +200,10 @@ describe('useNodeDefStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show all nodes when no filters are registered', () => {
|
it('should show all nodes when no filters are registered', () => {
|
||||||
|
const store = createFilterTestStore()
|
||||||
|
// Remove system node filter for this test
|
||||||
|
store.unregisterNodeDefFilter('test.no-system')
|
||||||
|
|
||||||
const nodes = [
|
const nodes = [
|
||||||
createMockNodeDef({ name: 'node1' }),
|
createMockNodeDef({ name: 'node1' }),
|
||||||
createMockNodeDef({ name: 'node2' }),
|
createMockNodeDef({ name: 'node2' }),
|
||||||
@@ -147,10 +211,12 @@ describe('useNodeDefStore', () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
store.updateNodeDefs(nodes)
|
store.updateNodeDefs(nodes)
|
||||||
expect(store.visibleNodeDefs).toHaveLength(3)
|
// Includes 3 test nodes + 4 system nodes
|
||||||
|
expect(store.visibleNodeDefs).toHaveLength(3 + SYSTEM_NODE_COUNT)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update visibility when filter is removed', () => {
|
it('should update visibility when filter is removed', () => {
|
||||||
|
const store = createFilterTestStore()
|
||||||
const deprecatedNode = createMockNodeDef({
|
const deprecatedNode = createMockNodeDef({
|
||||||
name: 'deprecated',
|
name: 'deprecated',
|
||||||
deprecated: true
|
deprecated: true
|
||||||
@@ -163,7 +229,7 @@ describe('useNodeDefStore', () => {
|
|||||||
predicate: (node) => !node.deprecated
|
predicate: (node) => !node.deprecated
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add filter - node should be hidden
|
// Add filter - node should be hidden (only system nodes remain, but they're filtered too)
|
||||||
store.registerNodeDefFilter(filter)
|
store.registerNodeDefFilter(filter)
|
||||||
expect(store.visibleNodeDefs).toHaveLength(0)
|
expect(store.visibleNodeDefs).toHaveLength(0)
|
||||||
|
|
||||||
@@ -175,6 +241,7 @@ describe('useNodeDefStore', () => {
|
|||||||
|
|
||||||
describe('core filters behavior', () => {
|
describe('core filters behavior', () => {
|
||||||
it('should hide deprecated nodes by default', () => {
|
it('should hide deprecated nodes by default', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const normalNode = createMockNodeDef({
|
const normalNode = createMockNodeDef({
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
deprecated: false
|
deprecated: false
|
||||||
@@ -186,11 +253,18 @@ describe('useNodeDefStore', () => {
|
|||||||
|
|
||||||
store.updateNodeDefs([normalNode, deprecatedNode])
|
store.updateNodeDefs([normalNode, deprecatedNode])
|
||||||
|
|
||||||
expect(store.visibleNodeDefs).toHaveLength(1)
|
// 1 normal test node + 4 system nodes
|
||||||
expect(store.visibleNodeDefs[0].name).toBe('normal')
|
expect(store.visibleNodeDefs).toHaveLength(1 + SYSTEM_NODE_COUNT)
|
||||||
|
expect(
|
||||||
|
store.visibleNodeDefs.find((n) => n.name === 'normal')
|
||||||
|
).toBeDefined()
|
||||||
|
expect(
|
||||||
|
store.visibleNodeDefs.find((n) => n.name === 'deprecated')
|
||||||
|
).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show deprecated nodes when showDeprecated is true', () => {
|
it('should show deprecated nodes when showDeprecated is true', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const normalNode = createMockNodeDef({
|
const normalNode = createMockNodeDef({
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
deprecated: false
|
deprecated: false
|
||||||
@@ -203,10 +277,12 @@ describe('useNodeDefStore', () => {
|
|||||||
store.updateNodeDefs([normalNode, deprecatedNode])
|
store.updateNodeDefs([normalNode, deprecatedNode])
|
||||||
store.showDeprecated = true
|
store.showDeprecated = true
|
||||||
|
|
||||||
expect(store.visibleNodeDefs).toHaveLength(2)
|
// 2 test nodes + 4 system nodes
|
||||||
|
expect(store.visibleNodeDefs).toHaveLength(2 + SYSTEM_NODE_COUNT)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should hide experimental nodes by default', () => {
|
it('should hide experimental nodes by default', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const normalNode = createMockNodeDef({
|
const normalNode = createMockNodeDef({
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
experimental: false
|
experimental: false
|
||||||
@@ -218,11 +294,18 @@ describe('useNodeDefStore', () => {
|
|||||||
|
|
||||||
store.updateNodeDefs([normalNode, experimentalNode])
|
store.updateNodeDefs([normalNode, experimentalNode])
|
||||||
|
|
||||||
expect(store.visibleNodeDefs).toHaveLength(1)
|
// 1 normal test node + 4 system nodes
|
||||||
expect(store.visibleNodeDefs[0].name).toBe('normal')
|
expect(store.visibleNodeDefs).toHaveLength(1 + SYSTEM_NODE_COUNT)
|
||||||
|
expect(
|
||||||
|
store.visibleNodeDefs.find((n) => n.name === 'normal')
|
||||||
|
).toBeDefined()
|
||||||
|
expect(
|
||||||
|
store.visibleNodeDefs.find((n) => n.name === 'experimental')
|
||||||
|
).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show experimental nodes when showExperimental is true', () => {
|
it('should show experimental nodes when showExperimental is true', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const normalNode = createMockNodeDef({
|
const normalNode = createMockNodeDef({
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
experimental: false
|
experimental: false
|
||||||
@@ -235,10 +318,12 @@ describe('useNodeDefStore', () => {
|
|||||||
store.updateNodeDefs([normalNode, experimentalNode])
|
store.updateNodeDefs([normalNode, experimentalNode])
|
||||||
store.showExperimental = true
|
store.showExperimental = true
|
||||||
|
|
||||||
expect(store.visibleNodeDefs).toHaveLength(2)
|
// 2 test nodes + 4 system nodes
|
||||||
|
expect(store.visibleNodeDefs).toHaveLength(2 + SYSTEM_NODE_COUNT)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should hide subgraph nodes by default', () => {
|
it('should hide subgraph nodes by default', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const normalNode = createMockNodeDef({
|
const normalNode = createMockNodeDef({
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
category: 'conditioning',
|
category: 'conditioning',
|
||||||
@@ -252,11 +337,18 @@ describe('useNodeDefStore', () => {
|
|||||||
|
|
||||||
store.updateNodeDefs([normalNode, subgraphNode])
|
store.updateNodeDefs([normalNode, subgraphNode])
|
||||||
|
|
||||||
expect(store.visibleNodeDefs).toHaveLength(1)
|
// 1 normal test node + 4 system nodes
|
||||||
expect(store.visibleNodeDefs[0].name).toBe('normal')
|
expect(store.visibleNodeDefs).toHaveLength(1 + SYSTEM_NODE_COUNT)
|
||||||
|
expect(
|
||||||
|
store.visibleNodeDefs.find((n) => n.name === 'normal')
|
||||||
|
).toBeDefined()
|
||||||
|
expect(
|
||||||
|
store.visibleNodeDefs.find((n) => n.name === 'MySubgraph')
|
||||||
|
).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show non-subgraph nodes with subgraph category', () => {
|
it('should show non-subgraph nodes with subgraph category', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
const normalNode = createMockNodeDef({
|
const normalNode = createMockNodeDef({
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
category: 'conditioning',
|
category: 'conditioning',
|
||||||
@@ -270,20 +362,23 @@ describe('useNodeDefStore', () => {
|
|||||||
|
|
||||||
store.updateNodeDefs([normalNode, fakeSubgraphNode])
|
store.updateNodeDefs([normalNode, fakeSubgraphNode])
|
||||||
|
|
||||||
expect(store.visibleNodeDefs).toHaveLength(2)
|
// 2 test nodes + 4 system nodes
|
||||||
expect(store.visibleNodeDefs.map((n) => n.name)).toEqual([
|
expect(store.visibleNodeDefs).toHaveLength(2 + SYSTEM_NODE_COUNT)
|
||||||
'normal',
|
const testNodes = store.visibleNodeDefs
|
||||||
'FakeSubgraph'
|
.filter((n) => !(n.name in SYSTEM_NODE_DEFS))
|
||||||
])
|
.map((n) => n.name)
|
||||||
|
expect(testNodes).toEqual(['normal', 'FakeSubgraph'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('performance', () => {
|
describe('performance', () => {
|
||||||
it('should perform single traversal for multiple filters', () => {
|
it('should perform single traversal for multiple filters', () => {
|
||||||
|
const store = useNodeDefStore()
|
||||||
let filterCallCount = 0
|
let filterCallCount = 0
|
||||||
|
const testFilterCount = 5
|
||||||
|
|
||||||
// Register multiple filters that count their calls
|
// Register multiple filters that count their calls
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < testFilterCount; i++) {
|
||||||
store.registerNodeDefFilter({
|
store.registerNodeDefFilter({
|
||||||
id: `test.counter-${i}`,
|
id: `test.counter-${i}`,
|
||||||
name: `Counter ${i}`,
|
name: `Counter ${i}`,
|
||||||
@@ -294,7 +389,8 @@ describe('useNodeDefStore', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = Array.from({ length: 10 }, (_, i) =>
|
const testNodeCount = 10
|
||||||
|
const nodes = Array.from({ length: testNodeCount }, (_, i) =>
|
||||||
createMockNodeDef({ name: `node${i}` })
|
createMockNodeDef({ name: `node${i}` })
|
||||||
)
|
)
|
||||||
store.updateNodeDefs(nodes)
|
store.updateNodeDefs(nodes)
|
||||||
@@ -302,8 +398,9 @@ describe('useNodeDefStore', () => {
|
|||||||
// Force recomputation by accessing visibleNodeDefs
|
// Force recomputation by accessing visibleNodeDefs
|
||||||
expect(store.visibleNodeDefs).toBeDefined()
|
expect(store.visibleNodeDefs).toBeDefined()
|
||||||
|
|
||||||
// Each node (10) should be checked by each filter (5 test + 2 core = 7 total)
|
// Each node (test nodes + system nodes) checked by each test filter
|
||||||
expect(filterCallCount).toBe(10 * 5)
|
const totalNodes = testNodeCount + SYSTEM_NODE_COUNT
|
||||||
|
expect(filterCallCount).toBe(totalNodes * testFilterCount)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { useAsyncState } from '@vueuse/core'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { retry } from 'es-toolkit'
|
||||||
import _ from 'es-toolkit/compat'
|
import _ from 'es-toolkit/compat'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
|
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
|
||||||
|
import { st } from '@/i18n'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration'
|
import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||||
import type {
|
import type {
|
||||||
@@ -17,7 +20,10 @@ import type {
|
|||||||
ComfyOutputTypesSpec as ComfyOutputSpecV1,
|
ComfyOutputTypesSpec as ComfyOutputSpecV1,
|
||||||
PriceBadge
|
PriceBadge
|
||||||
} from '@/schemas/nodeDefSchema'
|
} from '@/schemas/nodeDefSchema'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import type { ComfyApp } from '@/scripts/app'
|
||||||
import { NodeSearchService } from '@/services/nodeSearchService'
|
import { NodeSearchService } from '@/services/nodeSearchService'
|
||||||
|
import { useExtensionService } from '@/services/extensionService'
|
||||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||||
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
|
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
|
||||||
import type { NodeSource } from '@/types/nodeSource'
|
import type { NodeSource } from '@/types/nodeSource'
|
||||||
@@ -305,6 +311,28 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
|||||||
const showExperimental = ref(false)
|
const showExperimental = ref(false)
|
||||||
const nodeDefFilters = ref<NodeDefFilter[]>([])
|
const nodeDefFilters = ref<NodeDefFilter[]>([])
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: rawNodeDefs,
|
||||||
|
isReady,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
execute: fetchNodeDefs
|
||||||
|
} = useAsyncState<Record<string, ComfyNodeDefV1>>(
|
||||||
|
() =>
|
||||||
|
retry(() => api.getNodeDefs(), {
|
||||||
|
retries: 3,
|
||||||
|
delay: (attempt: number) => Math.min(1000 * Math.pow(2, attempt), 10000)
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (!isReady.value && !isLoading.value) {
|
||||||
|
return fetchNodeDefs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nodeDefs = computed(() => {
|
const nodeDefs = computed(() => {
|
||||||
const subgraphStore = useSubgraphStore()
|
const subgraphStore = useSubgraphStore()
|
||||||
// Blueprints first for discoverability in the node library sidebar
|
// Blueprints first for discoverability in the node library sidebar
|
||||||
@@ -335,18 +363,46 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
|||||||
)
|
)
|
||||||
const nodeTree = computed(() => buildNodeDefTree(visibleNodeDefs.value))
|
const nodeTree = computed(() => buildNodeDefTree(visibleNodeDefs.value))
|
||||||
|
|
||||||
function updateNodeDefs(nodeDefs: ComfyNodeDefV1[]) {
|
function translateNodeDef(def: ComfyNodeDefV1): ComfyNodeDefV1 {
|
||||||
|
return {
|
||||||
|
...def,
|
||||||
|
display_name: st(
|
||||||
|
`nodeDefs.${def.name}.display_name`,
|
||||||
|
def.display_name ?? def.name
|
||||||
|
),
|
||||||
|
description: def.description
|
||||||
|
? st(`nodeDefs.${def.name}.description`, def.description)
|
||||||
|
: '',
|
||||||
|
category: def.category
|
||||||
|
.split('/')
|
||||||
|
.map((category: string) => st(`nodeCategories.${category}`, category))
|
||||||
|
.join('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNodeDefs(defs: ComfyNodeDefV1[], app?: ComfyApp) {
|
||||||
|
const allDefs = [...Object.values(SYSTEM_NODE_DEFS), ...defs]
|
||||||
|
|
||||||
|
if (app) {
|
||||||
|
useExtensionService().invokeExtensions(
|
||||||
|
'beforeRegisterVueAppNodeDefs',
|
||||||
|
allDefs,
|
||||||
|
app
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const newNodeDefsByName: Record<string, ComfyNodeDefImpl> = {}
|
const newNodeDefsByName: Record<string, ComfyNodeDefImpl> = {}
|
||||||
const newNodeDefsByDisplayName: Record<string, ComfyNodeDefImpl> = {}
|
const newNodeDefsByDisplayName: Record<string, ComfyNodeDefImpl> = {}
|
||||||
|
|
||||||
for (const nodeDef of nodeDefs) {
|
for (const def of allDefs) {
|
||||||
|
const translatedDef = translateNodeDef(def)
|
||||||
const nodeDefImpl =
|
const nodeDefImpl =
|
||||||
nodeDef instanceof ComfyNodeDefImpl
|
translatedDef instanceof ComfyNodeDefImpl
|
||||||
? nodeDef
|
? translatedDef
|
||||||
: new ComfyNodeDefImpl(nodeDef)
|
: new ComfyNodeDefImpl(translatedDef)
|
||||||
|
|
||||||
newNodeDefsByName[nodeDef.name] = nodeDefImpl
|
newNodeDefsByName[translatedDef.name] = nodeDefImpl
|
||||||
newNodeDefsByDisplayName[nodeDef.display_name] = nodeDefImpl
|
newNodeDefsByDisplayName[translatedDef.display_name] = nodeDefImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeDefsByName.value = newNodeDefsByName
|
nodeDefsByName.value = newNodeDefsByName
|
||||||
@@ -448,6 +504,12 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
|||||||
showExperimental,
|
showExperimental,
|
||||||
nodeDefFilters,
|
nodeDefFilters,
|
||||||
|
|
||||||
|
rawNodeDefs,
|
||||||
|
isReady,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
load,
|
||||||
|
|
||||||
nodeDefs,
|
nodeDefs,
|
||||||
nodeDataTypes,
|
nodeDataTypes,
|
||||||
visibleNodeDefs,
|
visibleNodeDefs,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMagicKeys } from '@vueuse/core'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ import { useSidebarTabStore } from './workspace/sidebarTabStore'
|
|||||||
|
|
||||||
export const useWorkspaceStore = defineStore('workspace', () => {
|
export const useWorkspaceStore = defineStore('workspace', () => {
|
||||||
const spinner = ref(false)
|
const spinner = ref(false)
|
||||||
const shiftDown = ref(false)
|
const { shift: shiftDown } = useMagicKeys()
|
||||||
/**
|
/**
|
||||||
* Whether the workspace is in focus mode.
|
* Whether the workspace is in focus mode.
|
||||||
* When in focus mode, only the graph editor is visible.
|
* When in focus mode, only the graph editor is visible.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventListener } from '@vueuse/core'
|
import { useEventListener, useIntervalFn } from '@vueuse/core'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import type { ToastMessageOptions } from 'primevue/toast'
|
import type { ToastMessageOptions } from 'primevue/toast'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
@@ -79,7 +79,6 @@ import { useServerConfigStore } from '@/stores/serverConfigStore'
|
|||||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
|
||||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||||
import LinearView from '@/views/LinearView.vue'
|
import LinearView from '@/views/LinearView.vue'
|
||||||
import ManagerProgressToast from '@/workbench/extensions/manager/components/ManagerProgressToast.vue'
|
import ManagerProgressToast from '@/workbench/extensions/manager/components/ManagerProgressToast.vue'
|
||||||
@@ -102,9 +101,6 @@ const { linearMode } = storeToRefs(useCanvasStore())
|
|||||||
const telemetry = useTelemetry()
|
const telemetry = useTelemetry()
|
||||||
const firebaseAuthStore = useFirebaseAuthStore()
|
const firebaseAuthStore = useFirebaseAuthStore()
|
||||||
let hasTrackedLogin = false
|
let hasTrackedLogin = false
|
||||||
let visibilityListener: (() => void) | null = null
|
|
||||||
let tabCountInterval: number | null = null
|
|
||||||
let tabCountChannel: BroadcastChannel | null = null
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => colorPaletteStore.completedActivePalette,
|
() => colorPaletteStore.completedActivePalette,
|
||||||
@@ -198,15 +194,12 @@ watchEffect(() => {
|
|||||||
queueStore.maxHistoryItems = settingStore.get('Comfy.Queue.MaxHistoryItems')
|
queueStore.maxHistoryItems = settingStore.get('Comfy.Queue.MaxHistoryItems')
|
||||||
})
|
})
|
||||||
|
|
||||||
const init = () => {
|
const coreCommands = useCoreCommands()
|
||||||
const coreCommands = useCoreCommands()
|
useCommandStore().registerCommands(coreCommands)
|
||||||
useCommandStore().registerCommands(coreCommands)
|
useMenuItemStore().registerCoreMenuCommands()
|
||||||
useMenuItemStore().registerCoreMenuCommands()
|
useKeybindingService().registerCoreKeybindings()
|
||||||
useKeybindingService().registerCoreKeybindings()
|
useSidebarTabStore().registerCoreSidebarTabs()
|
||||||
useSidebarTabStore().registerCoreSidebarTabs()
|
useBottomPanelStore().registerCoreBottomPanelTabs()
|
||||||
useBottomPanelStore().registerCoreBottomPanelTabs()
|
|
||||||
app.extensionManager = useWorkspaceStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
|
||||||
const sidebarTabStore = useSidebarTabStore()
|
const sidebarTabStore = useSidebarTabStore()
|
||||||
@@ -274,15 +267,15 @@ if (isCloud) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEventListener(api, 'status', onStatus)
|
||||||
|
useEventListener(api, 'execution_success', onExecutionSuccess)
|
||||||
|
useEventListener(api, 'reconnecting', onReconnecting)
|
||||||
|
useEventListener(api, 'reconnected', onReconnected)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
api.addEventListener('status', onStatus)
|
|
||||||
api.addEventListener('execution_success', onExecutionSuccess)
|
|
||||||
api.addEventListener('reconnecting', onReconnecting)
|
|
||||||
api.addEventListener('reconnected', onReconnected)
|
|
||||||
executionStore.bindExecutionEvents()
|
executionStore.bindExecutionEvents()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
init()
|
|
||||||
// Relocate the legacy menu container to the graph canvas container so it is below other elements
|
// Relocate the legacy menu container to the graph canvas container so it is below other elements
|
||||||
graphCanvasContainerRef.value?.prepend(app.ui.menuContainer)
|
graphCanvasContainerRef.value?.prepend(app.ui.menuContainer)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -291,27 +284,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
api.removeEventListener('status', onStatus)
|
|
||||||
api.removeEventListener('execution_success', onExecutionSuccess)
|
|
||||||
api.removeEventListener('reconnecting', onReconnecting)
|
|
||||||
api.removeEventListener('reconnected', onReconnected)
|
|
||||||
executionStore.unbindExecutionEvents()
|
executionStore.unbindExecutionEvents()
|
||||||
|
|
||||||
// Clean up page visibility listener
|
|
||||||
if (visibilityListener) {
|
|
||||||
document.removeEventListener('visibilitychange', visibilityListener)
|
|
||||||
visibilityListener = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up tab count tracking
|
|
||||||
if (tabCountInterval) {
|
|
||||||
window.clearInterval(tabCountInterval)
|
|
||||||
tabCountInterval = null
|
|
||||||
}
|
|
||||||
if (tabCountChannel) {
|
|
||||||
tabCountChannel.close()
|
|
||||||
tabCountChannel = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useEventListener(window, 'keydown', useKeybindingService().keybindHandler)
|
useEventListener(window, 'keydown', useKeybindingService().keybindHandler)
|
||||||
@@ -337,18 +310,17 @@ const onGraphReady = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up page visibility tracking (cloud only)
|
// Set up page visibility tracking (cloud only)
|
||||||
if (isCloud && telemetry && !visibilityListener) {
|
if (isCloud && telemetry) {
|
||||||
visibilityListener = () => {
|
useEventListener(document, 'visibilitychange', () => {
|
||||||
telemetry.trackPageVisibilityChanged({
|
telemetry.trackPageVisibilityChanged({
|
||||||
visibility_state: document.visibilityState as 'visible' | 'hidden'
|
visibility_state: document.visibilityState as 'visible' | 'hidden'
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
document.addEventListener('visibilitychange', visibilityListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up tab count tracking (cloud only)
|
// Set up tab count tracking (cloud only)
|
||||||
if (isCloud && telemetry && !tabCountInterval) {
|
if (isCloud && telemetry) {
|
||||||
tabCountChannel = new BroadcastChannel('comfyui-tab-count')
|
const tabCountChannel = new BroadcastChannel('comfyui-tab-count')
|
||||||
const activeTabs = new Map<string, number>()
|
const activeTabs = new Map<string, number>()
|
||||||
const currentTabId = crypto.randomUUID()
|
const currentTabId = crypto.randomUUID()
|
||||||
|
|
||||||
@@ -363,7 +335,7 @@ const onGraphReady = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5-minute heartbeat interval
|
// 5-minute heartbeat interval
|
||||||
tabCountInterval = window.setInterval(() => {
|
useIntervalFn(() => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
// Clean up stale tabs (no heartbeat for 45 seconds)
|
// Clean up stale tabs (no heartbeat for 45 seconds)
|
||||||
@@ -374,7 +346,7 @@ const onGraphReady = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Broadcast our heartbeat
|
// Broadcast our heartbeat
|
||||||
tabCountChannel?.postMessage({
|
tabCountChannel.postMessage({
|
||||||
type: 'heartbeat',
|
type: 'heartbeat',
|
||||||
tabId: currentTabId
|
tabId: currentTabId
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user