mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
[feat] Node Definition Filter Registry System (#4497)
This commit is contained in:
@@ -251,11 +251,41 @@ export function createDummyFolderNodeDef(folderPath: string): ComfyNodeDefImpl {
|
|||||||
} as ComfyNodeDefV1)
|
} 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', () => {
|
export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||||
const nodeDefsByName = ref<Record<string, ComfyNodeDefImpl>>({})
|
const nodeDefsByName = ref<Record<string, ComfyNodeDefImpl>>({})
|
||||||
const nodeDefsByDisplayName = ref<Record<string, ComfyNodeDefImpl>>({})
|
const nodeDefsByDisplayName = ref<Record<string, ComfyNodeDefImpl>>({})
|
||||||
const showDeprecated = ref(false)
|
const showDeprecated = ref(false)
|
||||||
const showExperimental = ref(false)
|
const showExperimental = ref(false)
|
||||||
|
const nodeDefFilters = ref<NodeDefFilter[]>([])
|
||||||
|
|
||||||
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
|
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
|
||||||
const nodeDataTypes = computed(() => {
|
const nodeDataTypes = computed(() => {
|
||||||
@@ -270,13 +300,11 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
|||||||
}
|
}
|
||||||
return types
|
return types
|
||||||
})
|
})
|
||||||
const visibleNodeDefs = computed(() =>
|
const visibleNodeDefs = computed(() => {
|
||||||
nodeDefs.value.filter(
|
return nodeDefs.value.filter((nodeDef) =>
|
||||||
(nodeDef: ComfyNodeDefImpl) =>
|
nodeDefFilters.value.every((filter) => filter.predicate(nodeDef))
|
||||||
(showDeprecated.value || !nodeDef.deprecated) &&
|
|
||||||
(showExperimental.value || !nodeDef.experimental)
|
|
||||||
)
|
)
|
||||||
)
|
})
|
||||||
const nodeSearchService = computed(
|
const nodeSearchService = computed(
|
||||||
() => new NodeSearchService(visibleNodeDefs.value)
|
() => new NodeSearchService(visibleNodeDefs.value)
|
||||||
)
|
)
|
||||||
@@ -310,11 +338,53 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
|||||||
return nodeDefsByName.value[node.constructor?.nodeData?.name] ?? null
|
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 {
|
return {
|
||||||
nodeDefsByName,
|
nodeDefsByName,
|
||||||
nodeDefsByDisplayName,
|
nodeDefsByDisplayName,
|
||||||
showDeprecated,
|
showDeprecated,
|
||||||
showExperimental,
|
showExperimental,
|
||||||
|
nodeDefFilters,
|
||||||
|
|
||||||
nodeDefs,
|
nodeDefs,
|
||||||
nodeDataTypes,
|
nodeDataTypes,
|
||||||
@@ -324,7 +394,9 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
|||||||
|
|
||||||
updateNodeDefs,
|
updateNodeDefs,
|
||||||
addNodeDef,
|
addNodeDef,
|
||||||
fromLGraphNode
|
fromLGraphNode,
|
||||||
|
registerNodeDefFilter,
|
||||||
|
unregisterNodeDefFilter
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
isFloatInputSpec,
|
isFloatInputSpec,
|
||||||
isIntInputSpec
|
isIntInputSpec
|
||||||
} from '@/schemas/nodeDefSchema'
|
} from '@/schemas/nodeDefSchema'
|
||||||
|
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||||
|
|
||||||
import { lcm } from './mathUtil'
|
import { lcm } from './mathUtil'
|
||||||
|
|
||||||
@@ -138,3 +139,11 @@ export const mergeInputSpec = (
|
|||||||
|
|
||||||
return mergeCommonInputSpec(spec1, spec2)
|
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'
|
||||||
|
}
|
||||||
|
|||||||
269
tests-ui/tests/store/nodeDefStore.test.ts
Normal file
269
tests-ui/tests/store/nodeDefStore.test.ts
Normal file
@@ -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<typeof useNodeDefStore>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
store = useNodeDefStore()
|
||||||
|
})
|
||||||
|
|
||||||
|
const createMockNodeDef = (
|
||||||
|
overrides: Partial<ComfyNodeDef> = {}
|
||||||
|
): 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user