From 732b45a211f00e9fb3f53ec55fd63e565b394252 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Wed, 27 May 2026 17:04:32 +0200 Subject: [PATCH] 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 --- web/css/linker-main.css | 70 +++++++++++++++++++++++++++++++ web/linker.js | 91 +++++++++++++++++++++++++++++------------ web/utils/icon_utils.js | 1 + 3 files changed, 136 insertions(+), 26 deletions(-) diff --git a/web/css/linker-main.css b/web/css/linker-main.css index a424986..0e7f55f 100644 --- a/web/css/linker-main.css +++ b/web/css/linker-main.css @@ -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; diff --git a/web/linker.js b/web/linker.js index a46968d..dba8ade 100644 --- a/web/linker.js +++ b/web/linker.js @@ -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 + ? `` + : `${this.escapeHtml(nodeDisplay.text)}`; html += ` - + `; }); @@ -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 = `
`; @@ -4638,7 +4677,7 @@ class LinkerManagerDialog extends ComfyDialog { html += `
`; html += ``; html += ``; - html += ``; + html += ``; html += `
`; html += `
`; html += `
`; diff --git a/web/utils/icon_utils.js b/web/utils/icon_utils.js index b3c93d8..839a8ba 100644 --- a/web/utils/icon_utils.js +++ b/web/utils/icon_utils.js @@ -3,6 +3,7 @@ import { app } from "../../../scripts/app.js"; const SVG_ICONS = { search: ``, + refreshCw: ``, locate: ``, download: ``, externalLink: ``,