diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png index 37ceaa2a9..89f053aff 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png differ diff --git a/src/lib/litegraph/src/types/serialisation.ts b/src/lib/litegraph/src/types/serialisation.ts index c97293829..f34e9fcf8 100644 --- a/src/lib/litegraph/src/types/serialisation.ts +++ b/src/lib/litegraph/src/types/serialisation.ts @@ -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. */ diff --git a/src/services/nodeOrganizationService.ts b/src/services/nodeOrganizationService.ts index 044e3dea1..6f6746557 100644 --- a/src/services/nodeOrganizationService.ts +++ b/src/services/nodeOrganizationService.ts @@ -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 + 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 }] } diff --git a/src/stores/subgraphStore.test.ts b/src/stores/subgraphStore.test.ts index a354a1996..5cdc1a8fc 100644 --- a/src/stores/subgraphStore.test.ts +++ b/src/stores/subgraphStore.test.ts @@ -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 = {} diff --git a/src/stores/subgraphStore.ts b/src/stores/subgraphStore.ts index 7bf3b078f..752ed957f 100644 --- a/src/stores/subgraphStore.ts +++ b/src/stores/subgraphStore.ts @@ -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, diff --git a/src/types/nodeSource.ts b/src/types/nodeSource.ts index 2db509db4..333fd7bf8 100644 --- a/src/types/nodeSource.ts +++ b/src/types/nodeSource.ts @@ -35,7 +35,7 @@ const ESSENTIALS_CATEGORY_MOCK: Record = { SaveVideo: 'basics', Load3D: 'basics', SaveGLB: 'basics', - CLIPTextEncode: 'basics', + PrimitiveStringMultiline: 'basics', // image tools ImageBatch: 'image tools', ImageCrop: 'image tools',