mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-22 07:44:11 +00:00
feat: read category from blueprint subgraph definition (#9053)
Read `category` from `definitions.subgraphs[0].category` in blueprint JSON files as a fallback default for node categorization. This allows blueprint authors to set the category directly in the blueprint file without needing backend `index.json` support. The precedence order is: 1. Explicit overrides (e.g. `info.category` from API, or `'Subgraph Blueprints/User'` for user blueprints) 2. `definitions.subgraphs[0].category` from the blueprint JSON content 3. Bare `'Subgraph Blueprints'` fallback Companion PR: Comfy-Org/ComfyUI#12552 (adds essential blueprints with categories matching the Figma design) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9053-feat-read-category-from-blueprint-subgraph-definition-30e6d73d3650810ca23bfc5a1e97cb31) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
@@ -137,6 +137,8 @@ export interface ISerialisedGraph extends BaseExportedGraph {
|
||||
export interface ExportedSubgraph extends SerialisableGraph {
|
||||
/** The display name of the subgraph. */
|
||||
name: string
|
||||
/** Optional category for organizing subgraph blueprints in the node library. */
|
||||
category?: string
|
||||
inputNode: ExportedSubgraphIONode
|
||||
outputNode: ExportedSubgraphIONode
|
||||
/** Ordered list of inputs to the subgraph itself. Similar to a reroute, with the input side in the graph, and the output side in the subgraph. */
|
||||
|
||||
@@ -14,6 +14,46 @@ import { upperCase } from 'es-toolkit/string'
|
||||
|
||||
const DEFAULT_ICON = 'pi pi-sort'
|
||||
|
||||
const NODE_ORDER_BY_FOLDER = {
|
||||
basics: [
|
||||
'LoadImage',
|
||||
'LoadVideo',
|
||||
'Load3D',
|
||||
'SaveImage',
|
||||
'SaveVideo',
|
||||
'SaveGLB',
|
||||
'PrimitiveStringMultiline',
|
||||
'PreviewImage'
|
||||
],
|
||||
'image tools': [
|
||||
'ImageBatch',
|
||||
'ImageCrop',
|
||||
'ImageCropV2',
|
||||
'ImageScale',
|
||||
'ImageScaleBy',
|
||||
'ImageRotate',
|
||||
'ImageBlur',
|
||||
'ImageBlend',
|
||||
'ImageInvert',
|
||||
'Canny',
|
||||
'RecraftRemoveBackgroundNode',
|
||||
'LoadImageMask'
|
||||
],
|
||||
'video tools': ['GetVideoComponents', 'CreateVideo'],
|
||||
'image generation': [
|
||||
'LoraLoader',
|
||||
'LoraLoaderModelOnly',
|
||||
'ConditioningCombine'
|
||||
],
|
||||
audio: [
|
||||
'LoadAudio',
|
||||
'SaveAudio',
|
||||
'SaveAudioMP3',
|
||||
'StabilityTextToAudio',
|
||||
'EmptyLatentAudio'
|
||||
]
|
||||
} as const satisfies Record<string, readonly string[]>
|
||||
|
||||
export const DEFAULT_GROUPING_ID = 'category' as const
|
||||
export const DEFAULT_SORTING_ID = 'original' as const
|
||||
export const DEFAULT_TAB_ID = 'all' as const
|
||||
@@ -160,6 +200,25 @@ class NodeOrganizationService {
|
||||
const orderB = bi === -1 ? len + originalIndex.get(b)! : bi
|
||||
return orderA - orderB
|
||||
})
|
||||
for (const folder of tree.children) {
|
||||
if (!folder.children) continue
|
||||
const order =
|
||||
NODE_ORDER_BY_FOLDER[
|
||||
folder.label as keyof typeof NODE_ORDER_BY_FOLDER
|
||||
]
|
||||
if (!order) continue
|
||||
const nodeOrder: readonly string[] = order
|
||||
const orderLen = nodeOrder.length
|
||||
folder.children.sort((a, b) => {
|
||||
const nameA = a.data?.name ?? a.label ?? ''
|
||||
const nameB = b.data?.name ?? b.label ?? ''
|
||||
const ai = nodeOrder.indexOf(nameA)
|
||||
const bi = nodeOrder.indexOf(nameB)
|
||||
const orderA = ai === -1 ? orderLen : ai
|
||||
const orderB = bi === -1 ? orderLen : bi
|
||||
return orderA - orderB
|
||||
})
|
||||
}
|
||||
}
|
||||
return [{ tree }]
|
||||
}
|
||||
|
||||
@@ -363,6 +363,96 @@ describe('useSubgraphStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('subgraph definition category', () => {
|
||||
it('should use category from subgraph definition as default', async () => {
|
||||
const mockGraphWithCategory = {
|
||||
nodes: [{ type: '123' }],
|
||||
definitions: {
|
||||
subgraphs: [{ id: '123', category: 'Image Processing' }]
|
||||
}
|
||||
}
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
categorized: {
|
||||
name: 'Categorized Blueprint',
|
||||
info: { node_pack: 'test_pack' },
|
||||
data: JSON.stringify(mockGraphWithCategory)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.categorized'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.category).toBe('Subgraph Blueprints/Image Processing')
|
||||
})
|
||||
|
||||
it('should use User override for user blueprints even with definition category', async () => {
|
||||
const mockGraphWithCategory = {
|
||||
nodes: [{ type: '123' }],
|
||||
definitions: {
|
||||
subgraphs: [{ id: '123', category: 'Image Processing' }]
|
||||
}
|
||||
}
|
||||
await mockFetch({ 'user-bp.json': mockGraphWithCategory })
|
||||
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.user-bp'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.category).toBe('Subgraph Blueprints/User')
|
||||
})
|
||||
|
||||
it('should fallback to bare Subgraph Blueprints when no category anywhere', async () => {
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
no_cat_global: {
|
||||
name: 'No Category Global',
|
||||
info: { node_pack: 'test_pack' },
|
||||
data: JSON.stringify(mockGraph)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.no_cat_global'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.category).toBe('Subgraph Blueprints')
|
||||
})
|
||||
|
||||
it('should let overrides take precedence over definition category', async () => {
|
||||
const mockGraphWithCategory = {
|
||||
nodes: [{ type: '123' }],
|
||||
definitions: {
|
||||
subgraphs: [{ id: '123', category: 'Image Processing' }]
|
||||
}
|
||||
}
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
bp_override: {
|
||||
name: 'Override Blueprint',
|
||||
info: {
|
||||
node_pack: 'test_pack',
|
||||
category: 'Custom Category'
|
||||
},
|
||||
data: JSON.stringify(mockGraphWithCategory)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.bp_override'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.category).toBe('Subgraph Blueprints/Custom Category')
|
||||
})
|
||||
})
|
||||
|
||||
describe('global blueprint filtering', () => {
|
||||
function globalBlueprint(
|
||||
overrides: Partial<GlobalSubgraphData['info']> = {}
|
||||
|
||||
@@ -210,12 +210,12 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
const loaded = await blueprint.load()
|
||||
const category = v.info.category
|
||||
? `Subgraph Blueprints/${v.info.category}`
|
||||
: 'Subgraph Blueprints'
|
||||
: undefined
|
||||
registerNodeDef(
|
||||
loaded,
|
||||
{
|
||||
display_name: v.name,
|
||||
category,
|
||||
...(category && { category }),
|
||||
search_aliases: v.info.search_aliases,
|
||||
isGlobal: true
|
||||
},
|
||||
@@ -280,6 +280,11 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
const description =
|
||||
workflowExtra?.BlueprintDescription ?? 'User generated subgraph blueprint'
|
||||
const search_aliases = workflowExtra?.BlueprintSearchAliases
|
||||
const subgraphDefCategory =
|
||||
workflow.initialState.definitions?.subgraphs?.[0]?.category
|
||||
const category = subgraphDefCategory
|
||||
? `Subgraph Blueprints/${subgraphDefCategory}`
|
||||
: 'Subgraph Blueprints'
|
||||
const nodedefv1: ComfyNodeDefV1 = {
|
||||
input: { required: inputs },
|
||||
output: subgraphNode.outputs.map((o) => `${o.type}`),
|
||||
@@ -287,7 +292,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
name: typePrefix + name,
|
||||
display_name: name,
|
||||
description,
|
||||
category: 'Subgraph Blueprints',
|
||||
category,
|
||||
output_node: false,
|
||||
python_module: 'blueprint',
|
||||
search_aliases,
|
||||
|
||||
@@ -35,7 +35,7 @@ const ESSENTIALS_CATEGORY_MOCK: Record<string, string> = {
|
||||
SaveVideo: 'basics',
|
||||
Load3D: 'basics',
|
||||
SaveGLB: 'basics',
|
||||
CLIPTextEncode: 'basics',
|
||||
PrimitiveStringMultiline: 'basics',
|
||||
// image tools
|
||||
ImageBatch: 'image tools',
|
||||
ImageCrop: 'image tools',
|
||||
|
||||
Reference in New Issue
Block a user