Port dynamic node improvements from ComfyUI-JSON-Dynamic
- Deferred output cleanup (_configured flag + queueMicrotask) to prevent breaking links when other nodes (e.g. Kijai Set/Get) resolve outputs during graph loading - file_not_found error handling in refresh to keep existing outputs intact - Fallback widget sync in onConfigure when widget values are empty but serialized outputs exist Applied to both json_dynamic.js and project_dynamic.js. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,8 @@ if PromptServer is not None:
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
seq = 1
|
seq = 1
|
||||||
data = read_json_data(json_path)
|
data = read_json_data(json_path)
|
||||||
|
if not data:
|
||||||
|
return web.json_response({"keys": [], "types": [], "error": "file_not_found"})
|
||||||
target = get_batch_item(data, seq)
|
target = get_batch_item(data, seq)
|
||||||
keys = []
|
keys = []
|
||||||
types = []
|
types = []
|
||||||
|
|||||||
@@ -17,17 +17,31 @@ app.registerExtension({
|
|||||||
if (w) { w.type = "hidden"; w.computeSize = () => [0, -4]; }
|
if (w) { w.type = "hidden"; w.computeSize = () => [0, -4]; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all 32 default outputs from Python RETURN_TYPES
|
// Do NOT remove default outputs synchronously here.
|
||||||
while (this.outputs.length > 0) {
|
// During graph loading, ComfyUI creates all nodes (firing onNodeCreated)
|
||||||
this.removeOutput(0);
|
// before configuring them. Other nodes (e.g. Kijai Set/Get) may resolve
|
||||||
}
|
// links to our outputs during their configure step. If we remove outputs
|
||||||
|
// here, those nodes find no output slot and error out.
|
||||||
|
//
|
||||||
|
// Instead, defer cleanup: for loaded workflows onConfigure sets _configured
|
||||||
|
// before this runs; for new nodes the defaults are cleaned up.
|
||||||
|
this._configured = false;
|
||||||
|
|
||||||
// Add Refresh button
|
// Add Refresh button
|
||||||
this.addWidget("button", "Refresh Outputs", null, () => {
|
this.addWidget("button", "Refresh Outputs", null, () => {
|
||||||
this.refreshDynamicOutputs();
|
this.refreshDynamicOutputs();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setSize(this.computeSize());
|
queueMicrotask(() => {
|
||||||
|
if (!this._configured) {
|
||||||
|
// New node (not loading) — remove the 32 Python default outputs
|
||||||
|
while (this.outputs.length > 0) {
|
||||||
|
this.removeOutput(0);
|
||||||
|
}
|
||||||
|
this.setSize(this.computeSize());
|
||||||
|
app.graph?.setDirtyCanvas(true, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
nodeType.prototype.refreshDynamicOutputs = async function () {
|
nodeType.prototype.refreshDynamicOutputs = async function () {
|
||||||
@@ -39,7 +53,14 @@ app.registerExtension({
|
|||||||
const resp = await api.fetchApi(
|
const resp = await api.fetchApi(
|
||||||
`/json_manager/get_keys?path=${encodeURIComponent(pathWidget.value)}&sequence_number=${seqWidget?.value || 1}`
|
`/json_manager/get_keys?path=${encodeURIComponent(pathWidget.value)}&sequence_number=${seqWidget?.value || 1}`
|
||||||
);
|
);
|
||||||
const { keys, types } = await resp.json();
|
const data = await resp.json();
|
||||||
|
const { keys, types } = data;
|
||||||
|
|
||||||
|
// If the file wasn't found, keep existing outputs and links intact
|
||||||
|
if (data.error === "file_not_found") {
|
||||||
|
console.warn("[JSONLoaderDynamic] File not found, keeping existing outputs:", pathWidget.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Store keys and types in hidden widgets for persistence
|
// Store keys and types in hidden widgets for persistence
|
||||||
const okWidget = this.widgets?.find(w => w.name === "output_keys");
|
const okWidget = this.widgets?.find(w => w.name === "output_keys");
|
||||||
@@ -82,7 +103,6 @@ app.registerExtension({
|
|||||||
|
|
||||||
// Reassign the outputs array and fix link slot indices
|
// Reassign the outputs array and fix link slot indices
|
||||||
this.outputs = newOutputs;
|
this.outputs = newOutputs;
|
||||||
// Update link origin_slot to match new positions
|
|
||||||
if (this.graph) {
|
if (this.graph) {
|
||||||
for (let i = 0; i < this.outputs.length; i++) {
|
for (let i = 0; i < this.outputs.length; i++) {
|
||||||
const links = this.outputs[i].links;
|
const links = this.outputs[i].links;
|
||||||
@@ -105,6 +125,7 @@ app.registerExtension({
|
|||||||
const origOnConfigure = nodeType.prototype.onConfigure;
|
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||||
nodeType.prototype.onConfigure = function (info) {
|
nodeType.prototype.onConfigure = function (info) {
|
||||||
origOnConfigure?.apply(this, arguments);
|
origOnConfigure?.apply(this, arguments);
|
||||||
|
this._configured = true;
|
||||||
|
|
||||||
// Hide internal widgets
|
// Hide internal widgets
|
||||||
for (const name of ["output_keys", "output_types"]) {
|
for (const name of ["output_keys", "output_types"]) {
|
||||||
@@ -122,16 +143,23 @@ app.registerExtension({
|
|||||||
? otWidget.value.split(",")
|
? otWidget.value.split(",")
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// On load, LiteGraph already restored serialized outputs with links.
|
if (keys.length > 0) {
|
||||||
// Rename and set types to match stored state (preserves links).
|
// On load, LiteGraph already restored serialized outputs with links.
|
||||||
for (let i = 0; i < this.outputs.length && i < keys.length; i++) {
|
// Rename and set types to match stored state (preserves links).
|
||||||
this.outputs[i].name = keys[i].trim();
|
for (let i = 0; i < this.outputs.length && i < keys.length; i++) {
|
||||||
if (types[i]) this.outputs[i].type = types[i];
|
this.outputs[i].name = keys[i].trim();
|
||||||
}
|
if (types[i]) this.outputs[i].type = types[i];
|
||||||
|
}
|
||||||
|
|
||||||
// Remove any extra outputs beyond the key count
|
// Remove any extra outputs beyond the key count
|
||||||
while (this.outputs.length > keys.length) {
|
while (this.outputs.length > keys.length) {
|
||||||
this.removeOutput(this.outputs.length - 1);
|
this.removeOutput(this.outputs.length - 1);
|
||||||
|
}
|
||||||
|
} else if (this.outputs.length > 0) {
|
||||||
|
// Widget values empty but serialized outputs exist — sync widgets
|
||||||
|
// from the outputs LiteGraph already restored (fallback).
|
||||||
|
if (okWidget) okWidget.value = this.outputs.map(o => o.name).join(",");
|
||||||
|
if (otWidget) otWidget.value = this.outputs.map(o => o.type).join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setSize(this.computeSize());
|
this.setSize(this.computeSize());
|
||||||
|
|||||||
@@ -17,17 +17,31 @@ app.registerExtension({
|
|||||||
if (w) { w.type = "hidden"; w.computeSize = () => [0, -4]; }
|
if (w) { w.type = "hidden"; w.computeSize = () => [0, -4]; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all 32 default outputs from Python RETURN_TYPES
|
// Do NOT remove default outputs synchronously here.
|
||||||
while (this.outputs.length > 0) {
|
// During graph loading, ComfyUI creates all nodes (firing onNodeCreated)
|
||||||
this.removeOutput(0);
|
// before configuring them. Other nodes (e.g. Kijai Set/Get) may resolve
|
||||||
}
|
// links to our outputs during their configure step. If we remove outputs
|
||||||
|
// here, those nodes find no output slot and error out.
|
||||||
|
//
|
||||||
|
// Instead, defer cleanup: for loaded workflows onConfigure sets _configured
|
||||||
|
// before this runs; for new nodes the defaults are cleaned up.
|
||||||
|
this._configured = false;
|
||||||
|
|
||||||
// Add Refresh button
|
// Add Refresh button
|
||||||
this.addWidget("button", "Refresh Outputs", null, () => {
|
this.addWidget("button", "Refresh Outputs", null, () => {
|
||||||
this.refreshDynamicOutputs();
|
this.refreshDynamicOutputs();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setSize(this.computeSize());
|
queueMicrotask(() => {
|
||||||
|
if (!this._configured) {
|
||||||
|
// New node (not loading) — remove the 32 Python default outputs
|
||||||
|
while (this.outputs.length > 0) {
|
||||||
|
this.removeOutput(0);
|
||||||
|
}
|
||||||
|
this.setSize(this.computeSize());
|
||||||
|
app.graph?.setDirtyCanvas(true, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
nodeType.prototype.refreshDynamicOutputs = async function () {
|
nodeType.prototype.refreshDynamicOutputs = async function () {
|
||||||
@@ -42,7 +56,14 @@ app.registerExtension({
|
|||||||
const resp = await api.fetchApi(
|
const resp = await api.fetchApi(
|
||||||
`/json_manager/get_project_keys?url=${encodeURIComponent(urlWidget.value)}&project=${encodeURIComponent(projectWidget.value)}&file=${encodeURIComponent(fileWidget.value)}&seq=${seqWidget?.value || 1}`
|
`/json_manager/get_project_keys?url=${encodeURIComponent(urlWidget.value)}&project=${encodeURIComponent(projectWidget.value)}&file=${encodeURIComponent(fileWidget.value)}&seq=${seqWidget?.value || 1}`
|
||||||
);
|
);
|
||||||
const { keys, types } = await resp.json();
|
const data = await resp.json();
|
||||||
|
const { keys, types } = data;
|
||||||
|
|
||||||
|
// If the API returned an error, keep existing outputs and links intact
|
||||||
|
if (data.error) {
|
||||||
|
console.warn("[ProjectLoaderDynamic] API error, keeping existing outputs:", data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Store keys and types in hidden widgets for persistence
|
// Store keys and types in hidden widgets for persistence
|
||||||
const okWidget = this.widgets?.find(w => w.name === "output_keys");
|
const okWidget = this.widgets?.find(w => w.name === "output_keys");
|
||||||
@@ -105,6 +126,7 @@ app.registerExtension({
|
|||||||
const origOnConfigure = nodeType.prototype.onConfigure;
|
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||||
nodeType.prototype.onConfigure = function (info) {
|
nodeType.prototype.onConfigure = function (info) {
|
||||||
origOnConfigure?.apply(this, arguments);
|
origOnConfigure?.apply(this, arguments);
|
||||||
|
this._configured = true;
|
||||||
|
|
||||||
// Hide internal widgets
|
// Hide internal widgets
|
||||||
for (const name of ["output_keys", "output_types"]) {
|
for (const name of ["output_keys", "output_types"]) {
|
||||||
@@ -122,15 +144,23 @@ app.registerExtension({
|
|||||||
? otWidget.value.split(",")
|
? otWidget.value.split(",")
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Rename and set types to match stored state (preserves links)
|
if (keys.length > 0) {
|
||||||
for (let i = 0; i < this.outputs.length && i < keys.length; i++) {
|
// On load, LiteGraph already restored serialized outputs with links.
|
||||||
this.outputs[i].name = keys[i].trim();
|
// Rename and set types to match stored state (preserves links).
|
||||||
if (types[i]) this.outputs[i].type = types[i];
|
for (let i = 0; i < this.outputs.length && i < keys.length; i++) {
|
||||||
}
|
this.outputs[i].name = keys[i].trim();
|
||||||
|
if (types[i]) this.outputs[i].type = types[i];
|
||||||
|
}
|
||||||
|
|
||||||
// Remove any extra outputs beyond the key count
|
// Remove any extra outputs beyond the key count
|
||||||
while (this.outputs.length > keys.length) {
|
while (this.outputs.length > keys.length) {
|
||||||
this.removeOutput(this.outputs.length - 1);
|
this.removeOutput(this.outputs.length - 1);
|
||||||
|
}
|
||||||
|
} else if (this.outputs.length > 0) {
|
||||||
|
// Widget values empty but serialized outputs exist — sync widgets
|
||||||
|
// from the outputs LiteGraph already restored (fallback).
|
||||||
|
if (okWidget) okWidget.value = this.outputs.map(o => o.name).join(",");
|
||||||
|
if (otWidget) otWidget.value = this.outputs.map(o => o.type).join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setSize(this.computeSize());
|
this.setSize(this.computeSize());
|
||||||
|
|||||||
Reference in New Issue
Block a user