diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index bc13326..047aa55 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -24,7 +24,10 @@ import { toResolverContextModel, } from '../web/resolver/node_context_menu.js'; import { startSplitterDrag } from '../web/resolver/utils/splitter_drag.js'; -import { normalizeDownloadCategoryValue } from '../web/resolver/utils/category_utils.js'; +import { + collectNormalizedCategoryValues, + normalizeDownloadCategoryValue, +} from '../web/resolver/utils/category_utils.js'; import { CATEGORY_ALIASES, normalizeCategoryToken, @@ -48,6 +51,7 @@ import { } from '../web/resolver/custom_nodes/registry.js'; void normalizeDownloadCategoryValue; +void collectNormalizedCategoryValues; void CATEGORY_ALIASES; void normalizeCategoryToken; void normalizeSha256; @@ -6627,6 +6631,29 @@ test('missing model categories preserve node priority and fallback order', () => ); }); +test('missing browser folder categories normalize, filter, and deduplicate values', () => { + const getMissingSupportedFolderCategories = eval( + `(${extractMethod(missingBrowserMethodsSource, 'getMissingSupportedFolderCategories')})` + ); + const dialog = { + normalizeDownloadCategory(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized.replace(/\s+/g, '_'); + }, + getMissingSupportedDownloadCategories() { + return []; + }, + }; + + assert.deepEqual( + getMissingSupportedFolderCategories.call(dialog, { + category_hints: ['Checkpoints, loras', 'unknown'], + category: 'vae|loras', + }), + ['checkpoints', 'loras', 'vae'] + ); +}); + test('download category normalization maps select safetensors alias to diffusion models', () => { const normalizeDownloadCategory = eval(`(${extractMethod(downloadTargetMethodsSource, 'normalizeDownloadCategory')})`); diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index ef23bd9..b21c6cc 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -1,7 +1,10 @@ import { getSvgIcon } from "../../utils/icon_utils.js"; import { getCustomNodeModelCategory } from "../custom_nodes/registry.js"; import { createFloatingTreePicker } from "../utils/tree_picker.js"; -import { normalizeDownloadCategoryValue } from "../utils/category_utils.js"; +import { + collectNormalizedCategoryValues, + normalizeDownloadCategoryValue, +} from "../utils/category_utils.js"; import { safeStorage } from "../utils/html_utils.js"; import { getSha256Field } from "../utils/hash_utils.js"; import { joinPathPreservingStyle } from "../utils/path_utils.js"; @@ -112,24 +115,11 @@ export const downloadTargetMethods = { getMissingSupportedDownloadCategories(missing = {}) { const knownCategories = this.getKnownDownloadCategorySet(); - const collectCategories = (...values) => { - const categories = []; - const addCategory = (value) => { - if (value === undefined || value === null || String(value).trim() === '') return; - if (Array.isArray(value)) { - value.forEach(addCategory); - return; - } - String(value).split(/[,|;]/).forEach(part => { - const normalized = this.normalizeDownloadCategory(part); - if (normalized && knownCategories.has(normalized) && !categories.includes(normalized)) { - categories.push(normalized); - } - }); - }; - values.forEach(addCategory); - return categories; - }; + const collectCategories = (...values) => collectNormalizedCategoryValues( + values, + value => this.normalizeDownloadCategory(value), + { allowedCategories: knownCategories }, + ); const chooseSupported = (supported = []) => { if (!supported.length) return []; const nodeCategory = this.getMissingNodeTypeDownloadCategory(missing); diff --git a/web/resolver/utils/category_utils.js b/web/resolver/utils/category_utils.js index 63c0b97..dd89ea5 100644 --- a/web/resolver/utils/category_utils.js +++ b/web/resolver/utils/category_utils.js @@ -10,3 +10,36 @@ export function normalizeDownloadCategoryValue( : CATEGORY_ALIASES; return categoryAliases[token] || token || 'checkpoints'; } + +export function collectNormalizedCategoryValues( + values = [], + normalizeCategory, + { allowedCategories = null, excludedCategories = [] } = {}, +) { + const categories = []; + const allowed = allowedCategories instanceof Set ? allowedCategories : null; + const excluded = new Set(excludedCategories); + const addCategory = (value) => { + if (value === undefined || value === null || String(value).trim() === '') return; + if (Array.isArray(value)) { + value.forEach(addCategory); + return; + } + String(value).split(/[,|;]/).forEach(part => { + if (String(part || '').trim() === '') return; + const normalized = normalizeCategory(part); + if ( + !normalized + || excluded.has(normalized) + || (allowed && !allowed.has(normalized)) + || categories.includes(normalized) + ) { + return; + } + categories.push(normalized); + }); + }; + + values.forEach(addCategory); + return categories; +} diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index a3b0f6c..4bf2738 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -6,6 +6,7 @@ import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js"; import { matchesWorkflowModelReference } from "../node_context_menu.js"; import { classifyLocalMatches } from "../utils/local_match_utils.js"; import { joinPathPreservingStyle } from "../utils/path_utils.js"; +import { collectNormalizedCategoryValues } from "../utils/category_utils.js"; const MISSING_ROW_FALLBACK_HEIGHT = 70; const MISSING_VIRTUAL_INITIAL_ROWS = 24; @@ -96,36 +97,26 @@ export const missingBrowserMethods = { }, getMissingSupportedFolderCategories(missing = {}) { - const categories = []; - const addCategory = (value) => { - if (value === undefined || value === null || String(value).trim() === '') return; - if (Array.isArray(value)) { - value.forEach(addCategory); - return; - } - String(value).split(/[,|;]/).forEach(part => { - const normalized = this.normalizeDownloadCategory?.(part) || String(part || '').trim(); - if ( - normalized - && normalized !== 'unknown' - && !categories.includes(normalized) - ) { - categories.push(normalized); - } - }); - }; + const normalizeCategory = value => ( + this.normalizeDownloadCategory?.(value) || String(value || '').trim() + ); + const options = { excludedCategories: ['unknown'] }; + const supported = collectNormalizedCategoryValues( + [this.getMissingSupportedDownloadCategories?.(missing) || []], + normalizeCategory, + options, + ); + if (supported.length) return supported; - addCategory(this.getMissingSupportedDownloadCategories?.(missing) || []); - if (!categories.length) { - addCategory(missing.category_hints); - addCategory(missing.categoryHints); - addCategory(missing.model_widget_category_hints); - addCategory(missing.modelWidgetCategoryHints); - addCategory(missing.supported_categories); - addCategory(missing.supportedCategories); - addCategory(missing.category); - } - return categories; + return collectNormalizedCategoryValues([ + missing.category_hints, + missing.categoryHints, + missing.model_widget_category_hints, + missing.modelWidgetCategoryHints, + missing.supported_categories, + missing.supportedCategories, + missing.category, + ], normalizeCategory, options); }, getMissingSupportedFolderKeys(missing = {}) {