From c164d8d6763bf04ec44c7a1ab9f0f7730f137837 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 6 Aug 2026 18:22:56 +0200 Subject: [PATCH] refactor(frontend): share path template formatting --- tests/test_downloads_tab_workflow_route.mjs | 64 +++++++++++++++++++ .../search/download_target_methods.js | 16 +++-- web/resolver/views/options_methods.js | 7 +- 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 28464db..84fa240 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -4038,6 +4038,70 @@ test('download subfolder and path template normalization apply the same segment ); }); +test('download path templates render metadata into the configured subfolder', () => { + const formatDownloadPathTemplate = eval( + `(${extractMethod(downloadTargetMethodsSource, 'formatDownloadPathTemplate')})` + ); + const calculateDownloadPathTemplateSubfolder = eval( + `(${extractMethod(downloadTargetMethodsSource, 'calculateDownloadPathTemplateSubfolder')})` + ); + const dialog = { + getDownloadPathTemplates() { + return { loras: '{base_model}/{author}/{first_tag}/{model_name}/{version_name}/{unknown}' }; + }, + normalizeDownloadCategory(value) { + return value; + }, + formatDownloadPathTemplate, + normalizeDownloadPathValue(value) { + return String(value || '').replace(/\\/g, '/'); + }, + getBaseModelPathMappings() { + return { Pony: 'SDXL/Pony' }; + }, + resolveBaseModelPathMapping(value, mappings) { + return mappings[value] || value; + }, + sanitizeDownloadPathValue(value, fallback) { + return value || fallback; + }, + sanitizeDownloadPathSegment(value, fallback) { + return value || fallback; + }, + getPriorityDownloadTag(tags) { + return tags?.[0] || ''; + }, + normalizeTemplateSubfolder(value) { + return String(value || '') + .split('/') + .filter(Boolean) + .join('/'); + }, + }; + + assert.equal( + calculateDownloadPathTemplateSubfolder.call(dialog, 'loras', { + base_model: 'Pony', + author: 'Creator', + tags: ['Style'], + model_name: 'Model', + version_name: 'v1', + }), + 'SDXL/Pony/Creator/Style/Model/v1' + ); + assert.equal( + formatDownloadPathTemplate.call(dialog, '{base_model}/{unknown}', { + '{base_model}': 'SDXL', + }), + 'SDXL' + ); +}); + +test('options path preview delegates token rendering to the shared template formatter', () => { + assert.match(optionsMethodsSource, /this\.formatDownloadPathTemplate\(template, replacements\)/); + assert.doesNotMatch(optionsMethodsSource, /formatted\.split\(token\)\.join\(value\)/); +}); + test('download subfolder tooltip identifies a folder taken from the workflow model path', () => { const getDownloadSubfolderSuggestionReason = eval(`(${extractMethod(downloadTargetMethodsSource, 'getDownloadSubfolderSuggestionReason')})`); const dialog = { diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index dd4cac4..d24f7b7 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -1218,6 +1218,15 @@ export const downloadTargetMethods = { return list[0]; }, + formatDownloadPathTemplate(template = '', replacements = {}) { + let formatted = this.normalizeDownloadPathValue(template); + Object.entries(replacements).forEach(([token, value]) => { + formatted = formatted.split(token).join(value); + }); + formatted = formatted.replace(/\{[^{}]+\}/g, ''); + return this.normalizeTemplateSubfolder(formatted); + }, + getDownloadSourceContext(missing = {}, source = {}, { respectProvenanceBoundary = true } = {}) { const sourceData = source && typeof source === 'object' ? source : {}; const isProvidedUrl = Boolean( @@ -1482,12 +1491,7 @@ export const downloadTargetMethods = { '{model_name}': this.sanitizeDownloadPathSegment(metadata.model_name || metadata.name || metadata.filename?.replace(/\.[^.]+$/, '') || 'Model', 'Model'), '{version_name}': this.sanitizeDownloadPathSegment(metadata.version_name || metadata.versionName || metadata.version || '', '') }; - let formatted = template; - Object.entries(replacements).forEach(([token, value]) => { - formatted = formatted.split(token).join(value); - }); - formatted = formatted.replace(/\{[^{}]+\}/g, ''); - return this.normalizeTemplateSubfolder(formatted); + return this.formatDownloadPathTemplate(template, replacements); }, isAutoFillSubfolderEnabled() { diff --git a/web/resolver/views/options_methods.js b/web/resolver/views/options_methods.js index a441d3c..b5ba785 100644 --- a/web/resolver/views/options_methods.js +++ b/web/resolver/views/options_methods.js @@ -2926,12 +2926,7 @@ export const optionsMethods = { '{model_name}': this.sanitizeDownloadPathSegment(pathPreviewMetadata.model_name, 'Model'), '{version_name}': this.sanitizeDownloadPathSegment(pathPreviewMetadata.version_name, '') }; - let formatted = this.normalizeDownloadPathValue(template); - Object.entries(replacements).forEach(([token, value]) => { - formatted = formatted.split(token).join(value); - }); - formatted = formatted.replace(/\{[^{}]+\}/g, ''); - return this.normalizeTemplateSubfolder(formatted) || '(flat folder)'; + return this.formatDownloadPathTemplate(template, replacements) || '(flat folder)'; }; const syncTemplateControl = (categoryKey) => {