From 604656fb28a6f44318a407458d59d00a1964bf15 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Fri, 22 May 2026 04:04:50 +0000 Subject: [PATCH] refactor(schema): flatten V2 input spec defaults to top-level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The V1 to V2 input spec migration spreads input options at the top level of the spec, but the V2 schema declared default (COLOR), default+rows+cols (TEXTAREA), and content (MARKDOWN) nested under a separate `options` object. Two shapes, one wire format — silent fallback bugs like FE-800 were the inevitable consequence. Canonicalize on the top-level shape that the migration already produces: - nodeDefSchemaV2: hoist default to top level for COLOR/MARKDOWN; hoist rows/cols/default to top level for TEXTAREA. Drop the now-empty nested options objects on those three. Leave CHART/GALLERIA/IMAGE/IMAGECOMPARE alone — they are V2-native (no V1 migration path) and their nested options bag carries through to the runtime widget's options. - useColorWidget: single top-level read, no fallback chain. - useTextareaWidget: read rows/cols/default at the top level. - useColorWidget.test: drop the V1-vs-V2-shape parity cases; the schema now has one shape, so the tests pin the single contract. --- .../composables/useColorWidget.test.ts | 43 +++++-------------- .../widgets/composables/useColorWidget.ts | 5 +-- .../widgets/composables/useTextareaWidget.ts | 23 +++++----- src/schemas/nodeDef/nodeDefSchemaV2.ts | 22 +++------- 4 files changed, 27 insertions(+), 66 deletions(-) diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.test.ts b/src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.test.ts index 4dea9f184c..a89f5cfe7e 100644 --- a/src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.test.ts @@ -2,11 +2,10 @@ import { describe, expect, it, vi } from 'vitest' import { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' -import type { ColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import { useColorWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useColorWidget' +import type { ColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' -const TOP_LEVEL_DEFAULT = '#00ff00' -const NESTED_DEFAULT = '#abcdef' +const DECLARED_DEFAULT = '#00ff00' const BLACK_FALLBACK = '#000000' function createMockNode(): { @@ -34,48 +33,26 @@ function createColorSpec( } describe('useColorWidget', () => { - it('uses the top-level default produced by the V1 → V2 migration', () => { + it('uses the declared default from the input spec', () => { const { node, addWidget } = createMockNode() - const inputSpec = createColorSpec({ default: TOP_LEVEL_DEFAULT }) + const inputSpec = createColorSpec({ default: DECLARED_DEFAULT }) const widget = useColorWidget()(node, inputSpec) expect(addWidget).toHaveBeenCalledWith( 'color', 'color', - TOP_LEVEL_DEFAULT, + DECLARED_DEFAULT, expect.any(Function), { serialize: true } ) - expect(widget.value).toBe(TOP_LEVEL_DEFAULT) + expect(widget.value).toBe(DECLARED_DEFAULT) }) - it('uses options.default when the spec follows the V2 nested shape', () => { + it('falls back to black when no default is supplied', () => { const { node, addWidget } = createMockNode() - const inputSpec = createColorSpec({ options: { default: NESTED_DEFAULT } }) - useColorWidget()(node, inputSpec) - - expect(addWidget.mock.calls[0]![2]).toBe(NESTED_DEFAULT) - }) - - it('prefers the top-level default when both locations are present', () => { - const { node, addWidget } = createMockNode() - const inputSpec = createColorSpec({ - default: TOP_LEVEL_DEFAULT, - options: { default: NESTED_DEFAULT } - }) - - useColorWidget()(node, inputSpec) - - expect(addWidget.mock.calls[0]![2]).toBe(TOP_LEVEL_DEFAULT) - }) - - it('still produces a usable widget when no default is supplied', () => { - const { node, addWidget } = createMockNode() - const inputSpec = createColorSpec() - - const widget = useColorWidget()(node, inputSpec) + const widget = useColorWidget()(node, createColorSpec()) expect(addWidget).toHaveBeenCalledOnce() expect(widget.type).toBe('color') @@ -86,7 +63,7 @@ describe('useColorWidget', () => { it('serializes the widget so its value persists in saved workflows', () => { const { node, addWidget } = createMockNode() - useColorWidget()(node, createColorSpec({ default: TOP_LEVEL_DEFAULT })) + useColorWidget()(node, createColorSpec({ default: DECLARED_DEFAULT })) expect(addWidget.mock.calls[0]![4]).toEqual({ serialize: true }) }) @@ -96,7 +73,7 @@ describe('useColorWidget', () => { useColorWidget()( node, - createColorSpec({ name: 'bg_color', default: TOP_LEVEL_DEFAULT }) + createColorSpec({ name: 'bg_color', default: DECLARED_DEFAULT }) ) expect(addWidget.mock.calls[0]![1]).toBe('bg_color') diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.ts b/src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.ts index 6a9b2addc4..08d1aa5743 100644 --- a/src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.ts +++ b/src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.ts @@ -8,9 +8,8 @@ import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets' export const useColorWidget = (): ComfyWidgetConstructorV2 => { return (node: LGraphNode, inputSpec: InputSpecV2): IColorWidget => { - const colorSpec = inputSpec as ColorInputSpec - const { name, options } = colorSpec - const defaultValue = colorSpec.default ?? options?.default ?? '#000000' + const { name, default: defaultValue = '#000000' } = + inputSpec as ColorInputSpec const widget = node.addWidget('color', name, defaultValue, () => {}, { serialize: true diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useTextareaWidget.ts b/src/renderer/extensions/vueNodes/widgets/composables/useTextareaWidget.ts index 052c8935c7..8a8fad27d0 100644 --- a/src/renderer/extensions/vueNodes/widgets/composables/useTextareaWidget.ts +++ b/src/renderer/extensions/vueNodes/widgets/composables/useTextareaWidget.ts @@ -8,20 +8,17 @@ import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets' export const useTextareaWidget = (): ComfyWidgetConstructorV2 => { return (node: LGraphNode, inputSpec: InputSpecV2): ITextareaWidget => { - const { name, options = {} } = inputSpec as TextareaInputSpec + const textareaSpec = inputSpec as TextareaInputSpec + const { name, default: defaultValue = '' } = textareaSpec + const widgetOptions = { + rows: textareaSpec.rows ?? 5, + cols: textareaSpec.cols ?? 50 + } - const widget = node.addWidget( - 'textarea', - name, - options.default || '', - () => {}, - { - serialize: true, - rows: options.rows || 5, - cols: options.cols || 50, - ...options - } - ) as ITextareaWidget + const widget = node.addWidget('textarea', name, defaultValue, () => {}, { + serialize: true, + ...widgetOptions + }) as ITextareaWidget return widget } diff --git a/src/schemas/nodeDef/nodeDefSchemaV2.ts b/src/schemas/nodeDef/nodeDefSchemaV2.ts index a783f2edf0..5e16ebd54c 100644 --- a/src/schemas/nodeDef/nodeDefSchemaV2.ts +++ b/src/schemas/nodeDef/nodeDefSchemaV2.ts @@ -44,11 +44,7 @@ const zColorInputSpec = zBaseInputOptions.extend({ type: z.literal('COLOR'), name: z.string(), isOptional: z.boolean().optional(), - options: z - .object({ - default: z.string().optional() - }) - .optional() + default: z.string().optional() }) const zImageInputSpec = zBaseInputOptions.extend({ @@ -84,11 +80,7 @@ const zMarkdownInputSpec = zBaseInputOptions.extend({ type: z.literal('MARKDOWN'), name: z.string(), isOptional: z.boolean().optional(), - options: z - .object({ - content: z.string().optional() - }) - .optional() + default: z.string().optional() }) const zChartInputSpec = zBaseInputOptions.extend({ @@ -118,13 +110,9 @@ const zTextareaInputSpec = zBaseInputOptions.extend({ type: z.literal('TEXTAREA'), name: z.string(), isOptional: z.boolean().optional(), - options: z - .object({ - rows: z.number().optional(), - cols: z.number().optional(), - default: z.string().optional() - }) - .optional() + rows: z.number().optional(), + cols: z.number().optional(), + default: z.string().optional() }) const zCurvePoint = z.tuple([z.number(), z.number()])