mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-29 08:58:53 -03:00
fix(nodes): restore autocomplete widget sync after metadata insertion (#879)
This commit is contained in:
@@ -37,6 +37,13 @@ const updateConnectedTriggerWords = vi.fn();
|
||||
const mergeLoras = vi.fn();
|
||||
const getAllGraphNodes = vi.fn();
|
||||
const getNodeFromGraph = vi.fn();
|
||||
const getWidgetByName = vi.fn((node, name) =>
|
||||
node?.widgets?.find((widget) => widget?.name === name) ?? null
|
||||
);
|
||||
const getWidgetSerializedValue = vi.fn((node, name) => {
|
||||
const index = node?.widgets?.findIndex((widget) => widget?.name === name) ?? -1;
|
||||
return index >= 0 ? node.widgets_values?.[index] : undefined;
|
||||
});
|
||||
|
||||
vi.mock(UTILS_MODULE, () => ({
|
||||
collectActiveLorasFromChain,
|
||||
@@ -47,6 +54,8 @@ vi.mock(UTILS_MODULE, () => ({
|
||||
},
|
||||
getAllGraphNodes,
|
||||
getNodeFromGraph,
|
||||
getWidgetByName,
|
||||
getWidgetSerializedValue,
|
||||
LORA_PATTERN: /<lora:([^:]+):([-\d.]+)(?::([-\d.]+))?>/g,
|
||||
}));
|
||||
|
||||
@@ -71,6 +80,9 @@ describe("Lora Loader trigger word updates", () => {
|
||||
mergeLoras.mockClear();
|
||||
mergeLoras.mockImplementation(() => [{ name: "Alpha", active: true }]);
|
||||
|
||||
getWidgetByName.mockClear();
|
||||
getWidgetSerializedValue.mockClear();
|
||||
|
||||
addLorasWidget.mockClear();
|
||||
addLorasWidget.mockImplementation((_node, _name, _opts, callback) => ({
|
||||
widget: { value: [], callback },
|
||||
@@ -89,14 +101,21 @@ describe("Lora Loader trigger word updates", () => {
|
||||
|
||||
// Create mock widget (AUTOCOMPLETE_TEXT_LORAS type created by Vue widgets)
|
||||
const inputWidget = {
|
||||
name: "text",
|
||||
value: "",
|
||||
options: {},
|
||||
callback: null, // Will be set by onNodeCreated
|
||||
};
|
||||
|
||||
const metadataWidget = {
|
||||
name: "__autocomplete_metadata_text",
|
||||
value: { version: 1, textWidgetName: "text" },
|
||||
options: {},
|
||||
};
|
||||
|
||||
const node = {
|
||||
comfyClass: "Lora Loader (LoraManager)",
|
||||
widgets: [inputWidget],
|
||||
widgets: [metadataWidget, inputWidget],
|
||||
addInput: vi.fn(),
|
||||
graph: {},
|
||||
};
|
||||
@@ -106,6 +125,7 @@ describe("Lora Loader trigger word updates", () => {
|
||||
// The widget is now the AUTOCOMPLETE_TEXT_LORAS type, created automatically by Vue widgets
|
||||
expect(node.inputWidget).toBe(inputWidget);
|
||||
expect(node.lorasWidget).toBeDefined();
|
||||
expect(getWidgetByName).toHaveBeenCalledWith(node, "text");
|
||||
|
||||
// The callback should have been set up by onNodeCreated
|
||||
const inputCallback = inputWidget.callback;
|
||||
|
||||
@@ -50,6 +50,13 @@ const getAllGraphNodes = vi.fn();
|
||||
const getNodeFromGraph = vi.fn();
|
||||
const getNodeKey = vi.fn();
|
||||
const getLinkFromGraph = vi.fn();
|
||||
const getWidgetByName = vi.fn((node, name) =>
|
||||
node?.widgets?.find((widget) => widget?.name === name) ?? null
|
||||
);
|
||||
const getWidgetSerializedValue = vi.fn((node, name) => {
|
||||
const index = node?.widgets?.findIndex((widget) => widget?.name === name) ?? -1;
|
||||
return index >= 0 ? node.widgets_values?.[index] : undefined;
|
||||
});
|
||||
const chainCallback = vi.fn((proto, property, callback) => {
|
||||
proto[property] = callback;
|
||||
});
|
||||
@@ -68,6 +75,8 @@ vi.mock(UTILS_MODULE, async (importOriginal) => {
|
||||
getNodeFromGraph,
|
||||
getNodeKey,
|
||||
getLinkFromGraph,
|
||||
getWidgetByName,
|
||||
getWidgetSerializedValue,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -98,6 +107,9 @@ describe("Node mode change handling", () => {
|
||||
mergeLoras.mockClear();
|
||||
mergeLoras.mockImplementation(() => [{ name: "Alpha", active: true }]);
|
||||
|
||||
getWidgetByName.mockClear();
|
||||
getWidgetSerializedValue.mockClear();
|
||||
|
||||
addLorasWidget.mockClear();
|
||||
addLorasWidget.mockImplementation((_node, _name, _opts, callback) => ({
|
||||
widget: { value: [], callback },
|
||||
@@ -119,8 +131,13 @@ describe("Node mode change handling", () => {
|
||||
|
||||
await extension.beforeRegisterNodeDef(nodeType, nodeData, {});
|
||||
|
||||
// Create widgets with proper structure for lora_stacker.js
|
||||
// Widget at index 0 is the AUTOCOMPLETE_TEXT_LORAS widget (created by Vue widgets)
|
||||
// Include a hidden metadata widget ahead of the actual text widget to match runtime ordering.
|
||||
const metadataWidget = {
|
||||
name: "__autocomplete_metadata_text",
|
||||
value: { version: 1, textWidgetName: "text" },
|
||||
options: {},
|
||||
};
|
||||
|
||||
const inputWidget = {
|
||||
name: "text",
|
||||
value: "",
|
||||
@@ -139,7 +156,7 @@ describe("Node mode change handling", () => {
|
||||
|
||||
node = {
|
||||
comfyClass: "Lora Stacker (LoraManager)",
|
||||
widgets: [inputWidget, lorasWidget],
|
||||
widgets: [metadataWidget, inputWidget, lorasWidget],
|
||||
lorasWidget,
|
||||
addInput: vi.fn(),
|
||||
mode: 0, // Initial mode
|
||||
@@ -189,11 +206,18 @@ describe("Node mode change handling", () => {
|
||||
const nodeType = { comfyClass: "Lora Loader (LoraManager)", prototype: {} };
|
||||
await extension.beforeRegisterNodeDef(nodeType, {}, {});
|
||||
|
||||
// Widget at index 0 is the AUTOCOMPLETE_TEXT_LORAS widget (created by Vue widgets)
|
||||
const metadataWidget = {
|
||||
name: "__autocomplete_metadata_text",
|
||||
value: { version: 1, textWidgetName: "text" },
|
||||
options: {},
|
||||
};
|
||||
|
||||
node = {
|
||||
comfyClass: "Lora Loader (LoraManager)",
|
||||
widgets: [
|
||||
metadataWidget,
|
||||
{
|
||||
name: "text",
|
||||
value: "",
|
||||
options: {},
|
||||
callback: null, // Will be set by onNodeCreated
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
mergeLoras,
|
||||
getAllGraphNodes,
|
||||
getNodeFromGraph,
|
||||
getWidgetByName,
|
||||
getWidgetSerializedValue,
|
||||
} from "./utils.js";
|
||||
import { addLorasWidget } from "./loras_widget.js";
|
||||
import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js";
|
||||
@@ -148,7 +150,11 @@ app.registerExtension({
|
||||
};
|
||||
|
||||
// Get the text input widget (AUTOCOMPLETE_TEXT_LORAS type, created by Vue widgets)
|
||||
const inputWidget = this.widgets[0];
|
||||
const inputWidget = getWidgetByName(this, "text");
|
||||
if (!inputWidget) {
|
||||
console.warn("LoRA Manager: text widget not found for Lora Loader");
|
||||
return;
|
||||
}
|
||||
this.inputWidget = inputWidget;
|
||||
|
||||
const scheduleInputSync = debounce((lorasValue) => {
|
||||
@@ -227,12 +233,16 @@ app.registerExtension({
|
||||
// Restore saved value if exists
|
||||
let existingLoras = [];
|
||||
if (node.widgets_values && node.widgets_values.length > 0) {
|
||||
// 0 for input widget, 1 for loras widget
|
||||
const savedValue = node.widgets_values[1];
|
||||
const savedValue = getWidgetSerializedValue(node, "loras");
|
||||
existingLoras = savedValue || [];
|
||||
}
|
||||
// Merge the loras data
|
||||
const mergedLoras = mergeLoras(node.widgets[0].value, existingLoras);
|
||||
const inputWidget = node.inputWidget || getWidgetByName(node, "text");
|
||||
if (!inputWidget) {
|
||||
console.warn("LoRA Manager: text widget not found while restoring Lora Loader");
|
||||
return;
|
||||
}
|
||||
const mergedLoras = mergeLoras(inputWidget.value, existingLoras);
|
||||
node.lorasWidget.value = mergedLoras;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
updateDownstreamLoaders,
|
||||
chainCallback,
|
||||
mergeLoras,
|
||||
getWidgetByName,
|
||||
getWidgetSerializedValue,
|
||||
} from "./utils.js";
|
||||
import { addLorasWidget } from "./loras_widget.js";
|
||||
import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js";
|
||||
@@ -28,7 +30,11 @@ app.registerExtension({
|
||||
let isSyncingInput = false;
|
||||
|
||||
// Get the text input widget (AUTOCOMPLETE_TEXT_LORAS type, created by Vue widgets)
|
||||
const inputWidget = this.widgets[0];
|
||||
const inputWidget = getWidgetByName(this, "text");
|
||||
if (!inputWidget) {
|
||||
console.warn("LoRA Manager: text widget not found for Lora Stacker");
|
||||
return;
|
||||
}
|
||||
this.inputWidget = inputWidget;
|
||||
|
||||
const scheduleInputSync = debounce((lorasValue) => {
|
||||
@@ -122,12 +128,15 @@ app.registerExtension({
|
||||
// Restore saved value if exists
|
||||
let existingLoras = [];
|
||||
if (node.widgets_values && node.widgets_values.length > 0) {
|
||||
// 0 for input widget, 1 for loras widget
|
||||
const savedValue = node.widgets_values[1];
|
||||
const savedValue = getWidgetSerializedValue(node, "loras");
|
||||
existingLoras = savedValue || [];
|
||||
}
|
||||
// Merge the loras data
|
||||
const inputWidget = node.inputWidget || node.widgets[0];
|
||||
const inputWidget = node.inputWidget || getWidgetByName(node, "text");
|
||||
if (!inputWidget) {
|
||||
console.warn("LoRA Manager: text widget not found while restoring Lora Stacker");
|
||||
return;
|
||||
}
|
||||
const mergedLoras = mergeLoras(inputWidget.value, existingLoras);
|
||||
node.lorasWidget.value = mergedLoras;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,27 @@ export function getNodeKey(node) {
|
||||
return `${getNodeGraphId(node)}:${node.id}`;
|
||||
}
|
||||
|
||||
export function getWidgetByName(node, widgetName) {
|
||||
if (!node || !Array.isArray(node.widgets)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return node.widgets.find((widget) => widget?.name === widgetName) || null;
|
||||
}
|
||||
|
||||
export function getWidgetSerializedValue(node, widgetName) {
|
||||
if (!node || !Array.isArray(node.widgets) || !Array.isArray(node.widgets_values)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const widgetIndex = node.widgets.findIndex((widget) => widget?.name === widgetName);
|
||||
if (widgetIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return node.widgets_values[widgetIndex];
|
||||
}
|
||||
|
||||
export function getLinkFromGraph(graph, linkId) {
|
||||
if (!graph || graph.links == null) {
|
||||
return null;
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
updateConnectedTriggerWords,
|
||||
chainCallback,
|
||||
mergeLoras,
|
||||
getWidgetByName,
|
||||
getWidgetSerializedValue,
|
||||
} from "./utils.js";
|
||||
import { addLorasWidget } from "./loras_widget.js";
|
||||
import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js";
|
||||
@@ -31,7 +33,11 @@ app.registerExtension({
|
||||
let isSyncingInput = false;
|
||||
|
||||
// Get the text input widget (AUTOCOMPLETE_TEXT_LORAS type, at index 2 after low_mem_load and merge_loras)
|
||||
const inputWidget = this.widgets[2];
|
||||
const inputWidget = getWidgetByName(this, "text");
|
||||
if (!inputWidget) {
|
||||
console.warn("LoRA Manager: text widget not found for WanVideo Lora Select");
|
||||
return;
|
||||
}
|
||||
this.inputWidget = inputWidget;
|
||||
|
||||
const scheduleInputSync = debounce((lorasValue) => {
|
||||
@@ -107,12 +113,15 @@ app.registerExtension({
|
||||
// Restore saved value if exists
|
||||
let existingLoras = [];
|
||||
if (node.widgets_values && node.widgets_values.length > 0) {
|
||||
// 0 for low_mem_load, 1 for merge_loras, 2 for text widget, 3 for loras widget
|
||||
const savedValue = node.widgets_values[3];
|
||||
const savedValue = getWidgetSerializedValue(node, "loras");
|
||||
existingLoras = savedValue || [];
|
||||
}
|
||||
// Merge the loras data
|
||||
const inputWidget = node.inputWidget || node.widgets[2];
|
||||
const inputWidget = node.inputWidget || getWidgetByName(node, "text");
|
||||
if (!inputWidget) {
|
||||
console.warn("LoRA Manager: text widget not found while restoring WanVideo Lora Select");
|
||||
return;
|
||||
}
|
||||
const mergedLoras = mergeLoras(inputWidget.value, existingLoras);
|
||||
node.lorasWidget.value = mergedLoras;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user