diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue index 048bbcee9..0cd7a59cc 100644 --- a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -50,10 +50,10 @@ import { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD' // Import widget components directly import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue' import { - ESSENTIAL_WIDGET_TYPES, - useWidgetRenderer -} from '@/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer' -import { widgetTypeToComponent } from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry' + getComponent, + isEssential, + shouldRenderAsVue +} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry' import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget' import InputSlot from './InputSlot.vue' @@ -67,9 +67,6 @@ interface NodeWidgetsProps { const props = defineProps() -// Use widget renderer composable -const { getWidgetComponent, shouldRenderAsVue } = useWidgetRenderer() - // Error boundary implementation const renderError = ref(null) @@ -110,14 +107,9 @@ const processedWidgets = computed((): ProcessedWidget[] => { if (!widget.type) continue if (!shouldRenderAsVue(widget)) continue - if ( - lodLevel === LODLevel.REDUCED && - !ESSENTIAL_WIDGET_TYPES.has(widget.type) - ) - continue + if (lodLevel === LODLevel.REDUCED && !isEssential(widget.type)) continue - const componentName = getWidgetComponent(widget.type) - const vueComponent = widgetTypeToComponent[componentName] || WidgetInputText + const vueComponent = getComponent(widget.type) || WidgetInputText const simplified: SimplifiedWidget = { name: widget.name, diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer.ts b/src/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer.ts deleted file mode 100644 index c2f6d5774..000000000 --- a/src/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Widget renderer composable for Vue node system - * Maps LiteGraph widget types to Vue components - */ -import { WidgetType, widgetTypeToComponent } from '../registry/widgetRegistry' - -/** - * Static mapping of LiteGraph widget types to Vue widget component names - */ -const TYPE_TO_ENUM_MAP: Record = { - // Number inputs - number: WidgetType.NUMBER, - slider: WidgetType.SLIDER, - INT: WidgetType.INT, - FLOAT: WidgetType.FLOAT, - - // Text inputs - text: WidgetType.STRING, - string: WidgetType.STRING, - STRING: WidgetType.STRING, - - // Selection - combo: WidgetType.COMBO, - COMBO: WidgetType.COMBO, - selectbutton: WidgetType.SELECTBUTTON, - SELECTBUTTON: WidgetType.SELECTBUTTON, - multiselect: WidgetType.MULTISELECT, - MULTISELECT: WidgetType.MULTISELECT, - treeselect: WidgetType.TREESELECT, - TREESELECT: WidgetType.TREESELECT, - - // Boolean - toggle: WidgetType.TOGGLESWITCH, - boolean: WidgetType.BOOLEAN, - BOOLEAN: WidgetType.BOOLEAN, - - // Multiline text - multiline: WidgetType.TEXTAREA, - textarea: WidgetType.TEXTAREA, - TEXTAREA: WidgetType.TEXTAREA, - customtext: WidgetType.TEXTAREA, - MARKDOWN: WidgetType.MARKDOWN, - - // Advanced widgets - color: WidgetType.COLOR, - COLOR: WidgetType.COLOR, - imagecompare: WidgetType.IMAGECOMPARE, - IMAGECOMPARE: WidgetType.IMAGECOMPARE, - galleria: WidgetType.GALLERIA, - GALLERIA: WidgetType.GALLERIA, - file: WidgetType.FILEUPLOAD, - fileupload: WidgetType.FILEUPLOAD, - FILEUPLOAD: WidgetType.FILEUPLOAD, - - // Button widget - button: WidgetType.BUTTON, - BUTTON: WidgetType.BUTTON, - - // Chart widget - chart: WidgetType.CHART, - CHART: WidgetType.CHART -} as const - -/** - * Pre-computed widget support map for O(1) lookups - * Maps widget type directly to boolean for fast shouldRenderAsVue checks - */ -const WIDGET_SUPPORT_MAP = new Map( - Object.entries(TYPE_TO_ENUM_MAP).map(([type, enumValue]) => [ - type, - widgetTypeToComponent[enumValue] !== undefined - ]) -) - -export const ESSENTIAL_WIDGET_TYPES = new Set([ - 'combo', - 'COMBO', - 'select', - 'toggle', - 'boolean', - 'BOOLEAN', - 'slider', - 'number', - 'INT', - 'FLOAT' -]) - -export const useWidgetRenderer = () => { - const getWidgetComponent = (widgetType: string): string => { - const enumKey = TYPE_TO_ENUM_MAP[widgetType] - - if (enumKey && widgetTypeToComponent[enumKey]) { - return enumKey - } - - return WidgetType.STRING - } - - const shouldRenderAsVue = (widget: { - type?: string - options?: Record - }): boolean => { - if (widget.options?.canvasOnly) return false - if (!widget.type) return false - - // Check if widget type is explicitly supported - const isSupported = WIDGET_SUPPORT_MAP.get(widget.type) - if (isSupported !== undefined) return isSupported - - // Fallback: unknown types are rendered as STRING widget - return widgetTypeToComponent[WidgetType.STRING] !== undefined - } - - return { - getWidgetComponent, - shouldRenderAsVue - } -} diff --git a/src/renderer/extensions/vueNodes/widgets/registry/widgetRegistry.ts b/src/renderer/extensions/vueNodes/widgets/registry/widgetRegistry.ts index 3257e8c35..e72d90be1 100644 --- a/src/renderer/extensions/vueNodes/widgets/registry/widgetRegistry.ts +++ b/src/renderer/extensions/vueNodes/widgets/registry/widgetRegistry.ts @@ -3,7 +3,6 @@ */ import type { Component } from 'vue' -// Component imports import WidgetButton from '../components/WidgetButton.vue' import WidgetChart from '../components/WidgetChart.vue' import WidgetColorPicker from '../components/WidgetColorPicker.vue' @@ -20,54 +19,132 @@ import WidgetTextarea from '../components/WidgetTextarea.vue' import WidgetToggleSwitch from '../components/WidgetToggleSwitch.vue' import WidgetTreeSelect from '../components/WidgetTreeSelect.vue' -/** - * Enum of all available widget types - */ -export enum WidgetType { - BUTTON = 'BUTTON', - STRING = 'STRING', - INT = 'INT', - FLOAT = 'FLOAT', - NUMBER = 'NUMBER', - BOOLEAN = 'BOOLEAN', - COMBO = 'COMBO', - COLOR = 'COLOR', - MULTISELECT = 'MULTISELECT', - SELECTBUTTON = 'SELECTBUTTON', - SLIDER = 'SLIDER', - TEXTAREA = 'TEXTAREA', - TOGGLESWITCH = 'TOGGLESWITCH', - CHART = 'CHART', - IMAGECOMPARE = 'IMAGECOMPARE', - GALLERIA = 'GALLERIA', - FILEUPLOAD = 'FILEUPLOAD', - TREESELECT = 'TREESELECT', - MARKDOWN = 'MARKDOWN' +interface WidgetDefinition { + component: Component + aliases: string[] + essential: boolean } -/** - * Maps widget types to their corresponding Vue components - * Components will be added as they are implemented - */ -export const widgetTypeToComponent: Record = { - // Components will be uncommented as they are implemented - [WidgetType.BUTTON]: WidgetButton, - [WidgetType.STRING]: WidgetInputText, - [WidgetType.INT]: WidgetSlider, - [WidgetType.FLOAT]: WidgetSlider, - [WidgetType.NUMBER]: WidgetSlider, // For compatibility - [WidgetType.BOOLEAN]: WidgetToggleSwitch, - [WidgetType.COMBO]: WidgetSelect, - [WidgetType.COLOR]: WidgetColorPicker, - [WidgetType.MULTISELECT]: WidgetMultiSelect, - [WidgetType.SELECTBUTTON]: WidgetSelectButton, - [WidgetType.SLIDER]: WidgetSlider, - [WidgetType.TEXTAREA]: WidgetTextarea, - [WidgetType.TOGGLESWITCH]: WidgetToggleSwitch, - [WidgetType.CHART]: WidgetChart, - [WidgetType.IMAGECOMPARE]: WidgetImageCompare, - [WidgetType.GALLERIA]: WidgetGalleria, - [WidgetType.FILEUPLOAD]: WidgetFileUpload, - [WidgetType.TREESELECT]: WidgetTreeSelect, - [WidgetType.MARKDOWN]: WidgetMarkdown +const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [ + [ + 'button', + { component: WidgetButton, aliases: ['BUTTON'], essential: false } + ], + [ + 'string', + { + component: WidgetInputText, + aliases: ['STRING', 'text'], + essential: false + } + ], + ['int', { component: WidgetSlider, aliases: ['INT'], essential: true }], + [ + 'float', + { + component: WidgetSlider, + aliases: ['FLOAT', 'number', 'slider'], + essential: true + } + ], + [ + 'boolean', + { + component: WidgetToggleSwitch, + aliases: ['BOOLEAN', 'toggle'], + essential: true + } + ], + ['combo', { component: WidgetSelect, aliases: ['COMBO'], essential: true }], + [ + 'color', + { component: WidgetColorPicker, aliases: ['COLOR'], essential: false } + ], + [ + 'multiselect', + { component: WidgetMultiSelect, aliases: ['MULTISELECT'], essential: false } + ], + [ + 'selectbutton', + { + component: WidgetSelectButton, + aliases: ['SELECTBUTTON'], + essential: false + } + ], + [ + 'textarea', + { + component: WidgetTextarea, + aliases: ['TEXTAREA', 'multiline', 'customtext'], + essential: false + } + ], + ['chart', { component: WidgetChart, aliases: ['CHART'], essential: false }], + [ + 'imagecompare', + { + component: WidgetImageCompare, + aliases: ['IMAGECOMPARE'], + essential: false + } + ], + [ + 'galleria', + { component: WidgetGalleria, aliases: ['GALLERIA'], essential: false } + ], + [ + 'fileupload', + { + component: WidgetFileUpload, + aliases: ['FILEUPLOAD', 'file'], + essential: false + } + ], + [ + 'treeselect', + { component: WidgetTreeSelect, aliases: ['TREESELECT'], essential: false } + ], + [ + 'markdown', + { component: WidgetMarkdown, aliases: ['MARKDOWN'], essential: false } + ] +] + +// Build lookup maps +const widgets = new Map() +const aliasMap = new Map() + +for (const [type, def] of coreWidgetDefinitions) { + widgets.set(type, def) + for (const alias of def.aliases) { + aliasMap.set(alias, type) + } +} + +// Utility functions +const getCanonicalType = (type: string): string => aliasMap.get(type) || type + +export const getComponent = (type: string): Component | null => { + const canonicalType = getCanonicalType(type) + return widgets.get(canonicalType)?.component || null +} + +export const isSupported = (type: string): boolean => { + const canonicalType = getCanonicalType(type) + return widgets.has(canonicalType) +} + +export const isEssential = (type: string): boolean => { + const canonicalType = getCanonicalType(type) + return widgets.get(canonicalType)?.essential || false +} + +export const shouldRenderAsVue = (widget: { + type?: string + options?: Record +}): boolean => { + if (widget.options?.canvasOnly) return false + if (!widget.type) return false + return isSupported(widget.type) } diff --git a/tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer.test.ts index 4cf610474..4a363ebd1 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer.test.ts @@ -1,93 +1,100 @@ import { describe, expect, it } from 'vitest' -import { useWidgetRenderer } from '@/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer' -import { WidgetType } from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry' +import WidgetButton from '@/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue' +import WidgetColorPicker from '@/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue' +import WidgetFileUpload from '@/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue' +import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue' +import WidgetMarkdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue' +import WidgetSelect from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vue' +import WidgetSlider from '@/renderer/extensions/vueNodes/widgets/components/WidgetSlider.vue' +import WidgetTextarea from '@/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue' +import WidgetToggleSwitch from '@/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue' +import { + getComponent, + isEssential, + shouldRenderAsVue +} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry' -describe('useWidgetRenderer', () => { - const { getWidgetComponent, shouldRenderAsVue } = useWidgetRenderer() - - describe('getWidgetComponent', () => { +describe('widgetRegistry', () => { + describe('getComponent', () => { // Test number type mappings describe('number types', () => { - it('should map number type to NUMBER widget', () => { - expect(getWidgetComponent('number')).toBe(WidgetType.NUMBER) + it('should map int types to slider widget', () => { + expect(getComponent('int')).toBe(WidgetSlider) + expect(getComponent('INT')).toBe(WidgetSlider) }) - it('should map slider type to SLIDER widget', () => { - expect(getWidgetComponent('slider')).toBe(WidgetType.SLIDER) - }) - - it('should map INT type to INT widget', () => { - expect(getWidgetComponent('INT')).toBe(WidgetType.INT) - }) - - it('should map FLOAT type to FLOAT widget', () => { - expect(getWidgetComponent('FLOAT')).toBe(WidgetType.FLOAT) + it('should map float types to slider widget', () => { + expect(getComponent('float')).toBe(WidgetSlider) + expect(getComponent('FLOAT')).toBe(WidgetSlider) + expect(getComponent('number')).toBe(WidgetSlider) + expect(getComponent('slider')).toBe(WidgetSlider) }) }) // Test text type mappings describe('text types', () => { - it('should map text variations to STRING widget', () => { - expect(getWidgetComponent('text')).toBe(WidgetType.STRING) - expect(getWidgetComponent('string')).toBe(WidgetType.STRING) - expect(getWidgetComponent('STRING')).toBe(WidgetType.STRING) + it('should map text variations to input text widget', () => { + expect(getComponent('text')).toBe(WidgetInputText) + expect(getComponent('string')).toBe(WidgetInputText) + expect(getComponent('STRING')).toBe(WidgetInputText) }) - it('should map multiline text types to TEXTAREA widget', () => { - expect(getWidgetComponent('multiline')).toBe(WidgetType.TEXTAREA) - expect(getWidgetComponent('textarea')).toBe(WidgetType.TEXTAREA) - expect(getWidgetComponent('MARKDOWN')).toBe(WidgetType.MARKDOWN) - expect(getWidgetComponent('customtext')).toBe(WidgetType.TEXTAREA) + it('should map multiline text types to textarea widget', () => { + expect(getComponent('multiline')).toBe(WidgetTextarea) + expect(getComponent('textarea')).toBe(WidgetTextarea) + expect(getComponent('TEXTAREA')).toBe(WidgetTextarea) + expect(getComponent('customtext')).toBe(WidgetTextarea) + }) + + it('should map markdown to markdown widget', () => { + expect(getComponent('MARKDOWN')).toBe(WidgetMarkdown) + expect(getComponent('markdown')).toBe(WidgetMarkdown) }) }) // Test selection type mappings describe('selection types', () => { - it('should map combo types to COMBO widget', () => { - expect(getWidgetComponent('combo')).toBe(WidgetType.COMBO) - expect(getWidgetComponent('COMBO')).toBe(WidgetType.COMBO) + it('should map combo types to select widget', () => { + expect(getComponent('combo')).toBe(WidgetSelect) + expect(getComponent('COMBO')).toBe(WidgetSelect) }) }) // Test boolean type mappings describe('boolean types', () => { - it('should map boolean types to appropriate widgets', () => { - expect(getWidgetComponent('toggle')).toBe(WidgetType.TOGGLESWITCH) - expect(getWidgetComponent('boolean')).toBe(WidgetType.BOOLEAN) - expect(getWidgetComponent('BOOLEAN')).toBe(WidgetType.BOOLEAN) + it('should map boolean types to toggle switch widget', () => { + expect(getComponent('toggle')).toBe(WidgetToggleSwitch) + expect(getComponent('boolean')).toBe(WidgetToggleSwitch) + expect(getComponent('BOOLEAN')).toBe(WidgetToggleSwitch) }) }) // Test advanced widget mappings describe('advanced widgets', () => { - it('should map color types to COLOR widget', () => { - expect(getWidgetComponent('color')).toBe(WidgetType.COLOR) - expect(getWidgetComponent('COLOR')).toBe(WidgetType.COLOR) + it('should map color types to color picker widget', () => { + expect(getComponent('color')).toBe(WidgetColorPicker) + expect(getComponent('COLOR')).toBe(WidgetColorPicker) }) - it('should map file types to FILEUPLOAD widget', () => { - expect(getWidgetComponent('file')).toBe(WidgetType.FILEUPLOAD) - expect(getWidgetComponent('FILEUPLOAD')).toBe(WidgetType.FILEUPLOAD) + it('should map file types to file upload widget', () => { + expect(getComponent('file')).toBe(WidgetFileUpload) + expect(getComponent('fileupload')).toBe(WidgetFileUpload) + expect(getComponent('FILEUPLOAD')).toBe(WidgetFileUpload) }) - it('should map button types to BUTTON widget', () => { - expect(getWidgetComponent('button')).toBe(WidgetType.BUTTON) - expect(getWidgetComponent('BUTTON')).toBe(WidgetType.BUTTON) + it('should map button types to button widget', () => { + expect(getComponent('button')).toBe(WidgetButton) + expect(getComponent('BUTTON')).toBe(WidgetButton) }) }) // Test fallback behavior describe('fallback behavior', () => { - it('should return STRING widget for unknown types', () => { - expect(getWidgetComponent('unknown')).toBe(WidgetType.STRING) - expect(getWidgetComponent('custom_widget')).toBe(WidgetType.STRING) - expect(getWidgetComponent('')).toBe(WidgetType.STRING) - }) - - it('should return STRING widget for unmapped but valid types', () => { - expect(getWidgetComponent('datetime')).toBe(WidgetType.STRING) - expect(getWidgetComponent('json')).toBe(WidgetType.STRING) + it('should return null for unknown types', () => { + expect(getComponent('unknown')).toBe(null) + expect(getComponent('custom_widget')).toBe(null) + expect(getComponent('')).toBe(null) }) }) }) @@ -105,12 +112,12 @@ describe('useWidgetRenderer', () => { it('should return true for widgets with mapped types', () => { expect(shouldRenderAsVue({ type: 'text' })).toBe(true) - expect(shouldRenderAsVue({ type: 'number' })).toBe(true) + expect(shouldRenderAsVue({ type: 'int' })).toBe(true) expect(shouldRenderAsVue({ type: 'combo' })).toBe(true) }) - it('should return true even for unknown types (fallback to STRING)', () => { - expect(shouldRenderAsVue({ type: 'unknown_type' })).toBe(true) + it('should return false for unknown types', () => { + expect(shouldRenderAsVue({ type: 'unknown_type' })).toBe(false) }) it('should respect options while checking type', () => { @@ -119,18 +126,43 @@ describe('useWidgetRenderer', () => { }) }) + describe('isEssential', () => { + it('should identify essential widget types', () => { + expect(isEssential('int')).toBe(true) + expect(isEssential('INT')).toBe(true) + expect(isEssential('float')).toBe(true) + expect(isEssential('FLOAT')).toBe(true) + expect(isEssential('boolean')).toBe(true) + expect(isEssential('BOOLEAN')).toBe(true) + expect(isEssential('combo')).toBe(true) + expect(isEssential('COMBO')).toBe(true) + }) + + it('should identify non-essential widget types', () => { + expect(isEssential('button')).toBe(false) + expect(isEssential('color')).toBe(false) + expect(isEssential('chart')).toBe(false) + expect(isEssential('fileupload')).toBe(false) + }) + + it('should return false for unknown types', () => { + expect(isEssential('unknown')).toBe(false) + expect(isEssential('')).toBe(false) + }) + }) + describe('edge cases', () => { it('should handle widgets with empty options', () => { const widget = { type: 'text', options: {} } expect(shouldRenderAsVue(widget)).toBe(true) }) - it('should handle case sensitivity correctly', () => { + it('should handle case sensitivity correctly through aliases', () => { // Test that both lowercase and uppercase work - expect(getWidgetComponent('string')).toBe(WidgetType.STRING) - expect(getWidgetComponent('STRING')).toBe(WidgetType.STRING) - expect(getWidgetComponent('combo')).toBe(WidgetType.COMBO) - expect(getWidgetComponent('COMBO')).toBe(WidgetType.COMBO) + expect(getComponent('string')).toBe(WidgetInputText) + expect(getComponent('STRING')).toBe(WidgetInputText) + expect(getComponent('combo')).toBe(WidgetSelect) + expect(getComponent('COMBO')).toBe(WidgetSelect) }) }) })