mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-02 03:30:04 +00:00
feat: wire essentials_category for Essentials tab display (#9091)
## Summary Wire `essentials_category` through from backend to the Essentials tab UI. Creates a single source of truth for node categorization and ordering. ### Changes **New file — `src/constants/essentialsNodes.ts`:** - Single source of truth: `ESSENTIALS_NODES` (ordered nodes per category), `ESSENTIALS_CATEGORIES` (folder display order), `ESSENTIALS_CATEGORY_MAP` (flat lookup), `TOOLKIT_NOVEL_NODE_NAMES` (telemetry), `TOOLKIT_BLUEPRINT_MODULES` **Refactored files:** - `src/types/nodeSource.ts`: Removed inline `ESSENTIALS_CATEGORY_MOCK`, imports `ESSENTIALS_CATEGORY_MAP` from centralized constants - `src/services/nodeOrganizationService.ts`: Removed inline `NODE_ORDER_BY_FOLDER`, imports `ESSENTIALS_NODES` and `ESSENTIALS_CATEGORIES` - `src/constants/toolkitNodes.ts`: Re-exports from `essentialsNodes.ts` instead of maintaining a separate list **Subgraph passthrough:** - `src/stores/subgraphStore.ts`: Passes `essentials_category` from `GlobalSubgraphData` and extracts it from `definitions.subgraphs[0]` as fallback - `src/platform/workflow/validation/schemas/workflowSchema.ts`: Added `essentials_category` to `SubgraphDefinitionBase` and `zSubgraphDefinition` **Tests:** - `src/constants/essentialsNodes.test.ts`: 6 tests validating no duplicates, complete coverage, basics exclusion - `src/stores/subgraphStore.test.ts`: 2 tests for essentials_category passthrough All 43 relevant tests pass. Typecheck, lint, format clean. **Depends on:** Comfy-Org/ComfyUI#12573 Fixes COM-15221 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9091-feat-wire-essentials_category-for-Essentials-tab-display-30f6d73d3650814ab3d4c06b451c273b) 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: 43 KiB After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 42 KiB |
62
src/constants/essentialsNodes.test.ts
Normal file
62
src/constants/essentialsNodes.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ESSENTIALS_CATEGORIES,
|
||||
ESSENTIALS_CATEGORY_CANONICAL,
|
||||
ESSENTIALS_CATEGORY_MAP,
|
||||
ESSENTIALS_NODES,
|
||||
TOOLKIT_BLUEPRINT_MODULES,
|
||||
TOOLKIT_NOVEL_NODE_NAMES
|
||||
} from './essentialsNodes'
|
||||
|
||||
describe('essentialsNodes', () => {
|
||||
it('has no duplicate node names across categories', () => {
|
||||
const seen = new Map<string, string>()
|
||||
for (const [category, nodes] of Object.entries(ESSENTIALS_NODES)) {
|
||||
for (const node of nodes) {
|
||||
expect(
|
||||
seen.has(node),
|
||||
`"${node}" duplicated in "${category}" and "${seen.get(node)}"`
|
||||
).toBe(false)
|
||||
seen.set(node, category)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('ESSENTIALS_CATEGORY_MAP covers every node in ESSENTIALS_NODES', () => {
|
||||
for (const [category, nodes] of Object.entries(ESSENTIALS_NODES)) {
|
||||
for (const node of nodes) {
|
||||
expect(ESSENTIALS_CATEGORY_MAP[node]).toBe(category)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('TOOLKIT_NOVEL_NODE_NAMES excludes basics nodes', () => {
|
||||
for (const basicNode of ESSENTIALS_NODES.basics) {
|
||||
expect(TOOLKIT_NOVEL_NODE_NAMES.has(basicNode)).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('TOOLKIT_NOVEL_NODE_NAMES excludes SubgraphBlueprint-prefixed nodes', () => {
|
||||
for (const name of TOOLKIT_NOVEL_NODE_NAMES) {
|
||||
expect(name.startsWith('SubgraphBlueprint.')).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('ESSENTIALS_NODES keys match ESSENTIALS_CATEGORIES', () => {
|
||||
const nodeKeys = Object.keys(ESSENTIALS_NODES)
|
||||
expect(nodeKeys).toEqual([...ESSENTIALS_CATEGORIES])
|
||||
})
|
||||
|
||||
it('TOOLKIT_BLUEPRINT_MODULES contains comfy_essentials', () => {
|
||||
expect(TOOLKIT_BLUEPRINT_MODULES.has('comfy_essentials')).toBe(true)
|
||||
})
|
||||
|
||||
it('ESSENTIALS_CATEGORY_CANONICAL maps every category case-insensitively', () => {
|
||||
for (const category of ESSENTIALS_CATEGORIES) {
|
||||
expect(ESSENTIALS_CATEGORY_CANONICAL.get(category.toLowerCase())).toBe(
|
||||
category
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
115
src/constants/essentialsNodes.ts
Normal file
115
src/constants/essentialsNodes.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Single source of truth for Essentials tab node categorization and ordering.
|
||||
*
|
||||
* Adding a new node to the Essentials tab? Add it here and nowhere else.
|
||||
*
|
||||
* Source: https://www.notion.so/comfy-org/2fe6d73d365080d0a951d14cdf540778
|
||||
*/
|
||||
|
||||
export const ESSENTIALS_CATEGORIES = [
|
||||
'basics',
|
||||
'text generation',
|
||||
'image generation',
|
||||
'video generation',
|
||||
'image tools',
|
||||
'video tools',
|
||||
'audio',
|
||||
'3D'
|
||||
] as const
|
||||
|
||||
export type EssentialsCategory = (typeof ESSENTIALS_CATEGORIES)[number]
|
||||
|
||||
/**
|
||||
* Ordered list of nodes per category.
|
||||
* Array order = display order in the Essentials tab.
|
||||
* Presence in a category = the node's essentials_category mock fallback.
|
||||
*/
|
||||
export const ESSENTIALS_NODES: Record<EssentialsCategory, readonly string[]> = {
|
||||
basics: [
|
||||
'LoadImage',
|
||||
'LoadVideo',
|
||||
'Load3D',
|
||||
'SaveImage',
|
||||
'SaveVideo',
|
||||
'SaveGLB',
|
||||
'PrimitiveStringMultiline',
|
||||
'PreviewImage'
|
||||
],
|
||||
'text generation': ['OpenAIChatNode'],
|
||||
'image generation': [
|
||||
'LoraLoader',
|
||||
'LoraLoaderModelOnly',
|
||||
'ConditioningCombine'
|
||||
],
|
||||
'video generation': [
|
||||
'SubgraphBlueprint.pose_to_video_ltx_2_0',
|
||||
'SubgraphBlueprint.canny_to_video_ltx_2_0',
|
||||
'KlingLipSyncAudioToVideoNode',
|
||||
'KlingOmniProEditVideoNode'
|
||||
],
|
||||
'image tools': [
|
||||
'ImageBatch',
|
||||
'ImageCrop',
|
||||
'ImageCropV2',
|
||||
'ImageScale',
|
||||
'ImageScaleBy',
|
||||
'ImageRotate',
|
||||
'ImageBlur',
|
||||
'ImageBlend',
|
||||
'ImageInvert',
|
||||
'ImageCompare',
|
||||
'Canny',
|
||||
'RecraftRemoveBackgroundNode',
|
||||
'RecraftVectorizeImageNode',
|
||||
'LoadImageMask',
|
||||
'GLSLShader'
|
||||
],
|
||||
'video tools': ['GetVideoComponents', 'CreateVideo', 'Video Slice'],
|
||||
audio: [
|
||||
'LoadAudio',
|
||||
'SaveAudio',
|
||||
'SaveAudioMP3',
|
||||
'StabilityTextToAudio',
|
||||
'EmptyLatentAudio'
|
||||
],
|
||||
'3D': ['TencentTextToModelNode', 'TencentImageToModelNode']
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat map: node name → category (derived from ESSENTIALS_NODES).
|
||||
* Used as mock/fallback when backend doesn't provide essentials_category.
|
||||
*/
|
||||
export const ESSENTIALS_CATEGORY_MAP: Record<string, EssentialsCategory> =
|
||||
Object.fromEntries(
|
||||
Object.entries(ESSENTIALS_NODES).flatMap(([category, nodes]) =>
|
||||
nodes.map((node) => [node, category])
|
||||
)
|
||||
) as Record<string, EssentialsCategory>
|
||||
|
||||
/**
|
||||
* Case-insensitive lookup: lowercase category → canonical category.
|
||||
* Used to normalize backend categories (which may be title-cased) to the
|
||||
* canonical form used in ESSENTIALS_CATEGORIES.
|
||||
*/
|
||||
export const ESSENTIALS_CATEGORY_CANONICAL: ReadonlyMap<
|
||||
string,
|
||||
EssentialsCategory
|
||||
> = new Map(ESSENTIALS_CATEGORIES.map((c) => [c.toLowerCase(), c]))
|
||||
|
||||
/**
|
||||
* "Novel" toolkit nodes for telemetry — basics excluded.
|
||||
* Derived from ESSENTIALS_NODES minus the 'basics' category.
|
||||
*/
|
||||
export const TOOLKIT_NOVEL_NODE_NAMES: ReadonlySet<string> = new Set(
|
||||
Object.entries(ESSENTIALS_NODES)
|
||||
.filter(([cat]) => cat !== 'basics')
|
||||
.flatMap(([, nodes]) => nodes)
|
||||
.filter((n) => !n.startsWith('SubgraphBlueprint.'))
|
||||
)
|
||||
|
||||
/**
|
||||
* python_module values that identify toolkit blueprint nodes.
|
||||
*/
|
||||
export const TOOLKIT_BLUEPRINT_MODULES: ReadonlySet<string> = new Set([
|
||||
'comfy_essentials'
|
||||
])
|
||||
@@ -1,41 +1,10 @@
|
||||
/**
|
||||
* Toolkit (Essentials) node detection constants.
|
||||
*
|
||||
* Re-exported from essentialsNodes.ts — the single source of truth.
|
||||
* Used by telemetry to track toolkit node adoption and popularity.
|
||||
* Only novel nodes — basic nodes (LoadImage, SaveImage, etc.) are excluded.
|
||||
*
|
||||
* Source: https://www.notion.so/comfy-org/2fe6d73d365080d0a951d14cdf540778
|
||||
*/
|
||||
|
||||
/**
|
||||
* Canonical node type names for individual toolkit nodes.
|
||||
*/
|
||||
export const TOOLKIT_NODE_NAMES: ReadonlySet<string> = new Set([
|
||||
// Image Tools
|
||||
'ImageCrop',
|
||||
'ImageRotate',
|
||||
'ImageBlur',
|
||||
'ImageInvert',
|
||||
'ImageCompare',
|
||||
'Canny',
|
||||
|
||||
// Video Tools
|
||||
'Video Slice',
|
||||
|
||||
// API Nodes
|
||||
'RecraftRemoveBackgroundNode',
|
||||
'RecraftVectorizeImageNode',
|
||||
'KlingOmniProEditVideoNode',
|
||||
|
||||
// Shader Nodes
|
||||
'GLSLShader'
|
||||
])
|
||||
|
||||
/**
|
||||
* python_module values that identify toolkit blueprint nodes.
|
||||
* Essentials blueprints are registered with node_pack 'comfy_essentials',
|
||||
* which maps to python_module on the node def.
|
||||
*/
|
||||
export const TOOLKIT_BLUEPRINT_MODULES: ReadonlySet<string> = new Set([
|
||||
'comfy_essentials'
|
||||
])
|
||||
export {
|
||||
TOOLKIT_NOVEL_NODE_NAMES as TOOLKIT_NODE_NAMES,
|
||||
TOOLKIT_BLUEPRINT_MODULES
|
||||
} from './essentialsNodes'
|
||||
|
||||
@@ -405,6 +405,7 @@ interface SubgraphDefinitionBase<
|
||||
/** Optional description shown as tooltip when hovering over the subgraph node. */
|
||||
description?: string
|
||||
category?: string
|
||||
essentials_category?: string
|
||||
/** Custom metadata for the subgraph (description, searchAliases, etc.) */
|
||||
extra?: T extends ComfyWorkflow1BaseInput
|
||||
? z.input<typeof zExtra> | null
|
||||
@@ -443,6 +444,7 @@ const zSubgraphDefinition = zComfyWorkflow1
|
||||
/** Optional description shown as tooltip when hovering over the subgraph node. */
|
||||
description: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
essentials_category: z.string().optional(),
|
||||
inputNode: zExportedSubgraphIONode,
|
||||
outputNode: zExportedSubgraphIONode,
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type { EssentialsCategory } from '@/constants/essentialsNodes'
|
||||
import {
|
||||
ESSENTIALS_CATEGORIES,
|
||||
ESSENTIALS_NODES
|
||||
} from '@/constants/essentialsNodes'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { buildNodeDefTree } from '@/stores/nodeDefStore'
|
||||
import type {
|
||||
@@ -14,46 +19,6 @@ 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
|
||||
@@ -178,34 +143,25 @@ class NodeOrganizationService {
|
||||
const tree = buildNodeDefTree(essentialNodes, {
|
||||
pathExtractor: essentialsPathExtractor
|
||||
})
|
||||
const folderOrder = [
|
||||
'basics',
|
||||
'text generation',
|
||||
'image generation',
|
||||
'video generation',
|
||||
'image tools',
|
||||
'video tools',
|
||||
'audio',
|
||||
'3D'
|
||||
]
|
||||
if (tree.children) {
|
||||
const len = folderOrder.length
|
||||
const len = ESSENTIALS_CATEGORIES.length
|
||||
const originalIndex = new Map(
|
||||
tree.children.map((child, i) => [child, i])
|
||||
)
|
||||
tree.children.sort((a, b) => {
|
||||
const ai = folderOrder.indexOf(a.label ?? '')
|
||||
const bi = folderOrder.indexOf(b.label ?? '')
|
||||
const ai = ESSENTIALS_CATEGORIES.indexOf(
|
||||
a.label as EssentialsCategory
|
||||
)
|
||||
const bi = ESSENTIALS_CATEGORIES.indexOf(
|
||||
b.label as EssentialsCategory
|
||||
)
|
||||
const orderA = ai === -1 ? len + originalIndex.get(a)! : ai
|
||||
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
|
||||
]
|
||||
const order = ESSENTIALS_NODES[folder.label as EssentialsCategory]
|
||||
if (!order) continue
|
||||
const nodeOrder: readonly string[] = order
|
||||
const orderLen = nodeOrder.length
|
||||
|
||||
@@ -515,6 +515,104 @@ describe('useSubgraphStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('essentials_category passthrough', () => {
|
||||
it('should prefer GlobalSubgraphData essentials_category over definition fallback', async () => {
|
||||
const graphWithEssentials = {
|
||||
...mockGraph,
|
||||
definitions: {
|
||||
subgraphs: [
|
||||
{
|
||||
...mockGraph.definitions?.subgraphs?.[0],
|
||||
essentials_category: 'Image Tools'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
bp_precedence: {
|
||||
name: 'Precedence Blueprint',
|
||||
info: { node_pack: 'test_pack' },
|
||||
data: JSON.stringify(graphWithEssentials),
|
||||
essentials_category: 'Video Generation'
|
||||
}
|
||||
}
|
||||
)
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.bp_precedence'
|
||||
)
|
||||
expect(nodeDef?.essentials_category).toBe('video generation')
|
||||
})
|
||||
|
||||
it('should pass essentials_category from GlobalSubgraphData to node def', async () => {
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
bp_essentials: {
|
||||
name: 'Test Essentials Blueprint',
|
||||
info: { node_pack: 'test_pack', category: 'Test Category' },
|
||||
data: JSON.stringify(mockGraph),
|
||||
essentials_category: 'Image Generation'
|
||||
}
|
||||
}
|
||||
)
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.bp_essentials'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.essentials_category).toBe('image generation')
|
||||
})
|
||||
|
||||
it('should extract essentials_category from subgraph definition as fallback', async () => {
|
||||
const graphWithEssentials = {
|
||||
...mockGraph,
|
||||
definitions: {
|
||||
subgraphs: [
|
||||
{
|
||||
...mockGraph.definitions?.subgraphs?.[0],
|
||||
essentials_category: 'Image Tools'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
bp_fallback: {
|
||||
name: 'Fallback Blueprint',
|
||||
info: { node_pack: 'test_pack' },
|
||||
data: JSON.stringify(graphWithEssentials)
|
||||
}
|
||||
}
|
||||
)
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.bp_fallback'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.essentials_category).toBe('image tools')
|
||||
})
|
||||
|
||||
it('should normalize title-cased essentials_category to canonical form', async () => {
|
||||
await mockFetch(
|
||||
{},
|
||||
{
|
||||
bp_3d: {
|
||||
name: 'Test 3D Blueprint',
|
||||
info: { node_pack: 'test_pack', category: 'Test Category' },
|
||||
data: JSON.stringify(mockGraph),
|
||||
essentials_category: '3d'
|
||||
}
|
||||
}
|
||||
)
|
||||
const nodeDef = useNodeDefStore().nodeDefs.find(
|
||||
(d) => d.name === 'SubgraphBlueprint.bp_3d'
|
||||
)
|
||||
expect(nodeDef).toBeDefined()
|
||||
expect(nodeDef?.essentials_category).toBe('3D')
|
||||
})
|
||||
})
|
||||
|
||||
describe('global blueprint filtering', () => {
|
||||
function globalBlueprint(
|
||||
overrides: Partial<GlobalSubgraphData['info']> = {}
|
||||
|
||||
@@ -222,6 +222,9 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
{
|
||||
display_name: v.name,
|
||||
...(category && { category }),
|
||||
...(v.essentials_category && {
|
||||
essentials_category: v.essentials_category
|
||||
}),
|
||||
search_aliases: v.info.search_aliases,
|
||||
isGlobal: true
|
||||
},
|
||||
@@ -291,6 +294,8 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
const search_aliases = workflowExtra?.BlueprintSearchAliases
|
||||
const subgraphDefCategory =
|
||||
workflow.initialState.definitions?.subgraphs?.[0]?.category
|
||||
const subgraphDefEssentialsCategory =
|
||||
workflow.initialState.definitions?.subgraphs?.[0]?.essentials_category
|
||||
const category = subgraphDefCategory
|
||||
? `Subgraph Blueprints/${subgraphDefCategory}`
|
||||
: 'Subgraph Blueprints'
|
||||
@@ -305,6 +310,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
output_node: false,
|
||||
python_module: 'blueprint',
|
||||
search_aliases,
|
||||
essentials_category: subgraphDefEssentialsCategory,
|
||||
...overrides
|
||||
}
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodedefv1)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
|
||||
import {
|
||||
NodeSourceType,
|
||||
getEssentialsCategory,
|
||||
getNodeSource
|
||||
} from '@/types/nodeSource'
|
||||
|
||||
describe('getNodeSource', () => {
|
||||
it('should return UNKNOWN_NODE_SOURCE when python_module is undefined', () => {
|
||||
@@ -105,6 +109,36 @@ describe('getNodeSource', () => {
|
||||
const result = getNodeSource('nodes.some_module', undefined, 'LoadImage')
|
||||
expect(result.type).toBe(NodeSourceType.Essentials)
|
||||
})
|
||||
|
||||
it('should normalize title-cased backend categories to canonical form', () => {
|
||||
const result = getNodeSource(
|
||||
'nodes.some_module',
|
||||
'Image Generation',
|
||||
'SomeNode'
|
||||
)
|
||||
expect(result.type).toBe(NodeSourceType.Essentials)
|
||||
expect(getEssentialsCategory('SomeNode', 'Image Generation')).toBe(
|
||||
'image generation'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getEssentialsCategory', () => {
|
||||
it('should normalize title-cased essentials_category to canonical form', () => {
|
||||
expect(getEssentialsCategory('SomeNode', 'Image Generation')).toBe(
|
||||
'image generation'
|
||||
)
|
||||
expect(getEssentialsCategory('SomeNode', '3d')).toBe('3D')
|
||||
expect(getEssentialsCategory('SomeNode', '3D')).toBe('3D')
|
||||
})
|
||||
|
||||
it('should fall back to ESSENTIALS_CATEGORY_MAP when no category provided', () => {
|
||||
expect(getEssentialsCategory('LoadImage')).toBe('basics')
|
||||
})
|
||||
|
||||
it('should return undefined for unknown node without category', () => {
|
||||
expect(getEssentialsCategory('UnknownNode')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('blueprint nodes', () => {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
ESSENTIALS_CATEGORY_CANONICAL,
|
||||
ESSENTIALS_CATEGORY_MAP
|
||||
} from '@/constants/essentialsNodes'
|
||||
|
||||
export enum NodeSourceType {
|
||||
Core = 'core',
|
||||
CustomNodes = 'custom_nodes',
|
||||
@@ -26,44 +31,6 @@ const shortenNodeName = (name: string) => {
|
||||
.replace(/(-ComfyUI|_ComfyUI|-Comfy|_Comfy)$/, '')
|
||||
}
|
||||
|
||||
// TODO: Remove this mock mapping once object_info/global_subgraphs returns essentials_category
|
||||
const ESSENTIALS_CATEGORY_MOCK: Record<string, string> = {
|
||||
// basics
|
||||
LoadImage: 'basics',
|
||||
SaveImage: 'basics',
|
||||
LoadVideo: 'basics',
|
||||
SaveVideo: 'basics',
|
||||
Load3D: 'basics',
|
||||
SaveGLB: 'basics',
|
||||
PrimitiveStringMultiline: 'basics',
|
||||
// image tools
|
||||
ImageBatch: 'image tools',
|
||||
ImageCrop: 'image tools',
|
||||
ImageScale: 'image tools',
|
||||
ImageRotate: 'image tools',
|
||||
ImageBlur: 'image tools',
|
||||
ImageInvert: 'image tools',
|
||||
Canny: 'image tools',
|
||||
RecraftRemoveBackgroundNode: 'image tools',
|
||||
// video tools
|
||||
GetVideoComponents: 'video tools',
|
||||
// image gen
|
||||
LoraLoader: 'image generation',
|
||||
// video gen
|
||||
'SubgraphBlueprint.pose_to_video_ltx_2_0': 'video generation',
|
||||
'SubgraphBlueprint.canny_to_video_ltx_2_0': 'video generation',
|
||||
KlingLipSyncAudioToVideoNode: 'video generation',
|
||||
// text gen
|
||||
OpenAIChatNode: 'text generation',
|
||||
// 3d
|
||||
TencentTextToModelNode: '3D',
|
||||
TencentImageToModelNode: '3D',
|
||||
// audio
|
||||
LoadAudio: 'audio',
|
||||
SaveAudio: 'audio',
|
||||
StabilityTextToAudio: 'audio'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the essentials category for a node, falling back to mock data if not provided.
|
||||
*/
|
||||
@@ -71,9 +38,12 @@ export function getEssentialsCategory(
|
||||
name?: string,
|
||||
essentials_category?: string
|
||||
): string | undefined {
|
||||
return (
|
||||
essentials_category ?? (name ? ESSENTIALS_CATEGORY_MOCK[name] : undefined)
|
||||
)
|
||||
const normalizedCategory = essentials_category?.trim().toLowerCase()
|
||||
const canonical = normalizedCategory
|
||||
? (ESSENTIALS_CATEGORY_CANONICAL.get(normalizedCategory) ??
|
||||
normalizedCategory)
|
||||
: undefined
|
||||
return canonical ?? (name ? ESSENTIALS_CATEGORY_MAP[name] : undefined)
|
||||
}
|
||||
|
||||
export const getNodeSource = (
|
||||
|
||||
Reference in New Issue
Block a user