[backport cloud/1.38] feat: add category support for blueprints and protect global blueprints (#8466)

Backport of #8378 to `cloud/1.38`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8466-backport-cloud-1-38-feat-add-category-support-for-blueprints-and-protect-global-bluepr-2f86d73d3650814da4b4d8d773d2ffe5)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
This commit is contained in:
Comfy Org PR Bot
2026-01-30 12:15:47 +09:00
committed by GitHub
parent a5577a7f45
commit 1631f22efb
6 changed files with 86 additions and 31 deletions

View File

@@ -2,6 +2,7 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import type { GlobalSubgraphData } from '@/scripts/api'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { useLitegraphService } from '@/services/litegraphService'
@@ -24,6 +25,7 @@ vi.mock('@/scripts/api', () => ({
getUserData: vi.fn(),
storeUserData: vi.fn(),
listUserDataFullInfo: vi.fn(),
getGlobalSubgraphs: vi.fn(),
apiURL: vi.fn(),
addEventListener: vi.fn()
}
@@ -59,7 +61,10 @@ const mockGraph = {
describe('useSubgraphStore', () => {
let store: ReturnType<typeof useSubgraphStore>
const mockFetch = async (filenames: Record<string, unknown>) => {
async function mockFetch(
filenames: Record<string, unknown>,
globalSubgraphs: Record<string, GlobalSubgraphData> = {}
) {
vi.mocked(api.listUserDataFullInfo).mockResolvedValue(
Object.keys(filenames).map((filename) => ({
path: filename,
@@ -67,13 +72,13 @@ describe('useSubgraphStore', () => {
size: 1 // size !== -1 for remote workflows
}))
)
vi.mocked(api).getUserData = vi.fn(
(f) =>
({
status: 200,
text: () => JSON.stringify(filenames[f.slice(10)])
}) as any
vi.mocked(api).getUserData = vi.fn((f) =>
Promise.resolve({
status: 200,
text: () => Promise.resolve(JSON.stringify(filenames[f.slice(10)]))
} as Response)
)
vi.mocked(api.getGlobalSubgraphs).mockResolvedValue(globalSubgraphs)
return await store.fetchSubgraphs()
}
@@ -113,7 +118,7 @@ describe('useSubgraphStore', () => {
await mockFetch({ 'test.json': mockGraph })
expect(
useNodeDefStore().nodeDefs.filter(
(d) => d.category == 'Subgraph Blueprints'
(d) => d.category === 'Subgraph Blueprints/User'
)
).toHaveLength(1)
})
@@ -131,4 +136,25 @@ describe('useSubgraphStore', () => {
} as ComfyNodeDefV1)
expect(res).toBeTruthy()
})
it('should identify user blueprints as non-global', async () => {
await mockFetch({ 'test.json': mockGraph })
expect(store.isGlobalBlueprint('test')).toBe(false)
})
it('should identify global blueprints loaded from getGlobalSubgraphs', async () => {
await mockFetch(
{},
{
global_test: {
name: 'Global Test Blueprint',
info: { node_pack: 'comfy_essentials' },
data: JSON.stringify(mockGraph)
}
}
)
expect(store.isGlobalBlueprint('global_test')).toBe(true)
})
it('should return false for non-existent blueprints', async () => {
await mockFetch({ 'test.json': mockGraph })
expect(store.isGlobalBlueprint('nonexistent')).toBe(false)
})
})

View File

@@ -96,7 +96,9 @@ export const useSubgraphStore = defineStore('subgraph', () => {
this.hasPromptedSave = true
}
const ret = await super.save()
useSubgraphStore().updateDef(await this.load())
registerNodeDef(await this.load(), {
category: 'Subgraph Blueprints/User'
})
return ret
}
@@ -104,7 +106,9 @@ export const useSubgraphStore = defineStore('subgraph', () => {
this.validateSubgraph()
this.hasPromptedSave = true
const ret = await super.saveAs(path)
useSubgraphStore().updateDef(await this.load())
registerNodeDef(await this.load(), {
category: 'Subgraph Blueprints/User'
})
return ret
}
override async load({ force = false }: { force?: boolean } = {}): Promise<
@@ -151,7 +155,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
options.path = SubgraphBlueprint.basePath + options.path
const bp = await new SubgraphBlueprint(options, true).load()
useWorkflowStore().attachWorkflow(bp)
registerNodeDef(bp)
registerNodeDef(bp, { category: 'Subgraph Blueprints/User' })
}
async function loadInstalledBlueprints() {
async function loadGlobalBlueprint([k, v]: [string, GlobalSubgraphData]) {
@@ -165,11 +169,15 @@ export const useSubgraphStore = defineStore('subgraph', () => {
blueprint.filename = v.name
useWorkflowStore().attachWorkflow(blueprint)
const loaded = await blueprint.load()
const category = v.info.category
? `Subgraph Blueprints/${v.info.category}`
: 'Subgraph Blueprints'
registerNodeDef(
loaded,
{
python_module: v.info.node_pack,
display_name: v.name
display_name: v.name,
category
},
k
)
@@ -284,7 +292,6 @@ export const useSubgraphStore = defineStore('subgraph', () => {
await workflow.save()
//add to files list?
useWorkflowStore().attachWorkflow(loadedWorkflow)
registerNodeDef(loadedWorkflow)
useToastStore().add({
severity: 'success',
summary: t('subgraphStore.publishSuccess'),
@@ -292,9 +299,6 @@ export const useSubgraphStore = defineStore('subgraph', () => {
life: 4000
})
}
function updateDef(blueprint: LoadedComfyWorkflow) {
registerNodeDef(blueprint)
}
async function editBlueprint(nodeType: string) {
const name = nodeType.slice(typePrefix.length)
if (!(name in subgraphCache))
@@ -315,9 +319,17 @@ export const useSubgraphStore = defineStore('subgraph', () => {
}
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 (!(name in subgraphCache)) throw new Error('not yet loaded')
if (isGlobalBlueprint(name)) {
useToastStore().add({
severity: 'warn',
summary: t('subgraphStore.cannotDeleteGlobal'),
life: 4000
})
return
}
if (
!(await useDialogService().confirm({
title: t('subgraphStore.confirmDeleteTitle'),
@@ -338,15 +350,20 @@ export const useSubgraphStore = defineStore('subgraph', () => {
return workflow instanceof SubgraphBlueprint
}
function isGlobalBlueprint(name: string): boolean {
const nodeDef = subgraphDefCache.value.get(name)
return nodeDef !== undefined && nodeDef.python_module !== 'blueprint'
}
return {
deleteBlueprint,
editBlueprint,
fetchSubgraphs,
getBlueprint,
isGlobalBlueprint,
isSubgraphBlueprint,
publishSubgraph,
subgraphBlueprints,
typePrefix,
updateDef
typePrefix
}
})