From e151a19fcfad8a940c2165421dd623e88523b98c Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Tue, 3 Jun 2025 07:44:52 +0800 Subject: [PATCH] Implement bulk operations for LoRAs: add send to workflow and bulk delete functionality with modal confirmation. --- static/css/components/bulk.css | 12 +++ static/js/managers/BulkManager.js | 128 ++++++++++++++++++++++++++++- static/js/managers/ModalManager.js | 25 +++++- templates/components/controls.html | 6 ++ templates/components/modals.html | 15 ++++ 5 files changed, 182 insertions(+), 4 deletions(-) diff --git a/static/css/components/bulk.css b/static/css/components/bulk.css index ce60a52c..6800ef26 100644 --- a/static/css/components/bulk.css +++ b/static/css/components/bulk.css @@ -60,6 +60,18 @@ border-color: var(--lora-accent); } +/* Danger button style - updated to use proper theme variables */ +.bulk-operations-actions button.danger-btn { + background: oklch(70% 0.2 29); /* Light red background that works in both themes */ + color: oklch(98% 0.01 0); /* Almost white text for good contrast */ + border-color: var(--lora-error); +} + +.bulk-operations-actions button.danger-btn:hover { + background: var(--lora-error); + color: oklch(100% 0 0); /* Pure white text on hover for maximum contrast */ +} + /* Style for selected cards */ .lora-card.selected { box-shadow: 0 0 0 2px var(--lora-accent); diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 7826841d..c69ae888 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -1,6 +1,7 @@ import { state } from '../state/index.js'; -import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js'; import { updateCardsForBulkMode } from '../components/LoraCard.js'; +import { modalManager } from './ModalManager.js'; export class BulkManager { constructor() { @@ -208,6 +209,131 @@ export class BulkManager { await copyToClipboard(loraSyntaxes.join(', '), `Copied ${loraSyntaxes.length} LoRA syntaxes to clipboard`); } + // Add method to send all selected loras to workflow + async sendAllLorasToWorkflow() { + if (state.selectedLoras.size === 0) { + showToast('No LoRAs selected', 'warning'); + return; + } + + const loraSyntaxes = []; + const missingLoras = []; + + // Process all selected loras using our metadata cache + for (const filepath of state.selectedLoras) { + const metadata = state.loraMetadataCache.get(filepath); + + if (metadata) { + const usageTips = JSON.parse(metadata.usageTips || '{}'); + const strength = usageTips.strength || 1; + loraSyntaxes.push(``); + } else { + // If we don't have metadata, this is an error case + missingLoras.push(filepath); + } + } + + // Handle any loras with missing metadata + if (missingLoras.length > 0) { + console.warn('Missing metadata for some selected loras:', missingLoras); + showToast(`Missing data for ${missingLoras.length} LoRAs`, 'warning'); + } + + if (loraSyntaxes.length === 0) { + showToast('No valid LoRAs to send', 'error'); + return; + } + + // Send the loras to the workflow + await sendLoraToWorkflow(loraSyntaxes.join(', '), false, 'lora'); + } + + // Show the bulk delete confirmation modal + showBulkDeleteModal() { + if (state.selectedLoras.size === 0) { + showToast('No LoRAs selected', 'warning'); + return; + } + + // Update the count in the modal + const countElement = document.getElementById('bulkDeleteCount'); + if (countElement) { + countElement.textContent = state.selectedLoras.size; + } + + // Show the modal + modalManager.showModal('bulkDeleteModal'); + } + + // Confirm bulk delete action + async confirmBulkDelete() { + if (state.selectedLoras.size === 0) { + showToast('No LoRAs selected', 'warning'); + modalManager.closeModal('bulkDeleteModal'); + return; + } + + // Close the modal first before showing loading indicator + modalManager.closeModal('bulkDeleteModal'); + + try { + // Show loading indicator + state.loadingManager.showSimpleLoading('Deleting models...'); + + // Gather all file paths for deletion + const filePaths = Array.from(state.selectedLoras); + + // Call the backend API + const response = await fetch('/api/loras/bulk-delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + file_paths: filePaths + }) + }); + + const result = await response.json(); + + if (result.success) { + showToast(`Successfully deleted ${result.deleted_count} models`, 'success'); + + // If virtual scroller exists, update the UI without page reload + if (state.virtualScroller) { + // Remove each deleted item from the virtual scroller + filePaths.forEach(path => { + state.virtualScroller.removeItemByFilePath(path); + }); + + // Clear the selection + this.clearSelection(); + } else { + // Clear the selection + this.clearSelection(); + + // Fall back to page reload for non-virtual scroll mode + setTimeout(() => { + window.location.reload(); + }, 1500); + } + + if (window.modelDuplicatesManager) { + // Update duplicates badge after refresh + window.modelDuplicatesManager.updateDuplicatesBadgeAfterRefresh(); + } + } else { + showToast(`Error: ${result.error || 'Failed to delete models'}`, 'error'); + } + } catch (error) { + console.error('Error during bulk delete:', error); + showToast('Failed to delete models', 'error'); + } finally { + // Hide loading indicator + state.loadingManager.hide(); + } + } + // Create and show the thumbnail strip of selected LoRAs toggleThumbnailStrip() { // If no items are selected, do nothing diff --git a/static/js/managers/ModalManager.js b/static/js/managers/ModalManager.js index eb72ffee..99f00d86 100644 --- a/static/js/managers/ModalManager.js +++ b/static/js/managers/ModalManager.js @@ -195,6 +195,18 @@ export class ModalManager { }); } + // Add bulkDeleteModal registration + const bulkDeleteModal = document.getElementById('bulkDeleteModal'); + if (bulkDeleteModal) { + this.registerModal('bulkDeleteModal', { + element: bulkDeleteModal, + onClose: () => { + this.getModal('bulkDeleteModal').element.classList.remove('show'); + document.body.classList.remove('modal-open'); + } + }); + } + // Set up event listeners for modal toggles const supportToggle = document.getElementById('supportToggleBtn'); if (supportToggle) { @@ -275,10 +287,17 @@ export class ModalManager { // Store current scroll position before showing modal this.scrollPosition = window.scrollY; - if (id === 'deleteModal' || id === 'excludeModal' || id === 'duplicateDeleteModal' || id === 'modelDuplicateDeleteModal' || id === 'clearCacheModal') { - modal.element.classList.add('show'); + if ( + id === "deleteModal" || + id === "excludeModal" || + id === "duplicateDeleteModal" || + id === "modelDuplicateDeleteModal" || + id === "clearCacheModal" || + id === "bulkDeleteModal" + ) { + modal.element.classList.add("show"); } else { - modal.element.style.display = 'block'; + modal.element.style.display = "block"; } modal.isOpen = true; diff --git a/templates/components/controls.html b/templates/components/controls.html index bc396893..15d712bf 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -107,12 +107,18 @@ 0 selected
+ + diff --git a/templates/components/modals.html b/templates/components/modals.html index 5d55c570..5807f132 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -69,6 +69,21 @@
+ + +