Skip to content

[Subgraph] Add subgraph store #3240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/stores/subgraphStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { LGraph } from '@comfyorg/litegraph'
import { Subgraph } from '@comfyorg/litegraph/dist/subgraphInterfaces'
import { whenever } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue'

import { app } from '@/scripts/app'
import { useWorkflowStore } from '@/stores/workflowStore'
import { isSubgraph } from '@/utils/typeGuardUtil'

const UNSAVED_WORKFLOW_NAME = 'Unsaved Workflow'

export const useSubgraphStore = defineStore('subgraph', () => {
const workflowStore = useWorkflowStore()

const activeGraph = shallowRef<Subgraph | LGraph | null>(null)
const activeRootGraphName = ref<string | null>(null)

const graphIdPath = ref<LGraph['id'][]>([])
const graphNamePath = ref<string[]>([])

const isSubgraphActive = computed(
() => activeGraph.value !== null && isSubgraph(activeGraph.value)
)

const updateActiveGraph = () => {
activeGraph.value = app?.graph
}

const updateRootGraphName = () => {
const isNewRoot = !isSubgraph(activeGraph.value)
if (!isNewRoot) return

const activeWorkflowName = workflowStore.activeWorkflow?.filename
activeRootGraphName.value = activeWorkflowName ?? UNSAVED_WORKFLOW_NAME
}

const updateGraphPaths = () => {
const currentGraph = app?.graph
if (!currentGraph) {
graphIdPath.value = []
graphNamePath.value = []
return
}

const { activeWorkflow } = workflowStore

const namePath: string[] = []
const idPath: LGraph['id'][] = []

let cur: LGraph | Subgraph | null = currentGraph
while (cur) {
const name = isSubgraph(cur)
? cur.name
: activeWorkflow?.filename ?? UNSAVED_WORKFLOW_NAME

namePath.unshift(name)
idPath.unshift(cur.id)

cur = isSubgraph(cur) ? cur.parent : null
}

graphIdPath.value = idPath
graphNamePath.value = namePath
}

whenever(() => app?.graph, updateActiveGraph, {
immediate: true,
once: true
})
whenever(() => workflowStore.activeWorkflow, updateActiveGraph)
whenever(activeGraph, () => {
updateRootGraphName()
updateGraphPaths()
})

return {
graphIdPath,
graphNamePath,
isSubgraphActive,

updateActiveGraph
}
})
6 changes: 5 additions & 1 deletion src/utils/typeGuardUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LGraphNode } from '@comfyorg/litegraph'
import { LGraph, LGraphNode } from '@comfyorg/litegraph'
import { Subgraph } from '@comfyorg/litegraph/dist/subgraphInterfaces'

import type { PrimitiveNode } from '@/extensions/core/widgetInputs'

Expand All @@ -16,3 +17,6 @@ export const isAbortError = (
err: unknown
): err is DOMException & { name: 'AbortError' } =>
err instanceof DOMException && err.name === 'AbortError'

export const isSubgraph = (item: LGraph | Subgraph | null): item is Subgraph =>
!!item && typeof item === 'object' && 'parent' in item
97 changes: 97 additions & 0 deletions tests-ui/tests/store/subgraphStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'

import { app } from '@/scripts/app'
import { useSubgraphStore } from '@/stores/subgraphStore'

vi.mock('@/scripts/app', () => ({
app: {
graph: null
}
}))

const mockWorkflowStore = {
activeWorkflow: {
filename: 'test.workflow',
path: 'test.workflow'
}
}

vi.mock('@/stores/workflowStore', () => ({
useWorkflowStore: vi.fn(() => mockWorkflowStore)
}))

describe('useSubgraphStore', () => {
let store: ReturnType<typeof useSubgraphStore>

beforeEach(() => {
setActivePinia(createPinia())
store = useSubgraphStore()
vi.clearAllMocks()
})

it('should initialize with default values', () => {
expect(store.graphIdPath).toEqual([])
expect(store.graphNamePath).toEqual([])
expect(store.isSubgraphActive).toBe(false)
})

describe('No Subgraphs exist', () => {
it('should update paths when active workflow changes', async () => {
const mockName = 'Not a Subgraph'
const mockId = 'this-is-not-a-subgraph'
const mockRootGraph = {
id: mockId
// Non-subgraph does not have `parent` or a `name` properties for now
}

mockWorkflowStore.activeWorkflow.filename = mockName
mockWorkflowStore.activeWorkflow.path = mockId
vi.spyOn(app, 'graph', 'get').mockReturnValue(mockRootGraph as any)

store.updateActiveGraph()
await nextTick()

expect(store.graphIdPath).toEqual([mockId])
expect(store.graphNamePath).toEqual([mockName])
})
})

describe('Subgraphs exist', () => {
it('should update paths when active workflow changes', async () => {
const mockSubgraph = {
id: 'subgraph-2',
name: 'Subgraph 2',
parent: {
id: 'subgraph-1',
name: 'Subgraph 1',
parent: {
id: 'root-graph'
}
}
}

// Update the active workflow name
mockWorkflowStore.activeWorkflow.filename = 'Root Graph'

// Mock the app.graph getter
vi.spyOn(app, 'graph', 'get').mockReturnValue(mockSubgraph as any)

// Trigger the update
store.updateActiveGraph()
await nextTick()

expect(store.graphIdPath).toEqual([
'root-graph',
'subgraph-1',
'subgraph-2'
])
expect(store.graphNamePath).toEqual([
'Root Graph',
'Subgraph 1',
'Subgraph 2'
])
})
})
})