diff --git a/locales/de.json b/locales/de.json index fddefb9b..260b5244 100644 --- a/locales/de.json +++ b/locales/de.json @@ -443,7 +443,6 @@ "label": "Bereits heruntergeladene Modellversionen überspringen", "help": "Wenn aktiviert, überspringt LoRA Manager den Download einer Modellversion, wenn der Download-Verlaufsdienst diese spezifische Version als bereits heruntergeladen erfasst hat. Gilt für alle Download-Abläufe." }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Nach Modell gruppieren", "groupByModelHelp": "Wenn aktiviert, wird nur die neueste Version jedes Civitai-Modells als einzelne Karte angezeigt. Ältere Versionen werden ausgeblendet.", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "Modell löschen", "message": "Sind Sie sicher, dass Sie dieses Modell und alle zugehörigen Dateien löschen möchten?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/en.json b/locales/en.json index 7cbbc15e..125cee81 100644 --- a/locales/en.json +++ b/locales/en.json @@ -443,7 +443,6 @@ "label": "Skip previously downloaded model versions", "help": "When enabled, versions downloaded before will be skipped." }, - "deleteUndoEnabled": "Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Group by Model", "groupByModelHelp": "When enabled, only the latest version of each Civitai model is shown as a single card. Older versions are hidden.", @@ -1281,7 +1280,6 @@ "freesSpace": "Frees {size}", "title": "Delete Model", "message": "Are you sure you want to delete this model and all associated files?", - "permanentWarning": "This will permanently delete the file from disk.", "recoverableWarning": "This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/es.json b/locales/es.json index 5415617e..96aa6d4f 100644 --- a/locales/es.json +++ b/locales/es.json @@ -443,7 +443,6 @@ "label": "Omitir versiones de modelos previamente descargadas", "help": "Cuando está habilitado, LoRA Manager omitirá la descarga de una versión de modelo si el servicio de historial de descargas registra esa versión exacta como ya descargada. Aplica a todos los flujos de descarga." }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Agrupar por modelo", "groupByModelHelp": "Cuando está activado, solo se muestra la versión más reciente de cada modelo de Civitai como una tarjeta única. Las versiones anteriores están ocultas.", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "Eliminar modelo", "message": "¿Estás seguro de que quieres eliminar este modelo y todos los archivos asociados?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/fr.json b/locales/fr.json index 200a84bb..3b28744f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -443,7 +443,6 @@ "label": "Ignorer les versions de modèles précédemment téléchargées", "help": "Lorsque activé, LoRA Manager ignorera le téléchargement d'une version de modèle si le service d'historique des téléchargements enregistre cette version exacte comme déjà téléchargée. S'applique à tous les flux de téléchargement." }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Grouper par modèle", "groupByModelHelp": "Lorsque activé, seule la version la plus récente de chaque modèle Civitai s'affiche sous forme de carte unique. Les versions plus anciennes sont masquées.", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "Supprimer le modèle", "message": "Êtes-vous sûr de vouloir supprimer ce modèle et tous les fichiers associés ?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/he.json b/locales/he.json index 64a8cf2a..c0d22e04 100644 --- a/locales/he.json +++ b/locales/he.json @@ -443,7 +443,6 @@ "label": "דלג על גרסאות מודלים שהורדו בעבר", "help": "כאשר מופעל, LoRA Manager ידלג על הורדת גרסת מודל אם שירות היסטוריית ההורדות רושם את הגרסה המדויקת הזו ככבר שהורדה. חל על כל תהליכי ההורדה." }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "קיבוץ לפי דגם", "groupByModelHelp": "כאשר מופעל, רק הגרסה העדכנית ביותר של כל דגם Civitai מוצגת ככרטיס בודד. גרסאות ישנות יותר מוסתרות.", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "מחק מודל", "message": "האם אתה בטוח שברצונך למחוק מודל זה וכל הקבצים הנלווים?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/ja.json b/locales/ja.json index 36be7466..cc8cda98 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -443,7 +443,6 @@ "label": "以前にダウンロードしたモデルバージョンをスキップ", "help": "有効にすると、ダウンロード履歴サービスがそのバージョンが既にダウンロード済みと記録している場合、LoRA Managerはそのモデルバージョンのダウンロードをスキップします。すべてのダウンロードフローに適用されます。" }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "モデルでグループ化", "groupByModelHelp": "有効にすると、各Civitaiモデルの最新バージョンのみが1枚のカードとして表示され、古いバージョンは非表示になります。", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "モデルを削除", "message": "このモデルと関連するすべてのファイルを削除してもよろしいですか?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/ko.json b/locales/ko.json index b45d100b..fa6df6be 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -443,7 +443,6 @@ "label": "이전에 다운로드한 모델 버전 건너뛰기", "help": "활성화하면 다운로드 기록 서비스가 해당 버전이 이미 다운로드되었음을 기록한 경우 LoRA Manager는 해당 모델 버전 다운로드를 건너뜁니다. 모든 다운로드 플로우에 적용됩니다." }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "모델별 그룹화", "groupByModelHelp": "활성화하면 각 Civitai 모델의 최신 버전만 단일 카드로 표시되며, 이전 버전은 숨겨집니다.", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "모델 삭제", "message": "이 모델과 모든 관련 파일을 삭제하시겠습니까?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/ru.json b/locales/ru.json index 8f15fab8..dd19437b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -443,7 +443,6 @@ "label": "Пропускать ранее загруженные версии моделей", "help": "Если включено, LoRA Manager будет пропускать загрузку версии модели, если сервис истории загрузок записал, что эта конкретная версия уже загружена. Применяется ко всем потокам загрузки." }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Группировать по модели", "groupByModelHelp": "При включении отображается только последняя версия каждой модели Civitai в виде одной карточки. Старые версии скрыты.", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "Удалить модель", "message": "Вы уверены, что хотите удалить эту модель и все связанные файлы?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 656f071c..a34b044c 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -443,7 +443,6 @@ "label": "跳过已下载的模型版本", "help": "启用后,如果下载历史服务记录显示该版本已下载,LoRA Manager 将跳过下载该模型版本。适用于所有下载流程。" }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "按模型分组", "groupByModelHelp": "开启后,每个 Civitai 模型仅显示最新版本的单张卡片,旧版本将被隐藏。", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "删除模型", "message": "你确定要删除此模型及所有相关文件吗?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/locales/zh-TW.json b/locales/zh-TW.json index f128073f..e5fe09c3 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -443,7 +443,6 @@ "label": "跳過已下載的模型版本", "help": "啟用後,如果下載歷史服務記錄顯示該版本已下載,LoRA Manager 將跳過下載該模型版本。適用於所有下載流程。" }, - "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "按模型分組", "groupByModelHelp": "啟用後,每個 Civitai 模型僅顯示最新版本的單張卡片,舊版本將被隱藏。", @@ -1281,7 +1280,6 @@ "freesSpace": "[TODO: Translate] Frees {size}", "title": "刪除模型", "message": "您確定要刪除此模型及所有相關檔案嗎?", - "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { diff --git a/py/services/pending_delete_service.py b/py/services/pending_delete_service.py index dd19c102..1b0dc5ae 100644 --- a/py/services/pending_delete_service.py +++ b/py/services/pending_delete_service.py @@ -39,7 +39,6 @@ from typing import ( from ..utils.constants import PREVIEW_EXTENSIONS from ..utils import settings_paths -from .settings_manager import get_settings_manager logger = logging.getLogger(__name__) @@ -122,16 +121,13 @@ class PendingDeleteService: (``target_dir``), so staging/undo renames stay within one real directory - EXDEV is impossible even when the business path traverses nested symlinks to other volumes. Returns the batch id, or ``None`` - when undo is disabled, the model root cannot be resolved, or staging - failed (caller falls back to a hard delete). + when the model root cannot be resolved, or staging failed (caller + falls back to a hard delete). """ # LOCK-FREE section: opportunistic purge must never run while holding # the ops lock (the lock is not re-entrant). await self._opportunistic_purge() - if not self._undo_enabled(): - return None - async with self._ops_lock: batch_dir: Optional[str] = None staged_pairs: List[Dict[str, Any]] = [] @@ -190,9 +186,10 @@ class PendingDeleteService: # manifest's expires_at at fire time, so stale timers no-op. self._arm_purge_timer(batch_id) logger.info( - "Staged model delete batch %s with %d file(s)", + "Staged model delete batch %s with %d file(s): %s", batch_id, len(staged_pairs), + staged_pairs[0]["original"] if staged_pairs else None, ) return batch_id except OSError as exc: @@ -218,14 +215,11 @@ class PendingDeleteService: ) -> Optional[str]: """Copy a recipe JSON (and, when it exists, its image) into staging. - Returns the batch id, or ``None`` when undo is disabled / staging - failed. Missing or shared preview images are skipped. + Returns the batch id, or ``None`` when staging failed. Missing or + shared preview images are skipped. """ await self._opportunistic_purge() - if not self._undo_enabled(): - return None - async with self._ops_lock: batch_dir: Optional[str] = None staged_pairs: List[Dict[str, Any]] = [] @@ -255,9 +249,10 @@ class PendingDeleteService: self._write_manifest_atomic(batch_dir, manifest) self._arm_purge_timer(batch_id) logger.info( - "Staged recipe delete batch %s with %d file(s)", + "Staged recipe delete batch %s with %d file(s): %s", batch_id, len(staged_pairs), + staged_pairs[0]["original"] if staged_pairs else None, ) return batch_id except OSError as exc: @@ -573,13 +568,6 @@ class PendingDeleteService: except Exception as exc: # defensive - staging/undo must still proceed logger.warning("Opportunistic pending-delete purge failed: %s", exc) - def _undo_enabled(self) -> bool: - try: - return bool(get_settings_manager().get("delete_undo_enabled", True)) - except Exception as exc: # defensive - default to enabled - logger.warning("Failed to read delete_undo_enabled setting: %s", exc) - return True - def _remember_root(self, root: str) -> None: """Record a root the service has staged into (in-process registry).""" if root and root not in self._known_roots: diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index e266739f..86278fe8 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -111,7 +111,6 @@ DEFAULT_SETTINGS: Dict[str, Any] = { "backup_retention_count": 5, "use_new_license_icons": True, "group_by_model": False, - "delete_undo_enabled": True, # AI / LLM provider configuration (BYOK) "llm_provider": "openai", # "openai" | "ollama" | "custom" "llm_api_key": "", diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index e9f561b2..e276d70c 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -203,7 +203,7 @@ export class BaseModelApiClient { } const batchId = data.batch_id || null; if (!batchId) { - // Not staged (undo disabled or staging failed): keep the legacy toast. + // Not staged (staging failed): keep the legacy toast. // When staged, the caller shows the undo action toast instead. showToast('toast.api.deleteSuccess', { type: this.apiConfig.config.displayName }, 'success'); } diff --git a/static/js/components/DuplicatesManager.js b/static/js/components/DuplicatesManager.js index 32acc2fb..63f17be3 100644 --- a/static/js/components/DuplicatesManager.js +++ b/static/js/components/DuplicatesManager.js @@ -1,7 +1,6 @@ // Duplicates Manager Component import { showToast, showActionToast } from '../utils/uiHelpers.js'; import { handleUndoDelete } from '../utils/undoHelpers.js'; -import { armDeleteButton } from '../utils/modalUtils.js'; import { translate } from '../utils/i18nHelpers.js'; import { RecipeCard } from './RecipeCard.js'; import { state, getCurrentPageState } from '../state/index.js'; @@ -449,7 +448,6 @@ export class DuplicatesManager { // Use the modal manager to show the confirmation modal modalManager.showModal('duplicateDeleteModal'); - armDeleteButton(document.getElementById('duplicateDeleteModal')); } catch (error) { console.error('Error preparing delete:', error); showToast('toast.duplicates.deleteError', { message: error.message }, 'error'); diff --git a/static/js/components/ModelDuplicatesManager.js b/static/js/components/ModelDuplicatesManager.js index ebe21a7e..c1fa8758 100644 --- a/static/js/components/ModelDuplicatesManager.js +++ b/static/js/components/ModelDuplicatesManager.js @@ -1,7 +1,6 @@ // Model Duplicates Manager Component for LoRAs and Checkpoints import { showToast, showActionToast } from '../utils/uiHelpers.js'; import { handleUndoDelete } from '../utils/undoHelpers.js'; -import { armDeleteButton } from '../utils/modalUtils.js'; import { translate } from '../utils/i18nHelpers.js'; import { state, getCurrentPageState } from '../state/index.js'; import { formatDate } from '../utils/formatters.js'; @@ -703,7 +702,6 @@ export class ModelDuplicatesManager { // Use the modal manager to show the confirmation modal modalManager.showModal('modelDuplicateDeleteModal'); - armDeleteButton(document.getElementById('modelDuplicateDeleteModal')); } catch (error) { console.error('Error preparing delete:', error); showToast('toast.duplicates.deleteError', { message: error.message }, 'error'); diff --git a/static/js/components/RecipeCard.js b/static/js/components/RecipeCard.js index 780c861d..f6fade33 100644 --- a/static/js/components/RecipeCard.js +++ b/static/js/components/RecipeCard.js @@ -9,7 +9,6 @@ import { bulkManager } from '../managers/BulkManager.js'; import { NSFW_LEVELS, getBaseModelAbbreviation, getMatureBlurThreshold } from '../utils/constants.js'; import { translate } from '../utils/i18nHelpers.js'; import { handleUndoDelete } from '../utils/undoHelpers.js'; -import { armDeleteButton } from '../utils/modalUtils.js'; class RecipeCard { constructor(recipe, clickHandler) { @@ -366,7 +365,7 @@ class RecipeCard {

${this.recipe.title}

-

This action cannot be undone.

+

This action can be undone for 30 seconds.

Note: Deleting this recipe will not affect the LoRA files used in it.

@@ -378,13 +377,8 @@ class RecipeCard { `; // Show the modal with custom content and setup callbacks - let deleteArmTimer = null; modalManager.showModal('deleteModal', deleteModalContent, () => { // This is the onClose callback - if (deleteArmTimer) { - clearTimeout(deleteArmTimer); - deleteArmTimer = null; - } const deleteModal = document.getElementById('deleteModal'); const deleteBtn = deleteModal.querySelector('.delete-btn'); deleteBtn.textContent = 'Delete'; @@ -404,8 +398,6 @@ class RecipeCard { cancelBtn.onclick = () => modalManager.closeModal('deleteModal'); deleteBtn.onclick = () => this.confirmDeleteRecipe(); - deleteArmTimer = armDeleteButton(deleteModal); - } catch (error) { console.error('Error showing delete confirmation:', error); showToast('toast.recipes.deleteConfirmationError', {}, 'error'); diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index ca9c94fe..207b7ea5 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -1,7 +1,6 @@ import { state, getCurrentPageState } from '../state/index.js'; import { showToast, showActionToast, copyToClipboard, sendLoraToWorkflow, sendEmbeddingToWorkflow, buildLoraSyntax, getNSFWLevelName } from '../utils/uiHelpers.js'; import { handleUndoDelete } from '../utils/undoHelpers.js'; -import { armDeleteButton } from '../utils/modalUtils.js'; import { updateCardsForBulkMode } from '../components/shared/ModelCard.js'; import { modalManager } from './ModalManager.js'; import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js'; @@ -630,7 +629,6 @@ export class BulkManager { } modalManager.showModal('bulkDeleteModal'); - armDeleteButton(document.getElementById('bulkDeleteModal')); } async confirmBulkDelete() { diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index cd679e25..70b11958 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -1111,12 +1111,6 @@ export class SettingsManager { includeTriggerWordsCheckbox.checked = state.global.settings.include_trigger_words || false; } - // Set delete undo setting (defaults to enabled) - const deleteUndoEnabledCheckbox = document.getElementById('deleteUndoEnabled'); - if (deleteUndoEnabledCheckbox) { - deleteUndoEnabledCheckbox.checked = state.global.settings.delete_undo_enabled ?? true; - } - // Set lora syntax format const loraSyntaxFormatSelect = document.getElementById('loraSyntaxFormat'); if (loraSyntaxFormatSelect) { diff --git a/static/js/state/index.js b/static/js/state/index.js index f1468d99..4749f497 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -58,7 +58,6 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({ strip_lora_on_copy: false, use_new_license_icons: true, group_by_model: false, - delete_undo_enabled: true, llm_provider: 'openai', llm_api_key: '', llm_api_base: '', diff --git a/static/js/utils/modalUtils.js b/static/js/utils/modalUtils.js index 0b7377ae..45ffb9ec 100644 --- a/static/js/utils/modalUtils.js +++ b/static/js/utils/modalUtils.js @@ -3,31 +3,11 @@ import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js'; import { showActionToast } from './uiHelpers.js'; import { translate } from './i18nHelpers.js'; import { handleUndoDelete } from './undoHelpers.js'; -import { state } from '../state/index.js'; import { formatFileSize } from '../components/shared/utils.js'; -const DELETE_BUTTON_ARM_DELAY_MS = 1500; - let pendingDeletePath = null; let pendingDeleteName = null; let pendingExcludePath = null; -let pendingDeleteArmTimer = null; - -// Delay-activates every delete button inside a delete-confirmation modal so a -// misclick in the first moments after opening cannot confirm the deletion. -// Returns the pending timeout id so callers can cancel it when the modal closes early. -export function armDeleteButton(modalElement, delayMs = DELETE_BUTTON_ARM_DELAY_MS) { - if (!modalElement) return null; - - const deleteButtons = modalElement.querySelectorAll('.delete-btn'); - if (!deleteButtons.length) return null; - - deleteButtons.forEach((button) => { button.disabled = true; }); - - return setTimeout(() => { - deleteButtons.forEach((button) => { button.disabled = false; }); - }, delayMs); -} export function showDeleteModal(filePath) { pendingDeletePath = filePath; @@ -41,10 +21,6 @@ export function showDeleteModal(filePath) { const modal = modalManager.getModal('deleteModal').element; const modelInfo = modal.querySelector('.delete-model-info'); - const undoEnabled = state.global?.settings?.delete_undo_enabled; - const warningKey = undoEnabled - ? 'modals.deleteModel.recoverableWarning' - : 'modals.deleteModel.permanentWarning'; const fileSize = card?.dataset.file_size; const sizeLine = fileSize ? `
${translate('modals.deleteModel.freesSpace', { size: formatFileSize(parseInt(fileSize, 10)) })}` @@ -55,11 +31,10 @@ export function showDeleteModal(filePath) {
File: ${filePath}
- ${translate(warningKey)}${sizeLine} + ${translate('modals.deleteModel.recoverableWarning')}${sizeLine} `; modalManager.showModal('deleteModal'); - pendingDeleteArmTimer = armDeleteButton(modal); } export async function confirmDelete() { @@ -90,10 +65,6 @@ export async function confirmDelete() { export function closeDeleteModal() { modalManager.closeModal('deleteModal'); - if (pendingDeleteArmTimer) { - clearTimeout(pendingDeleteArmTimer); - pendingDeleteArmTimer = null; - } pendingDeletePath = null; pendingDeleteName = null; } diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index 9a7fdb80..df46df75 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -811,22 +811,6 @@ -
-
-
- -
-
- -
-
-
diff --git a/tests/frontend/components/contextMenu.interactions.test.js b/tests/frontend/components/contextMenu.interactions.test.js index 94aff178..a817f816 100644 --- a/tests/frontend/components/contextMenu.interactions.test.js +++ b/tests/frontend/components/contextMenu.interactions.test.js @@ -123,7 +123,6 @@ vi.mock('../../../static/js/state/index.js', () => ({ vi.mock('../../../static/js/utils/modalUtils.js', () => ({ showExcludeModal: vi.fn(), showDeleteModal: vi.fn(), - armDeleteButton: vi.fn(), })); vi.mock('../../../static/js/managers/MoveManager.js', () => ({ diff --git a/tests/frontend/components/duplicatesManager.test.js b/tests/frontend/components/duplicatesManager.test.js index 5e95e425..46a93e72 100644 --- a/tests/frontend/components/duplicatesManager.test.js +++ b/tests/frontend/components/duplicatesManager.test.js @@ -27,16 +27,7 @@ vi.mock('../../../static/js/components/RecipeCard.js', () => ({ }, })); -vi.mock('../../../static/js/utils/modalUtils.js', () => ({ - armDeleteButton: (modalElement) => { - if (!modalElement) return null; - const buttons = modalElement.querySelectorAll('.delete-btn'); - buttons.forEach((button) => { button.disabled = true; }); - return setTimeout(() => { - buttons.forEach((button) => { button.disabled = false; }); - }, 1500); - }, -})); +vi.mock('../../../static/js/utils/modalUtils.js', () => ({})); vi.mock('../../../static/js/utils/infiniteScroll.js', () => ({ recreateVirtualScroll: recreateVirtualScrollMock, @@ -340,41 +331,3 @@ describe('DuplicatesManager confirmDeleteDuplicates undo flows', () => { ); }); }); - -describe('DuplicatesManager deleteSelectedDuplicates delay-activate', () => { - beforeEach(() => { - vi.useFakeTimers(); - setCurrentPageType('recipes'); - setupDom(); - document.body.insertAdjacentHTML('beforeend', ` - - `); - globalThis.modalManager = { showModal: vi.fn(), closeModal: vi.fn() }; - }); - - afterEach(() => { - vi.useRealTimers(); - delete globalThis.modalManager; - }); - - it('opens with the delete button disabled and enables it after 1500ms', async () => { - const manager = new DuplicatesManager({}); - manager.selectedForDeletion.add('r1'); - - await manager.deleteSelectedDuplicates(); - - expect(globalThis.modalManager.showModal).toHaveBeenCalledWith('duplicateDeleteModal'); - const deleteBtn = document.querySelector('#duplicateDeleteModal .delete-btn'); - expect(deleteBtn.disabled).toBe(true); - - deleteBtn.click(); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(1500); - expect(deleteBtn.disabled).toBe(false); - }); -}); diff --git a/tests/frontend/components/modelDuplicatesManager.test.js b/tests/frontend/components/modelDuplicatesManager.test.js index f72e6533..14608d77 100644 --- a/tests/frontend/components/modelDuplicatesManager.test.js +++ b/tests/frontend/components/modelDuplicatesManager.test.js @@ -18,16 +18,7 @@ vi.mock('../../../static/js/api/modelApiFactory.js', () => ({ resetAndReload: resetAndReloadMock, })); -vi.mock('../../../static/js/utils/modalUtils.js', () => ({ - armDeleteButton: (modalElement) => { - if (!modalElement) return null; - const buttons = modalElement.querySelectorAll('.delete-btn'); - buttons.forEach((button) => { button.disabled = true; }); - return setTimeout(() => { - buttons.forEach((button) => { button.disabled = false; }); - }, 1500); - }, -})); +vi.mock('../../../static/js/utils/modalUtils.js', () => ({})); const { ModelDuplicatesManager } = await import('../../../static/js/components/ModelDuplicatesManager.js'); const { state } = await import('../../../static/js/state/index.js'); @@ -365,36 +356,3 @@ describe('ModelDuplicatesManager confirmDeleteDuplicates undo flows', () => { ); }); }); - -describe('ModelDuplicatesManager deleteSelectedDuplicates delay-activate', () => { - beforeEach(() => { - vi.useFakeTimers(); - globalThis.modalManager = { showModal: vi.fn(), closeModal: vi.fn() }; - }); - - afterEach(() => { - vi.useRealTimers(); - delete globalThis.modalManager; - }); - - it('opens with the delete button disabled and enables it after 1500ms', async () => { - const manager = await createManager(); - document.body.insertAdjacentHTML('beforeend', ` - - `); - manager.selectedForDeletion.add(carPath); - - await manager.deleteSelectedDuplicates(); - - expect(globalThis.modalManager.showModal).toHaveBeenCalledWith('modelDuplicateDeleteModal'); - const deleteBtn = document.querySelector('#modelDuplicateDeleteModal .delete-btn'); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(1500); - expect(deleteBtn.disabled).toBe(false); - }); -}); diff --git a/tests/frontend/components/recipeCard.deleteFriction.test.js b/tests/frontend/components/recipeCard.deleteFriction.test.js deleted file mode 100644 index 5104fb86..00000000 --- a/tests/frontend/components/recipeCard.deleteFriction.test.js +++ /dev/null @@ -1,160 +0,0 @@ -import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; - -const { - RECIPE_CARD_MODULE, - UI_HELPERS_MODULE, - RECIPE_API_MODULE, - MODEL_CARD_MODULE, - MODAL_MANAGER_MODULE, - BULK_MANAGER_MODULE, - I18N_MODULE, - UNDO_HELPERS_MODULE, - API_FACTORY_MODULE, - STATE_MODULE, -} = vi.hoisted(() => ({ - RECIPE_CARD_MODULE: new URL('../../../static/js/components/RecipeCard.js', import.meta.url).pathname, - UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname, - RECIPE_API_MODULE: new URL('../../../static/js/api/recipeApi.js', import.meta.url).pathname, - MODEL_CARD_MODULE: new URL('../../../static/js/components/shared/ModelCard.js', import.meta.url).pathname, - MODAL_MANAGER_MODULE: new URL('../../../static/js/managers/ModalManager.js', import.meta.url).pathname, - BULK_MANAGER_MODULE: new URL('../../../static/js/managers/BulkManager.js', import.meta.url).pathname, - I18N_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname, - UNDO_HELPERS_MODULE: new URL('../../../static/js/utils/undoHelpers.js', import.meta.url).pathname, - API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname, - STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname, -})); - -const showModalMock = vi.fn(); -const closeModalMock = vi.fn(); - -vi.mock(UI_HELPERS_MODULE, () => ({ - showToast: vi.fn(), - showActionToast: vi.fn(), - copyToClipboard: vi.fn(), - sendLoraToWorkflow: vi.fn(), -})); - -vi.mock(RECIPE_API_MODULE, () => ({ - updateRecipeMetadata: vi.fn(), -})); - -vi.mock(MODEL_CARD_MODULE, () => ({ - configureModelCardVideo: vi.fn(), -})); - -vi.mock(MODAL_MANAGER_MODULE, () => ({ - modalManager: { - showModal: showModalMock, - closeModal: closeModalMock, - }, -})); - -vi.mock(BULK_MANAGER_MODULE, () => ({ - bulkManager: {}, -})); - -vi.mock(I18N_MODULE, () => ({ - translate: vi.fn((key) => key), -})); - -vi.mock(UNDO_HELPERS_MODULE, () => ({ - handleUndoDelete: vi.fn(), -})); - -// modalUtils.js is intentionally NOT mocked — its real armDeleteButton drives -// the delay-activate behavior under test. Its own imports are mocked below. -vi.mock(API_FACTORY_MODULE, () => ({ - getModelApiClient: vi.fn(), - resetAndReload: vi.fn(), -})); - -describe('RecipeCard delete confirmation delay-activate', () => { - let capturedOnClose; - - beforeEach(async () => { - vi.useFakeTimers(); - showModalMock.mockReset(); - closeModalMock.mockReset(); - capturedOnClose = null; - document.body.innerHTML = ''; - showModalMock.mockImplementation((id, content, onClose) => { - if (content) { - document.getElementById(id).innerHTML = content; - } - capturedOnClose = onClose; - }); - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ success: true }), - }); - window.recipeManager = { loadRecipes: vi.fn() }; - const { state } = await import(STATE_MODULE); - state.virtualScroller = { removeItemByFilePath: vi.fn() }; - }); - - afterEach(() => { - vi.useRealTimers(); - delete global.fetch; - delete window.recipeManager; - document.body.innerHTML = ''; - }); - - async function createCard() { - const { RecipeCard } = await import(RECIPE_CARD_MODULE); - const card = Object.create(RecipeCard.prototype); - card.recipe = { id: 'recipe-1', title: 'My Recipe', file_path: '/recipes/r1.json', file_url: '/preview.png' }; - return card; - } - - it('opens with a disabled delete button that ignores clicks until 1500ms elapse', async () => { - const card = await createCard(); - card.showDeleteConfirmation(); - - const deleteBtn = document.querySelector('#deleteModal .delete-btn'); - expect(deleteBtn.disabled).toBe(true); - - deleteBtn.click(); - expect(global.fetch).not.toHaveBeenCalled(); - - vi.advanceTimersByTime(1500); - expect(deleteBtn.disabled).toBe(false); - - deleteBtn.click(); - expect(global.fetch).toHaveBeenCalledWith( - '/api/lm/recipe/recipe-1', - expect.objectContaining({ method: 'DELETE' }) - ); - }); - - it('clears the pending arm timer when the modal closes during the countdown', async () => { - const card = await createCard(); - card.showDeleteConfirmation(); - - const deleteBtn = document.querySelector('#deleteModal .delete-btn'); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(700); - capturedOnClose(); - - expect(deleteBtn.disabled).toBe(false); - expect(vi.getTimerCount()).toBe(0); - }); - - it('re-arms a full 1500ms countdown when the modal is reopened', async () => { - const card = await createCard(); - card.showDeleteConfirmation(); - - vi.advanceTimersByTime(1400); - capturedOnClose(); - - card.showDeleteConfirmation(); - const deleteBtn = document.querySelector('#deleteModal .delete-btn'); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(1499); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(1); - expect(deleteBtn.disabled).toBe(false); - }); -}); diff --git a/tests/frontend/managers/BulkManager.bulkDelete.test.js b/tests/frontend/managers/BulkManager.bulkDelete.test.js index fc939d16..28734189 100644 --- a/tests/frontend/managers/BulkManager.bulkDelete.test.js +++ b/tests/frontend/managers/BulkManager.bulkDelete.test.js @@ -332,41 +332,3 @@ describe('BulkManager.confirmBulkDelete undo flows', () => { expect(resetAndReloadMock).not.toHaveBeenCalled(); }); }); - -describe('BulkManager.showBulkDeleteModal delay-activate', () => { - beforeEach(() => { - vi.useFakeTimers(); - stateStub.currentPageType = 'loras'; - stateStub.selectedModels.clear(); - stateStub.selectedModels.add('/models/a.safetensors'); - document.body.innerHTML = ` - - `; - }); - - afterEach(() => { - vi.useRealTimers(); - document.body.innerHTML = ''; - }); - - it('opens with the delete button disabled and enables it after 1500ms', async () => { - const { BulkManager } = await import('../../../static/js/managers/BulkManager.js'); - const bulk = new BulkManager(); - bulk.showBulkDeleteModal(); - - const deleteBtn = document.querySelector('#bulkDeleteModal .delete-btn'); - expect(deleteBtn.disabled).toBe(true); - - deleteBtn.click(); - expect(bulkDeleteModelsMock).not.toHaveBeenCalled(); - - vi.advanceTimersByTime(1500); - expect(deleteBtn.disabled).toBe(false); - }); -}); diff --git a/tests/frontend/managers/settingsManager.deleteUndo.test.js b/tests/frontend/managers/settingsManager.deleteUndo.test.js deleted file mode 100644 index 4cc86d8b..00000000 --- a/tests/frontend/managers/settingsManager.deleteUndo.test.js +++ /dev/null @@ -1,228 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, resolve } from 'node:path'; - -vi.mock('../../../static/js/managers/ModalManager.js', () => ({ - modalManager: { - closeModal: vi.fn(), - }, -})); - -vi.mock('../../../static/js/utils/uiHelpers.js', () => ({ - showToast: vi.fn(), -})); - -vi.mock('../../../static/js/state/index.js', () => { - const settings = {}; - return { - state: { - global: { - settings, - }, - loadingManager: { - showSimpleLoading: vi.fn(), - hide: vi.fn(), - }, - }, - createDefaultSettings: () => ({ - language: 'en', - delete_undo_enabled: true, - }), - }; -}); - -vi.mock('../../../static/js/api/modelApiFactory.js', () => ({ - resetAndReload: vi.fn(), -})); - -vi.mock('../../../static/js/utils/constants.js', () => ({ - DOWNLOAD_PATH_TEMPLATES: {}, - DEFAULT_PATH_TEMPLATES: {}, - MAPPABLE_BASE_MODELS: [], - PATH_TEMPLATE_PLACEHOLDERS: {}, - DEFAULT_PRIORITY_TAG_CONFIG: { - lora: 'character, style', - checkpoint: 'base, guide', - embedding: 'hint', - }, - getMappableBaseModelsDynamic: () => [], -})); - -vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({ - translate: (_key, _params, fallback) => fallback ?? '', -})); - -vi.mock('../../../static/js/i18n/index.js', () => ({ - i18n: { - getCurrentLocale: () => 'en', - setLanguage: vi.fn().mockResolvedValue(), - }, -})); - -vi.mock('../../../static/js/components/shared/ModelCard.js', () => ({ - configureModelCardVideo: vi.fn(), -})); - -import { SettingsManager } from '../../../static/js/managers/SettingsManager.js'; -import { showToast } from '../../../static/js/utils/uiHelpers.js'; -import { state } from '../../../static/js/state/index.js'; - -const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); - -const createManager = () => { - state.global.settings = {}; - const initSettingsSpy = vi - .spyOn(SettingsManager.prototype, 'initializeSettings') - .mockResolvedValue(); - const initializeSpy = vi - .spyOn(SettingsManager.prototype, 'initialize') - .mockImplementation(() => {}); - - const manager = new SettingsManager(); - - initSettingsSpy.mockRestore(); - initializeSpy.mockRestore(); - - return manager; -}; - -const appendDeleteUndoCheckbox = () => { - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.id = 'deleteUndoEnabled'; - document.body.appendChild(checkbox); - return checkbox; -}; - -const stubLoadSettingsSubloaders = (manager) => { - vi.spyOn(manager, 'loadMetadataArchiveSettings').mockResolvedValue(); - vi.spyOn(manager, 'loadBackupSettings').mockResolvedValue(); - vi.spyOn(manager, 'loadLibraries').mockResolvedValue(); - vi.spyOn(manager, 'loadLoraRoots').mockResolvedValue(); - vi.spyOn(manager, 'loadCheckpointRoots').mockResolvedValue(); - vi.spyOn(manager, 'loadUnetRoots').mockResolvedValue(); - vi.spyOn(manager, 'loadEmbeddingRoots').mockResolvedValue(); -}; - -beforeEach(() => { - document.body.innerHTML = ''; - vi.clearAllMocks(); -}); - -afterEach(() => { - delete global.fetch; -}); - -describe('SettingsManager delete undo toggle', () => { - it('renders the checkbox markup with a resolvable i18n label', () => { - const template = readFileSync( - resolve(repoRoot, 'templates/components/modals/settings_modal.html'), - 'utf8', - ); - const locales = JSON.parse( - readFileSync(resolve(repoRoot, 'locales/en.json'), 'utf8'), - ); - - // The label must resolve to real text, not fall back to the raw key. - expect(locales.settings.deleteUndoEnabled).toBe( - 'Keep deleted items recoverable for 30 seconds (undo)', - ); - expect(template).toContain('id="deleteUndoEnabled"'); - expect(template).toContain("t('settings.deleteUndoEnabled')"); - expect(template).toContain( - "settingsManager.saveToggleSetting('deleteUndoEnabled', 'delete_undo_enabled')", - ); - }); - - it('restores the checkbox as unchecked when the saved setting is false', async () => { - const manager = createManager(); - const checkbox = appendDeleteUndoCheckbox(); - checkbox.checked = true; - - stubLoadSettingsSubloaders(manager); - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ success: true }), - }); - - state.global.settings = { delete_undo_enabled: false }; - - await manager.loadSettingsToUI(); - - expect(checkbox.checked).toBe(false); - }); - - it('restores the checkbox as checked when the saved setting is true or absent', async () => { - const manager = createManager(); - const checkbox = appendDeleteUndoCheckbox(); - - stubLoadSettingsSubloaders(manager); - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ success: true }), - }); - - state.global.settings = { delete_undo_enabled: true }; - await manager.loadSettingsToUI(); - expect(checkbox.checked).toBe(true); - - checkbox.checked = false; - state.global.settings = {}; - await manager.loadSettingsToUI(); - expect(checkbox.checked).toBe(true); - }); - - it('saves delete_undo_enabled to the backend when the checkbox is toggled', async () => { - const manager = createManager(); - const checkbox = appendDeleteUndoCheckbox(); - checkbox.checked = false; - - state.global.settings = { delete_undo_enabled: true }; - - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ success: true }), - }); - - await manager.saveToggleSetting('deleteUndoEnabled', 'delete_undo_enabled'); - - expect(state.global.settings.delete_undo_enabled).toBe(false); - expect(global.fetch).toHaveBeenCalledWith('/api/lm/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ delete_undo_enabled: false }), - }); - expect(showToast).toHaveBeenCalledWith( - 'toast.settings.settingsUpdated', - { setting: 'delete undo enabled' }, - 'success', - ); - }); - - it('saves delete_undo_enabled as true when re-enabled', async () => { - const manager = createManager(); - const checkbox = appendDeleteUndoCheckbox(); - checkbox.checked = true; - - state.global.settings = { delete_undo_enabled: false }; - - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ success: true }), - }); - - await manager.saveToggleSetting('deleteUndoEnabled', 'delete_undo_enabled'); - - expect(state.global.settings.delete_undo_enabled).toBe(true); - expect(global.fetch).toHaveBeenCalledWith('/api/lm/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ delete_undo_enabled: true }), - }); - }); -}); diff --git a/tests/frontend/pages/checkpointsPage.test.js b/tests/frontend/pages/checkpointsPage.test.js index d5b0462f..ea982e57 100644 --- a/tests/frontend/pages/checkpointsPage.test.js +++ b/tests/frontend/pages/checkpointsPage.test.js @@ -34,7 +34,6 @@ vi.mock('../../../static/js/utils/modalUtils.js', () => ({ closeDeleteModal: closeDeleteModalMock, confirmExclude: confirmExcludeMock, closeExcludeModal: closeExcludeModalMock, - armDeleteButton: vi.fn(), })); vi.mock('../../../static/js/components/ModelDuplicatesManager.js', () => ({ diff --git a/tests/frontend/pages/embeddingsPage.test.js b/tests/frontend/pages/embeddingsPage.test.js index 02bc12dd..4d0f754f 100644 --- a/tests/frontend/pages/embeddingsPage.test.js +++ b/tests/frontend/pages/embeddingsPage.test.js @@ -26,7 +26,6 @@ vi.mock('../../../static/js/utils/modalUtils.js', () => ({ closeDeleteModal: closeDeleteModalMock, confirmExclude: confirmExcludeMock, closeExcludeModal: closeExcludeModalMock, - armDeleteButton: vi.fn(), })); vi.mock('../../../static/js/api/apiConfig.js', () => ({ diff --git a/tests/frontend/pages/lorasPage.test.js b/tests/frontend/pages/lorasPage.test.js index 02a6637e..1768fa35 100644 --- a/tests/frontend/pages/lorasPage.test.js +++ b/tests/frontend/pages/lorasPage.test.js @@ -36,7 +36,6 @@ vi.mock('../../../static/js/utils/modalUtils.js', () => ({ closeDeleteModal: closeDeleteModalMock, confirmExclude: confirmExcludeMock, closeExcludeModal: closeExcludeModalMock, - armDeleteButton: vi.fn(), })); vi.mock('../../../static/js/components/ModelDuplicatesManager.js', () => ({ diff --git a/tests/frontend/utils/i18nHelpers.test.js b/tests/frontend/utils/i18nHelpers.test.js index d208aa78..466904fc 100644 --- a/tests/frontend/utils/i18nHelpers.test.js +++ b/tests/frontend/utils/i18nHelpers.test.js @@ -66,9 +66,6 @@ describe('translate() with real en.json locale', () => { it('resolves delete-model modal keys with interpolation', () => { installWindowI18n(); - expect(translate('modals.deleteModel.permanentWarning')).toBe( - 'This will permanently delete the file from disk.', - ); expect(translate('modals.deleteModel.recoverableWarning')).toBe( 'This will permanently delete the file after 30 seconds unless you undo.', ); @@ -77,14 +74,6 @@ describe('translate() with real en.json locale', () => { ); }); - it('resolves the delete-undo settings label', () => { - installWindowI18n(); - - expect(translate('settings.deleteUndoEnabled')).toBe( - 'Keep deleted items recoverable for 30 seconds (undo)', - ); - }); - it('returns the raw key when no translation exists (fallback contract)', () => { installWindowI18n(); const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); diff --git a/tests/frontend/utils/modalUtils.test.js b/tests/frontend/utils/modalUtils.test.js index b1cab3da..7308b6fa 100644 --- a/tests/frontend/utils/modalUtils.test.js +++ b/tests/frontend/utils/modalUtils.test.js @@ -1,4 +1,4 @@ -import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; +import { describe, it, beforeEach, expect, vi } from 'vitest'; const { MODAL_UTILS_MODULE, @@ -7,7 +7,6 @@ const { UI_HELPERS_MODULE, I18N_MODULE, UNDO_HELPERS_MODULE, - STATE_MODULE, } = vi.hoisted(() => ({ MODAL_UTILS_MODULE: new URL('../../../static/js/utils/modalUtils.js', import.meta.url).pathname, MODAL_MANAGER_MODULE: new URL('../../../static/js/managers/ModalManager.js', import.meta.url).pathname, @@ -15,7 +14,6 @@ const { UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname, I18N_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname, UNDO_HELPERS_MODULE: new URL('../../../static/js/utils/undoHelpers.js', import.meta.url).pathname, - STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname, })); const deleteModelMock = vi.fn(); @@ -136,75 +134,6 @@ describe('modalUtils confirmDelete undo flow', () => { }); }); -describe('modalUtils armDeleteButton delay-activate', () => { - beforeEach(() => { - vi.useFakeTimers(); - deleteModelMock.mockReset(); - showModalMock.mockReset(); - closeModalMock.mockReset(); - document.body.innerHTML = ` -
-
-
- - -
- `; - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('opens with the delete button disabled and enables it after exactly 1500ms', async () => { - const { showDeleteModal } = await import(MODAL_UTILS_MODULE); - - showDeleteModal('/models/foo.safetensors'); - - const deleteBtn = document.querySelector('#deleteModal .delete-btn'); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(1499); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(1); - expect(deleteBtn.disabled).toBe(false); - }); - - it('clicking the disabled delete button fires nothing', async () => { - const { showDeleteModal } = await import(MODAL_UTILS_MODULE); - - showDeleteModal('/models/foo.safetensors'); - - const deleteBtn = document.querySelector('#deleteModal .delete-btn'); - deleteBtn.click(); - - expect(deleteBtn.disabled).toBe(true); - expect(deleteModelMock).not.toHaveBeenCalled(); - }); - - it('closing during the countdown clears the timer and reopening re-arms a full 1500ms', async () => { - const { showDeleteModal, closeDeleteModal } = await import(MODAL_UTILS_MODULE); - - showDeleteModal('/models/foo.safetensors'); - const deleteBtn = document.querySelector('#deleteModal .delete-btn'); - - vi.advanceTimersByTime(1400); - closeDeleteModal(); - expect(closeModalMock).toHaveBeenCalledWith('deleteModal'); - - // Reopen — the stale timer must not enable the button early - showDeleteModal('/models/foo.safetensors'); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(1499); - expect(deleteBtn.disabled).toBe(true); - - vi.advanceTimersByTime(1); - expect(deleteBtn.disabled).toBe(false); - }); -}); - describe('modalUtils showDeleteModal warning copy and size line', () => { beforeEach(() => { showModalMock.mockReset(); @@ -220,19 +149,11 @@ describe('modalUtils showDeleteModal warning copy and size line', () => { `; }); - afterEach(async () => { - const { state } = await import(STATE_MODULE); - state.global.settings.delete_undo_enabled = true; - }); - function modelInfoHtml() { return document.querySelector('#deleteModal .delete-model-info').innerHTML; } - it('shows the recoverable warning when delete_undo_enabled is truthy', async () => { - const { state } = await import(STATE_MODULE); - state.global.settings.delete_undo_enabled = true; - + it('always shows the recoverable warning', async () => { const { showDeleteModal } = await import(MODAL_UTILS_MODULE); showDeleteModal('/models/foo.safetensors'); @@ -240,27 +161,6 @@ describe('modalUtils showDeleteModal warning copy and size line', () => { expect(modelInfoHtml()).not.toContain('modals.deleteModel.permanentWarning'); }); - it('shows the permanent warning when delete_undo_enabled is falsy', async () => { - const { state } = await import(STATE_MODULE); - state.global.settings.delete_undo_enabled = false; - - const { showDeleteModal } = await import(MODAL_UTILS_MODULE); - showDeleteModal('/models/foo.safetensors'); - - expect(modelInfoHtml()).toContain('modals.deleteModel.permanentWarning'); - expect(modelInfoHtml()).not.toContain('modals.deleteModel.recoverableWarning'); - }); - - it('falls back to the neutral permanent warning when the setting is unavailable', async () => { - const { state } = await import(STATE_MODULE); - delete state.global.settings.delete_undo_enabled; - - const { showDeleteModal } = await import(MODAL_UTILS_MODULE); - showDeleteModal('/models/foo.safetensors'); - - expect(modelInfoHtml()).toContain('modals.deleteModel.permanentWarning'); - }); - it('appends a formatted "Frees {size}" line when the card carries a file size', async () => { translateMock.mockImplementation((key, params) => params && params.size ? `${key} ${params.size}` : key diff --git a/tests/services/test_model_lifecycle_service.py b/tests/services/test_model_lifecycle_service.py index 2c37b3b5..ecf13ad6 100644 --- a/tests/services/test_model_lifecycle_service.py +++ b/tests/services/test_model_lifecycle_service.py @@ -10,7 +10,6 @@ import pytest from py.services.model_lifecycle_service import ModelLifecycleService, _require_path_in_library_roots from py.services.pending_delete_service import PENDING_DELETE_DIR_NAME, _reset_pending_delete_service -from py.services.settings_manager import get_settings_manager from py.utils.metadata_manager import MetadataManager from py.utils.models import LoraMetadata @@ -953,11 +952,11 @@ def _make_delete_service(scanner: Any) -> ModelLifecycleService: @pytest.mark.asyncio -async def test_delete_model_stages_file_when_undo_enabled(tmp_path: Path): - """Undo enabled (the default): artifacts are renamed into a - ``.lm-pending-delete//`` staging dir under the model root, the - response carries the batch_id, the cache entry is removed and the cache - is persisted (``_persist_calls`` tracked by ``ScannerForDelete``).""" +async def test_delete_model_stages_file(tmp_path: Path): + """Artifacts are renamed into a ``.lm-pending-delete//`` staging + dir under the model root, the response carries the batch_id, the cache + entry is removed and the cache is persisted (``_persist_calls`` tracked by + ``ScannerForDelete``).""" root = tmp_path / "loras" root.mkdir() model_path = root / "model.safetensors" @@ -999,41 +998,6 @@ async def test_delete_model_stages_file_when_undo_enabled(tmp_path: Path): assert scanner._persist_calls == [True] -@pytest.mark.asyncio -async def test_delete_model_hard_deletes_when_undo_disabled( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -): - """delete_undo_enabled=false: old os.remove behavior, batch_id is None and - no staging directory is ever created.""" - root = tmp_path / "loras" - root.mkdir() - model_path = root / "model.safetensors" - model_path.write_bytes(b"content") - - settings_manager = get_settings_manager() - monkeypatch.setattr( - settings_manager, - "get", - lambda key, default=None: False - if key == "delete_undo_enabled" - else default, - ) - - scanner = ScannerForDelete( - raw_data=[{"file_path": str(model_path)}], - roots=[str(root)], - ) - service = _make_delete_service(scanner) - - result = await service.delete_model(str(model_path)) - - assert result["success"] is True - assert result["batch_id"] is None - assert result["deleted_files"] - assert not model_path.exists() - assert not (root / PENDING_DELETE_DIR_NAME).exists() - - @pytest.mark.asyncio async def test_delete_model_falls_back_when_staging_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch diff --git a/tests/services/test_model_scanner.py b/tests/services/test_model_scanner.py index 4fdd71ec..3d93145f 100644 --- a/tests/services/test_model_scanner.py +++ b/tests/services/test_model_scanner.py @@ -22,7 +22,6 @@ from py.services.pending_delete_service import ( _reset_pending_delete_service, ) from py.services.persistent_model_cache import PersistentModelCache, DEFAULT_LICENSE_FLAGS -from py.services.settings_manager import get_settings_manager from py.utils.civitai_utils import build_license_flags from py.utils.models import BaseModelMetadata @@ -1166,33 +1165,6 @@ async def test_bulk_delete_merge_failure_falls_back_to_batch_ids( assert sorted(staged_files) == ["one.txt", "two.txt"] -@pytest.mark.asyncio -async def test_bulk_delete_undo_disabled_hard_deletes(tmp_path: Path): - """delete_undo_enabled=false -> old hard delete, no batch, no staging dirs.""" - root = tmp_path / "loras" - root.mkdir() - first = root / "one.txt" - first.write_text("one", encoding="utf-8") - second = root / "two.txt" - second.write_text("two", encoding="utf-8") - scanner = _make_bulk_scanner(root, [first, second]) - - get_settings_manager().settings["delete_undo_enabled"] = False - - result = await scanner.bulk_delete_models([str(first), str(second)]) - - assert result["success"] is True - assert result["status"] == "success" - assert result["total_deleted"] == 2 - assert result.get("batch_id") is None - assert "batch_ids" not in result - - # Old hard-delete behavior: files removed, zero staging dirs created. - assert not first.exists() - assert not second.exists() - assert not (root / PENDING_DELETE_DIR_NAME).exists() - - @pytest.mark.asyncio async def test_bulk_delete_cancelled_after_one_staged_batch_present( tmp_path: Path, monkeypatch diff --git a/tests/services/test_pending_delete_service.py b/tests/services/test_pending_delete_service.py index d9daa9f1..ba23bdd7 100644 --- a/tests/services/test_pending_delete_service.py +++ b/tests/services/test_pending_delete_service.py @@ -31,7 +31,6 @@ from py.services.pending_delete_service import ( ) from py.services.model_hash_index import ModelHashIndex from py.services.model_scanner import ModelScanner -from py.services.settings_manager import DEFAULT_SETTINGS, get_settings_manager from py.utils import settings_paths from py.utils.models import LoraMetadata @@ -771,32 +770,6 @@ async def test_l2_merge_basename_collision_aborts_without_dropping_files( assert (sub_b / "model.safetensors").read_bytes() == b"model-data" -# --------------------------------------------------------------------------- -# (m) delete_undo_enabled=false -> stage returns None, nothing created -# --------------------------------------------------------------------------- -async def test_m_undo_disabled_returns_none(tmp_path: Path) -> None: - root = tmp_path / "loras" - root.mkdir() - model = root / "model.safetensors" - model.write_bytes(b"data") - - get_settings_manager().settings["delete_undo_enabled"] = False - - service = await PendingDeleteService.get_instance() - batch_id = await service.stage_model_delete( - scanner=ScannerForStage([root]), - target_dir=str(root), - file_name="model", - main_extension=".safetensors", - original_file_path=str(model), - cached_entry=None, - ) - - assert batch_id is None - assert model.exists() - assert not (root / PENDING_DELETE_DIR_NAME).exists() - - # --------------------------------------------------------------------------- # (n) simulated OSError during staging -> rollback, no orphaned batch dir # --------------------------------------------------------------------------- @@ -839,13 +812,6 @@ async def test_n_staging_oserror_rolls_back(tmp_path: Path, monkeypatch) -> None assert not any(staging.iterdir()) -# --------------------------------------------------------------------------- -# (o) DEFAULT_SETTINGS contains delete_undo_enabled=True -# --------------------------------------------------------------------------- -def test_o_default_settings_contains_undo_enabled() -> None: - assert DEFAULT_SETTINGS.get("delete_undo_enabled") is True - - # --------------------------------------------------------------------------- # (p) SCANNER EXCLUSION # --------------------------------------------------------------------------- diff --git a/tests/services/test_recipe_persistence.py b/tests/services/test_recipe_persistence.py index 82f79b2d..3b2bca23 100644 --- a/tests/services/test_recipe_persistence.py +++ b/tests/services/test_recipe_persistence.py @@ -31,7 +31,6 @@ from py.services.recipes.persistence_service import ( PersistenceResult, RecipePersistenceService, ) -from py.services.settings_manager import get_settings_manager from py.utils import settings_paths @@ -223,29 +222,6 @@ async def test_delete_recipe_skips_missing_preview_image(tmp_path: Path) -> None assert scanner.removed == ["r2"] -# --------------------------------------------------------------------------- -# (3) undo disabled -> no staging, payload batch_id None, existing behavior -# --------------------------------------------------------------------------- -async def test_delete_recipe_undo_disabled_no_staging(tmp_path: Path) -> None: - get_settings_manager().settings["delete_undo_enabled"] = False - - scanner = RecipeScannerStub(tmp_path) - json_path, image_path, _recipe_data = _write_recipe(tmp_path, "r3") - scanner.register_recipe("r3", json_path) - - result = await _make_service().delete_recipe( - recipe_scanner=scanner, recipe_id="r3" - ) - - assert result.payload["batch_id"] is None - # No staging leftovers when undo is disabled. - assert not _staging_parent().exists() - # Existing hard delete behavior unchanged. - assert not json_path.exists() - assert not image_path.exists() - assert scanner.removed == ["r3"] - - # --------------------------------------------------------------------------- # (4) bulk_delete with 2 ids -> single batch_id, one batch dir with both # recipes, re-anchored expires_at in the merged manifest diff --git a/tests/services/test_settings_manager.py b/tests/services/test_settings_manager.py index dadaab47..3597b189 100644 --- a/tests/services/test_settings_manager.py +++ b/tests/services/test_settings_manager.py @@ -1178,8 +1178,3 @@ def test_skip_previously_downloaded_model_versions_coerces_string_input(manager) assert manager.get_skip_previously_downloaded_model_versions() is True assert manager.settings["skip_previously_downloaded_model_versions"] is True - - -def test_delete_undo_enabled_defaults_true(manager): - assert settings_manager_module.DEFAULT_SETTINGS.get("delete_undo_enabled") is True - assert manager.get("delete_undo_enabled") is True