mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-08 06:30:04 +00:00
App mode - more updates & fixes (#9137)
## Summary - fix sizing of sidebars in app mode - update feedback button to match design - update job queue notification - clickable queue spinner item to allow clear queue - refactor mode out of store to specific workflow instance - support different saved vs active mode - other styling/layout tweaks ## Changes - **What**: Changes the store to a composable and moves the mode state to the workflow. - This enables switching between tabs and maintaining the mode they were in ## Screenshots (if applicable) <img width="1866" height="1455" alt="image" src="https://github.com/user-attachments/assets/f9a8cd36-181f-4948-b48c-dd27bd9127cf" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9137-App-mode-more-updates-fixes-3106d73d365081a18ccff6ffe24fdec7) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -2,12 +2,58 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { PendingWarnings } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import type {
|
||||
LoadedComfyWorkflow,
|
||||
PendingWarnings
|
||||
} from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import { ComfyWorkflow as ComfyWorkflowClass } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import type { AppMode } from '@/composables/useAppMode'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
function createModeTestWorkflow(
|
||||
options: {
|
||||
path?: string
|
||||
initialMode?: AppMode | null
|
||||
activeMode?: AppMode | null
|
||||
loaded?: boolean
|
||||
} = {}
|
||||
): LoadedComfyWorkflow {
|
||||
const workflow = new ComfyWorkflowClass({
|
||||
path: options.path ?? 'workflows/test.json',
|
||||
modified: Date.now(),
|
||||
size: 100
|
||||
})
|
||||
if ('initialMode' in options) workflow.initialMode = options.initialMode
|
||||
workflow.activeMode = options.activeMode ?? null
|
||||
if (options.loaded !== false) {
|
||||
workflow.changeTracker = createMockChangeTracker()
|
||||
workflow.content = '{}'
|
||||
workflow.originalContent = '{}'
|
||||
}
|
||||
return workflow as LoadedComfyWorkflow
|
||||
}
|
||||
|
||||
function makeWorkflowData(
|
||||
extra: Record<string, unknown> = {}
|
||||
): ComfyWorkflowJSON {
|
||||
return {
|
||||
last_node_id: 5,
|
||||
last_link_id: 3,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
version: 0.4,
|
||||
extra
|
||||
}
|
||||
}
|
||||
|
||||
const { mockShowMissingNodes, mockShowMissingModels } = vi.hoisted(() => ({
|
||||
mockShowMissingNodes: vi.fn(),
|
||||
@@ -72,6 +118,14 @@ vi.mock('@/stores/domWidgetStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspaceStore', () => ({
|
||||
useWorkspaceStore: () => ({
|
||||
get workflow() {
|
||||
return useWorkflowStore()
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
const MISSING_MODELS: PendingWarnings['missingModels'] = {
|
||||
missingModels: [
|
||||
{ name: 'model.safetensors', url: '', directory: 'checkpoints' }
|
||||
@@ -83,7 +137,7 @@ function createWorkflow(
|
||||
warnings: PendingWarnings | null = null,
|
||||
options: { loadable?: boolean; path?: string } = {}
|
||||
): ComfyWorkflow {
|
||||
return {
|
||||
const wf = {
|
||||
pendingWarnings: warnings,
|
||||
...(options.loadable && {
|
||||
path: options.path ?? 'workflows/test.json',
|
||||
@@ -91,7 +145,8 @@ function createWorkflow(
|
||||
activeState: { nodes: [], links: [] },
|
||||
changeTracker: { reset: vi.fn(), restore: vi.fn() }
|
||||
})
|
||||
} as Partial<ComfyWorkflow> as ComfyWorkflow
|
||||
} as Partial<ComfyWorkflow>
|
||||
return wf as ComfyWorkflow
|
||||
}
|
||||
|
||||
function enableWarningSettings() {
|
||||
@@ -180,12 +235,7 @@ describe('useWorkflowService', () => {
|
||||
workflowStore = useWorkflowStore()
|
||||
vi.mocked(app.loadGraphData).mockImplementation(
|
||||
async (_data, _clean, _restore, wf) => {
|
||||
;(
|
||||
workflowStore as Partial<Record<string, unknown>> as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
).activeWorkflow = wf
|
||||
workflowStore.activeWorkflow = wf as LoadedComfyWorkflow
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -256,4 +306,231 @@ describe('useWorkflowService', () => {
|
||||
expect(mockShowMissingNodes).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('per-workflow mode switching', () => {
|
||||
let appMode: ReturnType<typeof useAppMode>
|
||||
let workflowStore: ReturnType<typeof useWorkflowStore>
|
||||
let service: ReturnType<typeof useWorkflowService>
|
||||
|
||||
function mockOpenWorkflow() {
|
||||
vi.spyOn(workflowStore, 'openWorkflow').mockImplementation(async (wf) => {
|
||||
// Simulate load() setting changeTracker on first open
|
||||
if (!wf.changeTracker) {
|
||||
wf.changeTracker = createMockChangeTracker()
|
||||
wf.content = '{}'
|
||||
wf.originalContent = '{}'
|
||||
}
|
||||
const loaded = wf as LoadedComfyWorkflow
|
||||
workflowStore.activeWorkflow = loaded
|
||||
return loaded
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
appMode = useAppMode()
|
||||
workflowStore = useWorkflowStore()
|
||||
service = useWorkflowService()
|
||||
})
|
||||
|
||||
describe('mode derivation from active workflow', () => {
|
||||
it('reflects initialMode of the active workflow', () => {
|
||||
const workflow = createModeTestWorkflow({ initialMode: 'app' })
|
||||
workflowStore.activeWorkflow = workflow
|
||||
|
||||
expect(appMode.mode.value).toBe('app')
|
||||
})
|
||||
|
||||
it('activeMode takes precedence over initialMode', () => {
|
||||
const workflow = createModeTestWorkflow({
|
||||
initialMode: 'app',
|
||||
activeMode: 'graph'
|
||||
})
|
||||
workflowStore.activeWorkflow = workflow
|
||||
|
||||
expect(appMode.mode.value).toBe('graph')
|
||||
})
|
||||
|
||||
it('defaults to graph when no active workflow', () => {
|
||||
expect(appMode.mode.value).toBe('graph')
|
||||
})
|
||||
|
||||
it('updates when activeWorkflow changes', () => {
|
||||
const workflow1 = createModeTestWorkflow({
|
||||
path: 'workflows/one.json',
|
||||
initialMode: 'app'
|
||||
})
|
||||
const workflow2 = createModeTestWorkflow({
|
||||
path: 'workflows/two.json',
|
||||
activeMode: 'builder:select'
|
||||
})
|
||||
|
||||
workflowStore.activeWorkflow = workflow1
|
||||
expect(appMode.mode.value).toBe('app')
|
||||
|
||||
workflowStore.activeWorkflow = workflow2
|
||||
expect(appMode.mode.value).toBe('builder:select')
|
||||
})
|
||||
})
|
||||
|
||||
describe('setMode writes to active workflow', () => {
|
||||
it('writes activeMode without changing initialMode', () => {
|
||||
const workflow = createModeTestWorkflow({ initialMode: 'graph' })
|
||||
workflowStore.activeWorkflow = workflow
|
||||
|
||||
appMode.setMode('builder:arrange')
|
||||
|
||||
expect(workflow.activeMode).toBe('builder:arrange')
|
||||
expect(workflow.initialMode).toBe('graph')
|
||||
expect(appMode.mode.value).toBe('builder:arrange')
|
||||
})
|
||||
})
|
||||
|
||||
describe('afterLoadNewGraph initializes initialMode', () => {
|
||||
beforeEach(() => {
|
||||
mockOpenWorkflow()
|
||||
})
|
||||
|
||||
it('sets initialMode from extra.linearMode on first load', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
await service.afterLoadNewGraph(
|
||||
workflow,
|
||||
makeWorkflowData({ linearMode: true })
|
||||
)
|
||||
|
||||
expect(workflow.initialMode).toBe('app')
|
||||
})
|
||||
|
||||
it('leaves initialMode null when extra.linearMode is absent', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
await service.afterLoadNewGraph(workflow, makeWorkflowData())
|
||||
|
||||
expect(workflow.initialMode).toBeNull()
|
||||
})
|
||||
|
||||
it('sets initialMode to graph when extra.linearMode is false', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
await service.afterLoadNewGraph(
|
||||
workflow,
|
||||
makeWorkflowData({ linearMode: false })
|
||||
)
|
||||
|
||||
expect(workflow.initialMode).toBe('graph')
|
||||
})
|
||||
|
||||
it('does not set initialMode on tab switch even if data has linearMode', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
// First load — no linearMode in data
|
||||
await service.afterLoadNewGraph(workflow, makeWorkflowData())
|
||||
expect(workflow.initialMode).toBeNull()
|
||||
|
||||
// User switches to app mode at runtime
|
||||
workflow.activeMode = 'app'
|
||||
|
||||
// Tab switch / reload — data now has linearMode (leaked from graph)
|
||||
await service.afterLoadNewGraph(
|
||||
workflow,
|
||||
makeWorkflowData({ linearMode: true })
|
||||
)
|
||||
|
||||
// initialMode should NOT have been updated — only builder save sets it
|
||||
expect(workflow.initialMode).toBeNull()
|
||||
})
|
||||
|
||||
it('preserves existing initialMode on tab switch', async () => {
|
||||
const workflow = createModeTestWorkflow({
|
||||
initialMode: 'app'
|
||||
})
|
||||
|
||||
await service.afterLoadNewGraph(workflow, makeWorkflowData())
|
||||
|
||||
expect(workflow.initialMode).toBe('app')
|
||||
})
|
||||
|
||||
it('sets initialMode to app for fresh string-based loads with linearMode', async () => {
|
||||
vi.spyOn(workflowStore, 'createNewTemporary').mockReturnValue(
|
||||
createModeTestWorkflow()
|
||||
)
|
||||
|
||||
await service.afterLoadNewGraph(
|
||||
'test.json',
|
||||
makeWorkflowData({ linearMode: true })
|
||||
)
|
||||
|
||||
expect(appMode.mode.value).toBe('app')
|
||||
})
|
||||
|
||||
it('syncs linearMode to rootGraph.extra for draft persistence', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
await service.afterLoadNewGraph(
|
||||
workflow,
|
||||
makeWorkflowData({ linearMode: true })
|
||||
)
|
||||
|
||||
expect(app.rootGraph.extra.linearMode).toBe(true)
|
||||
})
|
||||
|
||||
it('reads initialMode from file when draft lacks linearMode (restoration)', async () => {
|
||||
const filePath = 'workflows/saved-app.json'
|
||||
const fileInitialState = makeWorkflowData({ linearMode: true })
|
||||
const mockTracker = createMockChangeTracker()
|
||||
mockTracker.initialState = fileInitialState
|
||||
|
||||
// Persisted, not-loaded workflow in the store
|
||||
const persistedWorkflow = new ComfyWorkflowClass({
|
||||
path: filePath,
|
||||
modified: Date.now(),
|
||||
size: 100
|
||||
})
|
||||
|
||||
vi.spyOn(workflowStore, 'getWorkflowByPath').mockReturnValue(
|
||||
persistedWorkflow
|
||||
)
|
||||
vi.spyOn(workflowStore, 'openWorkflow').mockImplementation(
|
||||
async (wf) => {
|
||||
wf.changeTracker = mockTracker
|
||||
wf.content = JSON.stringify(fileInitialState)
|
||||
wf.originalContent = wf.content
|
||||
workflowStore.activeWorkflow = wf as LoadedComfyWorkflow
|
||||
return wf as LoadedComfyWorkflow
|
||||
}
|
||||
)
|
||||
|
||||
// Draft data has NO linearMode (simulates rootGraph serialization)
|
||||
const draftData = makeWorkflowData()
|
||||
|
||||
await service.afterLoadNewGraph('saved-app.json', draftData)
|
||||
|
||||
// initialMode should come from the file, not the draft
|
||||
expect(persistedWorkflow.initialMode).toBe('app')
|
||||
expect(app.rootGraph.extra.linearMode).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('round-trip mode preservation', () => {
|
||||
it('each workflow retains its own mode across tab switches', () => {
|
||||
const workflow1 = createModeTestWorkflow({
|
||||
path: 'workflows/one.json',
|
||||
activeMode: 'builder:select'
|
||||
})
|
||||
const workflow2 = createModeTestWorkflow({
|
||||
path: 'workflows/two.json',
|
||||
initialMode: 'app'
|
||||
})
|
||||
|
||||
workflowStore.activeWorkflow = workflow1
|
||||
expect(appMode.mode.value).toBe('builder:select')
|
||||
|
||||
workflowStore.activeWorkflow = workflow2
|
||||
expect(appMode.mode.value).toBe('app')
|
||||
|
||||
workflowStore.activeWorkflow = workflow1
|
||||
expect(appMode.mode.value).toBe('builder:select')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,24 +7,31 @@ import type { Point, SerialisableGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowDraftStore } from '@/platform/workflow/persistence/stores/workflowDraftStore'
|
||||
import { syncLinearMode } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
|
||||
import { app } from '@/scripts/app'
|
||||
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
||||
import { useMissingModelsDialog } from '@/composables/useMissingModelsDialog'
|
||||
import { useMissingNodesDialog } from '@/composables/useMissingNodesDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import type { AppMode } from '@/composables/useAppMode'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
|
||||
function linearModeToAppMode(linearMode: unknown): AppMode | null {
|
||||
if (typeof linearMode !== 'boolean') return null
|
||||
return linearMode ? 'app' : 'graph'
|
||||
}
|
||||
|
||||
export const useWorkflowService = () => {
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
@@ -93,24 +100,20 @@ export const useWorkflowService = () => {
|
||||
* Save a workflow as a new file
|
||||
* @param workflow The workflow to save
|
||||
* @param options.filename Pre-supplied filename (skips the prompt dialog)
|
||||
* @param options.openAsApp If set, updates linearMode extra before saving
|
||||
*/
|
||||
const saveWorkflowAs = async (
|
||||
workflow: ComfyWorkflow,
|
||||
options: { filename?: string; openAsApp?: boolean } = {}
|
||||
options: { filename?: string; initialMode?: AppMode } = {}
|
||||
): Promise<boolean> => {
|
||||
const newFilename = options.filename ?? (await workflow.promptSave())
|
||||
if (!newFilename) return false
|
||||
|
||||
if (options.openAsApp !== undefined) {
|
||||
app.rootGraph.extra ??= {}
|
||||
app.rootGraph.extra.linearMode = options.openAsApp
|
||||
workflow.changeTracker?.checkState()
|
||||
}
|
||||
|
||||
const newPath = workflow.directory + '/' + appendJsonExt(newFilename)
|
||||
const existingWorkflow = workflowStore.getWorkflowByPath(newPath)
|
||||
|
||||
const isSelfOverwrite =
|
||||
existingWorkflow?.path === workflow.path && !existingWorkflow?.isTemporary
|
||||
|
||||
if (existingWorkflow && !existingWorkflow.isTemporary) {
|
||||
const res = await dialogService.confirm({
|
||||
title: t('sideToolbar.workflowTab.confirmOverwriteTitle'),
|
||||
@@ -121,15 +124,20 @@ export const useWorkflowService = () => {
|
||||
|
||||
if (res !== true) return false
|
||||
|
||||
if (existingWorkflow.path === workflow.path) {
|
||||
await saveWorkflow(workflow)
|
||||
return true
|
||||
if (!isSelfOverwrite) {
|
||||
const deleted = await deleteWorkflow(existingWorkflow, true)
|
||||
if (!deleted) return false
|
||||
}
|
||||
const deleted = await deleteWorkflow(existingWorkflow, true)
|
||||
if (!deleted) return false
|
||||
}
|
||||
|
||||
if (workflow.isTemporary) {
|
||||
if (options.initialMode) workflow.initialMode = options.initialMode
|
||||
|
||||
syncLinearMode(workflow, [app.rootGraph], { flushLinearData: true })
|
||||
workflow.changeTracker?.checkState()
|
||||
|
||||
if (isSelfOverwrite) {
|
||||
await saveWorkflow(workflow)
|
||||
} else if (workflow.isTemporary) {
|
||||
await renameWorkflow(workflow, newPath)
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
} else {
|
||||
@@ -148,6 +156,8 @@ export const useWorkflowService = () => {
|
||||
if (workflow.isTemporary) {
|
||||
await saveWorkflowAs(workflow)
|
||||
} else {
|
||||
syncLinearMode(workflow, [app.rootGraph], { flushLinearData: true })
|
||||
workflow.changeTracker?.checkState()
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
@@ -358,13 +368,17 @@ export const useWorkflowService = () => {
|
||||
workflowData: ComfyWorkflowJSON
|
||||
) => {
|
||||
const workflowStore = useWorkspaceStore().workflow
|
||||
if (
|
||||
workflowData.extra?.linearMode !== undefined ||
|
||||
!workflowData.nodes.length
|
||||
) {
|
||||
if (workflowData.extra?.linearMode && !useCanvasStore().linearMode)
|
||||
const { isAppMode } = useAppMode()
|
||||
const wasAppMode = isAppMode.value
|
||||
|
||||
// Determine the initial app mode for fresh loads from serialized state.
|
||||
// null means linearMode was never explicitly set (not builder-saved).
|
||||
const freshLoadMode = linearModeToAppMode(workflowData.extra?.linearMode)
|
||||
|
||||
function trackIfEnteringApp(workflow: ComfyWorkflow) {
|
||||
if (!wasAppMode && workflow.initialMode === 'app') {
|
||||
useTelemetry()?.trackEnterLinear({ source: 'workflow' })
|
||||
useCanvasStore().linearMode = !!workflowData.extra?.linearMode
|
||||
}
|
||||
}
|
||||
|
||||
if (value === null || typeof value === 'string') {
|
||||
@@ -381,26 +395,39 @@ export const useWorkflowService = () => {
|
||||
if (existingWorkflow?.isPersisted && !existingWorkflow.isLoaded) {
|
||||
const loadedWorkflow =
|
||||
await workflowStore.openWorkflow(existingWorkflow)
|
||||
if (loadedWorkflow.initialMode === undefined) {
|
||||
// Prefer the file's linearMode over the draft's since the file
|
||||
// is the authoritative saved state.
|
||||
loadedWorkflow.initialMode =
|
||||
linearModeToAppMode(
|
||||
loadedWorkflow.initialState?.extra?.linearMode
|
||||
) ?? freshLoadMode
|
||||
trackIfEnteringApp(loadedWorkflow)
|
||||
}
|
||||
syncLinearMode(loadedWorkflow, [workflowData, app.rootGraph])
|
||||
loadedWorkflow.changeTracker.reset(workflowData)
|
||||
loadedWorkflow.changeTracker.restore()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (useCanvasStore().linearMode) {
|
||||
app.rootGraph.extra ??= {}
|
||||
app.rootGraph.extra.linearMode = true
|
||||
}
|
||||
|
||||
const tempWorkflow = workflowStore.createNewTemporary(
|
||||
path ? appendJsonExt(path) : undefined,
|
||||
workflowData
|
||||
)
|
||||
tempWorkflow.initialMode = freshLoadMode
|
||||
trackIfEnteringApp(tempWorkflow)
|
||||
syncLinearMode(tempWorkflow, [workflowData, app.rootGraph])
|
||||
await workflowStore.openWorkflow(tempWorkflow)
|
||||
return
|
||||
}
|
||||
|
||||
const loadedWorkflow = await workflowStore.openWorkflow(value)
|
||||
if (loadedWorkflow.initialMode === undefined) {
|
||||
loadedWorkflow.initialMode = freshLoadMode
|
||||
trackIfEnteringApp(loadedWorkflow)
|
||||
}
|
||||
syncLinearMode(loadedWorkflow, [workflowData, app.rootGraph])
|
||||
loadedWorkflow.changeTracker.reset(workflowData)
|
||||
loadedWorkflow.changeTracker.restore()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { markRaw } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import type { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import type { AppMode } from '@/composables/useAppMode'
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { UserFile } from '@/stores/userFileStore'
|
||||
import type {
|
||||
ComfyWorkflowJSON,
|
||||
@@ -9,6 +11,11 @@ import type {
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
|
||||
export interface LinearData {
|
||||
inputs: [NodeId, string][]
|
||||
outputs: NodeId[]
|
||||
}
|
||||
|
||||
export interface PendingWarnings {
|
||||
missingNodeTypes?: MissingNodeType[]
|
||||
missingModels?: {
|
||||
@@ -17,6 +24,31 @@ export interface PendingWarnings {
|
||||
}
|
||||
}
|
||||
|
||||
type LinearModeTarget = { extra?: Record<string, unknown> | null } | null
|
||||
|
||||
export function syncLinearMode(
|
||||
workflow: ComfyWorkflow,
|
||||
targets: LinearModeTarget[],
|
||||
options?: { flushLinearData?: boolean }
|
||||
): void {
|
||||
for (const target of targets) {
|
||||
if (!target) continue
|
||||
if (workflow.initialMode === 'app' || workflow.initialMode === 'graph') {
|
||||
const extra = (target.extra ??= {})
|
||||
extra.linearMode = workflow.initialMode === 'app'
|
||||
} else {
|
||||
delete target.extra?.linearMode
|
||||
}
|
||||
if (options?.flushLinearData && workflow.dirtyLinearData) {
|
||||
const extra = (target.extra ??= {})
|
||||
extra.linearData = workflow.dirtyLinearData
|
||||
}
|
||||
}
|
||||
if (options?.flushLinearData && workflow.dirtyLinearData) {
|
||||
workflow.dirtyLinearData = null
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyWorkflow extends UserFile {
|
||||
static readonly basePath: string = 'workflows/'
|
||||
readonly tintCanvasBg?: string
|
||||
@@ -33,6 +65,23 @@ export class ComfyWorkflow extends UserFile {
|
||||
* Warnings deferred from load time, shown when the workflow is first focused.
|
||||
*/
|
||||
pendingWarnings: PendingWarnings | null = null
|
||||
/**
|
||||
* Initial app mode derived from the serialized workflow (extra.linearMode).
|
||||
* - `undefined`: not yet resolved (first load hasn't happened)
|
||||
* - `null`: resolved, but no mode was set (never builder-saved)
|
||||
* - `AppMode`: resolved to a specific mode
|
||||
*/
|
||||
initialMode: AppMode | null | undefined = undefined
|
||||
/**
|
||||
* Current app mode set by the user during the session.
|
||||
* Takes precedence over initialMode when present.
|
||||
*/
|
||||
activeMode: AppMode | null = null
|
||||
/**
|
||||
* In-progress builder selections not yet persisted via save.
|
||||
* Preserved across tab switches, discarded on exitBuilder.
|
||||
*/
|
||||
dirtyLinearData: LinearData | null = null
|
||||
|
||||
/**
|
||||
* @param options The path, modified, and size of the workflow.
|
||||
@@ -129,6 +178,7 @@ export class ComfyWorkflow extends UserFile {
|
||||
|
||||
override unload(): void {
|
||||
this.changeTracker = null
|
||||
this.activeMode = null
|
||||
super.unload()
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
modified: Date.now(),
|
||||
size: -1
|
||||
})
|
||||
workflow.initialMode = existingWorkflow.initialMode
|
||||
workflow.originalContent = workflow.content = JSON.stringify(state)
|
||||
workflowLookup.value[workflow.path] = workflow
|
||||
return workflow
|
||||
|
||||
Reference in New Issue
Block a user