mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
feat: Show empty workflow dialog when entering app builder with no nodes (#9379)
## Summary Prompts users to load a template or return to graph when entering builder mode on an empty workflow ## Screenshots (if applicable) <img width="627" height="275" alt="image" src="https://github.com/user-attachments/assets/c1a35dc3-4e8f-4abd-95b9-2f92524e8ebf" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9379-feat-Show-empty-workflow-dialog-when-entering-app-builder-with-no-nodes-3196d73d36508123b643ec893cd86cac) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -3,17 +3,29 @@ import { setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
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 type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
const mockEmptyWorkflowDialog = vi.hoisted(() => {
|
||||
let lastOptions: { onEnterBuilder: () => void; onDismiss: () => void }
|
||||
return {
|
||||
show: vi.fn((options: typeof lastOptions) => {
|
||||
lastOptions = options
|
||||
}),
|
||||
get lastOptions() {
|
||||
return lastOptions
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
rootGraph: { extra: {} }
|
||||
rootGraph: { extra: {}, nodes: [{ id: 1 }] }
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -31,6 +43,10 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/components/builder/useEmptyWorkflowDialog', () => ({
|
||||
useEmptyWorkflowDialog: () => mockEmptyWorkflowDialog
|
||||
}))
|
||||
|
||||
import { useAppModeStore } from './appModeStore'
|
||||
|
||||
function createBuilderWorkflow(
|
||||
@@ -49,19 +65,22 @@ function createBuilderWorkflow(
|
||||
}
|
||||
|
||||
describe('appModeStore', () => {
|
||||
let workflowStore: ReturnType<typeof useWorkflowStore>
|
||||
let store: ReturnType<typeof useAppModeStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(app.rootGraph).extra = {}
|
||||
mockResolveNode.mockReturnValue(undefined)
|
||||
vi.mocked(app.rootGraph).nodes = [{ id: 1 } as LGraphNode]
|
||||
workflowStore = useWorkflowStore()
|
||||
store = useAppModeStore()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('enterBuilder', () => {
|
||||
it('navigates to builder:arrange when in app mode with outputs', () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('app')
|
||||
|
||||
const store = useAppModeStore()
|
||||
store.selectedOutputs.push(1)
|
||||
|
||||
store.enterBuilder()
|
||||
@@ -70,21 +89,15 @@ describe('appModeStore', () => {
|
||||
})
|
||||
|
||||
it('navigates to builder:select when in app mode without outputs', () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('app')
|
||||
|
||||
const store = useAppModeStore()
|
||||
|
||||
store.enterBuilder()
|
||||
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
|
||||
})
|
||||
|
||||
it('navigates to builder:select when in graph mode with outputs', () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
|
||||
|
||||
const store = useAppModeStore()
|
||||
store.selectedOutputs.push(1)
|
||||
|
||||
store.enterBuilder()
|
||||
@@ -93,15 +106,67 @@ describe('appModeStore', () => {
|
||||
})
|
||||
|
||||
it('navigates to builder:select when in graph mode without outputs', () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
|
||||
|
||||
const store = useAppModeStore()
|
||||
|
||||
store.enterBuilder()
|
||||
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
|
||||
})
|
||||
|
||||
it('shows empty workflow dialog when graph has no nodes', () => {
|
||||
vi.mocked(app.rootGraph).nodes = []
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
|
||||
|
||||
store.enterBuilder()
|
||||
|
||||
expect(mockEmptyWorkflowDialog.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onEnterBuilder: expect.any(Function),
|
||||
onDismiss: expect.any(Function)
|
||||
})
|
||||
)
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('graph')
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty workflow dialog callbacks', () => {
|
||||
function getDialogOptions() {
|
||||
vi.mocked(app.rootGraph).nodes = []
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
|
||||
store.enterBuilder()
|
||||
return mockEmptyWorkflowDialog.lastOptions
|
||||
}
|
||||
|
||||
it('onDismiss sets graph mode', () => {
|
||||
const options = getDialogOptions()
|
||||
|
||||
// Move to builder so onDismiss must actually transition back
|
||||
workflowStore.activeWorkflow!.activeMode = 'builder:select'
|
||||
|
||||
options.onDismiss()
|
||||
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('graph')
|
||||
})
|
||||
|
||||
it('onEnterBuilder enters builder when nodes exist', () => {
|
||||
const options = getDialogOptions()
|
||||
|
||||
// Simulate template having loaded nodes
|
||||
vi.mocked(app.rootGraph).nodes = [{ id: 1 } as LGraphNode]
|
||||
|
||||
options.onEnterBuilder()
|
||||
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
|
||||
})
|
||||
|
||||
it('onEnterBuilder shows dialog again when no nodes', () => {
|
||||
const options = getDialogOptions()
|
||||
|
||||
mockEmptyWorkflowDialog.show.mockClear()
|
||||
options.onEnterBuilder()
|
||||
|
||||
expect(mockEmptyWorkflowDialog.show).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadSelections pruning', () => {
|
||||
@@ -135,9 +200,6 @@ describe('appModeStore', () => {
|
||||
id == 1 ? (node1 as unknown as LGraphNode) : undefined
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = workflowWithLinearData(
|
||||
[
|
||||
[1, 'prompt'],
|
||||
@@ -156,9 +218,6 @@ describe('appModeStore', () => {
|
||||
id == 1 ? (node1 as unknown as LGraphNode) : undefined
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = workflowWithLinearData(
|
||||
[
|
||||
[1, 'prompt'],
|
||||
@@ -180,9 +239,6 @@ describe('appModeStore', () => {
|
||||
id == 1 ? (node1 as unknown as LGraphNode) : undefined
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = workflowWithLinearData([], [1, 99])
|
||||
await nextTick()
|
||||
|
||||
@@ -192,9 +248,6 @@ describe('appModeStore', () => {
|
||||
it('hasOutputs is false when all output nodes are deleted', async () => {
|
||||
mockResolveNode.mockReturnValue(undefined)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = workflowWithLinearData([], [10, 20])
|
||||
await nextTick()
|
||||
|
||||
@@ -205,9 +258,6 @@ describe('appModeStore', () => {
|
||||
|
||||
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()
|
||||
|
||||
@@ -221,12 +271,7 @@ describe('appModeStore', () => {
|
||||
})
|
||||
|
||||
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
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
|
||||
await nextTick()
|
||||
|
||||
store.selectedOutputs.push(1)
|
||||
@@ -236,9 +281,6 @@ describe('appModeStore', () => {
|
||||
})
|
||||
|
||||
it('does not write when rootGraph is null', async () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow()
|
||||
await nextTick()
|
||||
|
||||
@@ -255,9 +297,6 @@ describe('appModeStore', () => {
|
||||
})
|
||||
|
||||
it('reflects input changes in linearData', async () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow()
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive, computed, watch } from 'vue'
|
||||
|
||||
import { useEmptyWorkflowDialog } from '@/components/builder/useEmptyWorkflowDialog'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LinearData } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
@@ -13,6 +14,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
const { getCanvas } = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { mode, setMode, isBuilderMode } = useAppMode()
|
||||
const emptyWorkflowDialog = useEmptyWorkflowDialog()
|
||||
|
||||
const selectedInputs = reactive<[NodeId, string][]>([])
|
||||
const selectedOutputs = reactive<NodeId[]>([])
|
||||
@@ -92,6 +94,14 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
)
|
||||
|
||||
function enterBuilder() {
|
||||
if (!app.rootGraph?.nodes?.length) {
|
||||
emptyWorkflowDialog.show({
|
||||
onEnterBuilder: () => enterBuilder(),
|
||||
onDismiss: () => setMode('graph')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setMode(
|
||||
mode.value === 'app' && hasOutputs.value
|
||||
? 'builder:arrange'
|
||||
|
||||
Reference in New Issue
Block a user