From eb0f6dd3b6e64803721307f44b8b75c5911c1a63 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 11 Aug 2026 14:08:55 +0800 Subject: [PATCH] feat(settings): add delete_undo_enabled toggle --- static/js/managers/SettingsManager.js | 6 + static/js/state/index.js | 1 + .../components/modals/settings_modal.html | 16 ++ .../settingsManager.deleteUndo.test.js | 228 ++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 tests/frontend/managers/settingsManager.deleteUndo.test.js diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 70b11958..cd679e25 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -1111,6 +1111,12 @@ 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 4749f497..f1468d99 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -58,6 +58,7 @@ 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/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index df46df75..9a7fdb80 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -811,6 +811,22 @@ +
+
+
+ +
+
+ +
+
+
diff --git a/tests/frontend/managers/settingsManager.deleteUndo.test.js b/tests/frontend/managers/settingsManager.deleteUndo.test.js new file mode 100644 index 00000000..4cc86d8b --- /dev/null +++ b/tests/frontend/managers/settingsManager.deleteUndo.test.js @@ -0,0 +1,228 @@ +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 }), + }); + }); +});