Improve missing model display and node location in linker UI
- Add getMissingNodeDisplay() helper to centralize node label formatting - Make missing list rows keyboard-accessible (Enter/Space selection) - Add clickable locate buttons for missing nodes to center in graph - Style focus states for accessibility (outline, box-shadow) - Add refreshCw icon to icon_utils.js for combo refresh buttons - Refactor row HTML structure for better semantics
This commit is contained in:
@@ -342,6 +342,10 @@
|
||||
#model-linker-modal .ml-missing-list-row:hover {
|
||||
background: rgba(255,255,255,0.032);
|
||||
}
|
||||
#model-linker-modal .ml-missing-list-row:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: inset 3px 0 0 rgba(114,211,138,0.72), inset 0 0 0 1px rgba(114,211,138,0.16);
|
||||
}
|
||||
#model-linker-modal .ml-missing-list-row.is-selected {
|
||||
background: linear-gradient(90deg, rgba(114,211,138,0.12) 0%, rgba(114,211,138,0.055) 42%, rgba(114,211,138,0.018) 100%);
|
||||
box-shadow: inset 3px 0 0 rgba(114,211,138,0.92), inset 0 0 0 1px rgba(114,211,138,0.12);
|
||||
@@ -384,6 +388,12 @@
|
||||
color: var(--ml-text-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
.ml-missing-row-node-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ml-missing-row-type {
|
||||
justify-self: start;
|
||||
max-width: 100%;
|
||||
@@ -776,6 +786,37 @@
|
||||
height: var(--ml-spacing-12);
|
||||
display: block;
|
||||
}
|
||||
#model-linker-modal .ml-missing-row-locate {
|
||||
justify-self: start;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 18px;
|
||||
margin: 0;
|
||||
padding: 1px 6px 1px 5px;
|
||||
gap: 3px;
|
||||
background: rgba(255,255,255,0.026);
|
||||
border-color: rgba(255,255,255,0.065);
|
||||
color: rgba(255,255,255,0.72);
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
appearance: none;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
#model-linker-modal .ml-missing-row-locate:hover {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-color: rgba(255,255,255,0.11);
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
#model-linker-modal .ml-missing-row-locate:focus-visible {
|
||||
outline: none;
|
||||
border-color: rgba(114,211,138,0.24);
|
||||
box-shadow: 0 0 0 1px rgba(114,211,138,0.10);
|
||||
}
|
||||
#model-linker-modal .ml-missing-row-locate .ml-node-chip-icon,
|
||||
#model-linker-modal .ml-missing-row-locate .ml-node-chip-icon svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
.ml-category-chip {
|
||||
display: inline-flex;
|
||||
padding: var(--ml-spacing-2) var(--ml-spacing-7);
|
||||
@@ -3299,6 +3340,35 @@
|
||||
border-color: rgba(114,211,138,0.34);
|
||||
box-shadow: 0 0 0 2px rgba(114,211,138,0.08);
|
||||
}
|
||||
#model-linker-modal .ml-missing-detail-pane .ml-combo-refresh-btn {
|
||||
flex: 0 0 38px;
|
||||
width: 38px;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
min-height: 38px;
|
||||
padding: 0;
|
||||
aspect-ratio: auto;
|
||||
background: rgba(255,255,255,0.035);
|
||||
border-color: rgba(255,255,255,0.10);
|
||||
border-radius: 6px;
|
||||
box-shadow: none !important;
|
||||
color: #d6d6d6;
|
||||
}
|
||||
#model-linker-modal .ml-missing-detail-pane .ml-combo-refresh-btn:hover:not(:disabled) {
|
||||
background: rgba(255,255,255,0.055);
|
||||
border-color: rgba(255,255,255,0.16);
|
||||
color: #ffffff;
|
||||
}
|
||||
#model-linker-modal .ml-missing-detail-pane .ml-combo-refresh-btn:focus-visible {
|
||||
outline: none;
|
||||
border-color: rgba(114,211,138,0.30);
|
||||
box-shadow: 0 0 0 2px rgba(114,211,138,0.08) !important;
|
||||
}
|
||||
#model-linker-modal .ml-missing-detail-pane .ml-combo-refresh-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
display: block;
|
||||
}
|
||||
#model-linker-modal .ml-missing-detail-pane .ml-download-section {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
||||
+65
-26
@@ -4074,6 +4074,31 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
return missing.original_path?.split('/').pop()?.split('\\').pop() || missing.name || 'Missing model';
|
||||
}
|
||||
|
||||
getMissingNodeDisplay(missing = {}) {
|
||||
const isSubgraphNode = missing.node_type && missing.node_type.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
||||
let nodeLabel;
|
||||
if (missing.subgraph_name) {
|
||||
nodeLabel = missing.subgraph_name;
|
||||
} else if (isSubgraphNode) {
|
||||
nodeLabel = 'Subgraph';
|
||||
} else {
|
||||
nodeLabel = missing.node_type || 'Node';
|
||||
}
|
||||
|
||||
const nodeId = missing.node_id ?? '';
|
||||
const customNodeTitle = String(missing.node_title || '').trim();
|
||||
const hasCustomNodeTitle = customNodeTitle && customNodeTitle !== nodeLabel;
|
||||
const text = hasCustomNodeTitle
|
||||
? `${nodeLabel} #${nodeId} · ${customNodeTitle}`
|
||||
: `${nodeLabel} #${nodeId}`;
|
||||
|
||||
return {
|
||||
label: nodeLabel,
|
||||
text,
|
||||
canLocate: missing.is_top_level !== false && nodeId !== ''
|
||||
};
|
||||
}
|
||||
|
||||
getBestLocalMatch(missing = {}, minConfidence = 0) {
|
||||
const matches = Array.isArray(missing.matches) ? missing.matches : [];
|
||||
return matches
|
||||
@@ -4221,12 +4246,12 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
let typePx = 66;
|
||||
for (const missing of missingModels) {
|
||||
const filename = this.getMissingFilename(missing);
|
||||
const nodeLabel = `${missing.subgraph_name || missing.node_type || 'Node'} #${missing.node_id || ''}`;
|
||||
const nodeLabel = this.getMissingNodeDisplay(missing).text;
|
||||
const typeLabel = missing.category ? this.getCategoryDisplayName(missing.category) : 'unknown';
|
||||
modelPx = Math.max(
|
||||
modelPx,
|
||||
textWidth(filename, 7.2) + 12,
|
||||
textWidth(nodeLabel, 5.5) + 12
|
||||
textWidth(nodeLabel, 5.5) + 34
|
||||
);
|
||||
typePx = Math.max(typePx, textWidth(typeLabel, 5.8) + 18);
|
||||
}
|
||||
@@ -4283,16 +4308,20 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const matchDisplay = matchName || 'No local match';
|
||||
const matchClass = confidence === 100 ? 'exact' : (bestMatch ? 'partial' : 'none');
|
||||
const typeLabel = missing.category ? this.getCategoryDisplayName(missing.category) : 'unknown';
|
||||
const nodeLabel = missing.subgraph_name || missing.node_type || 'Node';
|
||||
const nodeDisplay = this.getMissingNodeDisplay(missing);
|
||||
const nodeId = missing.node_id ?? '';
|
||||
const rowNodeHtml = nodeDisplay.canLocate
|
||||
? `<button type="button" class="ml-node-chip is-locatable ml-missing-row-node ml-missing-row-locate" data-node-id="${this.escapeHtml(String(nodeId))}" data-tooltip="Center this node in the ComfyUI graph." aria-label="Center ${this.escapeHtml(nodeDisplay.text)} in the ComfyUI graph">${this.getLocateIconHtml()}<span class="ml-missing-row-node-label">${this.escapeHtml(nodeDisplay.text)}</span></button>`
|
||||
: `<span class="ml-missing-row-node">${this.escapeHtml(nodeDisplay.text)}</span>`;
|
||||
|
||||
html += `
|
||||
<button type="button"
|
||||
<div role="button" tabindex="0"
|
||||
class="ml-missing-list-row ${isSelected ? 'is-selected' : ''}"
|
||||
data-missing-key="${this.escapeHtml(key)}">
|
||||
<span class="ml-missing-row-index">${index + 1}</span>
|
||||
<span class="ml-missing-row-model">
|
||||
<span class="ml-missing-row-name" data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(formattedFilename.display)}</span>
|
||||
<span class="ml-missing-row-node">${this.escapeHtml(nodeLabel)} #${this.escapeHtml(String(missing.node_id || ''))}</span>
|
||||
${rowNodeHtml}
|
||||
</span>
|
||||
<span class="ml-missing-row-type">${this.escapeHtml(typeLabel)}</span>
|
||||
<span class="ml-missing-row-best" data-tooltip="${this.escapeHtml(matchDisplay)}">
|
||||
@@ -4302,7 +4331,7 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<strong>${bestMatch ? `${confidence.toFixed(confidence % 1 ? 1 : 0)}%` : '--'}</strong>
|
||||
</span>
|
||||
<span class="ml-missing-row-sources">${this.renderMissingSourcesSummary(missing)}</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
@@ -4318,12 +4347,34 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
wireMissingModelsBrowser(container, data, sortedMissingModels) {
|
||||
const selectRow = (row) => {
|
||||
const key = row.dataset.missingKey;
|
||||
if (!key || key === this.selectedMissingModelKey) return;
|
||||
this.selectedMissingModelKey = key;
|
||||
this.displayMissingModels(container, data);
|
||||
};
|
||||
|
||||
container.querySelectorAll('.ml-missing-list-row').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
const key = row.dataset.missingKey;
|
||||
if (!key || key === this.selectedMissingModelKey) return;
|
||||
this.selectedMissingModelKey = key;
|
||||
this.displayMissingModels(container, data);
|
||||
row.addEventListener('click', (event) => {
|
||||
const clickedLocate = event.target instanceof Element && event.target.closest('.ml-missing-row-locate');
|
||||
if (clickedLocate) return;
|
||||
selectRow(row);
|
||||
});
|
||||
|
||||
row.addEventListener('keydown', (event) => {
|
||||
if (event.target !== row || (event.key !== 'Enter' && event.key !== ' ')) return;
|
||||
event.preventDefault();
|
||||
selectRow(row);
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.ml-missing-row-locate').forEach(button => {
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const rawNodeId = button.dataset.nodeId;
|
||||
const numericNodeId = Number(rawNodeId);
|
||||
this.locateNodeInGraph(Number.isNaN(numericNodeId) ? rawNodeId : numericNodeId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4542,20 +4593,8 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const missingFilename = this.formatFilename(missing.original_path, 60);
|
||||
|
||||
// Determine node info for the chip
|
||||
const isSubgraphNode = missing.node_type && missing.node_type.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
||||
let nodeLabel;
|
||||
if (missing.subgraph_name) {
|
||||
nodeLabel = missing.subgraph_name;
|
||||
} else if (isSubgraphNode) {
|
||||
nodeLabel = 'Subgraph';
|
||||
} else {
|
||||
nodeLabel = missing.node_type || 'Node';
|
||||
}
|
||||
const customNodeTitle = String(missing.node_title || '').trim();
|
||||
const hasCustomNodeTitle = customNodeTitle && customNodeTitle !== nodeLabel;
|
||||
const nodeChipText = this.escapeHtml(hasCustomNodeTitle
|
||||
? `${nodeLabel} #${missing.node_id} · ${customNodeTitle}`
|
||||
: `${nodeLabel} #${missing.node_id}`);
|
||||
const nodeDisplay = this.getMissingNodeDisplay(missing);
|
||||
const nodeChipText = this.escapeHtml(nodeDisplay.text);
|
||||
|
||||
// Start card
|
||||
let html = `<div class="ml-card">`;
|
||||
@@ -4638,7 +4677,7 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
html += `<div class="ml-combo-row">`;
|
||||
html += `<label class="ml-combo-label">Model</label>`;
|
||||
html += `<input id="combo-input-${comboId}" class="ml-combo-input" type="text" placeholder="Type to filter local models...">`;
|
||||
html += `<button id="combo-refresh-${comboId}" data-tooltip="Reload local model list" class="ml-btn ml-btn-secondary ml-btn-sm ml-btn-icon-only">⟳</button>`;
|
||||
html += `<button id="combo-refresh-${comboId}" type="button" aria-label="Reload local model list" data-tooltip="Reload local model list" class="ml-btn ml-btn-secondary ml-btn-sm ml-btn-icon-only ml-combo-refresh-btn">${getSvgIcon('refreshCw', 'currentColor', 'ml-combo-refresh-icon')}</button>`;
|
||||
html += `</div>`;
|
||||
html += `<div id="combo-list-${comboId}" class="ml-combo-list"></div>`;
|
||||
html += `</div>`;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { app } from "../../../scripts/app.js";
|
||||
|
||||
const SVG_ICONS = {
|
||||
search: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="6.5"></circle><path d="M16 16l5 5"></path></svg>`,
|
||||
refreshCw: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"></path><path d="M21 3v6h-6"></path></svg>`,
|
||||
locate: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12h3"></path><path d="M19 12h3"></path><path d="M12 2v3"></path><path d="M12 19v3"></path><circle cx="12" cy="12" r="7"></circle><circle cx="12" cy="12" r="2.5"></circle></svg>`,
|
||||
download: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg>`,
|
||||
externalLink: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg>`,
|
||||
|
||||
Reference in New Issue
Block a user