From 9dd6da3dc2a8029d496c45a02b4f5fac4df283ae Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Tue, 20 Aug 2024 17:00:47 -0400 Subject: [PATCH] Support node deprecated/experimental flag (#563) * Add deprecated field * Hide deprecated nodes * Add experimental node show/hide * Add setting tooltips * nit * nit * nit --- src/components/graph/GraphCanvas.vue | 15 +++++- .../sidebar/tabs/NodeLibrarySidebarTab.vue | 1 + src/stores/nodeDefStore.ts | 39 ++++++++++++++-- src/stores/settingStore.ts | 18 ++++++++ src/types/apiTypes.ts | 6 ++- tests-ui/tests/apiTypes.test.ts | 4 +- tests-ui/tests/nodeDef.test.ts | 46 +++++++++++++++++++ 7 files changed, 120 insertions(+), 9 deletions(-) diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 05f31211d..5e3a9d1ef 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -16,7 +16,7 @@ import SideToolbar from '@/components/sidebar/SideToolbar.vue' import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue' import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue' import NodeTooltip from '@/components/graph/NodeTooltip.vue' -import { ref, computed, onUnmounted, watch, onMounted } from 'vue' +import { ref, computed, onUnmounted, watch, onMounted, watchEffect } from 'vue' import { app as comfyApp } from '@/scripts/app' import { useSettingStore } from '@/stores/settingStore' import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter' @@ -36,6 +36,7 @@ import { const emit = defineEmits(['ready']) const canvasRef = ref(null) const settingStore = useSettingStore() +const nodeDefStore = useNodeDefStore() const workspaceStore = useWorkspaceStore() const betaMenuEnabled = computed( @@ -63,6 +64,16 @@ watch( { immediate: true } ) +watchEffect(() => { + nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated') +}) + +watchEffect(() => { + nodeDefStore.showExperimental = settingStore.get( + 'Comfy.Node.ShowExperimental' + ) +}) + let dropTargetCleanup = () => {} onMounted(async () => { @@ -98,7 +109,7 @@ onMounted(async () => { const comfyNodeName = event.source.element.getAttribute( 'data-comfy-node-name' ) - const nodeDef = useNodeDefStore().nodeDefsByName[comfyNodeName] + const nodeDef = nodeDefStore.nodeDefsByName[comfyNodeName] comfyApp.addNodeOnGraph(nodeDef, { pos }) } }) diff --git a/src/components/sidebar/tabs/NodeLibrarySidebarTab.vue b/src/components/sidebar/tabs/NodeLibrarySidebarTab.vue index 598ba85da..69f9b7797 100644 --- a/src/components/sidebar/tabs/NodeLibrarySidebarTab.vue +++ b/src/components/sidebar/tabs/NodeLibrarySidebarTab.vue @@ -112,6 +112,7 @@ const settingStore = useSettingStore() const sidebarLocation = computed<'left' | 'right'>(() => settingStore.get('Comfy.Sidebar.Location') ) + const nodePreviewStyle = ref>({ position: 'absolute', top: '0px', diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts index bbac6e043..c63048161 100644 --- a/src/stores/nodeDefStore.ts +++ b/src/stores/nodeDefStore.ts @@ -1,7 +1,7 @@ import { NodeSearchService } from '@/services/nodeSearchService' import { ComfyNodeDef } from '@/types/apiTypes' import { defineStore } from 'pinia' -import { Type, Transform, plainToClass } from 'class-transformer' +import { Type, Transform, plainToClass, Expose } from 'class-transformer' import { ComfyWidgetConstructor } from '@/scripts/widgets' import { TreeNode } from 'primevue/treenode' import { buildTree } from '@/utils/treeUtil' @@ -166,6 +166,23 @@ export class ComfyNodeDefImpl { python_module: string description: string + @Transform(({ value, obj }) => value ?? obj.category === '', { + toClassOnly: true + }) + @Type(() => Boolean) + @Expose() + deprecated: boolean + + @Transform( + ({ value, obj }) => value ?? obj.category.startsWith('_for_testing'), + { + toClassOnly: true + } + ) + @Type(() => Boolean) + @Expose() + experimental: boolean + @Type(() => ComfyInputsSpec) input: ComfyInputsSpec @@ -229,22 +246,34 @@ export const SYSTEM_NODE_DEFS: Record = { interface State { nodeDefsByName: Record widgets: Record + showDeprecated: boolean + showExperimental: boolean } export const useNodeDefStore = defineStore('nodeDef', { state: (): State => ({ nodeDefsByName: {}, - widgets: {} + widgets: {}, + showDeprecated: false, + showExperimental: false }), getters: { nodeDefs(state) { return Object.values(state.nodeDefsByName) }, - nodeSearchService(state) { - return new NodeSearchService(Object.values(state.nodeDefsByName)) + // Node defs that are not deprecated + visibleNodeDefs(state) { + return this.nodeDefs.filter( + (nodeDef: ComfyNodeDefImpl) => + (state.showDeprecated || !nodeDef.deprecated) && + (state.showExperimental || !nodeDef.experimental) + ) + }, + nodeSearchService() { + return new NodeSearchService(this.visibleNodeDefs) }, nodeTree(): TreeNode { - return buildTree(this.nodeDefs, (nodeDef: ComfyNodeDefImpl) => [ + return buildTree(this.visibleNodeDefs, (nodeDef: ComfyNodeDefImpl) => [ ...nodeDef.category.split('/'), nodeDef.display_name ]) diff --git a/src/stores/settingStore.ts b/src/stores/settingStore.ts index 98550ba81..6cedb01ff 100644 --- a/src/stores/settingStore.ts +++ b/src/stores/settingStore.ts @@ -147,6 +147,24 @@ export const useSettingStore = defineStore('setting', { type: 'boolean', defaultValue: true }) + + app.ui.settings.addSetting({ + id: 'Comfy.Node.ShowDeprecated', + name: 'Show deprecated nodes in search', + tooltip: + 'Deprecated nodes are hidden by default in the UI, but remain functional in existing workflows that use them.', + type: 'boolean', + defaultValue: false + }) + + app.ui.settings.addSetting({ + id: 'Comfy.Node.ShowExperimental', + name: 'Show experimental nodes in search', + tooltip: + 'Experimental nodes are marked as such in the UI and may be subject to significant changes or removal in future versions. Use with caution in production workflows', + type: 'boolean', + defaultValue: true + }) }, set(key: K, value: Settings[K]) { diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts index 250ec8813..63d04b80c 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -335,7 +335,9 @@ const zComfyNodeDef = z.object({ description: z.string(), category: z.string(), output_node: z.boolean(), - python_module: z.string() + python_module: z.string(), + deprecated: z.boolean().optional(), + experimental: z.boolean().optional() }) // `/object_info` @@ -419,6 +421,8 @@ const zSettings = z.record(z.any()).and( 'Comfy.NodeSearchBoxImpl': z.enum(['default', 'simple']), 'Comfy.NodeSearchBoxImpl.ShowCategory': z.boolean(), 'Comfy.NodeSuggestions.number': z.number(), + 'Comfy.Node.ShowDeprecated': z.boolean(), + 'Comfy.Node.ShowExperimental': z.boolean(), 'Comfy.PreviewFormat': z.string(), 'Comfy.PromptFilename': z.boolean(), 'Comfy.Sidebar.Location': z.enum(['left', 'right']), diff --git a/tests-ui/tests/apiTypes.test.ts b/tests-ui/tests/apiTypes.test.ts index 7c55a6ac0..c2c1f4038 100644 --- a/tests-ui/tests/apiTypes.test.ts +++ b/tests-ui/tests/apiTypes.test.ts @@ -16,7 +16,9 @@ const EXAMPLE_NODE_DEF: ComfyNodeDef = { description: '', python_module: 'nodes', category: 'loaders', - output_node: false + output_node: false, + experimental: false, + deprecated: false } describe('validateNodeDef', () => { diff --git a/tests-ui/tests/nodeDef.test.ts b/tests-ui/tests/nodeDef.test.ts index 734b70b34..01fba5cb4 100644 --- a/tests-ui/tests/nodeDef.test.ts +++ b/tests-ui/tests/nodeDef.test.ts @@ -194,6 +194,52 @@ describe('ComfyNodeDefImpl', () => { is_list: false } ]) + expect(result.deprecated).toBe(false) + }) + + it('should transform a deprecated basic node definition', () => { + const plainObject = { + name: 'TestNode', + display_name: 'Test Node', + category: 'Testing', + python_module: 'test_module', + description: 'A test node', + input: { + required: { + intInput: ['INT', { min: 0, max: 100, default: 50 }] + } + }, + output: ['INT'], + output_is_list: [false], + output_name: ['intOutput'], + deprecated: true + } + + const result = plainToClass(ComfyNodeDefImpl, plainObject) + expect(result.deprecated).toBe(true) + }) + + // Legacy way of marking a node as deprecated + it('should mark deprecated with empty category', () => { + const plainObject = { + name: 'TestNode', + display_name: 'Test Node', + // Empty category should be treated as deprecated + category: '', + python_module: 'test_module', + description: 'A test node', + input: { + required: { + intInput: ['INT', { min: 0, max: 100, default: 50 }] + } + }, + output: ['INT'], + output_is_list: [false], + output_name: ['intOutput'] + } + + const result = plainToClass(ComfyNodeDefImpl, plainObject) + expect(result.deprecated).toBe(true) }) it('should handle multiple outputs including COMBO type', () => {