Improve workflow metadata filters and UI localization
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
getNodeParamEntries,
|
||||
getNodeType,
|
||||
getNodeTypeLabel,
|
||||
getNodeWidgetValueEntries,
|
||||
getWorkflowNodes,
|
||||
resolveAssetWorkflow,
|
||||
} from "./workflowGraphMapData.js";
|
||||
@@ -33,6 +34,8 @@ export class WorkflowGraphMapPanel {
|
||||
this._drag = null;
|
||||
this._previewMedia = null;
|
||||
this._previewKey = "";
|
||||
this._subgraphDisplayMode = "expand";
|
||||
this._modeButtons = new Map();
|
||||
this._el = this._build();
|
||||
}
|
||||
|
||||
@@ -93,6 +96,15 @@ export class WorkflowGraphMapPanel {
|
||||
this._status.className = "mjr-wgm-status";
|
||||
root.appendChild(this._status);
|
||||
|
||||
if (this._large) {
|
||||
this._toolbar = document.createElement("div");
|
||||
this._toolbar.className = "mjr-wgm-toolbar";
|
||||
this._toolbar.appendChild(this._makeModeButton("Expand subgraphs", "expand"));
|
||||
this._toolbar.appendChild(this._makeModeButton("Host nodes only", "host"));
|
||||
root.appendChild(this._toolbar);
|
||||
this._syncModeButtons();
|
||||
}
|
||||
|
||||
this._details = document.createElement("div");
|
||||
this._details.className = "mjr-wgm-details";
|
||||
root.appendChild(this._details);
|
||||
@@ -162,6 +174,7 @@ export class WorkflowGraphMapPanel {
|
||||
this._renderInfo = drawWorkflowMinimap(canvas, this._workflow, {
|
||||
showNodeLabels: true,
|
||||
showViewport: false,
|
||||
expandSubgraphs: this._subgraphDisplayMode !== "host",
|
||||
view: {
|
||||
hoveredNodeId: this._selectedNodeId || null,
|
||||
zoom: this._view.zoom,
|
||||
@@ -177,7 +190,8 @@ export class WorkflowGraphMapPanel {
|
||||
}
|
||||
|
||||
_renderDetails() {
|
||||
const nodeCount = getWorkflowNodes(this._workflow).length;
|
||||
const includeSubgraphs = this._subgraphDisplayMode !== "host";
|
||||
const nodeCount = getWorkflowNodes(this._workflow, { includeSubgraphs }).length;
|
||||
if (!this._workflow) {
|
||||
this._status.textContent = this._large
|
||||
? "No workflow graph in selected image"
|
||||
@@ -187,11 +201,11 @@ export class WorkflowGraphMapPanel {
|
||||
}
|
||||
this._status.textContent = this._large
|
||||
? this._selectedNodeId
|
||||
? `${nodeCount} nodes - selected #${this._selectedNodeId}`
|
||||
: `${nodeCount} nodes - select a node`
|
||||
? `${nodeCount} nodes (${this._subgraphDisplayMode}) - selected #${this._selectedNodeId}`
|
||||
: `${nodeCount} nodes (${this._subgraphDisplayMode}) - select a node`
|
||||
: `${nodeCount} nodes - graph opened in viewer`;
|
||||
|
||||
const node = findWorkflowNode(this._workflow, this._selectedNodeId);
|
||||
const node = findWorkflowNode(this._workflow, this._selectedNodeId, { includeSubgraphs });
|
||||
if (!node) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "mjr-ws-sidebar-empty";
|
||||
@@ -228,39 +242,151 @@ export class WorkflowGraphMapPanel {
|
||||
}),
|
||||
);
|
||||
|
||||
const params = document.createElement("div");
|
||||
params.className = "mjr-wgm-params";
|
||||
for (const [key, value] of getNodeParamEntries(node)) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "mjr-wgm-param";
|
||||
row.tabIndex = 0;
|
||||
row.role = "button";
|
||||
row.title = `Copy ${String(key)}`;
|
||||
const k = document.createElement("span");
|
||||
k.className = "mjr-wgm-param-key";
|
||||
k.textContent = String(key);
|
||||
const v = document.createElement("span");
|
||||
v.className = "mjr-wgm-param-value";
|
||||
v.textContent = _formatValue(value);
|
||||
row.appendChild(k);
|
||||
row.appendChild(v);
|
||||
row.addEventListener("click", () => this._copyParam(row, value));
|
||||
row.addEventListener("keydown", (event: any) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault?.();
|
||||
this._copyParam(row, value);
|
||||
});
|
||||
params.appendChild(row);
|
||||
}
|
||||
const visual = this._buildNodeVisual(node);
|
||||
|
||||
if (!params.childElementCount) {
|
||||
_replaceChildren(this._details, title, meta, visual, actions);
|
||||
}
|
||||
|
||||
_makeModeButton(label: any, mode: "expand" | "host") {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "mjr-wgm-mode";
|
||||
button.textContent = String(label);
|
||||
button.addEventListener("click", () => this._setSubgraphDisplayMode(mode));
|
||||
this._modeButtons.set(mode, button);
|
||||
return button;
|
||||
}
|
||||
|
||||
_setSubgraphDisplayMode(mode: "expand" | "host") {
|
||||
if (mode !== "expand" && mode !== "host") return;
|
||||
if (this._subgraphDisplayMode === mode) return;
|
||||
this._subgraphDisplayMode = mode;
|
||||
if (mode === "host" && String(this._selectedNodeId || "").includes("::")) {
|
||||
this._selectedNodeId = "";
|
||||
}
|
||||
this._syncModeButtons();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
_syncModeButtons() {
|
||||
for (const [mode, button] of this._modeButtons.entries()) {
|
||||
button.classList.toggle("is-active", mode === this._subgraphDisplayMode);
|
||||
}
|
||||
}
|
||||
|
||||
_buildNodeVisual(node: any) {
|
||||
const card = document.createElement("section");
|
||||
card.className = "mjr-wgm-node-visual";
|
||||
card.classList.add(`is-${_getNodeVisualCategory(node)}`);
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "mjr-wgm-node-visual-header";
|
||||
const nodeType = document.createElement("span");
|
||||
nodeType.className = "mjr-wgm-node-visual-type";
|
||||
nodeType.textContent = getNodeTypeLabel(node) || getNodeType(node) || "Node";
|
||||
const nodeId = document.createElement("span");
|
||||
nodeId.className = "mjr-wgm-node-visual-id";
|
||||
nodeId.textContent = `#${String(node?.id ?? this._selectedNodeId ?? "")}`;
|
||||
header.append(nodeType, nodeId);
|
||||
|
||||
const portsWrap = document.createElement("div");
|
||||
portsWrap.className = "mjr-wgm-node-ports";
|
||||
const inputsCol = this._buildPortColumn("Inputs", node?.inputs, "in");
|
||||
const outputsCol = this._buildPortColumn("Outputs", node?.outputs, "out");
|
||||
portsWrap.append(inputsCol, outputsCol);
|
||||
|
||||
const widgets = document.createElement("div");
|
||||
widgets.className = "mjr-wgm-node-widgets";
|
||||
const widgetsTitle = document.createElement("div");
|
||||
widgetsTitle.className = "mjr-wgm-node-widgets-title";
|
||||
widgetsTitle.textContent = "Widgets";
|
||||
widgets.appendChild(widgetsTitle);
|
||||
const widgetEntries = _normalizeWidgetEntries(node);
|
||||
if (!widgetEntries.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "mjr-ws-node-empty";
|
||||
empty.textContent = "No simple parameters found";
|
||||
params.appendChild(empty);
|
||||
empty.className = "mjr-wgm-node-widget-empty";
|
||||
empty.textContent = "No widget values";
|
||||
widgets.appendChild(empty);
|
||||
} else {
|
||||
for (const entry of widgetEntries.slice(0, 12)) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "mjr-wgm-node-widget";
|
||||
row.tabIndex = 0;
|
||||
row.role = "button";
|
||||
const widgetLabel = String(entry?.label || entry?.key || "value");
|
||||
row.title = `Copy ${widgetLabel}`;
|
||||
const key = document.createElement("span");
|
||||
key.className = "mjr-wgm-node-widget-key";
|
||||
key.textContent = widgetLabel;
|
||||
const value = document.createElement("span");
|
||||
value.className = "mjr-wgm-node-widget-value";
|
||||
const displayValue = _formatWidgetValue(entry?.value);
|
||||
value.textContent = displayValue;
|
||||
if (_isMultilineWidgetValue(entry?.value, displayValue)) {
|
||||
row.classList.add("is-multiline");
|
||||
}
|
||||
if (_isExpandedTextWidgetLabel(widgetLabel)) {
|
||||
row.classList.add("is-text-field");
|
||||
}
|
||||
row.append(key, value);
|
||||
row.addEventListener("click", () => this._copyParam(row, entry?.value));
|
||||
row.addEventListener("keydown", (event: any) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault?.();
|
||||
this._copyParam(row, entry?.value);
|
||||
});
|
||||
widgets.appendChild(row);
|
||||
}
|
||||
if (widgetEntries.length > 12) {
|
||||
const more = document.createElement("div");
|
||||
more.className = "mjr-wgm-node-widget-more";
|
||||
more.textContent = `+${widgetEntries.length - 12} more values`;
|
||||
widgets.appendChild(more);
|
||||
}
|
||||
}
|
||||
|
||||
_replaceChildren(this._details, title, meta, actions, params);
|
||||
card.append(header, portsWrap, widgets);
|
||||
return card;
|
||||
}
|
||||
|
||||
_buildPortColumn(title: any, ports: any, direction: "in" | "out") {
|
||||
const col = document.createElement("div");
|
||||
col.className = "mjr-wgm-node-ports-col";
|
||||
const titleEl = document.createElement("div");
|
||||
titleEl.className = "mjr-wgm-node-ports-title";
|
||||
titleEl.textContent = String(title);
|
||||
col.appendChild(titleEl);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "mjr-wgm-node-ports-list";
|
||||
const items = _normalizePorts(ports).slice(0, 8);
|
||||
if (!items.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "mjr-wgm-node-port-empty";
|
||||
empty.textContent = "-";
|
||||
list.appendChild(empty);
|
||||
} else {
|
||||
for (const item of items) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "mjr-wgm-node-port";
|
||||
const dot = document.createElement("span");
|
||||
dot.className = `mjr-wgm-node-port-dot is-${direction}`;
|
||||
const name = document.createElement("span");
|
||||
name.className = "mjr-wgm-node-port-name";
|
||||
const info = _formatPortInfo(item);
|
||||
name.textContent = info.label;
|
||||
row.append(dot, name);
|
||||
if (info.type) {
|
||||
const type = document.createElement("span");
|
||||
type.className = "mjr-wgm-node-port-type";
|
||||
type.textContent = info.type;
|
||||
row.appendChild(type);
|
||||
}
|
||||
list.appendChild(row);
|
||||
}
|
||||
}
|
||||
col.appendChild(list);
|
||||
return col;
|
||||
}
|
||||
|
||||
_makeAction(label: any, iconClass: any, action: any) {
|
||||
@@ -478,6 +604,89 @@ function _formatValue(value: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function _formatWidgetValue(value: any) {
|
||||
if (value == null) return "";
|
||||
if (typeof value === "string") return value.replace(/\r\n?/g, "\n").trim();
|
||||
return _formatValue(value);
|
||||
}
|
||||
|
||||
function _isMultilineWidgetValue(rawValue: any, displayValue: any) {
|
||||
if (typeof rawValue !== "string") return false;
|
||||
const text = String(displayValue || "");
|
||||
return text.includes("\n") || text.length > 120;
|
||||
}
|
||||
|
||||
function _isExpandedTextWidgetLabel(label: any) {
|
||||
const normalized = String(label || "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, "");
|
||||
if (!normalized) return false;
|
||||
return (
|
||||
normalized === "text" ||
|
||||
normalized === "prompt" ||
|
||||
normalized === "positive" ||
|
||||
normalized === "negative" ||
|
||||
normalized === "string" ||
|
||||
normalized === "caption"
|
||||
);
|
||||
}
|
||||
|
||||
function _normalizePorts(ports: any) {
|
||||
if (!Array.isArray(ports)) return [];
|
||||
return ports.filter(Boolean).map((port: any) => ({
|
||||
name: String(port?.name || port?.label || "").trim(),
|
||||
type: String(port?.type || port?.slot_type || port?.data_type || "").trim(),
|
||||
}));
|
||||
}
|
||||
|
||||
function _formatPortInfo(item: any) {
|
||||
const label = String(item?.name || item?.label || item?.type || "port").trim() || "port";
|
||||
const type = String(item?.type || "").trim();
|
||||
const normalizedLabel = _normalizePortToken(label);
|
||||
const normalizedType = _normalizePortToken(type);
|
||||
const hasDistinctType = Boolean(normalizedType) && normalizedType !== normalizedLabel;
|
||||
return {
|
||||
label,
|
||||
type: hasDistinctType ? type : "",
|
||||
};
|
||||
}
|
||||
|
||||
function _normalizePortToken(value: any) {
|
||||
return String(value || "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, "");
|
||||
}
|
||||
|
||||
function _normalizeWidgetEntries(node: any): Array<{ label: any; key?: any; value: any }> {
|
||||
const fromWidgets = getNodeWidgetValueEntries(node);
|
||||
if (Array.isArray(fromWidgets) && fromWidgets.length) {
|
||||
return fromWidgets.map((entry: any) => ({
|
||||
label: entry?.label,
|
||||
key: entry?.key,
|
||||
value: entry?.value,
|
||||
}));
|
||||
}
|
||||
const fromParams = getNodeParamEntries(node);
|
||||
const entries = Array.isArray(fromParams)
|
||||
? fromParams.map(([label, value]: any) => ({ label, value }))
|
||||
: [];
|
||||
return entries.slice(0, 160);
|
||||
}
|
||||
|
||||
function _getNodeVisualCategory(node: any) {
|
||||
const type = String(getNodeType(node) || "").toLowerCase();
|
||||
const label = String(getNodeTypeLabel(node) || "").toLowerCase();
|
||||
const text = `${type} ${label}`;
|
||||
if (!text.trim()) return "generic";
|
||||
if (/ksampler|sampler|scheduler|cfg|steps|noise|seed/.test(text)) return "sampler";
|
||||
if (/checkpoint|clip|vae|unet|lora|model|loader/.test(text)) return "model";
|
||||
if (/text|prompt|token|encode|decoder|caption|florence/.test(text)) return "text";
|
||||
if (/latent|image|mask|video|audio|preview|save|load|upscale/.test(text)) return "media";
|
||||
if (/controlnet|conditioning|guidance|adapter|ipadapter/.test(text)) return "control";
|
||||
if (/math|logic|switch|merge|concat|combine|route|branch|reroute/.test(text)) return "logic";
|
||||
return "generic";
|
||||
}
|
||||
|
||||
function _normalizePreviewAsset(asset: any) {
|
||||
if (!asset || typeof asset !== "object") return asset;
|
||||
const candidates = Array.isArray(asset.previewCandidates) ? asset.previewCandidates : [];
|
||||
|
||||
Reference in New Issue
Block a user