diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts index aaa6d000b..b92837205 100644 --- a/src/stores/nodeDefStore.ts +++ b/src/stores/nodeDefStore.ts @@ -251,11 +251,41 @@ export function createDummyFolderNodeDef(folderPath: string): ComfyNodeDefImpl { } as ComfyNodeDefV1) } +/** + * Defines a filter for node definitions in the node library. + * Filters are applied in a single pass to determine node visibility. + */ +export interface NodeDefFilter { + /** + * Unique identifier for the filter. + * Convention: Use dot notation like 'core.deprecated' or 'extension.myfilter' + */ + id: string + + /** + * Display name for the filter (used in UI/debugging). + */ + name: string + + /** + * Optional description explaining what the filter does. + */ + description?: string + + /** + * The filter function that returns true if the node should be visible. + * @param nodeDef - The node definition to evaluate + * @returns true if the node should be visible, false to hide it + */ + predicate: (nodeDef: ComfyNodeDefImpl) => boolean +} + export const useNodeDefStore = defineStore('nodeDef', () => { const nodeDefsByName = ref>({}) const nodeDefsByDisplayName = ref>({}) const showDeprecated = ref(false) const showExperimental = ref(false) + const nodeDefFilters = ref([]) const nodeDefs = computed(() => Object.values(nodeDefsByName.value)) const nodeDataTypes = computed(() => { @@ -270,13 +300,11 @@ export const useNodeDefStore = defineStore('nodeDef', () => { } return types }) - const visibleNodeDefs = computed(() => - nodeDefs.value.filter( - (nodeDef: ComfyNodeDefImpl) => - (showDeprecated.value || !nodeDef.deprecated) && - (showExperimental.value || !nodeDef.experimental) + const visibleNodeDefs = computed(() => { + return nodeDefs.value.filter((nodeDef) => + nodeDefFilters.value.every((filter) => filter.predicate(nodeDef)) ) - ) + }) const nodeSearchService = computed( () => new NodeSearchService(visibleNodeDefs.value) ) @@ -310,11 +338,53 @@ export const useNodeDefStore = defineStore('nodeDef', () => { return nodeDefsByName.value[node.constructor?.nodeData?.name] ?? null } + /** + * Registers a node definition filter. + * @param filter - The filter to register + */ + function registerNodeDefFilter(filter: NodeDefFilter) { + nodeDefFilters.value = [...nodeDefFilters.value, filter] + } + + /** + * Unregisters a node definition filter by ID. + * @param id - The ID of the filter to remove + */ + function unregisterNodeDefFilter(id: string) { + nodeDefFilters.value = nodeDefFilters.value.filter((f) => f.id !== id) + } + + /** + * Register the core node definition filters. + */ + function registerCoreNodeDefFilters() { + // Deprecated nodes filter + registerNodeDefFilter({ + id: 'core.deprecated', + name: 'Hide Deprecated Nodes', + description: 'Hides nodes marked as deprecated unless explicitly enabled', + predicate: (nodeDef) => showDeprecated.value || !nodeDef.deprecated + }) + + // Experimental nodes filter + registerNodeDefFilter({ + id: 'core.experimental', + name: 'Hide Experimental Nodes', + description: + 'Hides nodes marked as experimental unless explicitly enabled', + predicate: (nodeDef) => showExperimental.value || !nodeDef.experimental + }) + } + + // Register core filters on store initialization + registerCoreNodeDefFilters() + return { nodeDefsByName, nodeDefsByDisplayName, showDeprecated, showExperimental, + nodeDefFilters, nodeDefs, nodeDataTypes, @@ -324,7 +394,9 @@ export const useNodeDefStore = defineStore('nodeDef', () => { updateNodeDefs, addNodeDef, - fromLGraphNode + fromLGraphNode, + registerNodeDefFilter, + unregisterNodeDefFilter } }) diff --git a/src/utils/nodeDefUtil.ts b/src/utils/nodeDefUtil.ts index 8b130ff2d..cb8a7125b 100644 --- a/src/utils/nodeDefUtil.ts +++ b/src/utils/nodeDefUtil.ts @@ -15,6 +15,7 @@ import { isFloatInputSpec, isIntInputSpec } from '@/schemas/nodeDefSchema' +import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore' import { lcm } from './mathUtil' @@ -138,3 +139,11 @@ export const mergeInputSpec = ( return mergeCommonInputSpec(spec1, spec2) } + +/** + * Checks if a node definition represents a subgraph node. + * Subgraph nodes are created with category='subgraph' and python_module='nodes'. + */ +export const isSubgraphNode = (nodeDef: ComfyNodeDefImpl): boolean => { + return nodeDef.category === 'subgraph' && nodeDef.python_module === 'nodes' +} diff --git a/tests-ui/tests/store/nodeDefStore.test.ts b/tests-ui/tests/store/nodeDefStore.test.ts new file mode 100644 index 000000000..068bf1c0c --- /dev/null +++ b/tests-ui/tests/store/nodeDefStore.test.ts @@ -0,0 +1,269 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it } from 'vitest' + +import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' +import { type NodeDefFilter, useNodeDefStore } from '@/stores/nodeDefStore' + +describe('useNodeDefStore', () => { + let store: ReturnType + + beforeEach(() => { + setActivePinia(createPinia()) + store = useNodeDefStore() + }) + + const createMockNodeDef = ( + overrides: Partial = {} + ): ComfyNodeDef => ({ + name: 'TestNode', + display_name: 'Test Node', + category: 'test', + python_module: 'test_module', + description: 'Test node', + input: {}, + output: [], + output_is_list: [], + output_name: [], + output_node: false, + deprecated: false, + experimental: false, + ...overrides + }) + + describe('filter registry', () => { + it('should register a new filter', () => { + const filter: NodeDefFilter = { + id: 'test.filter', + name: 'Test Filter', + predicate: () => true + } + + store.registerNodeDefFilter(filter) + expect(store.nodeDefFilters).toContainEqual(filter) + }) + + it('should unregister a filter by id', () => { + const filter: NodeDefFilter = { + id: 'test.filter', + name: 'Test Filter', + predicate: () => true + } + + store.registerNodeDefFilter(filter) + store.unregisterNodeDefFilter('test.filter') + expect(store.nodeDefFilters).not.toContainEqual(filter) + }) + + it('should register core filters on initialization', () => { + const deprecatedFilter = store.nodeDefFilters.find( + (f) => f.id === 'core.deprecated' + ) + const experimentalFilter = store.nodeDefFilters.find( + (f) => f.id === 'core.experimental' + ) + + expect(deprecatedFilter).toBeDefined() + expect(experimentalFilter).toBeDefined() + }) + }) + + describe('filter application', () => { + beforeEach(() => { + // Clear existing filters for isolated tests + store.nodeDefFilters.splice(0) + }) + + it('should apply single filter to visible nodes', () => { + const normalNode = createMockNodeDef({ + name: 'normal', + deprecated: false + }) + const deprecatedNode = createMockNodeDef({ + name: 'deprecated', + deprecated: true + }) + + store.updateNodeDefs([normalNode, deprecatedNode]) + + // Register filter that hides deprecated nodes + store.registerNodeDefFilter({ + id: 'test.no-deprecated', + name: 'Hide Deprecated', + predicate: (node) => !node.deprecated + }) + + expect(store.visibleNodeDefs).toHaveLength(1) + expect(store.visibleNodeDefs[0].name).toBe('normal') + }) + + it('should apply multiple filters with AND logic', () => { + const node1 = createMockNodeDef({ + name: 'node1', + deprecated: false, + experimental: false + }) + const node2 = createMockNodeDef({ + name: 'node2', + deprecated: true, + experimental: false + }) + const node3 = createMockNodeDef({ + name: 'node3', + deprecated: false, + experimental: true + }) + const node4 = createMockNodeDef({ + name: 'node4', + deprecated: true, + experimental: true + }) + + store.updateNodeDefs([node1, node2, node3, node4]) + + // Register filters + store.registerNodeDefFilter({ + id: 'test.no-deprecated', + name: 'Hide Deprecated', + predicate: (node) => !node.deprecated + }) + + store.registerNodeDefFilter({ + id: 'test.no-experimental', + name: 'Hide Experimental', + predicate: (node) => !node.experimental + }) + + // Only node1 should be visible (not deprecated AND not experimental) + expect(store.visibleNodeDefs).toHaveLength(1) + expect(store.visibleNodeDefs[0].name).toBe('node1') + }) + + it('should show all nodes when no filters are registered', () => { + const nodes = [ + createMockNodeDef({ name: 'node1' }), + createMockNodeDef({ name: 'node2' }), + createMockNodeDef({ name: 'node3' }) + ] + + store.updateNodeDefs(nodes) + expect(store.visibleNodeDefs).toHaveLength(3) + }) + + it('should update visibility when filter is removed', () => { + const deprecatedNode = createMockNodeDef({ + name: 'deprecated', + deprecated: true + }) + store.updateNodeDefs([deprecatedNode]) + + const filter: NodeDefFilter = { + id: 'test.no-deprecated', + name: 'Hide Deprecated', + predicate: (node) => !node.deprecated + } + + // Add filter - node should be hidden + store.registerNodeDefFilter(filter) + expect(store.visibleNodeDefs).toHaveLength(0) + + // Remove filter - node should be visible + store.unregisterNodeDefFilter('test.no-deprecated') + expect(store.visibleNodeDefs).toHaveLength(1) + }) + }) + + describe('core filters behavior', () => { + it('should hide deprecated nodes by default', () => { + const normalNode = createMockNodeDef({ + name: 'normal', + deprecated: false + }) + const deprecatedNode = createMockNodeDef({ + name: 'deprecated', + deprecated: true + }) + + store.updateNodeDefs([normalNode, deprecatedNode]) + + expect(store.visibleNodeDefs).toHaveLength(1) + expect(store.visibleNodeDefs[0].name).toBe('normal') + }) + + it('should show deprecated nodes when showDeprecated is true', () => { + const normalNode = createMockNodeDef({ + name: 'normal', + deprecated: false + }) + const deprecatedNode = createMockNodeDef({ + name: 'deprecated', + deprecated: true + }) + + store.updateNodeDefs([normalNode, deprecatedNode]) + store.showDeprecated = true + + expect(store.visibleNodeDefs).toHaveLength(2) + }) + + it('should hide experimental nodes by default', () => { + const normalNode = createMockNodeDef({ + name: 'normal', + experimental: false + }) + const experimentalNode = createMockNodeDef({ + name: 'experimental', + experimental: true + }) + + store.updateNodeDefs([normalNode, experimentalNode]) + + expect(store.visibleNodeDefs).toHaveLength(1) + expect(store.visibleNodeDefs[0].name).toBe('normal') + }) + + it('should show experimental nodes when showExperimental is true', () => { + const normalNode = createMockNodeDef({ + name: 'normal', + experimental: false + }) + const experimentalNode = createMockNodeDef({ + name: 'experimental', + experimental: true + }) + + store.updateNodeDefs([normalNode, experimentalNode]) + store.showExperimental = true + + expect(store.visibleNodeDefs).toHaveLength(2) + }) + }) + + describe('performance', () => { + it('should perform single traversal for multiple filters', () => { + let filterCallCount = 0 + + // Register multiple filters that count their calls + for (let i = 0; i < 5; i++) { + store.registerNodeDefFilter({ + id: `test.counter-${i}`, + name: `Counter ${i}`, + predicate: () => { + filterCallCount++ + return true + } + }) + } + + const nodes = Array.from({ length: 10 }, (_, i) => + createMockNodeDef({ name: `node${i}` }) + ) + store.updateNodeDefs(nodes) + + // Force recomputation by accessing visibleNodeDefs + expect(store.visibleNodeDefs).toBeDefined() + + // Each node (10) should be checked by each filter (5 test + 2 core = 7 total) + expect(filterCallCount).toBe(10 * 5) + }) + }) +})