From 281c04dd2eae3dc45ad39a2b748e0ad974ce914f Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 3 Apr 2026 00:36:48 +0200 Subject: [PATCH] feat: ProjectResolution JS extension for ComfyUI frontend Co-Authored-By: Claude Sonnet 4.6 --- web/project_resolution.js | 191 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 web/project_resolution.js diff --git a/web/project_resolution.js b/web/project_resolution.js new file mode 100644 index 0000000..51a19b0 --- /dev/null +++ b/web/project_resolution.js @@ -0,0 +1,191 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +app.registerExtension({ + name: "json.manager.project.resolution", + + async beforeQueuePrompt() { + if (!app.graph?._nodes) return; + for (const node of app.graph._nodes) { + if (node.type === "ProjectResolution" && node._syncFromSource) { + node._syncFromSource(); + } + } + }, + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "ProjectResolution") return; + + function hideWidget(widget) { + if (widget.origType === undefined) widget.origType = widget.type; + widget.type = "hidden"; + widget.hidden = true; + widget.computeSize = () => [0, -4]; + } + + 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 : [""]; + 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; + } + + const origOnNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + origOnNodeCreated?.apply(this, arguments); + this._configured = false; + + // Hide synced config widgets — index stays visible, user wires it from loop node + for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) { + const w = this.widgets?.find(w => w.name === name); + if (w) hideWidget(w); + } + + const node = this; + const sourceLabels = this._getSourceLabels?.() || []; + const srcCombo = replaceWithCombo(this, "source_label", sourceLabels, function (value) { + node._syncFromSource(); + node._refreshKeys(); + }); + if (srcCombo) srcCombo.value = sourceLabels[0] || ""; + + const keyCombo = replaceWithCombo(this, "key_name", [], function (value) { + node.title = value ? `Resolution: ${value}` : "Project Resolution"; + app.graph?.setDirtyCanvas(true, true); + }); + if (keyCombo) keyCombo.value = ""; + + queueMicrotask(() => { + if (!this._configured) { + this.setSize(this.computeSize()); + } + }); + }; + + nodeType.prototype._getSourceLabels = function () { + const seen = new Set(); + const labels = []; + if (!this.graph) return labels; + for (const node of this.graph._nodes) { + if (node.type === "ProjectSource") { + const lw = node.widgets?.find(w => w.name === "label"); + if (lw?.value && !seen.has(lw.value)) { + seen.add(lw.value); + labels.push(lw.value); + } + } + } + return labels; + }; + + nodeType.prototype._findSource = function (label) { + if (!this.graph || !label) return null; + for (const node of this.graph._nodes) { + if (node.type === "ProjectSource") { + const lw = node.widgets?.find(w => w.name === "label"); + if (lw?.value === label) return node; + } + } + return null; + }; + + nodeType.prototype._syncFromSource = function () { + const srcWidget = this.widgets?.find(w => w.name === "source_label"); + const source = this._findSource(srcWidget?.value); + if (!source) return; + for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) { + const dst = this.widgets?.find(w => w.name === name); + const src = source.widgets?.find(w => w.name === name); + if (dst && src) dst.value = src.value; + } + }; + + nodeType.prototype._refreshKeys = async function () { + const urlW = this.widgets?.find(w => w.name === "manager_url"); + const projW = this.widgets?.find(w => w.name === "project_name"); + const fileW = this.widgets?.find(w => w.name === "file_name"); + const seqW = this.widgets?.find(w => w.name === "sequence_number"); + if (!urlW?.value || !projW?.value || !fileW?.value) return; + + try { + const resp = await api.fetchApi( + `/json_manager/get_project_keys?url=${encodeURIComponent(urlW.value)}&project=${encodeURIComponent(projW.value)}&file=${encodeURIComponent(fileW.value)}&seq=${seqW?.value || 1}` + ); + if (!resp.ok) return; + const data = await resp.json(); + if (data.error || !Array.isArray(data.keys)) return; + + const keyWidget = this.widgets?.find(w => w.name === "key_name"); + if (keyWidget) { + keyWidget.options.values = data.keys.length > 0 ? data.keys : [""]; + } + } catch (e) { + console.error("[ProjectResolution] Failed to refresh keys:", e); + } + }; + + const origOnMouseDown = nodeType.prototype.onMouseDown; + nodeType.prototype.onMouseDown = function (e, localPos, graphCanvas) { + origOnMouseDown?.apply(this, arguments); + const srcWidget = this.widgets?.find(w => w.name === "source_label"); + if (srcWidget) srcWidget.options.values = this._getSourceLabels(); + this._syncFromSource(); + }; + + const origOnConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function (info) { + origOnConfigure?.apply(this, arguments); + this._configured = true; + + for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) { + const w = this.widgets?.find(w => w.name === name); + if (w) hideWidget(w); + } + + const srcWidget = this.widgets?.find(w => w.name === "source_label"); + if (srcWidget && srcWidget.type !== "combo") { + const node = this; + replaceWithCombo(this, "source_label", this._getSourceLabels(), function (value) { + node._syncFromSource(); + node._refreshKeys(); + }); + } else if (srcWidget) { + srcWidget.options.values = this._getSourceLabels(); + } + + const keyWidget = this.widgets?.find(w => w.name === "key_name"); + if (keyWidget && keyWidget.type !== "combo") { + const node = this; + replaceWithCombo(this, "key_name", [], function (value) { + node.title = value ? `Resolution: ${value}` : "Project Resolution"; + app.graph?.setDirtyCanvas(true, true); + }); + } + + const finalKeyWidget = this.widgets?.find(w => w.name === "key_name"); + if (finalKeyWidget?.value) { + this.title = `Resolution: ${finalKeyWidget.value}`; + } + + this.setSize(this.computeSize()); + + const node = this; + queueMicrotask(() => { + node._syncFromSource(); + node._refreshKeys(); + }); + }; + }, +});