Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cf8cc1f0a | |||
| 545b864c08 | |||
| ad6cd76b08 | |||
| bd7d314ae8 | |||
| 628b256981 |
@@ -207,11 +207,91 @@ class ProjectLoaderDynamic:
|
|||||||
return (total_sequences,) + tuple(results)
|
return (total_sequences,) + tuple(results)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectSource:
|
||||||
|
"""Config-only node — holds project connection settings, no outputs."""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"manager_url": ("STRING", {"default": "http://localhost:8080", "multiline": False}),
|
||||||
|
"project_name": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"file_name": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"sequence_number": ("INT", {"default": 1, "min": 1, "max": 9999}),
|
||||||
|
"label": ("STRING", {"default": "source", "multiline": False}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
RETURN_NAMES = ()
|
||||||
|
FUNCTION = "hold_config"
|
||||||
|
CATEGORY = "utils/json/project"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
|
||||||
|
def hold_config(self, manager_url, project_name, file_name, sequence_number, label):
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectKey:
|
||||||
|
"""Single-output relay — fetches one key from a ProjectSource."""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"source_label": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"key_name": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"key_type": ("STRING", {"default": "STRING", "multiline": False}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"manager_url": ("STRING", {"default": "http://localhost:8080", "multiline": False}),
|
||||||
|
"project_name": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"file_name": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"sequence_number": ("INT", {"default": 1, "min": 1, "max": 9999}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (any_type,)
|
||||||
|
RETURN_NAMES = ("value",)
|
||||||
|
FUNCTION = "fetch_key"
|
||||||
|
CATEGORY = "utils/json/project"
|
||||||
|
OUTPUT_NODE = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def IS_CHANGED(cls, **kwargs):
|
||||||
|
return float("nan") # Always re-fetch from API
|
||||||
|
|
||||||
|
def fetch_key(self, source_label, key_name, key_type,
|
||||||
|
manager_url="http://localhost:8080", project_name="",
|
||||||
|
file_name="", sequence_number=1):
|
||||||
|
# source_label is used by JS to identify which ProjectSource to sync
|
||||||
|
# config from. The actual config arrives via the optional widgets below.
|
||||||
|
data = _fetch_data(manager_url, project_name, file_name, sequence_number)
|
||||||
|
if data.get("error") in ("http_error", "network_error", "parse_error"):
|
||||||
|
msg = data.get("message", "Unknown error")
|
||||||
|
raise RuntimeError(f"Failed to fetch data: {msg}")
|
||||||
|
|
||||||
|
val = data.get(key_name, "")
|
||||||
|
|
||||||
|
if key_type == "INT":
|
||||||
|
return (to_int(val),)
|
||||||
|
elif key_type == "FLOAT":
|
||||||
|
return (to_float(val),)
|
||||||
|
elif isinstance(val, bool):
|
||||||
|
return (str(val).lower(),)
|
||||||
|
elif isinstance(val, (int, float)):
|
||||||
|
return (val,)
|
||||||
|
else:
|
||||||
|
return (str(val),)
|
||||||
|
|
||||||
|
|
||||||
# --- Mappings ---
|
# --- Mappings ---
|
||||||
PROJECT_NODE_CLASS_MAPPINGS = {
|
PROJECT_NODE_CLASS_MAPPINGS = {
|
||||||
"ProjectLoaderDynamic": ProjectLoaderDynamic,
|
"ProjectLoaderDynamic": ProjectLoaderDynamic,
|
||||||
|
"ProjectSource": ProjectSource,
|
||||||
|
"ProjectKey": ProjectKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
PROJECT_NODE_DISPLAY_NAME_MAPPINGS = {
|
PROJECT_NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"ProjectLoaderDynamic": "Project Loader (Dynamic)",
|
"ProjectLoaderDynamic": "Project Loader (Dynamic)",
|
||||||
|
"ProjectSource": "Project Source",
|
||||||
|
"ProjectKey": "Project Key",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,9 +203,140 @@ class TestProjectLoaderDynamic:
|
|||||||
assert ProjectLoaderDynamic.CATEGORY == "utils/json/project"
|
assert ProjectLoaderDynamic.CATEGORY == "utils/json/project"
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectSource:
|
||||||
|
def test_input_types(self):
|
||||||
|
from project_loader import ProjectSource
|
||||||
|
inputs = ProjectSource.INPUT_TYPES()
|
||||||
|
assert "manager_url" in inputs["required"]
|
||||||
|
assert "project_name" in inputs["required"]
|
||||||
|
assert "file_name" in inputs["required"]
|
||||||
|
assert "sequence_number" in inputs["required"]
|
||||||
|
assert "label" in inputs["required"]
|
||||||
|
|
||||||
|
def test_no_outputs(self):
|
||||||
|
from project_loader import ProjectSource
|
||||||
|
assert ProjectSource.RETURN_TYPES == ()
|
||||||
|
assert ProjectSource.RETURN_NAMES == ()
|
||||||
|
|
||||||
|
def test_hold_config_returns_empty(self):
|
||||||
|
from project_loader import ProjectSource
|
||||||
|
node = ProjectSource()
|
||||||
|
result = node.hold_config(
|
||||||
|
manager_url="http://localhost:8080",
|
||||||
|
project_name="proj1",
|
||||||
|
file_name="batch_i2v",
|
||||||
|
sequence_number=1,
|
||||||
|
label="my_source"
|
||||||
|
)
|
||||||
|
assert result == ()
|
||||||
|
|
||||||
|
def test_category(self):
|
||||||
|
from project_loader import ProjectSource
|
||||||
|
assert ProjectSource.CATEGORY == "utils/json/project"
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectKey:
|
||||||
|
def test_input_types(self):
|
||||||
|
from project_loader import ProjectKey
|
||||||
|
inputs = ProjectKey.INPUT_TYPES()
|
||||||
|
assert "source_label" in inputs["required"]
|
||||||
|
assert "key_name" in inputs["required"]
|
||||||
|
assert "key_type" in inputs["required"]
|
||||||
|
|
||||||
|
def test_single_output(self):
|
||||||
|
from project_loader import ProjectKey
|
||||||
|
assert len(ProjectKey.RETURN_TYPES) == 1
|
||||||
|
assert len(ProjectKey.RETURN_NAMES) == 1
|
||||||
|
|
||||||
|
def test_fetch_key_string(self):
|
||||||
|
from project_loader import ProjectKey
|
||||||
|
node = ProjectKey()
|
||||||
|
data = {"prompt": "hello", "seed": 42}
|
||||||
|
with patch("project_loader._fetch_data", return_value=data):
|
||||||
|
result = node.fetch_key(
|
||||||
|
source_label="my_source",
|
||||||
|
key_name="prompt",
|
||||||
|
key_type="STRING",
|
||||||
|
manager_url="http://localhost:8080",
|
||||||
|
project_name="proj1",
|
||||||
|
file_name="batch_i2v",
|
||||||
|
sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == ("hello",)
|
||||||
|
|
||||||
|
def test_fetch_key_int_coercion(self):
|
||||||
|
from project_loader import ProjectKey
|
||||||
|
node = ProjectKey()
|
||||||
|
data = {"seed": "42"}
|
||||||
|
with patch("project_loader._fetch_data", return_value=data):
|
||||||
|
result = node.fetch_key(
|
||||||
|
source_label="my_source",
|
||||||
|
key_name="seed",
|
||||||
|
key_type="INT",
|
||||||
|
manager_url="http://localhost:8080",
|
||||||
|
project_name="proj1",
|
||||||
|
file_name="batch_i2v",
|
||||||
|
sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == (42,)
|
||||||
|
|
||||||
|
def test_fetch_key_float_coercion(self):
|
||||||
|
from project_loader import ProjectKey
|
||||||
|
node = ProjectKey()
|
||||||
|
data = {"cfg": "1.5"}
|
||||||
|
with patch("project_loader._fetch_data", return_value=data):
|
||||||
|
result = node.fetch_key(
|
||||||
|
source_label="my_source",
|
||||||
|
key_name="cfg",
|
||||||
|
key_type="FLOAT",
|
||||||
|
manager_url="http://localhost:8080",
|
||||||
|
project_name="proj1",
|
||||||
|
file_name="batch_i2v",
|
||||||
|
sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == (1.5,)
|
||||||
|
|
||||||
|
def test_fetch_key_missing_key(self):
|
||||||
|
from project_loader import ProjectKey
|
||||||
|
node = ProjectKey()
|
||||||
|
with patch("project_loader._fetch_data", return_value={}):
|
||||||
|
result = node.fetch_key(
|
||||||
|
source_label="my_source",
|
||||||
|
key_name="nonexistent",
|
||||||
|
key_type="STRING",
|
||||||
|
manager_url="http://localhost:8080",
|
||||||
|
project_name="proj1",
|
||||||
|
file_name="batch_i2v",
|
||||||
|
sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == ("",)
|
||||||
|
|
||||||
|
def test_fetch_key_network_error(self):
|
||||||
|
from project_loader import ProjectKey
|
||||||
|
node = ProjectKey()
|
||||||
|
error_resp = {"error": "network_error", "message": "Connection refused"}
|
||||||
|
with patch("project_loader._fetch_data", return_value=error_resp):
|
||||||
|
with pytest.raises(RuntimeError, match="Failed to fetch"):
|
||||||
|
node.fetch_key(
|
||||||
|
source_label="my_source",
|
||||||
|
key_name="prompt",
|
||||||
|
key_type="STRING",
|
||||||
|
manager_url="http://localhost:8080",
|
||||||
|
project_name="proj1",
|
||||||
|
file_name="batch_i2v",
|
||||||
|
sequence_number=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_category(self):
|
||||||
|
from project_loader import ProjectKey
|
||||||
|
assert ProjectKey.CATEGORY == "utils/json/project"
|
||||||
|
|
||||||
|
|
||||||
class TestNodeMappings:
|
class TestNodeMappings:
|
||||||
def test_mappings_exist(self):
|
def test_mappings_exist(self):
|
||||||
from project_loader import PROJECT_NODE_CLASS_MAPPINGS, PROJECT_NODE_DISPLAY_NAME_MAPPINGS
|
from project_loader import PROJECT_NODE_CLASS_MAPPINGS, PROJECT_NODE_DISPLAY_NAME_MAPPINGS
|
||||||
assert "ProjectLoaderDynamic" in PROJECT_NODE_CLASS_MAPPINGS
|
assert "ProjectLoaderDynamic" in PROJECT_NODE_CLASS_MAPPINGS
|
||||||
assert len(PROJECT_NODE_CLASS_MAPPINGS) == 1
|
assert "ProjectSource" in PROJECT_NODE_CLASS_MAPPINGS
|
||||||
assert len(PROJECT_NODE_DISPLAY_NAME_MAPPINGS) == 1
|
assert "ProjectKey" in PROJECT_NODE_CLASS_MAPPINGS
|
||||||
|
assert len(PROJECT_NODE_CLASS_MAPPINGS) == 3
|
||||||
|
assert len(PROJECT_NODE_DISPLAY_NAME_MAPPINGS) == 3
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
import { api } from "../../scripts/api.js";
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "json.manager.project.key",
|
||||||
|
|
||||||
|
// Re-sync all ProjectKey nodes from their sources before queueing
|
||||||
|
// This fixes stale config when the user edits a ProjectSource after
|
||||||
|
// a ProjectKey already selected it.
|
||||||
|
async beforeQueuePrompt() {
|
||||||
|
if (!app.graph?._nodes) return;
|
||||||
|
for (const node of app.graph._nodes) {
|
||||||
|
if (node.type === "ProjectKey" && node._syncFromSource) {
|
||||||
|
node._syncFromSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
|
if (nodeData.name !== "ProjectKey") return;
|
||||||
|
|
||||||
|
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
origOnNodeCreated?.apply(this, arguments);
|
||||||
|
this._configured = false;
|
||||||
|
|
||||||
|
// Hide the connection-config widgets (synced from source by JS)
|
||||||
|
for (const name of ["manager_url", "project_name", "file_name", "sequence_number", "key_type"]) {
|
||||||
|
const w = this.widgets?.find(w => w.name === name);
|
||||||
|
if (w) { w.type = "hidden"; w.computeSize = () => [0, -4]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert source_label to a dynamic combo
|
||||||
|
const srcWidget = this.widgets?.find(w => w.name === "source_label");
|
||||||
|
if (srcWidget) {
|
||||||
|
srcWidget.type = "combo";
|
||||||
|
srcWidget.options = { values: [] };
|
||||||
|
srcWidget.value = srcWidget.value || "";
|
||||||
|
const node = this;
|
||||||
|
const origCb = srcWidget.callback;
|
||||||
|
srcWidget.callback = function (...args) {
|
||||||
|
origCb?.apply(this, args);
|
||||||
|
node._syncFromSource();
|
||||||
|
node._refreshKeys();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert key_name to a dynamic combo
|
||||||
|
const keyWidget = this.widgets?.find(w => w.name === "key_name");
|
||||||
|
if (keyWidget) {
|
||||||
|
keyWidget.type = "combo";
|
||||||
|
keyWidget.options = { values: [] };
|
||||||
|
keyWidget.value = keyWidget.value || "";
|
||||||
|
const node = this;
|
||||||
|
const origCb = keyWidget.callback;
|
||||||
|
keyWidget.callback = function (...args) {
|
||||||
|
origCb?.apply(this, args);
|
||||||
|
node._applyKeySelection();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (!this._configured) {
|
||||||
|
// New node — set output to a generic slot
|
||||||
|
if (this.outputs.length === 0) {
|
||||||
|
this.addOutput("value", "*");
|
||||||
|
}
|
||||||
|
this.setSize(this.computeSize());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Find all ProjectSource nodes and their labels (deduplicated) ---
|
||||||
|
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);
|
||||||
|
} else if (lw?.value && seen.has(lw.value)) {
|
||||||
|
console.warn(`[ProjectKey] Duplicate source label "${lw.value}" (node ${node.id}) — only first will be used`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Find the ProjectSource node matching a label ---
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Copy config from source node into hidden widgets ---
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Fetch keys from API and populate key_name dropdown ---
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Store keys/types for lookup
|
||||||
|
this._availableKeys = data.keys;
|
||||||
|
this._availableTypes = data.types;
|
||||||
|
|
||||||
|
// Update key_name combo values
|
||||||
|
const keyWidget = this.widgets?.find(w => w.name === "key_name");
|
||||||
|
if (keyWidget) {
|
||||||
|
keyWidget.options.values = data.keys;
|
||||||
|
// Keep current selection if still valid
|
||||||
|
if (!data.keys.includes(keyWidget.value)) {
|
||||||
|
keyWidget.value = data.keys[0] || "";
|
||||||
|
}
|
||||||
|
this._applyKeySelection();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ProjectKey] Failed to refresh keys:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Update output slot based on selected key ---
|
||||||
|
nodeType.prototype._applyKeySelection = function () {
|
||||||
|
const keyWidget = this.widgets?.find(w => w.name === "key_name");
|
||||||
|
if (!keyWidget?.value) return;
|
||||||
|
|
||||||
|
const keyIdx = (this._availableKeys || []).indexOf(keyWidget.value);
|
||||||
|
const keyType = keyIdx >= 0 ? (this._availableTypes[keyIdx] || "*") : "*";
|
||||||
|
|
||||||
|
// Update hidden key_type widget
|
||||||
|
const ktWidget = this.widgets?.find(w => w.name === "key_type");
|
||||||
|
if (ktWidget) ktWidget.value = keyType;
|
||||||
|
|
||||||
|
// Update output slot
|
||||||
|
if (this.outputs.length > 0) {
|
||||||
|
this.outputs[0].name = keyWidget.value;
|
||||||
|
this.outputs[0].label = keyWidget.value;
|
||||||
|
this.outputs[0].type = keyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.title = keyWidget.value ? `Key: ${keyWidget.value}` : "Project Key";
|
||||||
|
this.setSize(this.computeSize());
|
||||||
|
app.graph?.setDirtyCanvas(true, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Populate source dropdown on click (lazy refresh) ---
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Restore state on workflow load ---
|
||||||
|
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||||
|
nodeType.prototype.onConfigure = function (info) {
|
||||||
|
origOnConfigure?.apply(this, arguments);
|
||||||
|
this._configured = true;
|
||||||
|
|
||||||
|
// Hide config widgets
|
||||||
|
for (const name of ["manager_url", "project_name", "file_name", "sequence_number", "key_type"]) {
|
||||||
|
const w = this.widgets?.find(w => w.name === name);
|
||||||
|
if (w) { w.type = "hidden"; w.computeSize = () => [0, -4]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore combo types
|
||||||
|
const srcWidget = this.widgets?.find(w => w.name === "source_label");
|
||||||
|
if (srcWidget) {
|
||||||
|
srcWidget.type = "combo";
|
||||||
|
srcWidget.options = { values: this._getSourceLabels() };
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyWidget = this.widgets?.find(w => w.name === "key_name");
|
||||||
|
if (keyWidget) {
|
||||||
|
keyWidget.type = "combo";
|
||||||
|
keyWidget.options = { values: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update title from saved key
|
||||||
|
if (keyWidget?.value) {
|
||||||
|
this.title = `Key: ${keyWidget.value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore output slot name from saved key_name
|
||||||
|
if (keyWidget?.value && this.outputs.length > 0) {
|
||||||
|
this.outputs[0].name = keyWidget.value;
|
||||||
|
this.outputs[0].label = keyWidget.value;
|
||||||
|
const ktWidget = this.widgets?.find(w => w.name === "key_type");
|
||||||
|
if (ktWidget?.value) this.outputs[0].type = ktWidget.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setSize(this.computeSize());
|
||||||
|
|
||||||
|
// Deferred: sync from source and refresh key dropdown once graph is ready
|
||||||
|
const node = this;
|
||||||
|
queueMicrotask(() => {
|
||||||
|
node._syncFromSource();
|
||||||
|
node._refreshKeys();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "json.manager.project.source",
|
||||||
|
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
|
if (nodeData.name !== "ProjectSource") return;
|
||||||
|
|
||||||
|
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
origOnNodeCreated?.apply(this, arguments);
|
||||||
|
|
||||||
|
// Update title when label changes
|
||||||
|
const labelWidget = this.widgets?.find(w => w.name === "label");
|
||||||
|
if (labelWidget) {
|
||||||
|
const node = this;
|
||||||
|
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);
|
||||||
|
const labelWidget = this.widgets?.find(w => w.name === "label");
|
||||||
|
if (labelWidget?.value) {
|
||||||
|
this.title = `Source: ${labelWidget.value}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user