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:
@@ -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;
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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)}_`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
Reference in New Issue
Block a user