diff --git a/locales/de.json b/locales/de.json index 6587ae98..832cb98f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -22,6 +22,7 @@ }, "status": { "loading": "Wird geladen...", + "cancelling": "Abbrechen...", "unknown": "Unbekannt", "date": "Datum", "version": "Version", diff --git a/locales/en.json b/locales/en.json index 376cafc0..35148560 100644 --- a/locales/en.json +++ b/locales/en.json @@ -22,6 +22,7 @@ }, "status": { "loading": "Loading...", + "cancelling": "Cancelling...", "unknown": "Unknown", "date": "Date", "version": "Version", diff --git a/locales/es.json b/locales/es.json index 20ad876f..5b74d23e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -22,6 +22,7 @@ }, "status": { "loading": "Cargando...", + "cancelling": "Cancelando...", "unknown": "Desconocido", "date": "Fecha", "version": "Versión", diff --git a/locales/fr.json b/locales/fr.json index 5314d690..d17a5ab5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -22,6 +22,7 @@ }, "status": { "loading": "Chargement...", + "cancelling": "Annulation...", "unknown": "Inconnu", "date": "Date", "version": "Version", diff --git a/locales/he.json b/locales/he.json index 974e5a5e..40c4dd30 100644 --- a/locales/he.json +++ b/locales/he.json @@ -22,6 +22,7 @@ }, "status": { "loading": "טוען...", + "cancelling": "מבטל...", "unknown": "לא ידוע", "date": "תאריך", "version": "גרסה", diff --git a/locales/ja.json b/locales/ja.json index 1327f481..69be6591 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -22,6 +22,7 @@ }, "status": { "loading": "読み込み中...", + "cancelling": "キャンセル中...", "unknown": "不明", "date": "日付", "version": "バージョン", diff --git a/locales/ko.json b/locales/ko.json index 158e2216..156e9a82 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -22,6 +22,7 @@ }, "status": { "loading": "로딩 중...", + "cancelling": "취소 중...", "unknown": "알 수 없음", "date": "날짜", "version": "버전", diff --git a/locales/ru.json b/locales/ru.json index 608ebca9..4d5c388b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -22,6 +22,7 @@ }, "status": { "loading": "Загрузка...", + "cancelling": "Отмена...", "unknown": "Неизвестно", "date": "Дата", "version": "Версия", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 98959980..a6047357 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -22,6 +22,7 @@ }, "status": { "loading": "加载中...", + "cancelling": "取消中...", "unknown": "未知", "date": "日期", "version": "版本", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 6fec6f57..a29a6ac7 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -22,6 +22,7 @@ }, "status": { "loading": "載入中...", + "cancelling": "取消中...", "unknown": "未知", "date": "日期", "version": "版本", diff --git a/py/services/model_scanner.py b/py/services/model_scanner.py index e2d5e642..5380ae08 100644 --- a/py/services/model_scanner.py +++ b/py/services/model_scanner.py @@ -532,6 +532,13 @@ class ModelScanner: if not scan_result or not getattr(self, '_persistent_cache', None): return + if self.is_cancelled(): + logger.info( + f"{self.model_type.capitalize()} Scanner: Skipping _save_persistent_cache " + "after cancellation" + ) + return + hash_snapshot = self._build_hash_index_snapshot(scan_result.hash_index) loop = asyncio.get_event_loop() try: @@ -705,14 +712,20 @@ class ModelScanner: # Determine the page type based on model type # Scan for new data scan_result = await self._gather_model_data() - await self._apply_scan_result(scan_result) - await self._save_persistent_cache(scan_result) - await self._sync_download_history(scan_result.raw_data, source='scan') + if not self.is_cancelled(): + await self._apply_scan_result(scan_result) + await self._save_persistent_cache(scan_result) + await self._sync_download_history(scan_result.raw_data, source='scan') - logger.info( - f"{self.model_type.capitalize()} Scanner: Cache initialization completed in {time.time() - start_time:.2f} seconds, " - f"found {len(scan_result.raw_data)} models" - ) + logger.info( + f"{self.model_type.capitalize()} Scanner: Cache initialization completed in {time.time() - start_time:.2f} seconds, " + f"found {len(scan_result.raw_data)} models" + ) + else: + logger.info( + f"{self.model_type.capitalize()} Scanner: Cache initialization cancelled " + f"after {time.time() - start_time:.2f} seconds" + ) except Exception as e: logger.error(f"{self.model_type.capitalize()} Scanner: Error initializing cache: {e}") # Ensure cache is at least an empty structure on error @@ -1096,6 +1109,13 @@ class ModelScanner: if scan_result is None: return + if self.is_cancelled(): + logger.info( + f"{self.model_type.capitalize()} Scanner: Skipping _apply_scan_result " + "after cancellation" + ) + return + self._hash_index = scan_result.hash_index self._tags_count = dict(scan_result.tags_count) self._excluded_models = list(scan_result.excluded_models) @@ -1764,6 +1784,13 @@ class ModelScanner: """ if not file_paths or self._cache is None: return False + + if self.is_cancelled(): + logger.info( + f"{self.model_type.capitalize()} Scanner: Skipping cache update " + "after cancelled bulk delete" + ) + return False try: # Get all models that need to be removed from cache diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index ba264d55..e78b3306 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -468,17 +468,21 @@ export class BaseModelApiClient { } async refreshModels(fullRebuild = false) { + const abortController = new AbortController(); try { state.loadingManager.show( `${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${this.apiConfig.config.displayName}s...`, 0 ); - state.loadingManager.showCancelButton(() => this.cancelTask()); + state.loadingManager.showCancelButton(() => { + this.cancelTask(); + abortController.abort(); + }); const url = new URL(this.apiConfig.endpoints.scan, window.location.origin); url.searchParams.append('full_rebuild', fullRebuild); - const response = await fetch(url); + const response = await fetch(url, { signal: abortController.signal }); if (!response.ok) { throw new Error(`Failed to refresh ${this.apiConfig.config.displayName}s: ${response.status} ${response.statusText}`); @@ -494,6 +498,10 @@ export class BaseModelApiClient { showToast('toast.api.refreshComplete', { action: fullRebuild ? 'Full rebuild' : 'Refresh' }, 'success'); } catch (error) { + if (error.name === 'AbortError') { + showToast('toast.api.operationCancelled', {}, 'info'); + return; + } console.error('Refresh failed:', error); showToast('toast.api.refreshFailed', { action: fullRebuild ? 'rebuild' : 'refresh', type: this.apiConfig.config.displayName }, 'error'); } finally { @@ -948,13 +956,19 @@ export class BaseModelApiClient { throw new Error('No model IDs provided'); } + const abortController = new AbortController(); + try { state.loadingManager.show('Checking for updates...', 0); - state.loadingManager.showCancelButton(() => this.cancelTask()); + state.loadingManager.showCancelButton(() => { + this.cancelTask(); + abortController.abort(); + }); const response = await fetch(this.apiConfig.endpoints.refreshUpdates, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + signal: abortController.signal, body: JSON.stringify({ model_ids: modelIds, force @@ -979,6 +993,10 @@ export class BaseModelApiClient { return payload; } catch (error) { + if (error.name === 'AbortError') { + showToast('toast.api.operationCancelled', {}, 'info'); + return null; + } console.error('Error refreshing updates for models:', error); throw error; } finally { @@ -991,13 +1009,19 @@ export class BaseModelApiClient { throw new Error('No folder path provided'); } + const abortController = new AbortController(); + try { state.loadingManager.show('Checking for updates...', 0); - state.loadingManager.showCancelButton(() => this.cancelTask()); + state.loadingManager.showCancelButton(() => { + this.cancelTask(); + abortController.abort(); + }); const response = await fetch(this.apiConfig.endpoints.refreshUpdates, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + signal: abortController.signal, body: JSON.stringify({ folder_path: folderPath, force @@ -1022,6 +1046,10 @@ export class BaseModelApiClient { return payload; } catch (error) { + if (error.name === 'AbortError') { + showToast('toast.api.operationCancelled', {}, 'info'); + return null; + } console.error('Error refreshing updates for folder:', error); throw error; } finally { @@ -1471,15 +1499,21 @@ export class BaseModelApiClient { throw new Error('No file paths provided'); } + const abortController = new AbortController(); + try { state.loadingManager.showSimpleLoading(`Deleting ${this.apiConfig.config.displayName.toLowerCase()}s...`); - state.loadingManager.showCancelButton(() => this.cancelTask()); + state.loadingManager.showCancelButton(() => { + this.cancelTask(); + abortController.abort(); + }); const response = await fetch(this.apiConfig.endpoints.bulkDelete, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + signal: abortController.signal, body: JSON.stringify({ file_paths: filePaths }) @@ -1502,6 +1536,10 @@ export class BaseModelApiClient { throw new Error(result.error || `Failed to delete ${this.apiConfig.config.displayName.toLowerCase()}s`); } } catch (error) { + if (error.name === 'AbortError') { + console.log(`Bulk delete cancelled by user for ${this.apiConfig.config.displayName.toLowerCase()}s`); + return { success: false, cancelled: true }; + } console.error(`Error during bulk delete of ${this.apiConfig.config.displayName.toLowerCase()}s:`, error); throw error; } finally { diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 8fa018f0..04da51ae 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -611,7 +611,9 @@ export class BulkManager { const result = await apiClient.bulkDeleteModels(filePaths); - if (result.success) { + if (result?.cancelled) { + showToast('toast.api.operationCancelled', {}, 'info'); + } else if (result.success) { const currentConfig = this.getCurrentDisplayConfig(); showToast('toast.models.deletedSuccessfully', { count: result.deleted_count, diff --git a/static/js/managers/LoadingManager.js b/static/js/managers/LoadingManager.js index 502902da..1a53d7e1 100644 --- a/static/js/managers/LoadingManager.js +++ b/static/js/managers/LoadingManager.js @@ -73,7 +73,7 @@ export class LoadingManager { if (this.onCancelCallback) { this.onCancelCallback(); this.cancelButton.disabled = true; - this.cancelButton.textContent = translate('common.status.loading', {}, 'Loading...'); + this.cancelButton.textContent = translate('common.status.cancelling', {}, 'Cancelling...'); } }; diff --git a/static/js/utils/updateCheckHelpers.js b/static/js/utils/updateCheckHelpers.js index 92ec02a6..5ecb7175 100644 --- a/static/js/utils/updateCheckHelpers.js +++ b/static/js/utils/updateCheckHelpers.js @@ -42,7 +42,12 @@ export async function performModelUpdateCheck({ onStart, onComplete } = {}) { onStart?.({ displayName, loadingMessage }); state.loadingManager?.showSimpleLoading?.(loadingMessage); - state.loadingManager?.showCancelButton?.(() => apiClient.cancelTask()); + + const abortController = new AbortController(); + state.loadingManager?.showCancelButton?.(() => { + apiClient.cancelTask(); + abortController.abort(); + }); let status = 'success'; let records = []; @@ -52,6 +57,7 @@ export async function performModelUpdateCheck({ onStart, onComplete } = {}) { const response = await fetch(apiConfig.endpoints.refreshUpdates, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + signal: abortController.signal, body: JSON.stringify({ force: false }) }); @@ -81,6 +87,11 @@ export async function performModelUpdateCheck({ onStart, onComplete } = {}) { await resetAndReload(false); } catch (err) { + if (err?.name === 'AbortError') { + showToast('toast.api.operationCancelled', {}, 'info'); + status = 'cancelled'; + return { status: 'cancelled', displayName, records: [], error: null }; + } status = 'error'; error = err instanceof Error ? err : new Error(String(err)); console.error('Error checking model updates:', error); @@ -126,7 +137,12 @@ export async function performFolderUpdateCheck(folderPath, { onComplete } = {}) ); state.loadingManager?.showSimpleLoading?.(loadingMessage); - state.loadingManager?.showCancelButton?.(() => apiClient.cancelTask()); + + const abortController = new AbortController(); + state.loadingManager?.showCancelButton?.(() => { + apiClient.cancelTask(); + abortController.abort(); + }); let status = 'success'; let records = []; @@ -136,6 +152,7 @@ export async function performFolderUpdateCheck(folderPath, { onComplete } = {}) const response = await fetch(apiConfig.endpoints.refreshUpdates, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + signal: abortController.signal, body: JSON.stringify({ folder_path: folderPath, force: false }) }); @@ -165,6 +182,11 @@ export async function performFolderUpdateCheck(folderPath, { onComplete } = {}) await resetAndReload(false); } catch (err) { + if (err?.name === 'AbortError') { + showToast('toast.api.operationCancelled', {}, 'info'); + status = 'cancelled'; + return { status: 'cancelled', records: [], error: null }; + } status = 'error'; error = err instanceof Error ? err : new Error(String(err)); console.error('Error checking folder model updates:', error); diff --git a/tests/frontend/components/contextMenu.interactions.test.js b/tests/frontend/components/contextMenu.interactions.test.js index fb0f6a55..9fcc577e 100644 --- a/tests/frontend/components/contextMenu.interactions.test.js +++ b/tests/frontend/components/contextMenu.interactions.test.js @@ -2190,6 +2190,7 @@ describe('Interaction-level regression coverage', () => { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: false }), + signal: expect.any(AbortSignal), }); const updateResponse = await global.fetch.mock.results[1].value;