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:
Christian Byrne
2026-02-26 18:40:15 -08:00
committed by GitHub
parent 54b710b239
commit 0698ec23c0
11 changed files with 347 additions and 135 deletions

View File

@@ -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']> = {}

View File

@@ -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)