feat(missing models): add type filter dropdown to missing models browser

This commit is contained in:
Dariusz L
2026-07-22 13:09:14 +02:00
parent c5215c2d3f
commit a822ef09c3
4 changed files with 252 additions and 3 deletions
+143
View File
@@ -0,0 +1,143 @@
.mr-missing-type-filter-cell {
overflow: visible;
}
.mr-missing-type-filter-wrap {
position: relative;
display: inline-flex;
min-width: 0;
}
#model-resolver-modal .mr-missing-type-filter-button {
position: relative;
display: inline-flex;
align-items: center;
gap: 5px;
max-width: 100%;
min-height: 20px;
padding: 0 15px 0 0;
border: 0 !important;
border-radius: 0;
color: var(--mr-text-muted);
background: transparent !important;
box-shadow: none !important;
font: inherit;
font-weight: 700;
line-height: 1.1;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
}
#model-resolver-modal .mr-missing-type-filter-button::after {
content: "";
position: absolute;
right: 1px;
width: 0;
height: 0;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
border-top: 5px solid currentColor;
opacity: 0.72;
}
#model-resolver-modal .mr-missing-type-filter-button:hover,
#model-resolver-modal .mr-missing-type-filter-button:focus-visible,
#model-resolver-modal .mr-missing-type-filter-button.is-filtered {
color: var(--mr-text);
outline: none;
}
.mr-missing-type-filter-chip {
min-width: 0;
max-width: 72px;
color: var(--mr-text-muted);
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mr-missing-type-filter-menu {
position: absolute;
top: calc(100% + 5px);
left: 50%;
z-index: 20;
display: grid;
gap: 0;
min-width: 170px;
max-height: 320px;
padding: 5px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
background: #121212;
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.38);
overflow-y: auto;
transform: translateX(-50%);
}
.mr-missing-type-filter-menu[hidden] {
display: none;
}
#model-resolver-modal .mr-missing-type-filter-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 26px;
padding: 5px 7px;
border: 0 !important;
border-radius: 4px;
color: var(--mr-text-secondary);
background: transparent !important;
box-shadow: none !important;
font-size: 12px;
font-weight: 500;
line-height: 1.1;
text-align: left;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
}
#model-resolver-modal .mr-missing-type-filter-option:hover,
#model-resolver-modal .mr-missing-type-filter-option:focus-visible {
color: var(--mr-text);
background: rgba(255, 255, 255, 0.055) !important;
outline: none;
}
#model-resolver-modal .mr-missing-type-filter-option.is-active {
color: var(--mr-text);
background: rgba(255, 255, 255, 0.075) !important;
}
#model-resolver-modal .mr-missing-type-filter-option span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#model-resolver-modal .mr-missing-type-filter-option-label.mr-type-chip {
display: inline-flex;
align-items: center;
max-width: 112px;
padding: 2px 7px;
border-radius: 999px;
color: var(--mr-type-text);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.02em;
line-height: normal;
text-transform: uppercase;
}
#model-resolver-modal .mr-missing-type-filter-option strong {
min-width: 34px;
color: var(--mr-text-muted);
font-size: 11px;
font-weight: 700;
text-align: right;
}
+2
View File
@@ -76,6 +76,8 @@ export class ResolverManagerDialog extends ComfyDialog {
this.showAutoDownloadModelsStorageKey = "model_resolver_show_auto_download_models";
this.localMatchAlternativesCollapsedStorageKey = "model_resolver_local_match_alternatives_collapsed";
this.showResolvedModels = safeStorage.getItem(this.showResolvedModelsStorageKey) === '1';
this.missingModelsTypeFilter = 'all';
this.missingModelsTypeFilterMenuOpen = false;
const storedShowAutoDownload = safeStorage.getItem(this.showAutoDownloadModelsStorageKey);
this.showAutoDownloadModels = storedShowAutoDownload === null
? true
+106 -3
View File
@@ -539,6 +539,11 @@ export const missingBrowserMethods = {
const resolvedCount = Number(options.resolvedCount || 0);
const rawMissingCount = Number(options.rawMissingCount ?? totalMissing);
const missingCount = Number(options.missingCount ?? rawMissingCount);
const typeFilterOptions = Array.isArray(options.typeFilterOptions) ? options.typeFilterOptions : [];
const activeTypeFilter = String(options.activeTypeFilter || 'all');
const activeTypeOption = typeFilterOptions.find(option => option.value === activeTypeFilter);
const activeTypeLabel = activeTypeOption?.label || 'All';
const typeFilterMenuOpen = Boolean(this.missingModelsTypeFilterMenuOpen);
const resolvedToggleCount = this.showResolvedModels ? resolvedCount : hiddenResolvedCount;
const autoDownloadToggleCount = this.showAutoDownloadModels ? autoDownloadCount : hiddenAutoDownloadCount;
const stats = this.getMissingModelSummaryStats(missingModels);
@@ -558,9 +563,24 @@ export const missingBrowserMethods = {
: '';
const listStyle = `--mr-missing-model-col:${listLayout.modelPx}px;--mr-missing-type-col:${listLayout.typePx}px;`;
const browserStyle = `${listStyle}${splitStyle}`;
const titleText = this.showResolvedModels && resolvedCount > 0
const typeFilterTotal = Number(typeFilterOptions.find(option => option.value === 'all')?.count || totalMissing);
const titleText = activeTypeFilter !== 'all'
? `${totalMissing} shown of ${typeFilterTotal}`
: this.showResolvedModels && resolvedCount > 0
? `${totalMissing} shown (${missingCount} missing / ${resolvedCount} resolved)`
: `${totalMissing} missing model${totalMissing === 1 ? '' : 's'}`;
const typeFilterMenuOptions = typeFilterOptions.map((option) => {
const isActive = option.value === activeTypeFilter;
const labelClass = option.colorClass
? `mr-missing-type-filter-option-label ${option.colorClass}`
: 'mr-missing-type-filter-option-label';
return `
<button type="button" class="mr-missing-type-filter-option ${isActive ? 'is-active' : ''}" data-missing-type-filter-option="${this.escapeHtml(option.value)}" role="menuitemradio" aria-checked="${isActive ? 'true' : 'false'}">
<span class="${this.escapeHtml(labelClass)}">${this.escapeHtml(option.label)}</span>
<strong>${Number(option.count || 0).toLocaleString()}</strong>
</button>
`;
}).join('');
let html = `
<div class="mr-missing-browser" style="${browserStyle}">
@@ -598,7 +618,17 @@ export const missingBrowserMethods = {
</span>
<span>#</span>
<span>Missing Model</span>
<span>Type</span>
<span class="mr-missing-type-filter-cell">
<span class="mr-missing-type-filter-wrap">
<button type="button" class="mr-missing-type-filter-button ${activeTypeFilter !== 'all' ? 'is-filtered' : ''}" data-missing-type-filter-toggle aria-haspopup="menu" aria-expanded="${typeFilterMenuOpen ? 'true' : 'false'}" title="Filter by type">
<span>Type</span>
<span class="mr-missing-type-filter-chip" ${activeTypeFilter !== 'all' ? '' : 'hidden'}>${this.escapeHtml(activeTypeLabel)}</span>
</button>
<span class="mr-missing-type-filter-menu" role="menu" ${typeFilterMenuOpen ? '' : 'hidden'}>
${typeFilterMenuOptions}
</span>
</span>
</span>
<span>Best Local Match</span>
<span>Match</span>
<span>Sources</span>
@@ -666,6 +696,46 @@ export const missingBrowserMethods = {
wireMissingModelsBrowser(container, data, sortedMissingModels) {
this.wireMissingBrowserSplitter(container);
const browser = container.querySelector('.mr-missing-browser');
const typeFilterToggle = browser?.querySelector('[data-missing-type-filter-toggle]');
const typeFilterMenu = browser?.querySelector('.mr-missing-type-filter-menu');
const setTypeFilterMenuOpen = (open) => {
this.missingModelsTypeFilterMenuOpen = Boolean(open);
if (typeFilterMenu) typeFilterMenu.hidden = !this.missingModelsTypeFilterMenuOpen;
if (typeFilterToggle) {
typeFilterToggle.setAttribute('aria-expanded', this.missingModelsTypeFilterMenuOpen ? 'true' : 'false');
}
};
typeFilterToggle?.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
setTypeFilterMenuOpen(!this.missingModelsTypeFilterMenuOpen);
});
browser?.querySelectorAll('[data-missing-type-filter-option]').forEach((option) => {
option.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
this.missingModelsTypeFilter = option.dataset.missingTypeFilterOption || 'all';
this.missingModelsTypeFilterMenuOpen = false;
this.displayMissingModels(container, data);
});
});
browser?.addEventListener('click', (event) => {
if (!this.missingModelsTypeFilterMenuOpen) return;
if (event.target instanceof Element && event.target.closest('.mr-missing-type-filter-wrap')) return;
setTypeFilterMenuOpen(false);
});
browser?.addEventListener('keydown', (event) => {
if (event.key !== 'Escape' || !this.missingModelsTypeFilterMenuOpen) return;
event.preventDefault();
setTypeFilterMenuOpen(false);
typeFilterToggle?.focus();
});
const refreshBtn = container.querySelector('#mr-refresh-missing-analysis');
if (refreshBtn && refreshBtn.dataset.mlRefreshBound !== 'true') {
refreshBtn.dataset.mlRefreshBound = 'true';
@@ -1838,9 +1908,40 @@ export const missingBrowserMethods = {
count + (this.isAutoDownloadModel(missing) ? 1 : 0)
), 0);
const hiddenAutoDownloadCount = this.showAutoDownloadModels ? 0 : autoDownloadCount;
const visibleMissingModels = this.showAutoDownloadModels
const typeFilterSourceModels = this.showAutoDownloadModels
? resolvedFilteredModels
: resolvedFilteredModels.filter(missing => !this.isAutoDownloadModel(missing));
const getTypeFilterValue = missing => String(missing?.category || 'unknown').trim().toLowerCase() || 'unknown';
const typeFilterCounts = new Map();
const typeFilterLabels = new Map();
typeFilterSourceModels.forEach((missing) => {
const value = getTypeFilterValue(missing);
typeFilterCounts.set(value, (typeFilterCounts.get(value) || 0) + 1);
if (!typeFilterLabels.has(value)) {
typeFilterLabels.set(
value,
missing.category ? this.getCategoryDisplayName(missing.category) : 'Unknown'
);
}
});
let activeTypeFilter = String(this.missingModelsTypeFilter || 'all').toLowerCase();
if (activeTypeFilter !== 'all' && !typeFilterCounts.has(activeTypeFilter)) {
activeTypeFilter = 'all';
this.missingModelsTypeFilter = 'all';
this.missingModelsTypeFilterMenuOpen = false;
}
const typeFilterOptions = [
{ value: 'all', label: 'All', count: typeFilterSourceModels.length },
...Array.from(typeFilterCounts, ([value, count]) => ({
value,
label: typeFilterLabels.get(value) || value,
count,
colorClass: this.getModelTypeColorClass(value)
})).sort((left, right) => left.label.localeCompare(right.label))
];
const visibleMissingModels = activeTypeFilter === 'all'
? typeFilterSourceModels
: typeFilterSourceModels.filter(missing => getTypeFilterValue(missing) === activeTypeFilter);
this.missingModels = visibleMissingModels;
this.syncBatchSelectionForMissingModels(visibleMissingModels);
@@ -1987,6 +2088,8 @@ export const missingBrowserMethods = {
resolvedCount,
rawMissingCount,
missingCount: unresolvedMissingCount,
typeFilterOptions,
activeTypeFilter,
}
);
this.wireMissingModelsBrowser(container, data, sortedMissingModels);
+1
View File
@@ -20,6 +20,7 @@ export function loadAllStyles() {
'./css/css-variables.css',
'./css/resolver-shell.css',
'./css/resolver-main.css',
'./resolver/missing_type_filter.css',
];
cssFiles.forEach(file => {