mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Merge main into austin/widgets-v2
This commit is contained in:
@@ -100,57 +100,64 @@ The following diagram illustrates the store architecture and data flow:
|
||||
|
||||
## Core Stores
|
||||
|
||||
The following table lists ALL stores in the system as of 2025-01-30:
|
||||
The following table lists ALL 46 store instances in the system as of 2025-09-01:
|
||||
|
||||
### Main Stores
|
||||
|
||||
| Store | Description | Category |
|
||||
|-------|-------------|----------|
|
||||
| aboutPanelStore.ts | Manages the About panel state and badges | UI |
|
||||
| apiKeyAuthStore.ts | Handles API key authentication | Auth |
|
||||
| comfyManagerStore.ts | Manages ComfyUI application state | Core |
|
||||
| comfyRegistryStore.ts | Handles extensions registry | Registry |
|
||||
| commandStore.ts | Manages commands and command execution | Core |
|
||||
| dialogStore.ts | Controls dialog/modal display and state | UI |
|
||||
| domWidgetStore.ts | Manages DOM widget state | Widgets |
|
||||
| electronDownloadStore.ts | Handles Electron-specific download operations | Platform |
|
||||
| executionStore.ts | Tracks workflow execution state | Execution |
|
||||
| extensionStore.ts | Manages extension registration and state | Extensions |
|
||||
| firebaseAuthStore.ts | Handles Firebase authentication | Auth |
|
||||
| graphStore.ts | Manages the graph canvas state | Core |
|
||||
| imagePreviewStore.ts | Controls image preview functionality | Media |
|
||||
| keybindingStore.ts | Manages keyboard shortcuts | Input |
|
||||
| maintenanceTaskStore.ts | Handles system maintenance tasks | System |
|
||||
| menuItemStore.ts | Handles menu items and their state | UI |
|
||||
| modelStore.ts | Manages AI models information | Models |
|
||||
| modelToNodeStore.ts | Maps models to compatible nodes | Models |
|
||||
| nodeBookmarkStore.ts | Manages node bookmarks and favorites | Nodes |
|
||||
| nodeDefStore.ts | Manages node definitions | Nodes |
|
||||
| queueStore.ts | Handles the execution queue | Execution |
|
||||
| releaseStore.ts | Manages application release information | System |
|
||||
| serverConfigStore.ts | Handles server configuration | Config |
|
||||
| settingStore.ts | Manages application settings | Config |
|
||||
| subgraphNavigationStore.ts | Handles subgraph navigation state | Navigation |
|
||||
| systemStatsStore.ts | Tracks system performance statistics | System |
|
||||
| toastStore.ts | Manages toast notifications | UI |
|
||||
| userFileStore.ts | Manages user file operations | Files |
|
||||
| userStore.ts | Manages user data and preferences | User |
|
||||
| versionCompatibilityStore.ts | Manages frontend/backend version compatibility warnings | Core |
|
||||
| widgetStore.ts | Manages widget configurations | Widgets |
|
||||
| workflowStore.ts | Handles workflow data and operations | Workflows |
|
||||
| workflowTemplatesStore.ts | Manages workflow templates | Workflows |
|
||||
| workspaceStore.ts | Manages overall workspace state | Workspace |
|
||||
| File | Store | Description | Category |
|
||||
|------|-------|-------------|----------|
|
||||
| aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI |
|
||||
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
|
||||
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
|
||||
| comfyManagerStore.ts | useManagerProgressDialogStore | Manages manager progress dialog state | UI |
|
||||
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
|
||||
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
|
||||
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
|
||||
| domWidgetStore.ts | useDomWidgetStore | Manages DOM widget state | Widgets |
|
||||
| electronDownloadStore.ts | useElectronDownloadStore | Handles Electron-specific download operations | Platform |
|
||||
| executionStore.ts | useExecutionStore | Tracks workflow execution state | Execution |
|
||||
| extensionStore.ts | useExtensionStore | Manages extension registration and state | Extensions |
|
||||
| firebaseAuthStore.ts | useFirebaseAuthStore | Handles Firebase authentication | Auth |
|
||||
| graphStore.ts | useTitleEditorStore | Manages title editing for nodes and groups | UI |
|
||||
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
|
||||
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
|
||||
| imagePreviewStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
|
||||
| keybindingStore.ts | useKeybindingStore | Manages keyboard shortcuts | Input |
|
||||
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
|
||||
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
|
||||
| modelStore.ts | useModelStore | Manages AI models information | Models |
|
||||
| modelToNodeStore.ts | useModelToNodeStore | Maps models to compatible nodes | Models |
|
||||
| nodeBookmarkStore.ts | useNodeBookmarkStore | Manages node bookmarks and favorites | Nodes |
|
||||
| nodeDefStore.ts | useNodeDefStore | Manages node definitions and schemas | Nodes |
|
||||
| nodeDefStore.ts | useNodeFrequencyStore | Tracks node usage frequency | Nodes |
|
||||
| queueStore.ts | useQueueStore | Manages execution queue and task history | Execution |
|
||||
| queueStore.ts | useQueuePendingTaskCountStore | Tracks pending task counts | Execution |
|
||||
| queueStore.ts | useQueueSettingsStore | Manages queue execution settings | Execution |
|
||||
| releaseStore.ts | useReleaseStore | Manages application release information | System |
|
||||
| serverConfigStore.ts | useServerConfigStore | Handles server configuration | Config |
|
||||
| settingStore.ts | useSettingStore | Manages application settings | Config |
|
||||
| subgraphNavigationStore.ts | useSubgraphNavigationStore | Handles subgraph navigation state | Navigation |
|
||||
| systemStatsStore.ts | useSystemStatsStore | Tracks system performance statistics | System |
|
||||
| toastStore.ts | useToastStore | Manages toast notifications | UI |
|
||||
| userFileStore.ts | useUserFileStore | Manages user file operations | Files |
|
||||
| userStore.ts | useUserStore | Manages user data and preferences | User |
|
||||
| versionCompatibilityStore.ts | useVersionCompatibilityStore | Manages frontend/backend version compatibility warnings | Core |
|
||||
| widgetStore.ts | useWidgetStore | Manages widget configurations | Widgets |
|
||||
| workflowStore.ts | useWorkflowStore | Handles workflow data and operations | Workflows |
|
||||
| workflowStore.ts | useWorkflowBookmarkStore | Manages workflow bookmarks and favorites | Workflows |
|
||||
| workflowTemplatesStore.ts | useWorkflowTemplatesStore | Manages workflow templates | Workflows |
|
||||
| workspaceStore.ts | useWorkspaceStore | Manages overall workspace state | Workspace |
|
||||
|
||||
### Workspace Stores
|
||||
Located in `stores/workspace/`:
|
||||
|
||||
| Store | Description |
|
||||
|-------|-------------|
|
||||
| bottomPanelStore.ts | Controls bottom panel visibility and state |
|
||||
| colorPaletteStore.ts | Manages color palette configurations |
|
||||
| nodeHelpStore.ts | Handles node help and documentation display |
|
||||
| searchBoxStore.ts | Manages search box functionality |
|
||||
| sidebarTabStore.ts | Controls sidebar tab states and navigation |
|
||||
| File | Store | Description | Category |
|
||||
|------|-------|-------------|----------|
|
||||
| bottomPanelStore.ts | useBottomPanelStore | Controls bottom panel visibility and state | UI |
|
||||
| colorPaletteStore.ts | useColorPaletteStore | Manages color palette configurations | UI |
|
||||
| nodeHelpStore.ts | useNodeHelpStore | Handles node help and documentation display | UI |
|
||||
| searchBoxStore.ts | useSearchBoxStore | Manages search box functionality | UI |
|
||||
| sidebarTabStore.ts | useSidebarTabStore | Controls sidebar tab states and navigation | UI |
|
||||
|
||||
## Store Development Guidelines
|
||||
|
||||
@@ -189,7 +196,7 @@ export const useExampleStore = defineStore('example', () => {
|
||||
async function fetchItems() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/items')
|
||||
const data = await response.json()
|
||||
@@ -207,11 +214,11 @@ export const useExampleStore = defineStore('example', () => {
|
||||
items,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
|
||||
// Getters
|
||||
itemCount,
|
||||
hasError,
|
||||
|
||||
|
||||
// Actions
|
||||
addItem,
|
||||
fetchItems
|
||||
@@ -238,7 +245,7 @@ export const useDataStore = defineStore('data', () => {
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await api.getData()
|
||||
const result = await api.getExtensions()
|
||||
data.value = result
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
@@ -266,21 +273,21 @@ import { useOtherStore } from './otherStore'
|
||||
export const useComposedStore = defineStore('composed', () => {
|
||||
const otherStore = useOtherStore()
|
||||
const { someData } = storeToRefs(otherStore)
|
||||
|
||||
|
||||
// Local state
|
||||
const localState = ref(0)
|
||||
|
||||
|
||||
// Computed value based on other store
|
||||
const derivedValue = computed(() => {
|
||||
return computeFromOtherData(someData.value, localState.value)
|
||||
})
|
||||
|
||||
|
||||
// Action that uses another store
|
||||
async function complexAction() {
|
||||
await otherStore.someAction()
|
||||
localState.value += 1
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
localState,
|
||||
derivedValue,
|
||||
@@ -299,20 +306,20 @@ export const usePreferencesStore = defineStore('preferences', () => {
|
||||
// Load from localStorage if available
|
||||
const theme = ref(localStorage.getItem('theme') || 'light')
|
||||
const fontSize = ref(parseInt(localStorage.getItem('fontSize') || '14'))
|
||||
|
||||
|
||||
// Save to localStorage when changed
|
||||
watch(theme, (newTheme) => {
|
||||
localStorage.setItem('theme', newTheme)
|
||||
})
|
||||
|
||||
|
||||
watch(fontSize, (newSize) => {
|
||||
localStorage.setItem('fontSize', newSize.toString())
|
||||
})
|
||||
|
||||
|
||||
function setTheme(newTheme) {
|
||||
theme.value = newTheme
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
theme,
|
||||
fontSize,
|
||||
@@ -347,7 +354,7 @@ describe('useExampleStore', () => {
|
||||
// Create a fresh pinia instance and make it active
|
||||
setActivePinia(createPinia())
|
||||
store = useExampleStore()
|
||||
|
||||
|
||||
// Clear all mocks
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
@@ -363,14 +370,14 @@ describe('useExampleStore', () => {
|
||||
expect(store.items).toEqual(['test'])
|
||||
expect(store.itemCount).toBe(1)
|
||||
})
|
||||
|
||||
|
||||
it('should fetch items', async () => {
|
||||
// Setup mock response
|
||||
vi.mocked(api.getData).mockResolvedValue(['item1', 'item2'])
|
||||
|
||||
|
||||
// Call the action
|
||||
await store.fetchItems()
|
||||
|
||||
|
||||
// Verify state changes
|
||||
expect(store.isLoading).toBe(false)
|
||||
expect(store.items).toEqual(['item1', 'item2'])
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import { useServerLogs } from '@/composables/useServerLogs'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import {
|
||||
InstallPackParams,
|
||||
InstalledPacksResponse,
|
||||
ManagerPackInfo,
|
||||
ManagerPackInstalled,
|
||||
TaskLog,
|
||||
UpdateAllPacksParams
|
||||
} from '@/types/comfyManagerTypes'
|
||||
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']
|
||||
type ManagerPackInfo = components['schemas']['ManagerPackInfo']
|
||||
type ManagerPackInstalled = components['schemas']['ManagerPackInstalled']
|
||||
type ManagerTaskHistory = Record<
|
||||
string,
|
||||
components['schemas']['TaskHistoryItem']
|
||||
>
|
||||
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
|
||||
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
|
||||
|
||||
/**
|
||||
* Store for state of installed node packs
|
||||
@@ -29,16 +38,78 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const enabledPacksIds = ref<Set<string>>(new Set())
|
||||
const disabledPacksIds = ref<Set<string>>(new Set())
|
||||
const installedPacksIds = ref<Set<string>>(new Set())
|
||||
const installingPacksIds = ref<Set<string>>(new Set())
|
||||
const isStale = ref(true)
|
||||
const taskLogs = ref<TaskLog[]>([])
|
||||
const succeededTasksLogs = ref<TaskLog[]>([])
|
||||
const failedTasksLogs = ref<TaskLog[]>([])
|
||||
|
||||
const { statusMessage, allTasksDone, enqueueTask, uncompletedCount } =
|
||||
useManagerQueue()
|
||||
const taskHistory = ref<ManagerTaskHistory>({})
|
||||
const succeededTasksIds = ref<string[]>([])
|
||||
const failedTasksIds = ref<string[]>([])
|
||||
const taskQueue = ref<ManagerTaskQueue>({
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
})
|
||||
|
||||
// Track task ID to pack ID mapping for proper state cleanup
|
||||
const taskIdToPackId = ref(new Map<string, string>())
|
||||
|
||||
const managerQueue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Listen for task completion events to clean up installing state
|
||||
useEventListener(app.api, 'cm-task-completed', (event: any) => {
|
||||
const taskId = event.detail?.ui_id
|
||||
if (taskId && taskIdToPackId.value.has(taskId)) {
|
||||
const packId = taskIdToPackId.value.get(taskId)!
|
||||
installingPacksIds.value.delete(packId)
|
||||
taskIdToPackId.value.delete(taskId)
|
||||
}
|
||||
})
|
||||
|
||||
const setStale = () => {
|
||||
isStale.value = true
|
||||
}
|
||||
|
||||
const partitionTaskLogs = () => {
|
||||
const successTaskLogs: TaskLog[] = []
|
||||
const failTaskLogs: TaskLog[] = []
|
||||
for (const log of taskLogs.value) {
|
||||
if (failedTasksIds.value.includes(log.taskId)) {
|
||||
failTaskLogs.push(log)
|
||||
} else {
|
||||
successTaskLogs.push(log)
|
||||
}
|
||||
}
|
||||
succeededTasksLogs.value = successTaskLogs
|
||||
failedTasksLogs.value = failTaskLogs
|
||||
}
|
||||
|
||||
const partitionTasks = () => {
|
||||
const successTasksIds = []
|
||||
const failTasksIds = []
|
||||
for (const task of Object.values(taskHistory.value)) {
|
||||
if (task.status?.status_str === 'success') {
|
||||
successTasksIds.push(task.ui_id)
|
||||
} else {
|
||||
failTasksIds.push(task.ui_id)
|
||||
}
|
||||
}
|
||||
succeededTasksIds.value = successTasksIds
|
||||
failedTasksIds.value = failTasksIds
|
||||
}
|
||||
|
||||
whenever(
|
||||
taskHistory,
|
||||
() => {
|
||||
partitionTasks()
|
||||
partitionTaskLogs()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const getPackId = (pack: ManagerPackInstalled) => pack.cnr_id || pack.aux_id
|
||||
|
||||
const isInstalledPackId = (packName: string | undefined): boolean =>
|
||||
@@ -49,6 +120,9 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
isInstalledPackId(packName) &&
|
||||
enabledPacksIds.value.has(packName)
|
||||
|
||||
const isInstallingPackId = (packName: string | undefined): boolean =>
|
||||
!!packName && installingPacksIds.value.has(packName)
|
||||
|
||||
const packsToIdSet = (packs: ManagerPackInstalled[]) =>
|
||||
packs.reduce((acc, pack) => {
|
||||
const id = pack.cnr_id || pack.aux_id
|
||||
@@ -110,28 +184,54 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
|
||||
const refreshInstalledList = async () => {
|
||||
const packs = await managerService.listInstalledPacks()
|
||||
if (packs) installedPacks.value = packs
|
||||
if (packs) {
|
||||
// Normalize pack keys to ensure consistent access
|
||||
installedPacks.value = normalizePackKeys(packs)
|
||||
}
|
||||
isStale.value = false
|
||||
}
|
||||
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
whenever(uncompletedCount, () => showManagerProgressDialog())
|
||||
|
||||
const withLogs = (task: () => Promise<null>, taskName: string) => {
|
||||
const { startListening, stopListening, logs } = useServerLogs()
|
||||
const enqueueTaskWithLogs = async (
|
||||
task: (taskId: string) => Promise<null>,
|
||||
taskName: string
|
||||
) => {
|
||||
const taskId = uuidv4()
|
||||
const { logs } = useServerLogs({
|
||||
ui_id: taskId,
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const loggedTask = async () => {
|
||||
taskLogs.value.push({ taskName, logs: logs.value })
|
||||
await startListening()
|
||||
return task()
|
||||
try {
|
||||
// Show progress dialog immediately when task is queued
|
||||
showManagerProgressDialog()
|
||||
managerQueue.isProcessing.value = true
|
||||
|
||||
// Prepare logging hook
|
||||
taskLogs.value.push({ taskName, taskId, logs: logs.value })
|
||||
|
||||
// Queue the task to the server
|
||||
await task(taskId)
|
||||
} catch (error) {
|
||||
// Reset processing state on error
|
||||
managerQueue.isProcessing.value = false
|
||||
|
||||
// The server has authority over task history in general, but in rare
|
||||
// case of client-side error, we add that to failed tasks from the client side
|
||||
taskHistory.value[taskId] = {
|
||||
ui_id: taskId,
|
||||
client_id: api.clientId || 'unknown',
|
||||
kind: 'error',
|
||||
result: 'failed',
|
||||
status: {
|
||||
status_str: 'error',
|
||||
completed: false,
|
||||
messages: [error instanceof Error ? error.message : String(error)]
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const onComplete = async () => {
|
||||
await stopListening()
|
||||
setStale()
|
||||
}
|
||||
|
||||
return { task: loggedTask, onComplete }
|
||||
}
|
||||
|
||||
const installPack = useCachedRequest<InstallPackParams, void>(
|
||||
@@ -152,39 +252,69 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const task = () => managerService.installPack(params, signal)
|
||||
enqueueTask(withLogs(task, `${actionDescription} ${params.id}`))
|
||||
installingPacksIds.value.add(params.id)
|
||||
const task = (taskId: string) => {
|
||||
taskIdToPackId.value.set(taskId, params.id)
|
||||
return managerService.installPack(params, taskId, signal)
|
||||
}
|
||||
await enqueueTaskWithLogs(task, `${actionDescription} ${params.id}`)
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const uninstallPack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const uninstallPack = async (
|
||||
params: ManagerPackInfo,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
installPack.clear()
|
||||
installPack.cancel()
|
||||
const task = () => managerService.uninstallPack(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id })))
|
||||
|
||||
installingPacksIds.value.add(params.id)
|
||||
const uninstallParams: components['schemas']['UninstallPackParams'] = {
|
||||
node_name: params.id,
|
||||
is_unknown: false
|
||||
}
|
||||
const task = (taskId: string) => {
|
||||
taskIdToPackId.value.set(taskId, params.id)
|
||||
return managerService.uninstallPack(uninstallParams, taskId, signal)
|
||||
}
|
||||
await enqueueTaskWithLogs(
|
||||
task,
|
||||
t('manager.uninstalling', { id: params.id })
|
||||
)
|
||||
}
|
||||
|
||||
const updatePack = useCachedRequest<ManagerPackInfo, void>(
|
||||
async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
updateAllPacks.cancel()
|
||||
const task = () => managerService.updatePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id })))
|
||||
const updateParams: components['schemas']['UpdatePackParams'] = {
|
||||
node_name: params.id,
|
||||
node_ver: params.version
|
||||
}
|
||||
const task = (taskId: string) =>
|
||||
managerService.updatePack(updateParams, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('g.updating', { id: params.id }))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const updateAllPacks = useCachedRequest<UpdateAllPacksParams, void>(
|
||||
async (params: UpdateAllPacksParams, signal?: AbortSignal) => {
|
||||
const task = () => managerService.updateAllPacks(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.updatingAllPacks')))
|
||||
const task = (taskId: string) =>
|
||||
managerService.updateAllPacks(params, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('manager.updatingAllPacks'))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const task = () => managerService.disablePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id })))
|
||||
const disablePack = async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const disableParams: components['schemas']['DisablePackParams'] = {
|
||||
node_name: params.id,
|
||||
is_unknown: false
|
||||
}
|
||||
const task = (taskId: string) =>
|
||||
managerService.disablePack(disableParams, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('g.disabling', { id: params.id }))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
@@ -196,15 +326,33 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
taskLogs.value = []
|
||||
}
|
||||
|
||||
const resetTaskState = () => {
|
||||
// Clear all task-related reactive state for fresh start after restart
|
||||
taskLogs.value = []
|
||||
taskHistory.value = {}
|
||||
succeededTasksIds.value = []
|
||||
failedTasksIds.value = []
|
||||
succeededTasksLogs.value = []
|
||||
failedTasksLogs.value = []
|
||||
installingPacksIds.value.clear()
|
||||
taskIdToPackId.value.clear()
|
||||
|
||||
// Reset task queue to initial state
|
||||
taskQueue.value = {
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Manager state
|
||||
isLoading: managerService.isLoading,
|
||||
error: managerService.error,
|
||||
statusMessage,
|
||||
allTasksDone,
|
||||
uncompletedCount,
|
||||
taskLogs,
|
||||
clearLogs,
|
||||
resetTaskState,
|
||||
setStale,
|
||||
|
||||
// Installed packs state
|
||||
@@ -212,9 +360,20 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
installedPacksIds,
|
||||
isPackInstalled: isInstalledPackId,
|
||||
isPackEnabled: isEnabledPackId,
|
||||
isPackInstalling: isInstallingPackId,
|
||||
getInstalledPackVersion,
|
||||
refreshInstalledList,
|
||||
|
||||
// Task queue state and actions
|
||||
taskHistory,
|
||||
taskQueue,
|
||||
isProcessingTasks: managerQueue.isProcessing,
|
||||
succeededTasksIds,
|
||||
failedTasksIds,
|
||||
succeededTasksLogs,
|
||||
failedTasksLogs,
|
||||
managerQueue,
|
||||
|
||||
// Pack actions
|
||||
installPack,
|
||||
uninstallPack,
|
||||
@@ -234,6 +393,15 @@ export const useManagerProgressDialogStore = defineStore(
|
||||
'managerProgressDialog',
|
||||
() => {
|
||||
const isExpanded = ref(false)
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
const setActiveTabIndex = (index: number) => {
|
||||
activeTabIndex.value = index
|
||||
}
|
||||
|
||||
const getActiveTabIndex = () => {
|
||||
return activeTabIndex.value
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
@@ -250,7 +418,9 @@ export const useManagerProgressDialogStore = defineStore(
|
||||
isExpanded,
|
||||
toggle,
|
||||
collapse,
|
||||
expand
|
||||
expand,
|
||||
setActiveTabIndex,
|
||||
getActiveTabIndex
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
70
src/stores/conflictDetectionStore.ts
Normal file
70
src/stores/conflictDetectionStore.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
|
||||
|
||||
export const useConflictDetectionStore = defineStore(
|
||||
'conflictDetection',
|
||||
() => {
|
||||
// State
|
||||
const conflictedPackages = ref<ConflictDetectionResult[]>([])
|
||||
const isDetecting = ref(false)
|
||||
const lastDetectionTime = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const hasConflicts = computed(() =>
|
||||
conflictedPackages.value.some((pkg) => pkg.has_conflict)
|
||||
)
|
||||
|
||||
const getConflictsForPackageByID = computed(
|
||||
() => (packageId: string) =>
|
||||
conflictedPackages.value.find((pkg) => pkg.package_id === packageId)
|
||||
)
|
||||
|
||||
const bannedPackages = computed(() =>
|
||||
conflictedPackages.value.filter((pkg) =>
|
||||
pkg.conflicts.some((conflict) => conflict.type === 'banned')
|
||||
)
|
||||
)
|
||||
|
||||
const securityPendingPackages = computed(() =>
|
||||
conflictedPackages.value.filter((pkg) =>
|
||||
pkg.conflicts.some((conflict) => conflict.type === 'pending')
|
||||
)
|
||||
)
|
||||
|
||||
// Actions
|
||||
function setConflictedPackages(packages: ConflictDetectionResult[]) {
|
||||
conflictedPackages.value = [...packages]
|
||||
}
|
||||
|
||||
function clearConflicts() {
|
||||
conflictedPackages.value = []
|
||||
}
|
||||
|
||||
function setDetecting(detecting: boolean) {
|
||||
isDetecting.value = detecting
|
||||
}
|
||||
|
||||
function setLastDetectionTime(time: string) {
|
||||
lastDetectionTime.value = time
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
conflictedPackages,
|
||||
isDetecting,
|
||||
lastDetectionTime,
|
||||
// Getters
|
||||
hasConflicts,
|
||||
getConflictsForPackageByID,
|
||||
bannedPackages,
|
||||
securityPendingPackages,
|
||||
// Actions
|
||||
setConflictedPackages,
|
||||
clearConflicts,
|
||||
setDetecting,
|
||||
setLastDetectionTime
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -43,6 +43,7 @@ interface DialogInstance {
|
||||
component: Component
|
||||
contentProps: Record<string, any>
|
||||
footerComponent?: Component
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps: DialogComponentProps
|
||||
priority: number
|
||||
}
|
||||
@@ -54,6 +55,7 @@ export interface ShowDialogOptions {
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
/**
|
||||
* Optional priority for dialog stacking.
|
||||
@@ -127,6 +129,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
footerProps?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
priority?: number
|
||||
}) {
|
||||
@@ -146,6 +149,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
: undefined,
|
||||
component: markRaw(options.component),
|
||||
contentProps: { ...options.props },
|
||||
footerProps: { ...options.footerProps },
|
||||
priority: options.priority ?? 1,
|
||||
dialogComponentProps: {
|
||||
maximizable: false,
|
||||
|
||||
@@ -31,7 +31,7 @@ import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { useCanvasStore } from './graphStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from './workflowStore'
|
||||
|
||||
export interface QueuedPrompt {
|
||||
interface QueuedPrompt {
|
||||
/**
|
||||
* The nodes that are queued to be executed. The key is the node id and the
|
||||
* value is a boolean indicating if the node has been executed.
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { ComfyExtension } from '@/types/comfy'
|
||||
/**
|
||||
* These extensions are always active, even if they are disabled in the setting.
|
||||
*/
|
||||
export const ALWAYS_ENABLED_EXTENSIONS: readonly string[] = []
|
||||
const ALWAYS_ENABLED_EXTENSIONS: readonly string[] = []
|
||||
|
||||
export const ALWAYS_DISABLED_EXTENSIONS: readonly string[] = [
|
||||
const ALWAYS_DISABLED_EXTENSIONS: readonly string[] = [
|
||||
// pysssss.Locking is replaced by pin/unpin in ComfyUI core.
|
||||
// https://github.com/Comfy-Org/litegraph.js/pull/117
|
||||
'pysssss.Locking',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type UserCredential,
|
||||
browserLocalPersistence,
|
||||
createUserWithEmailAndPassword,
|
||||
deleteUser,
|
||||
onAuthStateChanged,
|
||||
sendPasswordResetEmail,
|
||||
setPersistence,
|
||||
@@ -40,7 +41,7 @@ type AccessBillingPortalResponse =
|
||||
type AccessBillingPortalReqBody =
|
||||
operations['AccessBillingPortal']['requestBody']
|
||||
|
||||
export class FirebaseAuthStoreError extends Error {
|
||||
class FirebaseAuthStoreError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'FirebaseAuthStoreError'
|
||||
@@ -287,6 +288,14 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
await updatePassword(currentUser.value, newPassword)
|
||||
}
|
||||
|
||||
/** Delete the current user account */
|
||||
const _deleteAccount = async (): Promise<void> => {
|
||||
if (!currentUser.value) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
await deleteUser(currentUser.value)
|
||||
}
|
||||
|
||||
const addCredits = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse> => {
|
||||
@@ -385,6 +394,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
accessBillingPortal,
|
||||
sendPasswordReset,
|
||||
updatePassword: _updatePassword,
|
||||
deleteAccount: _deleteAccount,
|
||||
getAuthHeader
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphNode,
|
||||
Subgraph,
|
||||
SubgraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
ExecutedWsMessage,
|
||||
ResultItem,
|
||||
@@ -136,17 +140,22 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
) {
|
||||
if (!filenames || !node) return
|
||||
|
||||
const locatorId =
|
||||
node.graph instanceof Subgraph
|
||||
? nodeIdToNodeLocatorId(node.id, node.graph ?? undefined)
|
||||
: `${node.id}`
|
||||
if (!locatorId) return
|
||||
if (typeof filenames === 'string') {
|
||||
setNodeOutputsByNodeId(
|
||||
node.id,
|
||||
setOutputsByLocatorId(
|
||||
locatorId,
|
||||
createOutputs([filenames], folder, isAnimated)
|
||||
)
|
||||
} else if (!Array.isArray(filenames)) {
|
||||
setNodeOutputsByNodeId(node.id, filenames)
|
||||
setOutputsByLocatorId(locatorId, filenames)
|
||||
} else {
|
||||
const resultItems = createOutputs(filenames, folder, isAnimated)
|
||||
if (!resultItems?.images?.length) return
|
||||
setNodeOutputsByNodeId(node.id, resultItems)
|
||||
setOutputsByLocatorId(locatorId, resultItems)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,26 +179,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
setOutputsByLocatorId(nodeLocatorId, outputs, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node outputs by node ID.
|
||||
* Uses the current graph context to create the appropriate NodeLocatorId.
|
||||
*
|
||||
* @param nodeId - The node ID
|
||||
* @param outputs - The outputs to store
|
||||
* @param options - Options for setting outputs
|
||||
* @param options.merge - If true, merge with existing outputs (arrays are concatenated)
|
||||
*/
|
||||
function setNodeOutputsByNodeId(
|
||||
nodeId: string | number,
|
||||
outputs: ExecutedWsMessage['output'] | ResultItem,
|
||||
options: SetOutputOptions = {}
|
||||
) {
|
||||
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
|
||||
if (!nodeLocatorId) return
|
||||
|
||||
setOutputsByLocatorId(nodeLocatorId, outputs, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node preview images by execution ID (hierarchical ID from backend).
|
||||
* Converts the execution ID to a NodeLocatorId before storing.
|
||||
@@ -288,7 +277,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
getNodePreviews,
|
||||
setNodeOutputs,
|
||||
setNodeOutputsByExecutionId,
|
||||
setNodeOutputsByNodeId,
|
||||
setNodePreviewsByExecutionId,
|
||||
setNodePreviewsByNodeId,
|
||||
revokePreviewsByExecutionId,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { ModelFile } from '@/schemas/assetSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { assetService } from '@/services/assetService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
/** (Internal helper) finds a value in a metadata object from any of a list of keys. */
|
||||
function _findInMetadata(metadata: any, ...keys: string[]): string | null {
|
||||
@@ -153,7 +156,10 @@ export class ModelFolder {
|
||||
models: Record<string, ComfyModelDef> = {}
|
||||
state: ResourceState = ResourceState.Uninitialized
|
||||
|
||||
constructor(public directory: string) {}
|
||||
constructor(
|
||||
public directory: string,
|
||||
private getModelsFunc: (folder: string) => Promise<ModelFile[]>
|
||||
) {}
|
||||
|
||||
get key(): string {
|
||||
return this.directory + '/'
|
||||
@@ -167,7 +173,7 @@ export class ModelFolder {
|
||||
return this
|
||||
}
|
||||
this.state = ResourceState.Loading
|
||||
const models = await api.getModels(this.directory)
|
||||
const models = await this.getModelsFunc(this.directory)
|
||||
for (const model of models) {
|
||||
this.models[`${model.pathIndex}/${model.name}`] = new ComfyModelDef(
|
||||
model.name,
|
||||
@@ -182,6 +188,7 @@ export class ModelFolder {
|
||||
|
||||
/** Model store handler, wraps individual per-folder model stores */
|
||||
export const useModelStore = defineStore('models', () => {
|
||||
const settingStore = useSettingStore()
|
||||
const modelFolderNames = ref<string[]>([])
|
||||
const modelFolderByName = ref<Record<string, ModelFolder>>({})
|
||||
const modelFolders = computed<ModelFolder[]>(() =>
|
||||
@@ -197,11 +204,22 @@ export const useModelStore = defineStore('models', () => {
|
||||
* Loads the model folders from the server
|
||||
*/
|
||||
async function loadModelFolders() {
|
||||
const resData = await api.getModelFolders()
|
||||
const useAssetAPI: boolean = settingStore.get('Comfy.Assets.UseAssetAPI')
|
||||
|
||||
const resData = useAssetAPI
|
||||
? await assetService.getAssetModelFolders()
|
||||
: await api.getModelFolders()
|
||||
modelFolderNames.value = resData.map((folder) => folder.name)
|
||||
modelFolderByName.value = {}
|
||||
for (const folderName of modelFolderNames.value) {
|
||||
modelFolderByName.value[folderName] = new ModelFolder(folderName)
|
||||
const getModelsFunc = useAssetAPI
|
||||
? (folder: string) => assetService.getAssetModels(folder)
|
||||
: (folder: string) => api.getModels(folder)
|
||||
|
||||
modelFolderByName.value[folderName] = new ModelFolder(
|
||||
folderName,
|
||||
getModelsFunc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -22,6 +22,22 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
const modelToNodeMap = ref<Record<string, ModelNodeProvider[]>>({})
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const haveDefaultsLoaded = ref(false)
|
||||
|
||||
/** Internal computed for reactive caching of registered node types */
|
||||
const registeredNodeTypes = computed(() => {
|
||||
return new Set(
|
||||
Object.values(modelToNodeMap.value)
|
||||
.flat()
|
||||
.map((provider) => provider.nodeDef.name)
|
||||
)
|
||||
})
|
||||
|
||||
/** Get set of all registered node types for efficient lookup */
|
||||
function getRegisteredNodeTypes(): Set<string> {
|
||||
registerDefaults()
|
||||
return registeredNodeTypes.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node provider for the given model type name.
|
||||
* @param modelType The name of the model type to get the node provider for.
|
||||
@@ -83,10 +99,15 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
|
||||
quickRegister('loras', 'LoraLoaderModelOnly', 'lora_name')
|
||||
quickRegister('vae', 'VAELoader', 'vae_name')
|
||||
quickRegister('controlnet', 'ControlNetLoader', 'control_net_name')
|
||||
quickRegister('unet', 'UNETLoader', 'unet_name')
|
||||
quickRegister('upscale_models', 'UpscaleModelLoader', 'model_name')
|
||||
quickRegister('style_models', 'StyleModelLoader', 'style_model')
|
||||
quickRegister('gligen', 'GLIGENLoader', 'gligen_name')
|
||||
}
|
||||
|
||||
return {
|
||||
modelToNodeMap,
|
||||
getRegisteredNodeTypes,
|
||||
getNodeProvider,
|
||||
getAllNodeProviders,
|
||||
registerNodeProvider,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ComfyNodeDefImpl, createDummyFolderNodeDef } from './nodeDefStore'
|
||||
import { buildNodeDefTree } from './nodeDefStore'
|
||||
import { useSettingStore } from './settingStore'
|
||||
|
||||
export const BOOKMARK_SETTING_ID = 'Comfy.NodeLibrary.Bookmarks.V2'
|
||||
const BOOKMARK_SETTING_ID = 'Comfy.NodeLibrary.Bookmarks.V2'
|
||||
|
||||
export const useNodeBookmarkStore = defineStore('nodeBookmark', () => {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ComfyOutputTypesSpec as ComfyOutputSpecV1
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { NodeSearchService } from '@/services/nodeSearchService'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import {
|
||||
type NodeSource,
|
||||
NodeSourceType,
|
||||
@@ -223,7 +224,7 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDefV1> = {
|
||||
}
|
||||
}
|
||||
|
||||
export interface BuildNodeDefTreeOptions {
|
||||
interface BuildNodeDefTreeOptions {
|
||||
/**
|
||||
* Custom function to extract the tree path from a node definition.
|
||||
* If not provided, uses the default path based on nodeDef.nodePath.
|
||||
@@ -291,8 +292,14 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const showDeprecated = ref(false)
|
||||
const showExperimental = ref(false)
|
||||
const nodeDefFilters = ref<NodeDefFilter[]>([])
|
||||
const subgraphStore = useSubgraphStore()
|
||||
|
||||
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
|
||||
const nodeDefs = computed(() => {
|
||||
return [
|
||||
...Object.values(nodeDefsByName.value),
|
||||
...subgraphStore.subgraphBlueprints
|
||||
]
|
||||
})
|
||||
const nodeDataTypes = computed(() => {
|
||||
const types = new Set<string>()
|
||||
for (const nodeDef of nodeDefs.value) {
|
||||
@@ -383,7 +390,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
})
|
||||
|
||||
// Subgraph nodes filter
|
||||
// @todo Remove this filter when subgraph v2 is released
|
||||
// Filter out litegraph typed subgraphs, saved blueprints are added in separately
|
||||
registerNodeDefFilter({
|
||||
id: 'core.subgraph',
|
||||
name: 'Hide Subgraph Nodes',
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useExtensionService } from '@/services/extensionService'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
|
||||
// Task type used in the API.
|
||||
export type APITaskType = 'queue' | 'history'
|
||||
type APITaskType = 'queue' | 'history'
|
||||
|
||||
export enum TaskItemDisplayStatus {
|
||||
Running = 'Running',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
|
||||
|
||||
export type ServerConfigWithValue<T> = ServerConfig<T> & {
|
||||
type ServerConfigWithValue<T> = ServerConfig<T> & {
|
||||
/**
|
||||
* Current value.
|
||||
*/
|
||||
|
||||
@@ -188,6 +188,48 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
)
|
||||
}
|
||||
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.
|
||||
* Preserves the exact zoom threshold behavior by converting it to equivalent font size.
|
||||
*/
|
||||
async function migrateZoomThresholdToFontSize() {
|
||||
const oldKey = 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold'
|
||||
const newKey = 'LiteGraph.Canvas.MinFontSizeForLOD'
|
||||
|
||||
// Only migrate if old setting exists and new setting doesn't
|
||||
if (
|
||||
settingValues.value[oldKey] !== undefined &&
|
||||
settingValues.value[newKey] === undefined
|
||||
) {
|
||||
const oldValue = settingValues.value[oldKey] as number
|
||||
|
||||
// Convert zoom threshold to equivalent font size to preserve exact behavior
|
||||
// The threshold formula is: threshold = font_size / (14 * sqrt(DPR))
|
||||
// For DPR=1: threshold = font_size / 14
|
||||
// Therefore: font_size = threshold * 14
|
||||
//
|
||||
// Examples:
|
||||
// - Old 0.6 threshold → 0.6 * 14 = 8.4px → rounds to 8px (preserves ~60% zoom threshold)
|
||||
// - Old 0.5 threshold → 0.5 * 14 = 7px (preserves 50% zoom threshold)
|
||||
// - Old 1.0 threshold → 1.0 * 14 = 14px (preserves 100% zoom threshold)
|
||||
const mappedFontSize = Math.round(oldValue * 14)
|
||||
const clampedFontSize = Math.max(1, Math.min(24, mappedFontSize))
|
||||
|
||||
// Set the new value
|
||||
settingValues.value[newKey] = clampedFontSize
|
||||
|
||||
// Remove the old setting to prevent confusion
|
||||
delete settingValues.value[oldKey]
|
||||
|
||||
// Store the migrated setting
|
||||
await api.storeSetting(newKey, clampedFontSize)
|
||||
await api.storeSetting(oldKey, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
321
src/stores/subgraphStore.ts
Normal file
321
src/stores/subgraphStore.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type {
|
||||
ComfyNodeDef as ComfyNodeDefV1,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { UserFile } from '@/stores/userFileStore'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
LoadedComfyWorkflow,
|
||||
useWorkflowStore
|
||||
} from '@/stores/workflowStore'
|
||||
|
||||
async function confirmOverwrite(name: string): Promise<boolean | null> {
|
||||
return await useDialogService().confirm({
|
||||
title: t('subgraphStore.overwriteBlueprintTitle'),
|
||||
type: 'overwriteBlueprint',
|
||||
message: t('subgraphStore.overwriteBlueprint'),
|
||||
itemList: [name]
|
||||
})
|
||||
}
|
||||
|
||||
export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
class SubgraphBlueprint extends ComfyWorkflow {
|
||||
static override readonly basePath = 'subgraphs/'
|
||||
override readonly tintCanvasBg = '#22227740'
|
||||
|
||||
hasPromptedSave: boolean = false
|
||||
|
||||
constructor(
|
||||
options: { path: string; modified: number; size: number },
|
||||
confirmFirstSave: boolean = false
|
||||
) {
|
||||
super(options)
|
||||
this.hasPromptedSave = !confirmFirstSave
|
||||
}
|
||||
|
||||
validateSubgraph() {
|
||||
if (!this.activeState?.definitions)
|
||||
throw new Error(
|
||||
'The root graph of a subgraph blueprint must consist of only a single subgraph node'
|
||||
)
|
||||
const { subgraphs } = this.activeState.definitions
|
||||
const { nodes } = this.activeState
|
||||
//Instanceof doesn't function as nodes are serialized
|
||||
function isSubgraphNode(node: ComfyNode) {
|
||||
return node && subgraphs.some((s) => s.id === node.type)
|
||||
}
|
||||
if (nodes.length == 1 && isSubgraphNode(nodes[0])) return
|
||||
const errors: Record<NodeId, NodeError> = {}
|
||||
//mark errors for all but first subgraph node
|
||||
let firstSubgraphFound = false
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (!firstSubgraphFound && isSubgraphNode(nodes[i])) {
|
||||
firstSubgraphFound = true
|
||||
continue
|
||||
}
|
||||
errors[nodes[i].id] = {
|
||||
errors: [],
|
||||
class_type: nodes[i].type,
|
||||
dependent_outputs: []
|
||||
}
|
||||
}
|
||||
useExecutionStore().lastNodeErrors = errors
|
||||
useCanvasStore().getCanvas().draw(true, true)
|
||||
throw new Error(
|
||||
'The root graph of a subgraph blueprint must consist of only a single subgraph node'
|
||||
)
|
||||
}
|
||||
|
||||
override async save(): Promise<UserFile> {
|
||||
this.validateSubgraph()
|
||||
if (
|
||||
!this.hasPromptedSave &&
|
||||
useSettingStore().get('Comfy.Workflow.WarnBlueprintOverwrite')
|
||||
) {
|
||||
if (!(await confirmOverwrite(this.filename))) return this
|
||||
this.hasPromptedSave = true
|
||||
}
|
||||
const ret = await super.save()
|
||||
useSubgraphStore().updateDef(await this.load())
|
||||
return ret
|
||||
}
|
||||
|
||||
override async saveAs(path: string) {
|
||||
this.validateSubgraph()
|
||||
this.hasPromptedSave = true
|
||||
const ret = await super.saveAs(path)
|
||||
useSubgraphStore().updateDef(await this.load())
|
||||
return ret
|
||||
}
|
||||
override async load({
|
||||
force = false
|
||||
}: { force?: boolean } = {}): Promise<LoadedComfyWorkflow> {
|
||||
if (!force && this.isLoaded) return await super.load({ force })
|
||||
const loaded = await super.load({ force })
|
||||
const st = loaded.activeState
|
||||
const sg = (st.definitions?.subgraphs ?? []).find(
|
||||
(sg) => sg.id == st.nodes[0].type
|
||||
)
|
||||
if (!sg)
|
||||
throw new Error(
|
||||
'Loaded subgraph blueprint does not contain valid subgraph'
|
||||
)
|
||||
sg.name = st.nodes[0].title = this.filename
|
||||
return loaded
|
||||
}
|
||||
override async promptSave(): Promise<string | null> {
|
||||
return await useDialogService().prompt({
|
||||
title: t('subgraphStore.saveBlueprint'),
|
||||
message: t('subgraphStore.blueprintName') + ':',
|
||||
defaultValue: this.filename
|
||||
})
|
||||
}
|
||||
override unload(): void {
|
||||
//Skip unloading. Even if a workflow is closed after editing,
|
||||
//it must remain loaded in order to be added to the graph
|
||||
}
|
||||
}
|
||||
const subgraphCache: Record<string, LoadedComfyWorkflow> = {}
|
||||
const typePrefix = 'SubgraphBlueprint.'
|
||||
const subgraphDefCache = ref<Map<string, ComfyNodeDefImpl>>(new Map())
|
||||
const canvasStore = useCanvasStore()
|
||||
const subgraphBlueprints = computed(() => [
|
||||
...subgraphDefCache.value.values()
|
||||
])
|
||||
async function fetchSubgraphs() {
|
||||
async function loadBlueprint(options: {
|
||||
path: string
|
||||
modified: number
|
||||
size: number
|
||||
}): Promise<void> {
|
||||
const name = options.path.slice(0, -'.json'.length)
|
||||
options.path = SubgraphBlueprint.basePath + options.path
|
||||
const bp = await new SubgraphBlueprint(options, true).load()
|
||||
useWorkflowStore().attachWorkflow(bp)
|
||||
const nodeDef = convertToNodeDef(bp)
|
||||
|
||||
subgraphDefCache.value.set(name, nodeDef)
|
||||
subgraphCache[name] = bp
|
||||
}
|
||||
|
||||
const res = (
|
||||
await api.listUserDataFullInfo(SubgraphBlueprint.basePath)
|
||||
).filter((f) => f.path.endsWith('.json'))
|
||||
const settled = await Promise.allSettled(res.map(loadBlueprint))
|
||||
const errors = settled.filter((i) => 'reason' in i).map((i) => i.reason)
|
||||
errors.forEach((e) => console.error('Failed to load subgraph blueprint', e))
|
||||
if (errors.length > 0) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('subgraphStore.loadFailure'),
|
||||
detail: errors.length > 3 ? `x${errors.length}` : `${errors}`,
|
||||
life: 6000
|
||||
})
|
||||
}
|
||||
}
|
||||
function convertToNodeDef(workflow: LoadedComfyWorkflow): ComfyNodeDefImpl {
|
||||
const name = workflow.filename
|
||||
const subgraphNode = workflow.changeTracker.initialState.nodes[0]
|
||||
if (!subgraphNode) throw new Error('Invalid Subgraph Blueprint')
|
||||
subgraphNode.inputs ??= []
|
||||
subgraphNode.outputs ??= []
|
||||
//NOTE: Types are cast to string. This is only used for input coloring on previews
|
||||
const inputs = Object.fromEntries(
|
||||
subgraphNode.inputs.map((i) => [
|
||||
i.name,
|
||||
[`${i.type}`, undefined] satisfies InputSpec
|
||||
])
|
||||
)
|
||||
let description = 'User generated subgraph blueprint'
|
||||
if (workflow.initialState.extra?.BlueprintDescription)
|
||||
description = `${workflow.initialState.extra.BlueprintDescription}`
|
||||
const nodedefv1: ComfyNodeDefV1 = {
|
||||
input: { required: inputs },
|
||||
output: subgraphNode.outputs.map((o) => `${o.type}`),
|
||||
output_name: subgraphNode.outputs.map((o) => o.name),
|
||||
name: typePrefix + name,
|
||||
display_name: name,
|
||||
description,
|
||||
category: 'Subgraph Blueprints',
|
||||
output_node: false,
|
||||
python_module: 'blueprint'
|
||||
}
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodedefv1)
|
||||
return nodeDefImpl
|
||||
}
|
||||
async function publishSubgraph() {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const subgraphNode = [...canvas.selectedItems][0]
|
||||
if (
|
||||
canvas.selectedItems.size !== 1 ||
|
||||
!(subgraphNode instanceof SubgraphNode)
|
||||
)
|
||||
throw new TypeError('Must have single SubgraphNode selected to publish')
|
||||
|
||||
const { nodes = [], subgraphs = [] } = canvas._serializeItems([
|
||||
subgraphNode
|
||||
])
|
||||
if (nodes.length != 1) {
|
||||
throw new TypeError('Must have single SubgraphNode selected to publish')
|
||||
}
|
||||
//create minimal workflow
|
||||
const workflowData = {
|
||||
revision: 0,
|
||||
last_node_id: subgraphNode.id,
|
||||
last_link_id: 0,
|
||||
nodes,
|
||||
links: [],
|
||||
version: 0.4,
|
||||
definitions: { subgraphs }
|
||||
}
|
||||
//prompt name
|
||||
const name = await useDialogService().prompt({
|
||||
title: t('subgraphStore.saveBlueprint'),
|
||||
message: t('subgraphStore.blueprintName') + ':',
|
||||
defaultValue: subgraphNode.title
|
||||
})
|
||||
if (!name) return
|
||||
if (subgraphDefCache.value.has(name) && !(await confirmOverwrite(name)))
|
||||
//User has chosen not to overwrite.
|
||||
return
|
||||
|
||||
//upload file
|
||||
const path = SubgraphBlueprint.basePath + name + '.json'
|
||||
const workflow = new SubgraphBlueprint({
|
||||
path,
|
||||
size: -1,
|
||||
modified: Date.now()
|
||||
})
|
||||
workflow.originalContent = JSON.stringify(workflowData)
|
||||
const loadedWorkflow = await workflow.load()
|
||||
//Mark non-temporary
|
||||
workflow.size = 1
|
||||
await workflow.save()
|
||||
//add to files list?
|
||||
useWorkflowStore().attachWorkflow(loadedWorkflow)
|
||||
subgraphDefCache.value.set(name, convertToNodeDef(loadedWorkflow))
|
||||
subgraphCache[name] = loadedWorkflow
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: t('subgraphStore.publishSuccess'),
|
||||
detail: t('subgraphStore.publishSuccessMessage'),
|
||||
life: 4000
|
||||
})
|
||||
}
|
||||
function updateDef(blueprint: LoadedComfyWorkflow) {
|
||||
subgraphDefCache.value.set(blueprint.filename, convertToNodeDef(blueprint))
|
||||
}
|
||||
async function editBlueprint(nodeType: string) {
|
||||
const name = nodeType.slice(typePrefix.length)
|
||||
if (!(name in subgraphCache))
|
||||
//As loading is blocked on in startup, this can likely be changed to invalid type
|
||||
throw new Error('not yet loaded')
|
||||
useWorkflowStore().attachWorkflow(subgraphCache[name])
|
||||
await useWorkflowService().openWorkflow(subgraphCache[name])
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (canvas.graph && 'subgraph' in canvas.graph.nodes[0])
|
||||
canvas.setGraph(canvas.graph.nodes[0].subgraph)
|
||||
}
|
||||
function getBlueprint(nodeType: string): ComfyWorkflowJSON {
|
||||
const name = nodeType.slice(typePrefix.length)
|
||||
if (!(name in subgraphCache))
|
||||
//As loading is blocked on in startup, this can likely be changed to invalid type
|
||||
throw new Error('not yet loaded')
|
||||
return subgraphCache[name].changeTracker.initialState
|
||||
}
|
||||
async function deleteBlueprint(nodeType: string) {
|
||||
const name = nodeType.slice(typePrefix.length)
|
||||
if (!(name in subgraphCache))
|
||||
//As loading is blocked on in startup, this can likely be changed to invalid type
|
||||
throw new Error('not yet loaded')
|
||||
if (
|
||||
!(await useDialogService().confirm({
|
||||
title: t('subgraphStore.confirmDeleteTitle'),
|
||||
type: 'delete',
|
||||
message: t('subgraphStore.confirmDelete'),
|
||||
itemList: [name]
|
||||
}))
|
||||
)
|
||||
return
|
||||
|
||||
await subgraphCache[name].delete()
|
||||
delete subgraphCache[name]
|
||||
subgraphDefCache.value.delete(name)
|
||||
}
|
||||
function isSubgraphBlueprint(
|
||||
workflow: unknown
|
||||
): workflow is SubgraphBlueprint {
|
||||
return workflow instanceof SubgraphBlueprint
|
||||
}
|
||||
|
||||
return {
|
||||
deleteBlueprint,
|
||||
editBlueprint,
|
||||
fetchSubgraphs,
|
||||
getBlueprint,
|
||||
isSubgraphBlueprint,
|
||||
publishSubgraph,
|
||||
subgraphBlueprints,
|
||||
typePrefix,
|
||||
updateDef
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -182,7 +182,7 @@ export class UserFile {
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadedUserFile extends UserFile {
|
||||
interface LoadedUserFile extends UserFile {
|
||||
isLoaded: true
|
||||
originalContent: string
|
||||
content: string
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
@@ -10,6 +11,7 @@ import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import {
|
||||
createNodeExecutionId,
|
||||
@@ -24,7 +26,8 @@ import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
import { UserFile } from './userFileStore'
|
||||
|
||||
export class ComfyWorkflow extends UserFile {
|
||||
static readonly basePath = 'workflows/'
|
||||
static readonly basePath: string = 'workflows/'
|
||||
readonly tintCanvasBg?: string
|
||||
|
||||
/**
|
||||
* The change tracker for the workflow. Non-reactive raw object.
|
||||
@@ -120,6 +123,14 @@ export class ComfyWorkflow extends UserFile {
|
||||
this.content = JSON.stringify(this.activeState)
|
||||
return await super.saveAs(path)
|
||||
}
|
||||
|
||||
async promptSave(): Promise<string | null> {
|
||||
return await useDialogService().prompt({
|
||||
title: t('workflowService.saveWorkflow'),
|
||||
message: t('workflowService.enterFilename') + ':',
|
||||
defaultValue: this.filename
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadedComfyWorkflow extends ComfyWorkflow {
|
||||
@@ -137,8 +148,9 @@ export interface LoadedComfyWorkflow extends ComfyWorkflow {
|
||||
* error TS7056: The inferred type of this node exceeds the maximum length the
|
||||
* compiler will serialize. An explicit type annotation is needed.
|
||||
*/
|
||||
export interface WorkflowStore {
|
||||
interface WorkflowStore {
|
||||
activeWorkflow: LoadedComfyWorkflow | null
|
||||
attachWorkflow: (workflow: ComfyWorkflow, openIndex?: number) => void
|
||||
isActive: (workflow: ComfyWorkflow) => boolean
|
||||
openWorkflows: ComfyWorkflow[]
|
||||
openedWorkflowIndexShift: (shift: number) => ComfyWorkflow | null
|
||||
@@ -290,6 +302,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
}
|
||||
const loadedWorkflow = await workflow.load()
|
||||
activeWorkflow.value = loadedWorkflow
|
||||
comfyApp.canvas.bg_tint = loadedWorkflow.tintCanvasBg
|
||||
console.debug('[workflowStore] open workflow', workflow.path)
|
||||
return loadedWorkflow
|
||||
}
|
||||
@@ -304,11 +317,37 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
const saveAs = (
|
||||
existingWorkflow: ComfyWorkflow,
|
||||
path: string
|
||||
): ComfyWorkflow => {
|
||||
const workflow: ComfyWorkflow = new (existingWorkflow.constructor as any)({
|
||||
path,
|
||||
modified: Date.now(),
|
||||
size: -1
|
||||
})
|
||||
workflow.originalContent = workflow.content = existingWorkflow.content
|
||||
workflowLookup.value[workflow.path] = workflow
|
||||
return workflow
|
||||
}
|
||||
|
||||
const createTemporary = (path?: string, workflowData?: ComfyWorkflowJSON) => {
|
||||
const fullPath = getUnconflictedPath(
|
||||
ComfyWorkflow.basePath + (path ?? 'Unsaved Workflow.json')
|
||||
)
|
||||
const existingWorkflow = workflows.value.find((w) => w.fullFilename == path)
|
||||
if (
|
||||
path &&
|
||||
workflowData &&
|
||||
existingWorkflow?.changeTracker &&
|
||||
!existingWorkflow.directory.startsWith(
|
||||
ComfyWorkflow.basePath.slice(0, -1)
|
||||
)
|
||||
) {
|
||||
existingWorkflow.changeTracker.reset(workflowData)
|
||||
return existingWorkflow
|
||||
}
|
||||
|
||||
const workflow = new ComfyWorkflow({
|
||||
path: fullPath,
|
||||
modified: Date.now(),
|
||||
@@ -357,7 +396,10 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
}
|
||||
|
||||
const persistedWorkflows = computed(() =>
|
||||
Array.from(workflows.value).filter((workflow) => workflow.isPersisted)
|
||||
Array.from(workflows.value).filter(
|
||||
(workflow) =>
|
||||
workflow.isPersisted && !workflow.path.startsWith('subgraphs/')
|
||||
)
|
||||
)
|
||||
const syncWorkflows = async (dir: string = '') => {
|
||||
await syncEntities(
|
||||
@@ -647,6 +689,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
|
||||
return {
|
||||
activeWorkflow,
|
||||
attachWorkflow,
|
||||
isActive,
|
||||
openWorkflows,
|
||||
openedWorkflowIndexShift,
|
||||
@@ -658,6 +701,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
createTemporary,
|
||||
renameWorkflow,
|
||||
deleteWorkflow,
|
||||
saveAs,
|
||||
saveWorkflow,
|
||||
reorderWorkflows,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user