Refactor missing model key generation and DOM ID generation

- Centralize model key logic: replaced inline key construction across 5 files with
  getMissingModelKey() and new getMissingModelDomKey() helpers in lifecycle_graph_methods.

- Extend getMissingModelKey(): now incorporates nested_key, category, and an identity
  field (original_lora_name, original_path, expected_filename, name, filename, urn_string)
  so that distinct models within the same subgraph produce unique keys.

- Fix duplicate DOM ID collisions: getMissingModelDomKey() encodes the full key as a
  URL-safe base64 string for all element IDs (local-matches-body, selected-bar, combo,
  locate, local-matches-refresh, selected-apply, selected-remove), preventing collisions
  between subgraph instances that share a node_id/widget_index.

- Store missing_key on resolution objects in queue_methods to keep the canonical key in
  sync between queue and browser views.
This commit is contained in:
Dariusz L
2026-06-22 03:27:22 +02:00
parent 4cefb35b71
commit b4cd98e1ca
5 changed files with 68 additions and 33 deletions
+14 -17
View File
@@ -1280,8 +1280,7 @@ export const queueMethods = {
this.rebuildPendingIndex();
this.savePendingQueueForActiveWorkflow();
// Update per-item selected bar
const m = { node_id: r.node_id, widget_index: r.widget_index, subgraph_id: r.subgraph_id, is_top_level: r.is_top_level };
this.updateSelectedBarForMissing?.(m);
this.updateSelectedBarForMissing?.(r);
this.updateApplyPendingButton?.();
this.updateQueuePanel();
this.refreshMissingModelsBrowserFromCache?.();
@@ -1303,13 +1302,10 @@ export const queueMethods = {
// Update selected bar for a specific missing model slot
updateSelectedBarForMissing(missing) {
if (!missing) return;
const nodeId = missing.node_id;
const widgetIndex = missing.widget_index;
const subgraphId = missing.subgraph_id || '';
const isTopLevel = missing.is_top_level !== false;
const key = `${nodeId}:${widgetIndex}:${subgraphId}:${isTopLevel ? 'T' : 'F'}`;
const key = this.getMissingModelKey(missing);
const domKey = this.getMissingModelDomKey(missing);
const selectedBar = document.getElementById(`selected-bar-${nodeId}-${widgetIndex}`);
const selectedBar = document.getElementById(`selected-bar-${domKey}`);
if (!selectedBar) return;
// Find selection for this slot
@@ -1360,8 +1356,8 @@ export const queueMethods = {
selectedBar.removeAttribute('data-tooltip');
selectedBar.oncontextmenu = null;
}
const applyBtnId = `selected-apply-${nodeId}-${widgetIndex}`;
const removeBtnId = `selected-remove-${nodeId}-${widgetIndex}`;
const applyBtnId = `selected-apply-${domKey}`;
const removeBtnId = `selected-remove-${domKey}`;
selectedBar.innerHTML = `<div class="mr-selected-bar-inner"${selectedContextAttrs}>`;
selectedBar.innerHTML += `<span class="mr-selected-label">✓ Selected:</span>`;
@@ -1398,7 +1394,7 @@ export const queueMethods = {
if (idx >= 0 && idx < this.pendingResolutions.length) {
const r = this.pendingResolutions[idx];
// Update the selected bar before removing
const m = { node_id: r.node_id, widget_index: r.widget_index, subgraph_id: r.subgraph_id, is_top_level: r.is_top_level };
const m = r;
// Remove from array
this.pendingResolutions.splice(idx, 1);
@@ -1438,11 +1434,7 @@ export const queueMethods = {
},
getResolutionQueueKey(resolution = {}) {
const nodeId = resolution.node_id ?? '';
const widgetIndex = resolution.widget_index ?? '';
const subgraphId = resolution.subgraph_id || '';
const scope = resolution.is_top_level !== false ? 'T' : 'F';
return `${nodeId}:${widgetIndex}:${subgraphId}:${scope}`;
return resolution.missing_key || this.getMissingModelKey(resolution);
},
getResolutionNodeRefs(missing = {}) {
@@ -1719,17 +1711,22 @@ export const queueMethods = {
return;
}
const missingKey = this.getMissingModelKey(missing);
const resolution = {
missing_key: missingKey,
node_id: missing.node_id,
widget_index: missing.widget_index,
resolved_path: resolvedModel.path,
category: missing.category,
resolved_model: resolvedModel,
original_path: missing.original_path,
expected_filename: missing.expected_filename || missing.civitai_info?.expected_filename || missing.download_source?.filename,
filename: missing.filename,
urn_string: missing.urn_string,
subgraph_id: missing.subgraph_id,
is_top_level: missing.is_top_level,
is_lora_v2: missing.is_lora_v2,
original_lora_name: missing.name || missing.original_path,
original_lora_name: missing.original_lora_name || missing.name || missing.original_path,
nested_key: missing.nested_key,
node_refs: this.getResolutionNodeRefs(missing),
node_type: missing.node_type,
@@ -891,7 +891,7 @@ export const resolveDownloadMethods = {
{ category, downloadPath, downloadDirectory, filename: targetFilename }
);
}
const body = this.contentElement?.querySelector(`#local-matches-body-${missing.node_id}-${missing.widget_index}`);
const body = this.contentElement?.querySelector(`#local-matches-body-${this.getMissingModelDomKey(missing)}`);
if (body) {
body.innerHTML = `<div class="mr-no-matches">Checking local matches for ${this.escapeHtml(targetFilename)}...</div>`;
}
@@ -931,7 +931,7 @@ export const resolveDownloadMethods = {
refreshLocalMatchesUiForMissing(missing) {
if (!missing || !this.contentElement) return;
const body = this.contentElement.querySelector(`#local-matches-body-${missing.node_id}-${missing.widget_index}`);
const body = this.contentElement.querySelector(`#local-matches-body-${this.getMissingModelDomKey(missing)}`);
const displayIndex = Number.isFinite(missing.__displayIndex) ? missing.__displayIndex : 0;
if (body) {
body.innerHTML = this.renderLocalMatchesContent(missing, displayIndex);
@@ -962,7 +962,7 @@ export const resolveDownloadMethods = {
const minRefreshFeedback = new Promise(resolve => setTimeout(resolve, 420));
const refreshAnimation = this.startRefreshButtonAnimation?.(button);
const body = this.contentElement?.querySelector(`#local-matches-body-${missing.node_id}-${missing.widget_index}`);
const body = this.contentElement?.querySelector(`#local-matches-body-${this.getMissingModelDomKey(missing)}`);
const displayName = targetFilename.split('/').pop()?.split('\\').pop() || targetFilename;
const missingKey = this.getMissingModelKey(missing);
const currentMissing = (this.missingModels || []).find(item => this.getMissingModelKey(item) === missingKey) || missing;
+1 -1
View File
@@ -2111,7 +2111,7 @@ export const searchPanelMethods = {
async refreshUrnLocalMatches(missing) {
if (!missing?.civitai_info?.expected_filename || !this.contentElement) return;
const bodyId = `local-matches-body-${missing.node_id}-${missing.widget_index}`;
const bodyId = `local-matches-body-${this.getMissingModelDomKey(missing)}`;
const container = this.contentElement.querySelector(`#${bodyId}`);
if (container) {
container.innerHTML = `<div class="mr-no-matches">Searching local matches for "${missing.civitai_info.expected_filename}"...</div>`;
+39 -1
View File
@@ -745,11 +745,49 @@ export const lifecycleGraphMethods = {
}
},
encodeMissingModelKeyPart(value) {
return encodeURIComponent(String(value ?? '').trim());
},
getMissingModelIdentityPart(missing = {}) {
return missing.original_lora_name
|| missing.original_path
|| missing.expected_filename
|| missing.name
|| missing.filename
|| missing.urn_string
|| '';
},
getMissingModelKey(missing = {}) {
if (missing.missing_key) {
return String(missing.missing_key);
}
const nodeId = missing.node_id ?? '';
const widgetIndex = missing.widget_index ?? '';
const subgraphId = missing.subgraph_id || '';
const scope = missing.is_top_level !== false ? 'T' : 'F';
return `${nodeId}:${widgetIndex}:${subgraphId}:${scope}`;
const nestedKey = missing.nested_key || '';
const category = missing.category || '';
const identity = this.getMissingModelIdentityPart(missing);
return [
nodeId,
widgetIndex,
subgraphId,
scope,
nestedKey,
category,
identity
].map(value => this.encodeMissingModelKeyPart(value)).join(':');
},
getMissingModelDomKey(missing = {}) {
const key = this.getMissingModelKey(missing);
try {
return btoa(key).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
} catch (e) {
return key.replace(/[^A-Za-z0-9_-]/g, char => `_${char.charCodeAt(0).toString(16)}_`);
}
}
};
+11 -11
View File
@@ -225,9 +225,7 @@ export const missingBrowserMethods = {
isMissingModelResolved(missing = {}) {
if (!missing) return false;
if (missing.__isExistingResolved) return true;
const key = this.getMissingModelKey(missing);
return Boolean(key && this.pendingIndex?.has?.(key));
return Boolean(missing.__isExistingResolved);
},
getResolvedWorkflowModels(data = {}) {
@@ -686,7 +684,8 @@ export const missingBrowserMethods = {
this.wireDownloadSearchPanel(container, missing);
this.updateSelectedBarForMissing(missing);
const localRefreshId = `local-matches-refresh-${missing.node_id}-${missing.widget_index}`;
const detailDomKey = this.getMissingModelDomKey(missing);
const localRefreshId = `local-matches-refresh-${detailDomKey}`;
const localRefreshBtn = container.querySelector(`#${localRefreshId}`);
if (localRefreshBtn) {
localRefreshBtn.addEventListener('click', () => this.refreshLocalMatchesForMissing?.(missing, {
@@ -694,7 +693,7 @@ export const missingBrowserMethods = {
}));
}
const locateId = `locate-${missing.node_id}-${missing.widget_index}`;
const locateId = `locate-${detailDomKey}`;
const locateBtn = container.querySelector(`#${locateId}`);
const locateTarget = this.getMissingLocateTarget(missing);
if (locateBtn && locateTarget.nodeId !== undefined && locateTarget.nodeId !== null && locateTarget.nodeId !== '') {
@@ -706,7 +705,7 @@ export const missingBrowserMethods = {
});
}
const comboId = `combo-${missing.node_id}-${missing.widget_index}`;
const comboId = `combo-${detailDomKey}`;
const comboInput = container.querySelector(`#combo-input-${comboId}`);
const comboList = container.querySelector(`#combo-list-${comboId}`);
const comboRefresh = container.querySelector(`#combo-refresh-${comboId}`);
@@ -1102,7 +1101,8 @@ export const missingBrowserMethods = {
html += titleSecondaryHtml;
}
html += `</div>`;
const locateId = `locate-${missing.node_id}-${missing.widget_index}`;
const detailDomKey = this.getMissingModelDomKey(missing);
const locateId = `locate-${detailDomKey}`;
const nodeChipClasses = nodeDisplay.canLocate ? 'mr-node-chip is-locatable' : 'mr-node-chip';
const nodeChipTitle = nodeDisplay.canLocate ? nodeDisplay.locateTooltip : '';
@@ -1120,7 +1120,7 @@ export const missingBrowserMethods = {
html += `</div>`;
// Selected bar - shows if this slot has a queued selection (BELOW card header)
const selectedBarId = `selected-bar-${missing.node_id}-${missing.widget_index}`;
const selectedBarId = `selected-bar-${detailDomKey}`;
html += `<div id="${selectedBarId}" class="model-resolver-selected"></div>`;
// Two-column layout
@@ -1128,17 +1128,17 @@ export const missingBrowserMethods = {
// LEFT COLUMN: Local Matches
html += `<div class="mr-column">`;
const localRefreshId = `local-matches-refresh-${missing.node_id}-${missing.widget_index}`;
const localRefreshId = `local-matches-refresh-${detailDomKey}`;
html += `<div class="mr-column-header mr-local-matches-header">`;
html += `<span>Local Matches</span>`;
html += `<button id="${localRefreshId}" type="button" aria-label="Refresh local matches" data-tooltip="Rescan local model folders and refresh matches for this model" class="mr-btn mr-btn-secondary mr-btn-sm mr-btn-icon-only mr-local-matches-refresh-btn"><span class="mr-refresh-spin-target">${getSvgIcon('refreshCw', 'currentColor', 'mr-combo-refresh-icon')}</span></button>`;
html += `</div>`;
html += `<div id="local-matches-body-${missing.node_id}-${missing.widget_index}">`;
html += `<div id="local-matches-body-${detailDomKey}">`;
html += this.renderLocalMatchesContent(missing, missingIndex);
html += `</div>`;
// Add all-models search picker - combo-style dropdown
const comboId = `combo-${missing.node_id}-${missing.widget_index}`;
const comboId = `combo-${detailDomKey}`;
html += `<div class="mr-combo-section">`;
html += `<div class="mr-combo-row">`;
html += `<label class="mr-combo-label">Model</label>`;