mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-24 00:34:09 +00:00
feat: add display name mappings for Essentials tab nodes (#9072)
## Summary Add frontend-only display name mappings for nodes shown in the Essentials tab, plus parse the new `essentials_category` field from the backend. ## Changes - **What**: Created `src/constants/essentialsDisplayNames.ts` with a static mapping of node names to user-friendly display names (e.g. `CLIPTextEncode` → "Text", `ImageScale` → "Resize Image"). Regular nodes use exact name matching; blueprint nodes use prefix matching since their filenames include model-specific suffixes. Integrated into `NodeLibrarySidebarTab.vue`'s `renderedRoot` computed for leaf node labels with fallback to `display_name`. Added `essentials_category` (z.string().optional()) to the node def schema and `ComfyNodeDefImpl` to parse the field already sent by the backend (PR #12357). ## Review Focus Display names are resolved only in the Essentials tab tree view (`NodeLibrarySidebarTab.vue`), not globally, to avoid side effects on search, bookmarks, or other views. Blueprint prefix matching is ordered longest-first so more specific prefixes (e.g. `image_inpainting_`) match before shorter ones (e.g. `image_edit`). ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9072-feat-add-display-name-mappings-for-Essentials-tab-nodes-30f6d73d3650817c9acdc9b0315ed0be) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -154,6 +154,7 @@ import {
|
||||
render
|
||||
} from 'vue'
|
||||
|
||||
import { resolveEssentialsDisplayName } from '@/constants/essentialsDisplayNames'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import type { SearchFilter } from '@/components/common/SearchFilterChip.vue'
|
||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
||||
@@ -276,7 +277,9 @@ const renderedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(() => {
|
||||
|
||||
return {
|
||||
key: node.key,
|
||||
label: node.leaf ? node.data.display_name : node.label,
|
||||
label: node.leaf
|
||||
? (resolveEssentialsDisplayName(node.data) ?? node.data.display_name)
|
||||
: node.label,
|
||||
leaf: node.leaf,
|
||||
data: node.data,
|
||||
getIcon() {
|
||||
|
||||
@@ -114,6 +114,7 @@ import {
|
||||
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { resolveEssentialsDisplayName } from '@/constants/essentialsDisplayNames'
|
||||
import SearchBox from '@/components/common/SearchBoxV2.vue'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useNodeDragToCanvas } from '@/composables/node/useNodeDragToCanvas'
|
||||
@@ -230,16 +231,23 @@ function findFirstLeaf(node: TreeNode): TreeNode | undefined {
|
||||
}
|
||||
|
||||
function fillNodeInfo(
|
||||
node: TreeNode
|
||||
node: TreeNode,
|
||||
{ useEssentialsLabels = false }: { useEssentialsLabels?: boolean } = {}
|
||||
): RenderedTreeExplorerNode<ComfyNodeDefImpl> {
|
||||
const children = node.children?.map(fillNodeInfo)
|
||||
const children = node.children?.map((child) =>
|
||||
fillNodeInfo(child, { useEssentialsLabels })
|
||||
)
|
||||
const totalLeaves = node.leaf
|
||||
? 1
|
||||
: (children?.reduce((acc, child) => acc + child.totalLeaves, 0) ?? 0)
|
||||
|
||||
return {
|
||||
key: node.key,
|
||||
label: node.leaf ? node.data?.display_name : node.label,
|
||||
label: node.leaf
|
||||
? useEssentialsLabels
|
||||
? (resolveEssentialsDisplayName(node.data) ?? node.data?.display_name)
|
||||
: node.data?.display_name
|
||||
: node.label,
|
||||
leaf: node.leaf,
|
||||
data: node.data,
|
||||
icon: node.leaf ? 'icon-[comfy--node]' : getFolderIcon(node),
|
||||
@@ -274,7 +282,7 @@ const essentialSections = computed(() => {
|
||||
const renderedEssentialRoot = computed(() => {
|
||||
const section = essentialSections.value[0]
|
||||
return section
|
||||
? fillNodeInfo(applySorting(section.tree))
|
||||
? fillNodeInfo(applySorting(section.tree), { useEssentialsLabels: true })
|
||||
: fillNodeInfo({ key: 'root', label: '', children: [] })
|
||||
})
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('EssentialNodeCard', () => {
|
||||
|
||||
return {
|
||||
key: 'test-key',
|
||||
label: 'Test Node',
|
||||
label: data.display_name,
|
||||
icon: 'icon-[comfy--node]',
|
||||
type: 'node',
|
||||
totalLeaves: 1,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="group relative flex flex-col items-center justify-center py-4 px-2 rounded-2xl cursor-pointer select-none transition-colors duration-150 box-content bg-component-node-background hover:bg-secondary-background-hover border border-component-node-border aspect-square"
|
||||
:data-node-name="node.data?.display_name"
|
||||
:data-node-name="node.label"
|
||||
draggable="true"
|
||||
@click="handleClick"
|
||||
@dragstart="handleDragStart"
|
||||
@@ -16,7 +16,7 @@
|
||||
<TextTickerMultiLine
|
||||
class="shrink-0 h-8 w-full text-xs font-bold text-foreground leading-4"
|
||||
>
|
||||
{{ node.data?.display_name }}
|
||||
{{ node.label }}
|
||||
</TextTickerMultiLine>
|
||||
</div>
|
||||
|
||||
|
||||
105
src/constants/essentialsDisplayNames.test.ts
Normal file
105
src/constants/essentialsDisplayNames.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: vi.fn((key: string) => key)
|
||||
}))
|
||||
|
||||
import { resolveEssentialsDisplayName } from '@/constants/essentialsDisplayNames'
|
||||
|
||||
describe('resolveEssentialsDisplayName', () => {
|
||||
describe('exact name matches', () => {
|
||||
it.each([
|
||||
['LoadImage', 'essentials.loadImage'],
|
||||
['SaveImage', 'essentials.saveImage'],
|
||||
['PrimitiveStringMultiline', 'essentials.text'],
|
||||
['ImageScale', 'essentials.resizeImage'],
|
||||
['LoraLoader', 'essentials.loadStyleLora'],
|
||||
['OpenAIChatNode', 'essentials.textGenerationLLM'],
|
||||
['RecraftRemoveBackgroundNode', 'essentials.removeBackground'],
|
||||
['ImageCompare', 'essentials.imageCompare'],
|
||||
['StabilityTextToAudio', 'essentials.musicGeneration'],
|
||||
['BatchImagesNode', 'essentials.batchImage'],
|
||||
['Video Slice', 'essentials.extractFrame'],
|
||||
['KlingLipSyncAudioToVideoNode', 'essentials.lipsync'],
|
||||
['KlingLipSyncTextToVideoNode', 'essentials.lipsync']
|
||||
])('%s -> %s', (name, expected) => {
|
||||
expect(resolveEssentialsDisplayName({ name })).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('3D API node alternatives', () => {
|
||||
it.each([
|
||||
['TencentTextToModelNode', 'essentials.textTo3DModel'],
|
||||
['MeshyTextToModelNode', 'essentials.textTo3DModel'],
|
||||
['TripoTextToModelNode', 'essentials.textTo3DModel'],
|
||||
['TencentImageToModelNode', 'essentials.imageTo3DModel'],
|
||||
['MeshyImageToModelNode', 'essentials.imageTo3DModel'],
|
||||
['TripoImageToModelNode', 'essentials.imageTo3DModel']
|
||||
])('%s -> %s', (name, expected) => {
|
||||
expect(resolveEssentialsDisplayName({ name })).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('blueprint prefix matches', () => {
|
||||
it.each([
|
||||
[
|
||||
'SubgraphBlueprint.text_to_image_flux_schnell.json',
|
||||
'essentials.textToImage'
|
||||
],
|
||||
['SubgraphBlueprint.text_to_image_sd15.json', 'essentials.textToImage'],
|
||||
[
|
||||
'SubgraphBlueprint.image_edit_something.json',
|
||||
'essentials.imageToImage'
|
||||
],
|
||||
['SubgraphBlueprint.pose_to_image_v2.json', 'essentials.poseToImage'],
|
||||
[
|
||||
'SubgraphBlueprint.canny_to_image_z_image_turbo.json',
|
||||
'essentials.cannyToImage'
|
||||
],
|
||||
[
|
||||
'SubgraphBlueprint.depth_to_image_z_image_turbo.json',
|
||||
'essentials.depthToImage'
|
||||
],
|
||||
['SubgraphBlueprint.text_to_video_ltx.json', 'essentials.textToVideo'],
|
||||
['SubgraphBlueprint.image_to_video_wan.json', 'essentials.imageToVideo'],
|
||||
[
|
||||
'SubgraphBlueprint.pose_to_video_ltx_2_0.json',
|
||||
'essentials.poseToVideo'
|
||||
],
|
||||
[
|
||||
'SubgraphBlueprint.canny_to_video_ltx_2_0.json',
|
||||
'essentials.cannyToVideo'
|
||||
],
|
||||
[
|
||||
'SubgraphBlueprint.depth_to_video_ltx_2_0.json',
|
||||
'essentials.depthToVideo'
|
||||
],
|
||||
[
|
||||
'SubgraphBlueprint.image_inpainting_qwen_image_instantx.json',
|
||||
'essentials.inpaintImage'
|
||||
],
|
||||
[
|
||||
'SubgraphBlueprint.image_outpainting_qwen_image_instantx.json',
|
||||
'essentials.outpaintImage'
|
||||
]
|
||||
])('%s -> %s', (name, expected) => {
|
||||
expect(resolveEssentialsDisplayName({ name })).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unmapped nodes', () => {
|
||||
it('returns undefined for unknown node names', () => {
|
||||
expect(resolveEssentialsDisplayName({ name: 'SomeRandomNode' })).toBe(
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('returns undefined for unknown blueprint prefixes', () => {
|
||||
expect(
|
||||
resolveEssentialsDisplayName({
|
||||
name: 'SubgraphBlueprint.unknown_workflow.json'
|
||||
})
|
||||
).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
108
src/constants/essentialsDisplayNames.ts
Normal file
108
src/constants/essentialsDisplayNames.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { t } from '@/i18n'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
|
||||
const BLUEPRINT_PREFIX = 'SubgraphBlueprint.'
|
||||
|
||||
/**
|
||||
* Static mapping of node names to their Essentials tab display name i18n keys.
|
||||
*/
|
||||
const EXACT_NAME_MAP: Record<string, string> = {
|
||||
// Basics
|
||||
LoadImage: 'essentials.loadImage',
|
||||
SaveImage: 'essentials.saveImage',
|
||||
LoadVideo: 'essentials.loadVideo',
|
||||
SaveVideo: 'essentials.saveVideo',
|
||||
Load3D: 'essentials.load3DModel',
|
||||
SaveGLB: 'essentials.save3DModel',
|
||||
PrimitiveStringMultiline: 'essentials.text',
|
||||
|
||||
// Image Tools
|
||||
BatchImagesNode: 'essentials.batchImage',
|
||||
ImageCrop: 'essentials.cropImage',
|
||||
ImageScale: 'essentials.resizeImage',
|
||||
ImageRotate: 'essentials.rotate',
|
||||
ImageInvert: 'essentials.invert',
|
||||
Canny: 'essentials.canny',
|
||||
RecraftRemoveBackgroundNode: 'essentials.removeBackground',
|
||||
ImageCompare: 'essentials.imageCompare',
|
||||
|
||||
// Video Tools
|
||||
'Video Slice': 'essentials.extractFrame',
|
||||
|
||||
// Image Generation
|
||||
LoraLoader: 'essentials.loadStyleLora',
|
||||
|
||||
// Video Generation
|
||||
KlingLipSyncAudioToVideoNode: 'essentials.lipsync',
|
||||
KlingLipSyncTextToVideoNode: 'essentials.lipsync',
|
||||
|
||||
// Text Generation
|
||||
OpenAIChatNode: 'essentials.textGenerationLLM',
|
||||
|
||||
// 3D
|
||||
TencentTextToModelNode: 'essentials.textTo3DModel',
|
||||
TencentImageToModelNode: 'essentials.imageTo3DModel',
|
||||
MeshyTextToModelNode: 'essentials.textTo3DModel',
|
||||
MeshyImageToModelNode: 'essentials.imageTo3DModel',
|
||||
TripoTextToModelNode: 'essentials.textTo3DModel',
|
||||
TripoImageToModelNode: 'essentials.imageTo3DModel',
|
||||
|
||||
// Audio
|
||||
StabilityTextToAudio: 'essentials.musicGeneration',
|
||||
LoadAudio: 'essentials.loadAudio',
|
||||
SaveAudio: 'essentials.saveAudio'
|
||||
}
|
||||
|
||||
/**
|
||||
* Blueprint prefix patterns mapped to display name i18n keys.
|
||||
* Entries are matched by checking if the blueprint filename
|
||||
* (after removing the SubgraphBlueprint. prefix) starts with the key.
|
||||
* Ordered longest-first so more specific prefixes match before shorter ones.
|
||||
*/
|
||||
const BLUEPRINT_PREFIX_MAP: [prefix: string, displayNameKey: string][] = [
|
||||
// Image Generation
|
||||
['image_inpainting_', 'essentials.inpaintImage'],
|
||||
['image_outpainting_', 'essentials.outpaintImage'],
|
||||
['image_edit', 'essentials.imageToImage'],
|
||||
['text_to_image', 'essentials.textToImage'],
|
||||
['pose_to_image', 'essentials.poseToImage'],
|
||||
['canny_to_image', 'essentials.cannyToImage'],
|
||||
['depth_to_image', 'essentials.depthToImage'],
|
||||
|
||||
// Video Generation
|
||||
['text_to_video', 'essentials.textToVideo'],
|
||||
['image_to_video', 'essentials.imageToVideo'],
|
||||
['pose_to_video', 'essentials.poseToVideo'],
|
||||
['canny_to_video', 'essentials.cannyToVideo'],
|
||||
['depth_to_video', 'essentials.depthToVideo']
|
||||
]
|
||||
|
||||
function resolveBlueprintDisplayName(
|
||||
blueprintName: string
|
||||
): string | undefined {
|
||||
for (const [prefix, displayNameKey] of BLUEPRINT_PREFIX_MAP) {
|
||||
if (blueprintName.startsWith(prefix)) {
|
||||
return t(displayNameKey)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the Essentials tab display name for a given node definition.
|
||||
* Returns `undefined` if the node has no Essentials display name mapping.
|
||||
*/
|
||||
export function resolveEssentialsDisplayName(
|
||||
nodeDef: Pick<ComfyNodeDefImpl, 'name'> | undefined
|
||||
): string | undefined {
|
||||
if (!nodeDef) return undefined
|
||||
const { name } = nodeDef
|
||||
|
||||
if (name.startsWith(BLUEPRINT_PREFIX)) {
|
||||
const blueprintName = name.slice(BLUEPRINT_PREFIX.length)
|
||||
return resolveBlueprintDisplayName(blueprintName)
|
||||
}
|
||||
|
||||
const key = EXACT_NAME_MAP[name]
|
||||
return key ? t(key) : undefined
|
||||
}
|
||||
@@ -3152,5 +3152,43 @@
|
||||
"duplicateName": "A secret with this name already exists",
|
||||
"duplicateProvider": "A secret for this provider already exists"
|
||||
}
|
||||
},
|
||||
"essentials": {
|
||||
"loadImage": "Load Image",
|
||||
"saveImage": "Save Image",
|
||||
"loadVideo": "Load Video",
|
||||
"saveVideo": "Save Video",
|
||||
"load3DModel": "Load 3D model",
|
||||
"save3DModel": "Save 3D Model",
|
||||
"text": "Text",
|
||||
"batchImage": "Batch Image",
|
||||
"cropImage": "Crop Image",
|
||||
"resizeImage": "Resize Image",
|
||||
"rotate": "Rotate",
|
||||
"invert": "Invert",
|
||||
"canny": "Canny",
|
||||
"removeBackground": "Remove Background",
|
||||
"imageCompare": "Image compare",
|
||||
"extractFrame": "Extract frame",
|
||||
"loadStyleLora": "Load style (LoRA)",
|
||||
"lipsync": "Lipsync",
|
||||
"textGenerationLLM": "Text generation (LLM)",
|
||||
"textTo3DModel": "Text to 3D model",
|
||||
"imageTo3DModel": "Image to 3D Model",
|
||||
"musicGeneration": "Music generation",
|
||||
"loadAudio": "Load Audio",
|
||||
"saveAudio": "Save Audio",
|
||||
"inpaintImage": "Inpaint image",
|
||||
"outpaintImage": "Outpaint image",
|
||||
"imageToImage": "Image to image",
|
||||
"textToImage": "Text to image",
|
||||
"poseToImage": "Pose to image",
|
||||
"cannyToImage": "Canny to image",
|
||||
"depthToImage": "Depth to image",
|
||||
"textToVideo": "Text to video",
|
||||
"imageToVideo": "Image to video",
|
||||
"poseToVideo": "Pose to video",
|
||||
"cannyToVideo": "Canny to video",
|
||||
"depthToVideo": "Depth to video"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user