Fix incorrect widgetValue migration (#8625)

Under a combination of many edge cases, the `widget_values` migration
code added in #3326 would cause the progress text on a "Recraft Text to
Image" node to incorrectly deserialize into the `control_after_generate`
- widgets_values is of length 1 greater than it should be because
progress text serializes
  - It should not, there is no code to deserialize it
- negative_prompt has force_input set and skips serialization
- Migration only applies when `widgets_values` is equal to actual inputs
length. The two above edge cases cancel to make this true
- Seed is accounted for when calculating the length of widgets, but not
when applying the migration
- Migration occurs even though we track workflow version now and have an
accurate way of determining that it can not be needed

The two primary edge cases which cause the bug are both addressed
- `options.serialize` does nothing and has never done anything. I've
been guilty of making the same mistake in the ancient past, and want to
clean up the misconception where I can.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8625-Fix-incorrect-widgetValue-migration-2fe6d73d365081a683b4c675eaeebb6c)
by [Unito](https://www.unito.io)
This commit is contained in:
AustinMroz
2026-02-05 21:27:11 -08:00
committed by GitHub
parent d05e4eac58
commit 8283438ee6
3 changed files with 34 additions and 16 deletions

View File

@@ -42,11 +42,11 @@ export function useTextPreviewWidget(
widgetValue.value = typeof value === 'string' ? value : String(value)
},
getMinHeight: () => options.minHeight ?? 42 + PADDING,
serialize: false,
read_only: true
},
type: inputSpec.type
})
widget.serialize = false
addWidget(node, widget)
return widget
}

View File

@@ -89,6 +89,30 @@ describe('migrateWidgetsValues', () => {
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
expect(result).toEqual(['first value', 'last value'])
})
it('should correctly handle seed with unexpected value', () => {
const inputDefs: Record<string, InputSpec> = {
normalInput: {
type: 'INT',
name: 'normalInput',
control_after_generate: true
},
forceInputField: {
type: 'STRING',
name: 'forceInputField',
forceInput: true
}
}
const widgets = [
{ name: 'normalInput', type: 'number' },
{ name: 'control_after_generate', type: 'string' }
] as Partial<IWidget>[] as IWidget[]
const widgetValues = [42, 'fixed', 'unexpected widget value']
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
expect(result).toEqual([42, 'fixed'])
})
})
describe('compressWidgetInputSlots', () => {

View File

@@ -112,23 +112,17 @@ export function migrateWidgetsValues<TWidgetValue>(
const originalWidgetsInputs = Object.values(inputDefs).filter(
(input) => widgetNames.has(input.name) || input.forceInput
)
// Count the number of original widgets inputs.
const numOriginalWidgets = _.sum(
originalWidgetsInputs.map((input) =>
// If the input has control, it will have 2 widgets.
input.control_after_generate ||
['seed', 'noise_seed'].includes(input.name)
? 2
: 1
)
const widgetIndexHasForceInput = originalWidgetsInputs.flatMap((input) =>
input.control_after_generate
? [!!input.forceInput, false]
: [!!input.forceInput]
)
if (numOriginalWidgets === widgetsValues?.length) {
return _.zip(originalWidgetsInputs, widgetsValues)
.filter(([input]) => !input?.forceInput)
.map(([_, value]) => value as TWidgetValue)
}
return widgetsValues
if (widgetIndexHasForceInput.length !== widgetsValues?.length)
return widgetsValues
return widgetsValues.filter((_, index) => !widgetIndexHasForceInput[index])
}
/**