mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-25 00:39:49 +00:00
feat: App mode saving rework (#9338)
## Summary Change app mode changes to be written directly to the workflow on change instead of requiring explicit save via builder. Temporary: Adds `.app.json` file extension to app files for identification since we don't currently have a way to identify them with metadata Removes app builder save dialog and replaces it with default mode selection ## Changes - **What**: - ensure all save locations handle app mode - remove dirtyLinearData and flushing - **Breaking**: - if people are relying on workflow names and are converting to/from app mode in the same workflow, they will gain/lose the `.app` part of the extension ## Screenshots (if applicable) <img width="689" height="84" alt="image" src="https://github.com/user-attachments/assets/335596ee-dce9-4e3a-a7b5-f0715c294e41" /> <img width="421" height="324" alt="image" src="https://github.com/user-attachments/assets/ad3cd33c-e9f0-4c30-8874-d4507892fc6b" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9338-feat-App-mode-saving-rework-3176d73d3650813f9ae1f6c5a234da8c) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,32 +1,18 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { AppMode } from '@/composables/useAppMode'
|
||||
import type { LoadedComfyWorkflow } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import { ComfyWorkflow as ComfyWorkflowClass } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
|
||||
const modeRef = ref<AppMode>('graph')
|
||||
const mockSetMode = vi.fn()
|
||||
|
||||
vi.mock('@/composables/useAppMode', () => ({
|
||||
useAppMode: () => ({
|
||||
mode: computed(() => modeRef.value),
|
||||
setMode: mockSetMode,
|
||||
isBuilderMode: computed(
|
||||
() =>
|
||||
modeRef.value === 'builder:select' ||
|
||||
modeRef.value === 'builder:arrange'
|
||||
),
|
||||
isAppMode: computed(
|
||||
() => modeRef.value === 'app' || modeRef.value === 'builder:arrange'
|
||||
),
|
||||
isSelectMode: computed(() => modeRef.value === 'builder:select'),
|
||||
isArrangeMode: computed(() => modeRef.value === 'builder:arrange'),
|
||||
isGraphMode: computed(
|
||||
() => modeRef.value === 'graph' || modeRef.value === 'builder:select'
|
||||
)
|
||||
})
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
rootGraph: { extra: {} }
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
@@ -35,56 +21,143 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: null
|
||||
import { useAppModeStore } from './appModeStore'
|
||||
|
||||
function createBuilderWorkflow(
|
||||
activeMode: string = 'builder:select'
|
||||
): LoadedComfyWorkflow {
|
||||
const workflow = new ComfyWorkflowClass({
|
||||
path: 'workflows/test.json',
|
||||
modified: Date.now(),
|
||||
size: 100
|
||||
})
|
||||
}))
|
||||
workflow.changeTracker = createMockChangeTracker()
|
||||
workflow.content = '{}'
|
||||
workflow.originalContent = '{}'
|
||||
workflow.activeMode = activeMode as LoadedComfyWorkflow['activeMode']
|
||||
return workflow as LoadedComfyWorkflow
|
||||
}
|
||||
|
||||
describe('appModeStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
modeRef.value = 'graph'
|
||||
mockSetMode.mockClear()
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(app.rootGraph).extra = {}
|
||||
})
|
||||
|
||||
describe('enterBuilder', () => {
|
||||
it('navigates to builder:arrange when in app mode with outputs', () => {
|
||||
modeRef.value = 'app'
|
||||
const workflowStore = useWorkflowStore()
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('app')
|
||||
|
||||
const store = useAppModeStore()
|
||||
store.selectedOutputs.push('1')
|
||||
store.selectedOutputs.push(1)
|
||||
|
||||
store.enterBuilder()
|
||||
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:arrange')
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:arrange')
|
||||
})
|
||||
|
||||
it('navigates to builder:select when in app mode without outputs', () => {
|
||||
modeRef.value = 'app'
|
||||
const workflowStore = useWorkflowStore()
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('app')
|
||||
|
||||
const store = useAppModeStore()
|
||||
|
||||
store.enterBuilder()
|
||||
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:select')
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
|
||||
})
|
||||
|
||||
it('navigates to builder:select when in graph mode with outputs', () => {
|
||||
modeRef.value = 'graph'
|
||||
const workflowStore = useWorkflowStore()
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
|
||||
|
||||
const store = useAppModeStore()
|
||||
store.selectedOutputs.push('1')
|
||||
store.selectedOutputs.push(1)
|
||||
|
||||
store.enterBuilder()
|
||||
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:select')
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
|
||||
})
|
||||
|
||||
it('navigates to builder:select when in graph mode without outputs', () => {
|
||||
modeRef.value = 'graph'
|
||||
const workflowStore = useWorkflowStore()
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
|
||||
|
||||
const store = useAppModeStore()
|
||||
|
||||
store.enterBuilder()
|
||||
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:select')
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
|
||||
})
|
||||
})
|
||||
|
||||
describe('linearData sync watcher', () => {
|
||||
it('writes linearData to rootGraph.extra when in builder mode', async () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow()
|
||||
await nextTick()
|
||||
|
||||
store.selectedOutputs.push(1)
|
||||
await nextTick()
|
||||
|
||||
expect(app.rootGraph.extra.linearData).toEqual({
|
||||
inputs: [],
|
||||
outputs: [1]
|
||||
})
|
||||
})
|
||||
|
||||
it('does not write linearData when not in builder mode', async () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
const workflow = createBuilderWorkflow()
|
||||
workflow.activeMode = 'graph'
|
||||
workflowStore.activeWorkflow = workflow
|
||||
await nextTick()
|
||||
|
||||
store.selectedOutputs.push(1)
|
||||
await nextTick()
|
||||
|
||||
expect(app.rootGraph.extra.linearData).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not write when rootGraph is null', async () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow()
|
||||
await nextTick()
|
||||
|
||||
const originalRootGraph = app.rootGraph
|
||||
Object.defineProperty(app, 'rootGraph', { value: null, writable: true })
|
||||
|
||||
store.selectedOutputs.push(1)
|
||||
await nextTick()
|
||||
|
||||
Object.defineProperty(app, 'rootGraph', {
|
||||
value: originalRootGraph,
|
||||
writable: true
|
||||
})
|
||||
})
|
||||
|
||||
it('reflects input changes in linearData', async () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow()
|
||||
await nextTick()
|
||||
|
||||
store.selectedInputs.push([42, 'prompt'])
|
||||
await nextTick()
|
||||
|
||||
expect(app.rootGraph.extra.linearData).toEqual({
|
||||
inputs: [[42, 'prompt']],
|
||||
outputs: []
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user