Files
Comfyui-JSON-Manager/web/project_source.js
T
Ethanfel 3b11a4e974 feat: file_name combo on ProjectSource, sequence_number output
- file_name is now a combo dropdown populated from the API when
  manager_url and project_name are set
- ProjectSource outputs sequence_number (INT) for downstream use
- Refreshes file list when project_name or manager_url changes
- Updated tests for new output and error-default behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:20:41 +01:00

159 lines
6.7 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "json.manager.project.source",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "ProjectSource") return;
// Helper: replace a STRING widget with a proper combo widget
function replaceWithCombo(node, name, values, callback) {
const idx = node.widgets?.findIndex(w => w.name === name);
if (idx === -1 || idx === undefined) return null;
const oldWidget = node.widgets[idx];
const savedValue = oldWidget.value || "";
const comboValues = values.length > 0 ? values : [""];
// Always preserve saved value (may not be in list yet)
if (savedValue && !comboValues.includes(savedValue)) {
comboValues.unshift(savedValue);
}
const defaultValue = savedValue || comboValues[0];
node.widgets.splice(idx, 1);
const combo = node.addWidget("combo", name, defaultValue, callback, { values: comboValues });
if (node.widgets.length > 1) {
node.widgets.splice(node.widgets.length - 1, 1);
node.widgets.splice(idx, 0, combo);
}
return combo;
}
// Fetch file list from API and update file_name combo
async function refreshFiles(node) {
const urlW = node.widgets?.find(w => w.name === "manager_url");
const projW = node.widgets?.find(w => w.name === "project_name");
if (!urlW?.value || !projW?.value) return;
try {
const resp = await api.fetchApi(
`/json_manager/list_project_files?url=${encodeURIComponent(urlW.value)}&project=${encodeURIComponent(projW.value)}`
);
if (!resp.ok) return;
const data = await resp.json();
if (!Array.isArray(data)) return;
const fileW = node.widgets?.find(w => w.name === "file_name");
if (fileW) {
const currentValue = fileW.value;
fileW.options.values = data.length > 0 ? data : [""];
// Keep current selection if still valid
if (currentValue && data.includes(currentValue)) {
fileW.value = currentValue;
}
console.log(`[ProjectSource] refreshFiles: ${data.length} files loaded`);
}
} catch (e) {
console.error("[ProjectSource] Failed to refresh files:", e);
}
}
// Notify all ProjectKey nodes referencing this source to re-sync
function notifyRelays(sourceNode) {
if (!sourceNode.graph?._nodes) return;
const labelW = sourceNode.widgets?.find(w => w.name === "label");
if (!labelW?.value) return;
console.log(`[ProjectSource] notifyRelays: label="${labelW.value}", scanning ${sourceNode.graph._nodes.length} nodes`);
let matched = 0;
for (const node of sourceNode.graph._nodes) {
if (node.type === "ProjectKey" && node._syncFromSource && node._refreshKeys) {
const srcW = node.widgets?.find(w => w.name === "source_label");
console.log(`[ProjectSource] ProjectKey id=${node.id} source_label="${srcW?.value}"`);
if (srcW?.value === labelW.value) {
matched++;
node._syncFromSource();
node._refreshKeys();
}
}
}
console.log(`[ProjectSource] notifyRelays: matched ${matched} relays`);
}
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnNodeCreated?.apply(this, arguments);
const node = this;
// Replace file_name STRING with a combo
replaceWithCombo(this, "file_name", [], function (value) {
notifyRelays(node);
});
// Hook manager_url and project_name to refresh file list + notify relays
for (const name of ["manager_url", "project_name"]) {
const w = this.widgets?.find(w => w.name === name);
if (w) {
const origCb = w.callback;
w.callback = function (...args) {
origCb?.apply(this, args);
refreshFiles(node);
notifyRelays(node);
};
}
}
// Hook sequence_number to notify relays
const seqW = this.widgets?.find(w => w.name === "sequence_number");
if (seqW) {
const origCb = seqW.callback;
seqW.callback = function (...args) {
origCb?.apply(this, args);
notifyRelays(node);
};
}
// Update title when label changes
const labelWidget = this.widgets?.find(w => w.name === "label");
if (labelWidget) {
const origCallback = labelWidget.callback;
labelWidget.callback = function (...args) {
origCallback?.apply(this, args);
node.title = labelWidget.value
? `Source: ${labelWidget.value}`
: "Project Source";
app.graph?.setDirtyCanvas(true, true);
};
// Set initial title
if (labelWidget.value) {
this.title = `Source: ${labelWidget.value}`;
}
}
};
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
origOnConfigure?.apply(this, arguments);
// Ensure file_name is a combo (may be STRING from serialization)
const fileW = this.widgets?.find(w => w.name === "file_name");
if (fileW && fileW.type !== "combo") {
const node = this;
replaceWithCombo(this, "file_name", [], function (value) {
notifyRelays(node);
});
}
const labelWidget = this.widgets?.find(w => w.name === "label");
if (labelWidget?.value) {
this.title = `Source: ${labelWidget.value}`;
}
// Deferred: refresh file list once graph is ready
const node = this;
queueMicrotask(() => {
refreshFiles(node);
});
};
},
});