feat(settings): empty filename template reverts to recorded original filename (#1071)

Redefine the empty download filename template from a no-op to a bulk
revert: FilenameTemplateUseCase resolves the target from each model's
recorded original_file_name sidecar entry (skipping models without one),
which resolves follow-ups 1 and 2 with a single coherent semantic shared
by the download and bulk-apply paths.

Also replace the browser-native confirm() with a self-managed
confirmation modal (filenameTemplateConfirmModal) that stacks above the
settings modal, since ModalManager would close the settings modal when
opening a registered one.
This commit is contained in:
Will Miao
2026-09-19 10:44:43 +08:00
parent 78d38b449e
commit 474da1b264
17 changed files with 388 additions and 110 deletions
@@ -2,8 +2,9 @@
**Issue:** [#1071 — Lora Renaming](https://github.com/willmiao/ComfyUI-Lora-Manager/issues/1071) **Issue:** [#1071 — Lora Renaming](https://github.com/willmiao/ComfyUI-Lora-Manager/issues/1071)
**Status:** Core feature **implemented** (2026-09-19, commit `2bc9860b`, **Status:** Core feature **implemented** (2026-09-19, commit `2bc9860b`,
preceded by the settings-tab split in `327da046`). This document records the preceded by the settings-tab split in `327da046`). Follow-ups 1 and 2 were
follow-up items deliberately left out of that commit, for a future session. resolved together on 2026-09-19 by redefining the empty template as
"revert to recorded original filename" (see below). Follow-up 3 remains open.
## What shipped in `2bc9860b` ## What shipped in `2bc9860b`
@@ -30,47 +31,67 @@ metadata pointer updates, `original_file_name` recording, idempotency,
conflict handling (failure counted, batch continues), empty-template no-op, conflict handling (failure counted, batch continues), empty-template no-op,
GET variant. GET variant.
## Follow-up 1 — Reword the empty-template validation message ## Follow-ups 1 & 2 — RESOLVED: empty template = revert to recorded original
**Problem:** the empty-template state currently reads "Valid (keep original Follow-up 1 asked to reword the ambiguous "Valid (keep original filename)"
filename)" (`settings.filenameTemplates.validation.keepOriginal` in empty-template message; Follow-up 2 asked for a bulk revert to the recorded
`locales/en.json`). "Original" is ambiguous — it reads as "restore the `original_file_name`. Both were resolved by a single semantic change: **an
recorded original name", but the semantics are "leave the current filename empty template now means "restore the recorded original filename"** instead of
untouched" (empty template = no-op, confirmed in E2E). "leave the current filename untouched".
**Fix:** reword the `en.json` value, e.g. "Valid (leave files unchanged)", Rationale: for never-renamed models a revert is a no-op (no recorded
then re-run `python scripts/sync_translation_keys.py` and retranslate the key original), for renamed models it restores the pre-rename name, and new
in the 9 non-English locales (see `docs/i18n-translation-guidelines.md` §7 — downloads with an empty template keep the download name as before — so the
the owner explicitly asking for translations is the trigger). two contexts (download path and bulk apply) share one coherent meaning, and
no separate revert feature or `{recorded_original}` placeholder is needed.
## Follow-up 2 — Bulk revert to recorded original filename Implemented changes:
**Problem:** renames record `original_file_name` in each model's - `FilenameTemplateUseCase._process_model`: an empty template now resolves
`.metadata.json`, but there is no way to undo in bulk. Restoring today means the target name from the sidecar's `original_file_name` via the injected
renaming each model by hand via the existing single-model rename dialog. `metadata_loader` (default `load_local_metadata`); models without a
Note: the `{original_name}` template placeholder resolves to the **current** recorded original or whose original matches the current name are skipped.
filename stem (`calculate_filename_for_model` reads `file_path` / Cache entries do not project `original_file_name`, so the sidecar is read
`file_name` from the cache entry), so "set template to `{original_name}` and per model.
apply" is **not** a revert path. - `SettingsManager.js`: removed the empty-template early return and the
apply-button disable (`updateFilenameTemplateApplyButton` deleted — the
button is now always enabled). The browser-native `confirm()` was replaced
with `filenameTemplateConfirmModal`
(`templates/components/modals/confirm_modals.html`), a **self-managed**
modal (like `DirectoryPickerModal`, NOT registered with ModalManager):
ModalManager's "close current modal on open" behavior would kill the
settings modal underneath. It stacks via `z-index: 10010`
(`delete-modal.css`), handles ESC in capture phase with
`stopPropagation`, and shows apply vs revert wording
(`modals.filenameTemplateConfirm.titleApply` / `titleRevert` /
`revertButton`; messages reuse `settings.filenameTemplates.confirmApply` /
`confirmRevert`).
- `locales/en.json`: reworded `help` / `applyHelp`, replaced
`validation.keepOriginal` with `validation.restoreOriginal`
("Valid (empty template restores original filenames)"), added
`confirmRevert`, removed the now-unused `emptyTemplateInfo`. Other locales
re-synced with `[TODO: Translate]` placeholders — retranslation waits for
the feature owner's request per `docs/i18n-translation-guidelines.md` §7.
- Tests: revert / no-record-skip / same-name-skip cases in
`tests/services/test_use_cases.py`; modal confirm-and-revert and
cancel paths in
`tests/frontend/managers/settingsManager.filenameTemplates.test.js`.
**Suggested shape (smallest change):** add a new placeholder, e.g. Sandbox E2E verified (standalone server, sandboxed settings + library under
`{recorded_original}`, resolved in `calculate_filename_for_model` from `/tmp`, 2026-09-19): template apply renames and records
`model_data.get("original_file_name")` (cache entries need to carry the `original_file_name`; empty-template apply reverts to the recorded name;
field — check whether the scanner cache surfaces sidecar extras; if not, revert target occupied by a newer file counts as failure and keeps the
read it in the use case via the metadata loader). Setting the template to current name; models without a recorded original are skipped;
`{recorded_original}` and running "Apply to Library Now" then reverts the apply → revert → re-apply cycles repeat cleanly.
library. Models never renamed render an empty segment → skipped, which is
the desired behaviour.
Alternative shape: a dedicated revert endpoint + button. More code, clearer Standing caveats (unchanged):
UX; only worth it if the placeholder approach proves too obscure.
**Caveats either way:** - The revert target may collide with an existing file — the existing conflict
- The revert target may collide with an existing file (a file downloaded
after the rename may already carry that name) — the existing conflict
handling (count as failure, keep current name) covers this. handling (count as failure, keep current name) covers this.
- `original_file_name` only exists for models renamed after `2bc9860b`; - `original_file_name` only exists for models renamed after `2bc9860b`;
older renames have no recorded original and must be skipped. older renames have no recorded original and are skipped.
- `original_file_name` is kept (not cleared) after a revert, so
apply → revert → re-apply stays repeatable.
## Follow-up 3 — Cross-page refresh after bulk apply ## Follow-up 3 — Cross-page refresh after bulk apply
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "Möchten Sie in Etappen prüfen? Wechseln Sie in den Massenmodus, wählen Sie die benötigten Modelle aus und nutzen Sie anschließend \"Auswahl auf Updates prüfen\".", "tip": "Möchten Sie in Etappen prüfen? Wechseln Sie in den Massenmodus, wählen Sie die benötigten Modelle aus und nutzen Sie anschließend \"Auswahl auf Updates prüfen\".",
"action": "Alles prüfen" "action": "Alles prüfen"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Tags zu mehreren Modellen hinzufügen", "title": "Tags zu mehreren Modellen hinzufügen",
"description": "Tags hinzufügen zu", "description": "Tags hinzufügen zu",
+9 -4
View File
@@ -683,15 +683,15 @@
}, },
"filenameTemplates": { "filenameTemplates": {
"title": "Filename Templates", "title": "Filename Templates",
"help": "Configure filenames for downloaded models per model type. Leave empty to keep the original filename. The original filename is always preserved in the model's metadata.", "help": "Configure filenames for downloaded models per model type. Leave empty to keep original filenames on download; applying an empty template restores the recorded original filenames of previously renamed models. The original filename is always preserved in the model's metadata.",
"availablePlaceholders": "Available placeholders:", "availablePlaceholders": "Available placeholders:",
"templatePlaceholder": "Enter filename template (e.g., {base_model}-{model_name}-{version_name})", "templatePlaceholder": "Enter filename template (e.g., {base_model}-{model_name}-{version_name})",
"applyButton": "Apply to Library Now", "applyButton": "Apply to Library Now",
"applyHelp": "Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "Renames all existing files of this model type according to the template; with an empty template, restores the recorded original filenames instead. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "Valid (keep original filename)", "restoreOriginal": "Valid (empty template restores original filenames)",
"invalidChars": "Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "Invalid placeholder: {placeholder}", "invalidPlaceholder": "Invalid placeholder: {placeholder}",
"validTemplate": "Valid template" "validTemplate": "Valid template"
@@ -1626,6 +1626,11 @@
"tip": "To work in smaller batches, switch to bulk mode, choose the ones you need, then use \"Check Updates for Selected\".", "tip": "To work in smaller batches, switch to bulk mode, choose the ones you need, then use \"Check Updates for Selected\".",
"action": "Check All" "action": "Check All"
}, },
"filenameTemplateConfirm": {
"titleApply": "Apply filename template to library?",
"titleRevert": "Restore original filenames?",
"revertButton": "Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Add Tags to Multiple Models", "title": "Add Tags to Multiple Models",
"description": "Add tags to", "description": "Add tags to",
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "¿Quieres hacerlo por partes? Activa el modo por lotes, selecciona los modelos que necesites y usa \"Comprobar actualizaciones para la selección\".", "tip": "¿Quieres hacerlo por partes? Activa el modo por lotes, selecciona los modelos que necesites y usa \"Comprobar actualizaciones para la selección\".",
"action": "Comprobar todo" "action": "Comprobar todo"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Añadir etiquetas a múltiples modelos", "title": "Añadir etiquetas a múltiples modelos",
"description": "Añadir etiquetas a", "description": "Añadir etiquetas a",
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "Besoin de procéder par étapes ? Passez en mode groupé, sélectionnez les modèles souhaités puis utilisez \"Vérifier les mises à jour pour la sélection\".", "tip": "Besoin de procéder par étapes ? Passez en mode groupé, sélectionnez les modèles souhaités puis utilisez \"Vérifier les mises à jour pour la sélection\".",
"action": "Tout vérifier" "action": "Tout vérifier"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Ajouter des tags à plusieurs modèles", "title": "Ajouter des tags à plusieurs modèles",
"description": "Ajouter des tags à", "description": "Ajouter des tags à",
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "רוצים לחלק למנות קטנות? עברו למצב בכמות גדולה, בחרו את המודלים הדרושים ואז השתמשו ב\"בדוק עדכונים לנבחרים\".", "tip": "רוצים לחלק למנות קטנות? עברו למצב בכמות גדולה, בחרו את המודלים הדרושים ואז השתמשו ב\"בדוק עדכונים לנבחרים\".",
"action": "בדוק הכל" "action": "בדוק הכל"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "הוסף תגיות למספר מודלים", "title": "הוסף תגיות למספר מודלים",
"description": "הוסף תגיות ל-", "description": "הוסף תגיות ל-",
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "少しずつ確認したい場合は一括モードに切り替え、必要なモデルを選んで「選択項目の更新を確認」を使ってください。", "tip": "少しずつ確認したい場合は一括モードに切り替え、必要なモデルを選んで「選択項目の更新を確認」を使ってください。",
"action": "すべて確認" "action": "すべて確認"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "複数モデルにタグを追加", "title": "複数モデルにタグを追加",
"description": "タグを追加するモデル:", "description": "タグを追加するモデル:",
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "나눠서 진행하고 싶다면 일괄 모드로 전환해 필요한 모델만 선택한 뒤 \"선택 항목 업데이트 확인\"을 사용하세요.", "tip": "나눠서 진행하고 싶다면 일괄 모드로 전환해 필요한 모델만 선택한 뒤 \"선택 항목 업데이트 확인\"을 사용하세요.",
"action": "전체 확인" "action": "전체 확인"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "여러 모델에 태그 추가", "title": "여러 모델에 태그 추가",
"description": "다음에 태그를 추가합니다:", "description": "다음에 태그를 추가합니다:",
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "Хотите проверять по частям? Переключитесь в массовый режим, выберите нужные модели и используйте \"Проверить обновления для выбранных\".", "tip": "Хотите проверять по частям? Переключитесь в массовый режим, выберите нужные модели и используйте \"Проверить обновления для выбранных\".",
"action": "Проверить всё" "action": "Проверить всё"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Добавить теги к нескольким моделям", "title": "Добавить теги к нескольким моделям",
"description": "Добавить теги к", "description": "Добавить теги к",
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "想分批进行?切换到批量模式,选中需要的模型,然后使用“检查所选更新”。", "tip": "想分批进行?切换到批量模式,选中需要的模型,然后使用“检查所选更新”。",
"action": "检查全部" "action": "检查全部"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "批量添加标签", "title": "批量添加标签",
"description": "为多个模型添加标签", "description": "为多个模型添加标签",
+7 -2
View File
@@ -689,9 +689,9 @@
"applyButton": "[TODO: Translate] Apply to Library Now", "applyButton": "[TODO: Translate] Apply to Library Now",
"applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.", "applyHelp": "[TODO: Translate] Renames all existing files of this model type according to the template. Warning: renaming changes the relative path seen by ComfyUI loaders, so existing workflows referencing the old filename may need to be updated. The original filename is preserved in each model's metadata.",
"confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.", "confirmApply": "[TODO: Translate] Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model's metadata.",
"emptyTemplateInfo": "[TODO: Translate] No filename template configured for this model type — original filenames are kept, nothing to apply.", "confirmRevert": "[TODO: Translate] Restore the recorded original filenames of all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.",
"validation": { "validation": {
"keepOriginal": "[TODO: Translate] Valid (keep original filename)", "restoreOriginal": "[TODO: Translate] Valid (empty template restores original filenames)",
"invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)", "invalidChars": "[TODO: Translate] Invalid characters detected (a filename cannot contain / \\ < > : \" | ? *)",
"invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}", "invalidPlaceholder": "[TODO: Translate] Invalid placeholder: {placeholder}",
"validTemplate": "[TODO: Translate] Valid template" "validTemplate": "[TODO: Translate] Valid template"
@@ -1626,6 +1626,11 @@
"tip": "想分批處理?切換到批次模式,選擇需要的模型,然後使用「檢查所選更新」。", "tip": "想分批處理?切換到批次模式,選擇需要的模型,然後使用「檢查所選更新」。",
"action": "全部檢查" "action": "全部檢查"
}, },
"filenameTemplateConfirm": {
"titleApply": "[TODO: Translate] Apply filename template to library?",
"titleRevert": "[TODO: Translate] Restore original filenames?",
"revertButton": "[TODO: Translate] Restore Original Filenames"
},
"bulkAddTags": { "bulkAddTags": {
"title": "新增標籤到多個模型", "title": "新增標籤到多個模型",
"description": "新增標籤到", "description": "新增標籤到",
@@ -1,16 +1,20 @@
"""Filename template use case: bulk-rename library models per the configured template.""" """Filename template use case: bulk-rename library models per the configured template.
An empty template reverts previously renamed models to the original filename
recorded in their ``.metadata.json`` sidecar (``original_file_name``).
"""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging import logging
import os import os
from typing import Any, Dict, List, Optional, Sequence from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence
from ...utils.constants import AUTO_ORGANIZE_BATCH_SIZE from ...utils.constants import AUTO_ORGANIZE_BATCH_SIZE
from ...utils.utils import calculate_filename_for_model from ...utils.utils import calculate_filename_for_model
from ..model_file_service import AutoOrganizeResult, ProgressCallback from ..model_file_service import AutoOrganizeResult, ProgressCallback
from ..model_lifecycle_service import ModelLifecycleService from ..model_lifecycle_service import ModelLifecycleService, load_local_metadata
from ..settings_manager import get_settings_manager from ..settings_manager import get_settings_manager
from .auto_organize_use_case import ( from .auto_organize_use_case import (
AutoOrganizeInProgressError, AutoOrganizeInProgressError,
@@ -25,8 +29,10 @@ _PROGRESS_TYPE = "filename_template_progress"
class FilenameTemplateUseCase: class FilenameTemplateUseCase:
"""Apply the download filename template to existing library models. """Apply the download filename template to existing library models.
Shares the auto-organize lock (and its in-progress error) so a bulk An empty template restores the recorded original filename instead of
rename never runs concurrently with an auto-organize operation. rendering a template. Shares the auto-organize lock (and its in-progress
error) so a bulk rename never runs concurrently with an auto-organize
operation.
""" """
def __init__( def __init__(
@@ -36,11 +42,13 @@ class FilenameTemplateUseCase:
lifecycle_service: ModelLifecycleService, lifecycle_service: ModelLifecycleService,
lock_provider: AutoOrganizeLockProvider, lock_provider: AutoOrganizeLockProvider,
model_type: str, model_type: str,
metadata_loader: Callable[[str], Awaitable[Dict[str, Any]]] = load_local_metadata,
) -> None: ) -> None:
self._scanner = scanner self._scanner = scanner
self._lifecycle_service = lifecycle_service self._lifecycle_service = lifecycle_service
self._lock_provider = lock_provider self._lock_provider = lock_provider
self._model_type = model_type self._model_type = model_type
self._metadata_loader = metadata_loader
async def execute( async def execute(
self, self,
@@ -151,10 +159,11 @@ class FilenameTemplateUseCase:
return return
if not template: if not template:
result.skipped_count += 1 # Empty template = revert to the original filename recorded
return # by the first rename; models without a record are skipped.
new_stem = await self._resolve_recorded_original(file_path)
new_stem = calculate_filename_for_model(model, self._model_type) else:
new_stem = calculate_filename_for_model(model, self._model_type)
if not new_stem: if not new_stem:
result.skipped_count += 1 result.skipped_count += 1
return return
@@ -184,6 +193,20 @@ class FilenameTemplateUseCase:
self._add_result(result, model_name, False, f"Error: {exc}") self._add_result(result, model_name, False, f"Error: {exc}")
result.failure_count += 1 result.failure_count += 1
async def _resolve_recorded_original(self, file_path: str) -> str:
"""Return the original filename stem recorded at the first rename.
Reads the ``.metadata.json`` sidecar; returns an empty string when no
sidecar or no ``original_file_name`` entry exists (models never
renamed, or renamed before the recording shipped).
"""
metadata_path = f"{os.path.splitext(file_path)[0]}.metadata.json"
metadata = await self._metadata_loader(metadata_path)
original = metadata.get("original_file_name")
if not isinstance(original, str):
return ""
return original.strip()
async def _emit_progress( async def _emit_progress(
self, self,
progress_callback: Optional[ProgressCallback], progress_callback: Optional[ProgressCallback],
@@ -27,6 +27,12 @@
justify-content: center; justify-content: center;
} }
/* Self-managed by SettingsManager: stacks above the settings modal like the
directory picker (settings panels sit at 10000/10002). */
#filenameTemplateConfirmModal {
z-index: 10010;
}
.delete-modal-content { .delete-modal-content {
max-width: 500px; max-width: 500px;
width: 90%; width: 90%;
+91 -20
View File
@@ -463,7 +463,6 @@ export class SettingsManager {
const template = e.target.value; const template = e.target.value;
settingsManager.validateFilenameTemplate(modelType, template); settingsManager.validateFilenameTemplate(modelType, template);
settingsManager.updateFilenamePreview(modelType, template); settingsManager.updateFilenamePreview(modelType, template);
settingsManager.updateFilenameTemplateApplyButton(modelType, template);
}); });
filenameInput.addEventListener('blur', (e) => { filenameInput.addEventListener('blur', (e) => {
@@ -2864,7 +2863,6 @@ export class SettingsManager {
input.value = template; input.value = template;
this.validateFilenameTemplate(modelType, template); this.validateFilenameTemplate(modelType, template);
this.updateFilenamePreview(modelType, template); this.updateFilenamePreview(modelType, template);
this.updateFilenameTemplateApplyButton(modelType, template);
}); });
} }
@@ -2877,7 +2875,7 @@ export class SettingsManager {
validationElement.className = 'template-validation'; validationElement.className = 'template-validation';
if (!template) { if (!template) {
validationElement.innerHTML = `<i class="fas fa-check"></i> ${translate('settings.filenameTemplates.validation.keepOriginal', {}, 'Valid (keep original filename)')}`; validationElement.innerHTML = `<i class="fas fa-check"></i> ${translate('settings.filenameTemplates.validation.restoreOriginal', {}, 'Valid (empty template restores original filenames)')}`;
validationElement.classList.add('valid'); validationElement.classList.add('valid');
return true; return true;
} }
@@ -2922,28 +2920,19 @@ export class SettingsManager {
} }
state.global.settings.download_filename_templates[modelType] = template; state.global.settings.download_filename_templates[modelType] = template;
// Update preview and apply-button state // Update preview
this.updateFilenamePreview(modelType, template); this.updateFilenamePreview(modelType, template);
this.updateFilenameTemplateApplyButton(modelType, template);
// Save settings // Save settings
this.saveFilenameTemplates(); this.saveFilenameTemplates();
} }
updateFilenameTemplateApplyButton(modelType, template) {
const button = document.getElementById(`${modelType}ApplyFilenameTemplate`);
if (!button) return;
// An empty template keeps original filenames, so there is nothing to apply
button.disabled = !template;
}
updateFilenamePreview(modelType, template) { updateFilenamePreview(modelType, template) {
const previewElement = document.getElementById(`${modelType}FilenamePreview`); const previewElement = document.getElementById(`${modelType}FilenamePreview`);
if (!previewElement) return; if (!previewElement) return;
if (!template) { if (!template) {
// Empty template keeps the original filename untouched // Empty template restores the recorded original filename
previewElement.textContent = 'V1.safetensors'; previewElement.textContent = 'V1.safetensors';
} else { } else {
const exampleStem = template const exampleStem = template
@@ -2972,14 +2961,96 @@ export class SettingsManager {
} }
} }
async applyFilenameTemplate(modelType) { /**
const template = state.global.settings.download_filename_templates?.[modelType] || ''; * Confirm the bulk apply/revert via a dedicated modal. Self-managed (NOT
if (!template) { * through ModalManager): it stacks above the settings modal, and
showToast('settings.filenameTemplates.emptyTemplateInfo', {}, 'info'); * ModalManager's "close current modal on open" behavior would kill the
return; * settings modal underneath. Resolves true when the user confirms.
*/
confirmFilenameTemplateApply(isRevert) {
const modalElement = document.getElementById('filenameTemplateConfirmModal');
if (!modalElement) {
return Promise.resolve(true);
} }
if (!confirm(translate('settings.filenameTemplates.confirmApply', {}, 'Rename all existing files of this model type according to the filename template? The original filename is preserved in each model\'s metadata.'))) { const titleElement = modalElement.querySelector('[data-role="title"]');
if (titleElement) {
titleElement.textContent = isRevert
? translate('modals.filenameTemplateConfirm.titleRevert', {}, 'Restore original filenames?')
: translate('modals.filenameTemplateConfirm.titleApply', {}, 'Apply filename template to library?');
}
const messageElement = modalElement.querySelector('[data-role="message"]');
if (messageElement) {
messageElement.textContent = isRevert
? translate('settings.filenameTemplates.confirmRevert', {}, 'Restore the recorded original filename for all previously renamed files of this model type? This changes the relative path seen by ComfyUI loaders. Files without a recorded original filename are skipped.')
: translate('settings.filenameTemplates.confirmApply', {}, 'Rename all existing files of this model type according to the filename template? This changes the relative path seen by ComfyUI loaders. The original filename is preserved in each model\'s metadata.');
}
const confirmButton = modalElement.querySelector('[data-action="confirm-filename-template"]');
const cancelButton = modalElement.querySelector('[data-action="cancel-filename-template"]');
if (!confirmButton || !cancelButton) {
return Promise.resolve(true);
}
confirmButton.textContent = isRevert
? translate('modals.filenameTemplateConfirm.revertButton', {}, 'Restore Original Filenames')
: translate('settings.filenameTemplates.applyButton', {}, 'Apply to Library Now');
return new Promise((resolve) => {
let resolved = false;
const cleanup = () => {
confirmButton.removeEventListener('click', handleConfirm);
cancelButton.removeEventListener('click', handleCancel);
document.removeEventListener('keydown', handleEscape, true);
};
const finalize = (proceed) => {
if (resolved) {
return;
}
resolved = true;
cleanup();
modalElement.classList.remove('show');
// Keep body.modal-open: the settings modal underneath is still open.
resolve(proceed);
};
const handleConfirm = (event) => {
event.preventDefault();
finalize(true);
};
const handleCancel = (event) => {
event.preventDefault();
finalize(false);
};
// Capture phase + stopPropagation so ESC never reaches the
// settings modal's own ESC handler underneath.
const handleEscape = (event) => {
if (event.key === 'Escape') {
event.stopPropagation();
finalize(false);
}
};
confirmButton.addEventListener('click', handleConfirm);
cancelButton.addEventListener('click', handleCancel);
document.addEventListener('keydown', handleEscape, true);
modalElement.classList.add('show');
cancelButton.focus();
});
}
async applyFilenameTemplate(modelType) {
const template = state.global.settings.download_filename_templates?.[modelType] || '';
// An empty template reverts renamed models to their recorded original
// filename instead of rendering a template.
const confirmed = await this.confirmFilenameTemplateApply(!template);
if (!confirmed) {
return; return;
} }
@@ -82,6 +82,20 @@
</div> </div>
</div> </div>
<!-- Filename Template Apply/Revert Confirmation Modal
Self-managed by SettingsManager (NOT registered with ModalManager): it
stacks above the settings modal, like the directory picker. -->
<div id="filenameTemplateConfirmModal" class="modal delete-modal">
<div class="modal-content delete-modal-content">
<h2 data-role="title"></h2>
<p class="delete-message" data-role="message"></p>
<div class="modal-actions">
<button class="cancel-btn" data-action="cancel-filename-template">{{ t('common.actions.cancel') }}</button>
<button class="primary-btn" data-action="confirm-filename-template"></button>
</div>
</div>
</div>
<!-- Sidebar Folder Delete Confirmation Modal --> <!-- Sidebar Folder Delete Confirmation Modal -->
<!-- Shared by two states: 'confirm' (model-free folder) and 'blocked' (the <!-- Shared by two states: 'confirm' (model-free folder) and 'blocked' (the
subtree still holds models, so a cascade delete is refused). --> subtree still holds models, so a cascade delete is refused). -->
@@ -81,7 +81,6 @@ vi.mock('../../../static/js/managers/BannerService.js', () => ({
import { SettingsManager } from '../../../static/js/managers/SettingsManager.js'; import { SettingsManager } from '../../../static/js/managers/SettingsManager.js';
import { state } from '../../../static/js/state/index.js'; import { state } from '../../../static/js/state/index.js';
import { showToast } from '../../../static/js/utils/uiHelpers.js';
import { resetAndReload, getModelApiClient } from '../../../static/js/api/modelApiFactory.js'; import { resetAndReload, getModelApiClient } from '../../../static/js/api/modelApiFactory.js';
const createManager = () => { const createManager = () => {
@@ -109,6 +108,17 @@ const appendFilenameTemplateUi = (modelType = 'lora') => {
`; `;
}; };
const appendConfirmModal = () => {
document.body.insertAdjacentHTML('beforeend', `
<div id="filenameTemplateConfirmModal" class="modal delete-modal">
<h2 data-role="title"></h2>
<p data-role="message"></p>
<button data-action="cancel-filename-template"></button>
<button data-action="confirm-filename-template"></button>
</div>
`);
};
describe('SettingsManager filename templates', () => { describe('SettingsManager filename templates', () => {
beforeEach(() => { beforeEach(() => {
document.body.innerHTML = ''; document.body.innerHTML = '';
@@ -118,7 +128,7 @@ describe('SettingsManager filename templates', () => {
}; };
}); });
it('treats an empty template as valid (keep original filename)', () => { it('treats an empty template as valid (restores original filenames)', () => {
appendFilenameTemplateUi(); appendFilenameTemplateUi();
const manager = createManager(); const manager = createManager();
@@ -126,7 +136,7 @@ describe('SettingsManager filename templates', () => {
const validation = document.getElementById('loraFilenameValidation'); const validation = document.getElementById('loraFilenameValidation');
expect(validation.classList.contains('valid')).toBe(true); expect(validation.classList.contains('valid')).toBe(true);
expect(validation.textContent).toContain('keep original filename'); expect(validation.textContent).toContain('restores original filenames');
}); });
it('rejects templates with path separators or OS-illegal characters', () => { it('rejects templates with path separators or OS-illegal characters', () => {
@@ -184,7 +194,7 @@ describe('SettingsManager filename templates', () => {
expect(manager.saveSetting).not.toHaveBeenCalled(); expect(manager.saveSetting).not.toHaveBeenCalled();
}); });
it('previews the original filename when the template is empty', () => { it('previews the recorded original filename when the template is empty', () => {
appendFilenameTemplateUi(); appendFilenameTemplateUi();
const manager = createManager(); const manager = createManager();
@@ -203,15 +213,44 @@ describe('SettingsManager filename templates', () => {
.toBe('Flux.1 D-model-name-v3-a1b2c3d4e5.safetensors'); .toBe('Flux.1 D-model-name-v3-a1b2c3d4e5.safetensors');
}); });
it('disables the apply button when the template is empty', () => { it('applies an empty template as a revert after modal confirmation', async () => {
appendFilenameTemplateUi(); appendFilenameTemplateUi();
appendConfirmModal();
const manager = createManager();
const apiClient = { applyFilenameTemplate: vi.fn().mockResolvedValue() };
getModelApiClient.mockReturnValue(apiClient);
const applyPromise = manager.applyFilenameTemplate('lora');
const modal = document.getElementById('filenameTemplateConfirmModal');
expect(modal.classList.contains('show')).toBe(true);
expect(modal.querySelector('[data-role="title"]').textContent)
.toBe('Restore original filenames?');
expect(modal.querySelector('[data-role="message"]').textContent)
.toContain('Restore the recorded original filename');
expect(modal.querySelector('[data-action="confirm-filename-template"]').textContent)
.toBe('Restore Original Filenames');
modal.querySelector('[data-action="confirm-filename-template"]').click();
await applyPromise;
expect(getModelApiClient).toHaveBeenCalledWith('loras');
expect(apiClient.applyFilenameTemplate).toHaveBeenCalledWith();
expect(resetAndReload).toHaveBeenCalledWith(true);
expect(modal.classList.contains('show')).toBe(false);
});
it('does not revert when the modal is cancelled', async () => {
appendFilenameTemplateUi();
appendConfirmModal();
const manager = createManager(); const manager = createManager();
manager.updateFilenameTemplateApplyButton('lora', ''); const applyPromise = manager.applyFilenameTemplate('lora');
expect(document.getElementById('loraApplyFilenameTemplate').disabled).toBe(true); document.querySelector('[data-action="cancel-filename-template"]').click();
await applyPromise;
manager.updateFilenameTemplateApplyButton('lora', '{model_name}'); expect(getModelApiClient).not.toHaveBeenCalled();
expect(document.getElementById('loraApplyFilenameTemplate').disabled).toBe(false); expect(resetAndReload).not.toHaveBeenCalled();
}); });
it('merges backend download_filename_templates over defaults', () => { it('merges backend download_filename_templates over defaults', () => {
@@ -236,22 +275,10 @@ describe('SettingsManager filename templates', () => {
}); });
}); });
it('shows an info toast and does nothing when applying an empty template', async () => {
appendFilenameTemplateUi();
const manager = createManager();
vi.stubGlobal('confirm', vi.fn(() => true));
await manager.applyFilenameTemplate('lora');
expect(showToast).toHaveBeenCalledWith('settings.filenameTemplates.emptyTemplateInfo', {}, 'info');
expect(getModelApiClient).not.toHaveBeenCalled();
});
it('applies the template through the model API client and reloads', async () => { it('applies the template through the model API client and reloads', async () => {
appendFilenameTemplateUi(); appendFilenameTemplateUi();
state.global.settings.download_filename_templates.lora = '{model_name}'; state.global.settings.download_filename_templates.lora = '{model_name}';
const manager = createManager(); const manager = createManager();
vi.stubGlobal('confirm', vi.fn(() => true));
const apiClient = { applyFilenameTemplate: vi.fn().mockResolvedValue() }; const apiClient = { applyFilenameTemplate: vi.fn().mockResolvedValue() };
getModelApiClient.mockReturnValue(apiClient); getModelApiClient.mockReturnValue(apiClient);
@@ -262,15 +289,26 @@ describe('SettingsManager filename templates', () => {
expect(resetAndReload).toHaveBeenCalledWith(true); expect(resetAndReload).toHaveBeenCalledWith(true);
}); });
it('does not apply when the confirm dialog is declined', async () => { it('shows the apply wording for a non-empty template and honours cancellation', async () => {
appendFilenameTemplateUi(); appendFilenameTemplateUi();
appendConfirmModal();
state.global.settings.download_filename_templates.lora = '{model_name}'; state.global.settings.download_filename_templates.lora = '{model_name}';
const manager = createManager(); const manager = createManager();
vi.stubGlobal('confirm', vi.fn(() => false));
await manager.applyFilenameTemplate('lora'); const applyPromise = manager.applyFilenameTemplate('lora');
const modal = document.getElementById('filenameTemplateConfirmModal');
expect(modal.classList.contains('show')).toBe(true);
expect(modal.querySelector('[data-role="title"]').textContent)
.toBe('Apply filename template to library?');
expect(modal.querySelector('[data-role="message"]').textContent)
.toContain('Rename all existing files');
modal.querySelector('[data-action="cancel-filename-template"]').click();
await applyPromise;
expect(getModelApiClient).not.toHaveBeenCalled(); expect(getModelApiClient).not.toHaveBeenCalled();
expect(resetAndReload).not.toHaveBeenCalled(); expect(resetAndReload).not.toHaveBeenCalled();
expect(modal.classList.contains('show')).toBe(false);
}); });
}); });
+56 -1
View File
@@ -549,12 +549,17 @@ def _make_filename_template_use_case(
scanner: MockScanner, scanner: MockScanner,
lifecycle: StubLifecycleService, lifecycle: StubLifecycleService,
lock_provider: Optional[StubLockProvider] = None, lock_provider: Optional[StubLockProvider] = None,
metadata_loader: Optional[Any] = None,
) -> FilenameTemplateUseCase: ) -> FilenameTemplateUseCase:
kwargs: Dict[str, Any] = {}
if metadata_loader is not None:
kwargs["metadata_loader"] = metadata_loader
return FilenameTemplateUseCase( return FilenameTemplateUseCase(
scanner=scanner, scanner=scanner,
lifecycle_service=lifecycle, # pyright: ignore[reportArgumentType] lifecycle_service=lifecycle, # pyright: ignore[reportArgumentType]
lock_provider=lock_provider or StubLockProvider(), lock_provider=lock_provider or StubLockProvider(),
model_type="lora", model_type="lora",
**kwargs,
) )
@@ -600,7 +605,36 @@ async def test_filename_template_use_case_skips_unchanged_names() -> None:
assert lifecycle.renames == [] assert lifecycle.renames == []
async def test_filename_template_use_case_skips_all_when_template_empty() -> None: async def test_filename_template_use_case_reverts_to_recorded_original_when_template_empty() -> None:
_set_filename_template("")
scanner = MockScanner(cache=MockCache([
_filename_template_model("/library/alpha-renamed.safetensors", "Alpha"),
_filename_template_model("/library/beta.safetensors", "Beta"),
]))
lifecycle = StubLifecycleService()
async def metadata_loader(metadata_path: str) -> Dict[str, Any]:
if metadata_path == "/library/alpha-renamed.metadata.json":
return {"original_file_name": "alpha-original"}
return {}
use_case = _make_filename_template_use_case(
scanner, lifecycle, metadata_loader=metadata_loader
)
result = await use_case.execute(progress_callback=None)
assert result.success_count == 1
assert result.skipped_count == 1
assert lifecycle.renames == [
{
"file_path": "/library/alpha-renamed.safetensors",
"new_file_name": "alpha-original",
}
]
async def test_filename_template_use_case_skips_revert_without_recorded_original() -> None:
_set_filename_template("") _set_filename_template("")
scanner = MockScanner(cache=MockCache([ scanner = MockScanner(cache=MockCache([
_filename_template_model("/library/alpha.safetensors", "Alpha"), _filename_template_model("/library/alpha.safetensors", "Alpha"),
@@ -614,6 +648,27 @@ async def test_filename_template_use_case_skips_all_when_template_empty() -> Non
assert lifecycle.renames == [] assert lifecycle.renames == []
async def test_filename_template_use_case_skips_revert_matching_current_name() -> None:
_set_filename_template("")
scanner = MockScanner(cache=MockCache([
_filename_template_model("/library/alpha.safetensors", "Alpha"),
]))
lifecycle = StubLifecycleService()
async def metadata_loader(metadata_path: str) -> Dict[str, Any]:
return {"original_file_name": "alpha"}
use_case = _make_filename_template_use_case(
scanner, lifecycle, metadata_loader=metadata_loader
)
result = await use_case.execute(progress_callback=None)
assert result.success_count == 0
assert result.skipped_count == 1
assert lifecycle.renames == []
async def test_filename_template_use_case_counts_conflicts_as_failures() -> None: async def test_filename_template_use_case_counts_conflicts_as_failures() -> None:
_set_filename_template("{model_name}") _set_filename_template("{model_name}")
scanner = MockScanner(cache=MockCache([ scanner = MockScanner(cache=MockCache([