mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
[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:
@@ -13,10 +13,7 @@
|
|||||||
severity="danger"
|
severity="danger"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template v-if="isUserBlueprint" #actions>
|
||||||
v-if="nodeDef.name.startsWith(useSubgraphStore().typePrefix)"
|
|
||||||
#actions
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
@@ -128,8 +125,18 @@ const editBlueprint = async () => {
|
|||||||
await useSubgraphStore().editBlueprint(props.node.data.name)
|
await useSubgraphStore().editBlueprint(props.node.data.name)
|
||||||
}
|
}
|
||||||
const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
|
const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||||
|
const subgraphStore = useSubgraphStore()
|
||||||
|
const isUserBlueprint = computed(() => {
|
||||||
|
const name = nodeDef.value.name
|
||||||
|
if (!name.startsWith(subgraphStore.typePrefix)) return false
|
||||||
|
return !subgraphStore.isGlobalBlueprint(
|
||||||
|
name.slice(subgraphStore.typePrefix.length)
|
||||||
|
)
|
||||||
|
})
|
||||||
const menuItems = computed<MenuItem[]>(() => {
|
const menuItems = computed<MenuItem[]>(() => {
|
||||||
const items: MenuItem[] = [
|
if (!isUserBlueprint.value) return []
|
||||||
|
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
label: t('g.delete'),
|
label: t('g.delete'),
|
||||||
icon: 'pi pi-trash',
|
icon: 'pi pi-trash',
|
||||||
@@ -137,15 +144,14 @@ const menuItems = computed<MenuItem[]>(() => {
|
|||||||
command: deleteBlueprint
|
command: deleteBlueprint
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
return items
|
|
||||||
})
|
})
|
||||||
function handleContextMenu(event: Event) {
|
function handleContextMenu(event: Event) {
|
||||||
if (!nodeDef.value.name.startsWith(useSubgraphStore().typePrefix)) return
|
if (!isUserBlueprint.value) return
|
||||||
menu.value?.show(event)
|
menu.value?.show(event)
|
||||||
}
|
}
|
||||||
function deleteBlueprint() {
|
function deleteBlueprint() {
|
||||||
if (!props.node.data) return
|
if (!props.node.data) return
|
||||||
void useSubgraphStore().deleteBlueprint(props.node.data.name)
|
void subgraphStore.deleteBlueprint(props.node.data.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodePreviewStyle = ref<CSSProperties>({
|
const nodePreviewStyle = ref<CSSProperties>({
|
||||||
|
|||||||
@@ -993,7 +993,8 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"hidden": "Hidden / nested parameters",
|
"hidden": "Hidden / nested parameters",
|
||||||
"hideAll": "Hide all",
|
"hideAll": "Hide all",
|
||||||
"showRecommended": "Show recommended widgets"
|
"showRecommended": "Show recommended widgets",
|
||||||
|
"cannotDeleteGlobal": "Cannot delete installed blueprints"
|
||||||
},
|
},
|
||||||
"electronFileDownload": {
|
"electronFileDownload": {
|
||||||
"inProgress": "In Progress",
|
"inProgress": "In Progress",
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ interface SubgraphDefinitionBase<
|
|||||||
id: string
|
id: string
|
||||||
revision: number
|
revision: number
|
||||||
name: string
|
name: string
|
||||||
|
category?: string
|
||||||
|
|
||||||
inputNode: T extends ComfyWorkflow1BaseInput
|
inputNode: T extends ComfyWorkflow1BaseInput
|
||||||
? z.input<typeof zExportedSubgraphIONode>
|
? z.input<typeof zExportedSubgraphIONode>
|
||||||
@@ -425,6 +426,7 @@ const zSubgraphDefinition = zComfyWorkflow1
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
revision: z.number(),
|
revision: z.number(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
category: z.string().optional(),
|
||||||
inputNode: zExportedSubgraphIONode,
|
inputNode: zExportedSubgraphIONode,
|
||||||
outputNode: zExportedSubgraphIONode,
|
outputNode: zExportedSubgraphIONode,
|
||||||
|
|
||||||
|
|||||||
@@ -231,7 +231,10 @@ type ComplexApiEvents = keyof NeverNever<ApiEventTypes>
|
|||||||
|
|
||||||
export type GlobalSubgraphData = {
|
export type GlobalSubgraphData = {
|
||||||
name: string
|
name: string
|
||||||
info: { node_pack: string }
|
info: {
|
||||||
|
node_pack: string
|
||||||
|
category?: string
|
||||||
|
}
|
||||||
data: string | Promise<string>
|
data: string | Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createPinia, setActivePinia } from 'pinia'
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||||
|
import type { GlobalSubgraphData } from '@/scripts/api'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
import { useLitegraphService } from '@/services/litegraphService'
|
import { useLitegraphService } from '@/services/litegraphService'
|
||||||
@@ -24,6 +25,7 @@ vi.mock('@/scripts/api', () => ({
|
|||||||
getUserData: vi.fn(),
|
getUserData: vi.fn(),
|
||||||
storeUserData: vi.fn(),
|
storeUserData: vi.fn(),
|
||||||
listUserDataFullInfo: vi.fn(),
|
listUserDataFullInfo: vi.fn(),
|
||||||
|
getGlobalSubgraphs: vi.fn(),
|
||||||
apiURL: vi.fn(),
|
apiURL: vi.fn(),
|
||||||
addEventListener: vi.fn()
|
addEventListener: vi.fn()
|
||||||
}
|
}
|
||||||
@@ -59,7 +61,10 @@ const mockGraph = {
|
|||||||
|
|
||||||
describe('useSubgraphStore', () => {
|
describe('useSubgraphStore', () => {
|
||||||
let store: ReturnType<typeof 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(
|
vi.mocked(api.listUserDataFullInfo).mockResolvedValue(
|
||||||
Object.keys(filenames).map((filename) => ({
|
Object.keys(filenames).map((filename) => ({
|
||||||
path: filename,
|
path: filename,
|
||||||
@@ -67,13 +72,13 @@ describe('useSubgraphStore', () => {
|
|||||||
size: 1 // size !== -1 for remote workflows
|
size: 1 // size !== -1 for remote workflows
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
vi.mocked(api).getUserData = vi.fn(
|
vi.mocked(api).getUserData = vi.fn((f) =>
|
||||||
(f) =>
|
Promise.resolve({
|
||||||
({
|
status: 200,
|
||||||
status: 200,
|
text: () => Promise.resolve(JSON.stringify(filenames[f.slice(10)]))
|
||||||
text: () => JSON.stringify(filenames[f.slice(10)])
|
} as Response)
|
||||||
}) as any
|
|
||||||
)
|
)
|
||||||
|
vi.mocked(api.getGlobalSubgraphs).mockResolvedValue(globalSubgraphs)
|
||||||
return await store.fetchSubgraphs()
|
return await store.fetchSubgraphs()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +118,7 @@ describe('useSubgraphStore', () => {
|
|||||||
await mockFetch({ 'test.json': mockGraph })
|
await mockFetch({ 'test.json': mockGraph })
|
||||||
expect(
|
expect(
|
||||||
useNodeDefStore().nodeDefs.filter(
|
useNodeDefStore().nodeDefs.filter(
|
||||||
(d) => d.category == 'Subgraph Blueprints'
|
(d) => d.category === 'Subgraph Blueprints/User'
|
||||||
)
|
)
|
||||||
).toHaveLength(1)
|
).toHaveLength(1)
|
||||||
})
|
})
|
||||||
@@ -131,4 +136,25 @@ describe('useSubgraphStore', () => {
|
|||||||
} as ComfyNodeDefV1)
|
} as ComfyNodeDefV1)
|
||||||
expect(res).toBeTruthy()
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
|||||||
this.hasPromptedSave = true
|
this.hasPromptedSave = true
|
||||||
}
|
}
|
||||||
const ret = await super.save()
|
const ret = await super.save()
|
||||||
useSubgraphStore().updateDef(await this.load())
|
registerNodeDef(await this.load(), {
|
||||||
|
category: 'Subgraph Blueprints/User'
|
||||||
|
})
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +106,9 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
|||||||
this.validateSubgraph()
|
this.validateSubgraph()
|
||||||
this.hasPromptedSave = true
|
this.hasPromptedSave = true
|
||||||
const ret = await super.saveAs(path)
|
const ret = await super.saveAs(path)
|
||||||
useSubgraphStore().updateDef(await this.load())
|
registerNodeDef(await this.load(), {
|
||||||
|
category: 'Subgraph Blueprints/User'
|
||||||
|
})
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
override async load({ force = false }: { force?: boolean } = {}): Promise<
|
override async load({ force = false }: { force?: boolean } = {}): Promise<
|
||||||
@@ -151,7 +155,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
|||||||
options.path = SubgraphBlueprint.basePath + options.path
|
options.path = SubgraphBlueprint.basePath + options.path
|
||||||
const bp = await new SubgraphBlueprint(options, true).load()
|
const bp = await new SubgraphBlueprint(options, true).load()
|
||||||
useWorkflowStore().attachWorkflow(bp)
|
useWorkflowStore().attachWorkflow(bp)
|
||||||
registerNodeDef(bp)
|
registerNodeDef(bp, { category: 'Subgraph Blueprints/User' })
|
||||||
}
|
}
|
||||||
async function loadInstalledBlueprints() {
|
async function loadInstalledBlueprints() {
|
||||||
async function loadGlobalBlueprint([k, v]: [string, GlobalSubgraphData]) {
|
async function loadGlobalBlueprint([k, v]: [string, GlobalSubgraphData]) {
|
||||||
@@ -165,11 +169,15 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
|||||||
blueprint.filename = v.name
|
blueprint.filename = v.name
|
||||||
useWorkflowStore().attachWorkflow(blueprint)
|
useWorkflowStore().attachWorkflow(blueprint)
|
||||||
const loaded = await blueprint.load()
|
const loaded = await blueprint.load()
|
||||||
|
const category = v.info.category
|
||||||
|
? `Subgraph Blueprints/${v.info.category}`
|
||||||
|
: 'Subgraph Blueprints'
|
||||||
registerNodeDef(
|
registerNodeDef(
|
||||||
loaded,
|
loaded,
|
||||||
{
|
{
|
||||||
python_module: v.info.node_pack,
|
python_module: v.info.node_pack,
|
||||||
display_name: v.name
|
display_name: v.name,
|
||||||
|
category
|
||||||
},
|
},
|
||||||
k
|
k
|
||||||
)
|
)
|
||||||
@@ -284,7 +292,6 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
|||||||
await workflow.save()
|
await workflow.save()
|
||||||
//add to files list?
|
//add to files list?
|
||||||
useWorkflowStore().attachWorkflow(loadedWorkflow)
|
useWorkflowStore().attachWorkflow(loadedWorkflow)
|
||||||
registerNodeDef(loadedWorkflow)
|
|
||||||
useToastStore().add({
|
useToastStore().add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: t('subgraphStore.publishSuccess'),
|
summary: t('subgraphStore.publishSuccess'),
|
||||||
@@ -292,9 +299,6 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
|||||||
life: 4000
|
life: 4000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function updateDef(blueprint: LoadedComfyWorkflow) {
|
|
||||||
registerNodeDef(blueprint)
|
|
||||||
}
|
|
||||||
async function editBlueprint(nodeType: string) {
|
async function editBlueprint(nodeType: string) {
|
||||||
const name = nodeType.slice(typePrefix.length)
|
const name = nodeType.slice(typePrefix.length)
|
||||||
if (!(name in subgraphCache))
|
if (!(name in subgraphCache))
|
||||||
@@ -315,9 +319,17 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
|||||||
}
|
}
|
||||||
async function deleteBlueprint(nodeType: string) {
|
async function deleteBlueprint(nodeType: string) {
|
||||||
const name = nodeType.slice(typePrefix.length)
|
const name = nodeType.slice(typePrefix.length)
|
||||||
if (!(name in subgraphCache))
|
if (!(name in subgraphCache)) throw new Error('not yet loaded')
|
||||||
//As loading is blocked on in startup, this can likely be changed to invalid type
|
|
||||||
throw new Error('not yet loaded')
|
if (isGlobalBlueprint(name)) {
|
||||||
|
useToastStore().add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('subgraphStore.cannotDeleteGlobal'),
|
||||||
|
life: 4000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!(await useDialogService().confirm({
|
!(await useDialogService().confirm({
|
||||||
title: t('subgraphStore.confirmDeleteTitle'),
|
title: t('subgraphStore.confirmDeleteTitle'),
|
||||||
@@ -338,15 +350,20 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
|||||||
return workflow instanceof SubgraphBlueprint
|
return workflow instanceof SubgraphBlueprint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGlobalBlueprint(name: string): boolean {
|
||||||
|
const nodeDef = subgraphDefCache.value.get(name)
|
||||||
|
return nodeDef !== undefined && nodeDef.python_module !== 'blueprint'
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deleteBlueprint,
|
deleteBlueprint,
|
||||||
editBlueprint,
|
editBlueprint,
|
||||||
fetchSubgraphs,
|
fetchSubgraphs,
|
||||||
getBlueprint,
|
getBlueprint,
|
||||||
|
isGlobalBlueprint,
|
||||||
isSubgraphBlueprint,
|
isSubgraphBlueprint,
|
||||||
publishSubgraph,
|
publishSubgraph,
|
||||||
subgraphBlueprints,
|
subgraphBlueprints,
|
||||||
typePrefix,
|
typePrefix
|
||||||
updateDef
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user