mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-14 09:43:22 -03:00
refactor(delete): make undo unconditional, remove undo toggle and button delay
- Remove delete_undo_enabled setting (backend default, frontend state, settings modal UI, 10 locales); staged deletes with 30s undo are now the only delete path and stale settings keys are silently ignored - Remove the 1500ms delete-button arm delay (armDeleteButton) from all delete modals; misclicks are recoverable via the undo toast - Delete modal always shows the recoverable warning - Log the first staged file path in staging log lines for easier support
This commit is contained in:
@@ -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', () => ({
|
||||
|
||||
@@ -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', `
|
||||
<div id="duplicateDeleteModal" class="modal delete-modal">
|
||||
<div class="delete-model-info"><p><span id="duplicateDeleteCount">0</span></p></div>
|
||||
<button class="cancel-btn">Cancel</button>
|
||||
<button class="delete-btn">Delete</button>
|
||||
</div>
|
||||
`);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', `
|
||||
<div id="modelDuplicateDeleteModal" class="modal delete-modal">
|
||||
<div class="delete-model-info"><p><span id="modelDuplicateDeleteCount">0</span></p></div>
|
||||
<button class="cancel-btn">Cancel</button>
|
||||
<button class="delete-btn">Delete</button>
|
||||
</div>
|
||||
`);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = '<div id="deleteModal" class="modal delete-modal"></div>';
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user