410c80afc8
Added /api/active-project endpoint that reads current_project from config. ProjectSource now hides the project_name widget and fetches the active project automatically on create, load, and click. Title shows "Source: <label> [<project>]" for at-a-glance status. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
8.7 KiB
JavaScript
201 lines
8.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 active project from Manager and update project_name + title
|
|
async function refreshActiveProject(node) {
|
|
const urlW = node.widgets?.find(w => w.name === "manager_url");
|
|
if (!urlW?.value) return;
|
|
try {
|
|
const resp = await fetch(`${urlW.value}/api/active-project`);
|
|
if (!resp.ok) return;
|
|
const data = await resp.json();
|
|
const project = data.project || "";
|
|
const projW = node.widgets?.find(w => w.name === "project_name");
|
|
if (projW && projW.value !== project) {
|
|
projW.value = project;
|
|
await refreshFiles(node);
|
|
}
|
|
_updateTitle(node);
|
|
} catch (e) {
|
|
console.warn("[ProjectSource] Failed to fetch active project:", e);
|
|
}
|
|
}
|
|
|
|
function _updateTitle(node) {
|
|
const labelW = node.widgets?.find(w => w.name === "label");
|
|
const projW = node.widgets?.find(w => w.name === "project_name");
|
|
const label = labelW?.value || "";
|
|
const project = projW?.value || "?";
|
|
node.title = label ? `Source: ${label} [${project}]` : `Project Source [${project}]`;
|
|
app.graph?.setDirtyCanvas(true, true);
|
|
}
|
|
|
|
// 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();
|
|
const fileList = (data.files || []).map(f => f.name || f);
|
|
console.log(`[ProjectSource] refreshFiles: got ${fileList.length} files:`, fileList);
|
|
|
|
const fileW = node.widgets?.find(w => w.name === "file_name");
|
|
if (fileW) {
|
|
const currentValue = fileW.value;
|
|
fileW.options.values = fileList.length > 0 ? fileList : [""];
|
|
// Keep current selection if still valid
|
|
if (currentValue && fileList.includes(currentValue)) {
|
|
fileW.value = currentValue;
|
|
}
|
|
}
|
|
} 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;
|
|
|
|
// Hide project_name — it is auto-filled from the Manager's active project
|
|
const projW = this.widgets?.find(w => w.name === "project_name");
|
|
if (projW) {
|
|
if (projW.origType === undefined) projW.origType = projW.type;
|
|
projW.type = "hidden";
|
|
projW.hidden = true;
|
|
projW.computeSize = () => [0, -4];
|
|
}
|
|
|
|
// Replace file_name STRING with a combo
|
|
replaceWithCombo(this, "file_name", [], function (value) {
|
|
notifyRelays(node);
|
|
});
|
|
|
|
// Hook manager_url to refresh active project + files + notify relays
|
|
const urlW = this.widgets?.find(w => w.name === "manager_url");
|
|
if (urlW) {
|
|
const origCb = urlW.callback;
|
|
urlW.callback = function (...args) {
|
|
origCb?.apply(this, args);
|
|
refreshActiveProject(node).then(() => 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);
|
|
_updateTitle(node);
|
|
};
|
|
}
|
|
|
|
// Auto-fetch active project on creation
|
|
queueMicrotask(() => refreshActiveProject(node));
|
|
};
|
|
|
|
const origOnConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function (info) {
|
|
origOnConfigure?.apply(this, arguments);
|
|
|
|
// Hide project_name (may have been serialized as visible)
|
|
const projW = this.widgets?.find(w => w.name === "project_name");
|
|
if (projW) {
|
|
if (projW.origType === undefined) projW.origType = projW.type;
|
|
projW.type = "hidden";
|
|
projW.hidden = true;
|
|
projW.computeSize = () => [0, -4];
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
}
|
|
|
|
_updateTitle(this);
|
|
|
|
// Deferred: fetch active project (and files) once graph is ready
|
|
const node = this;
|
|
queueMicrotask(() => refreshActiveProject(node));
|
|
};
|
|
|
|
// Re-check active project on click (picks up changes made in the Manager)
|
|
const origOnMouseDown = nodeType.prototype.onMouseDown;
|
|
nodeType.prototype.onMouseDown = function (e, localPos, graphCanvas) {
|
|
origOnMouseDown?.apply(this, arguments);
|
|
refreshActiveProject(this);
|
|
};
|
|
},
|
|
});
|