diff --git a/static/js/api/apiConfig.js b/static/js/api/apiConfig.js index d64769ba..14c9a04f 100644 --- a/static/js/api/apiConfig.js +++ b/static/js/api/apiConfig.js @@ -29,7 +29,7 @@ export const MODEL_CONFIG = { defaultPageSize: 100, supportsLetterFilter: false, supportsBulkOperations: true, - supportsMove: false, + supportsMove: true, templateName: 'checkpoints.html' }, [MODEL_TYPES.EMBEDDING]: { @@ -63,6 +63,10 @@ export function getApiEndpoints(modelType) { // Bulk operations bulkDelete: `/api/${modelType}/bulk-delete`, + + // Move operations (now common for all model types that support move) + moveModel: `/api/${modelType}/move_model`, + moveBulk: `/api/${modelType}/move_models_bulk`, // CivitAI integration fetchCivitai: `/api/${modelType}/fetch-civitai`, @@ -99,8 +103,6 @@ export const MODEL_SPECIFIC_ENDPOINTS = { previewUrl: `/api/${MODEL_TYPES.LORA}/preview-url`, civitaiUrl: `/api/${MODEL_TYPES.LORA}/civitai-url`, modelDescription: `/api/${MODEL_TYPES.LORA}/model-description`, - moveModel: `/api/${MODEL_TYPES.LORA}/move_model`, - moveBulk: `/api/${MODEL_TYPES.LORA}/move_models_bulk`, getTriggerWordsPost: `/api/${MODEL_TYPES.LORA}/get_trigger_words`, civitaiModelByVersion: `/api/${MODEL_TYPES.LORA}/civitai/model/version`, civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`, diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 89f2ceab..eb60c2eb 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -672,10 +672,105 @@ export class BaseModelApiClient { } async moveSingleModel(filePath, targetPath) { - throw new Error("moveSingleModel must be implemented by subclass"); + // Only allow move if supported + if (!this.apiConfig.config.supportsMove) { + showToast(`Moving ${this.apiConfig.config.displayName}s is not supported`, 'warning'); + return null; + } + if (filePath.substring(0, filePath.lastIndexOf('/')) === targetPath) { + showToast(`${this.apiConfig.config.displayName} is already in the selected folder`, 'info'); + return null; + } + + const response = await fetch(this.apiConfig.endpoints.moveModel, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_path: filePath, + target_path: targetPath + }) + }); + + const result = await response.json(); + + if (!response.ok) { + if (result && result.error) { + throw new Error(result.error); + } + throw new Error(`Failed to move ${this.apiConfig.config.displayName}`); + } + + if (result && result.message) { + showToast(result.message, 'info'); + } else { + showToast(`${this.apiConfig.config.displayName} moved successfully`, 'success'); + } + + if (result.success) { + return result.new_file_path; + } + return null; } async moveBulkModels(filePaths, targetPath) { - throw new Error("moveBulkModels must be implemented by subclass"); + if (!this.apiConfig.config.supportsMove) { + showToast(`Moving ${this.apiConfig.config.displayName}s is not supported`, 'warning'); + return []; + } + const movedPaths = filePaths.filter(path => { + return path.substring(0, path.lastIndexOf('/')) !== targetPath; + }); + + if (movedPaths.length === 0) { + showToast(`All selected ${this.apiConfig.config.displayName}s are already in the target folder`, 'info'); + return []; + } + + const response = await fetch(this.apiConfig.endpoints.moveBulk, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_paths: movedPaths, + target_path: targetPath + }) + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(`Failed to move ${this.apiConfig.config.displayName}s`); + } + + let successFilePaths = []; + if (result.success) { + if (result.failure_count > 0) { + showToast(`Moved ${result.success_count} ${this.apiConfig.config.displayName}s, ${result.failure_count} failed`, 'warning'); + console.log('Move operation results:', result.results); + const failedFiles = result.results + .filter(r => !r.success) + .map(r => { + const fileName = r.path.substring(r.path.lastIndexOf('/') + 1); + return `${fileName}: ${r.message}`; + }); + if (failedFiles.length > 0) { + const failureMessage = failedFiles.length <= 3 + ? failedFiles.join('\n') + : failedFiles.slice(0, 3).join('\n') + `\n(and ${failedFiles.length - 3} more)`; + showToast(`Failed moves:\n${failureMessage}`, 'warning', 6000); + } + } else { + showToast(`Successfully moved ${result.success_count} ${this.apiConfig.config.displayName}s`, 'success'); + } + successFilePaths = result.results + .filter(r => r.success) + .map(r => r.path); + } else { + throw new Error(result.message || `Failed to move ${this.apiConfig.config.displayName}s`); + } + return successFilePaths; } } \ No newline at end of file diff --git a/static/js/api/checkpointApi.js b/static/js/api/checkpointApi.js index 50a6edc0..b7e32542 100644 --- a/static/js/api/checkpointApi.js +++ b/static/js/api/checkpointApi.js @@ -5,22 +5,6 @@ import { showToast } from '../utils/uiHelpers.js'; * Checkpoint-specific API client */ export class CheckpointApiClient extends BaseModelApiClient { - /** - * Checkpoints don't support move operations - */ - async moveSingleModel(filePath, targetPath) { - showToast('Moving checkpoints is not supported', 'warning'); - return null; - } - - /** - * Checkpoints don't support bulk move operations - */ - async moveBulkModels(filePaths, targetPath) { - showToast('Moving checkpoints is not supported', 'warning'); - return []; - } - /** * Get checkpoint information */ diff --git a/static/js/api/embeddingApi.js b/static/js/api/embeddingApi.js index fe9ca363..e266550e 100644 --- a/static/js/api/embeddingApi.js +++ b/static/js/api/embeddingApi.js @@ -5,26 +5,4 @@ import { showToast } from '../utils/uiHelpers.js'; * Embedding-specific API client */ export class EmbeddingApiClient extends BaseModelApiClient { - /** - * Move a single embedding to target path - */ - async moveSingleModel(filePath, targetPath) { - if (filePath.substring(0, filePath.lastIndexOf('/')) === targetPath) { - showToast('Embedding is already in the selected folder', 'info'); - return null; - } - - // TODO: Implement embedding move endpoint when available - showToast('Moving embeddings is not yet implemented', 'info'); - return null; - } - - /** - * Move multiple embeddings to target path - */ - async moveBulkModels(filePaths, targetPath) { - // TODO: Implement embedding bulk move endpoint when available - showToast('Moving embeddings is not yet implemented', 'info'); - return []; - } } diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index eee8d1dc..693fe5bc 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -26,106 +26,6 @@ export class LoraApiClient extends BaseModelApiClient { } } - /** - * Move a single LoRA to target path - */ - async moveSingleModel(filePath, targetPath) { - if (filePath.substring(0, filePath.lastIndexOf('/')) === targetPath) { - showToast('LoRA is already in the selected folder', 'info'); - return null; - } - - const response = await fetch(this.apiConfig.endpoints.specific.moveModel, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_path: filePath, - target_path: targetPath - }) - }); - - const result = await response.json(); - - if (!response.ok) { - if (result && result.error) { - throw new Error(result.error); - } - throw new Error('Failed to move LoRA'); - } - - if (result && result.message) { - showToast(result.message, 'info'); - } else { - showToast('LoRA moved successfully', 'success'); - } - - if (result.success) { - return result.new_file_path; - } - return null; - } - - /** - * Move multiple LoRAs to target path - */ - async moveBulkModels(filePaths, targetPath) { - const movedPaths = filePaths.filter(path => { - return path.substring(0, path.lastIndexOf('/')) !== targetPath; - }); - - if (movedPaths.length === 0) { - showToast('All selected LoRAs are already in the target folder', 'info'); - return []; - } - - const response = await fetch(this.apiConfig.endpoints.specific.moveBulk, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_paths: movedPaths, - target_path: targetPath - }) - }); - - const result = await response.json(); - - if (!response.ok) { - throw new Error('Failed to move LoRAs'); - } - - let successFilePaths = []; - if (result.success) { - if (result.failure_count > 0) { - showToast(`Moved ${result.success_count} LoRAs, ${result.failure_count} failed`, 'warning'); - console.log('Move operation results:', result.results); - const failedFiles = result.results - .filter(r => !r.success) - .map(r => { - const fileName = r.path.substring(r.path.lastIndexOf('/') + 1); - return `${fileName}: ${r.message}`; - }); - if (failedFiles.length > 0) { - const failureMessage = failedFiles.length <= 3 - ? failedFiles.join('\n') - : failedFiles.slice(0, 3).join('\n') + `\n(and ${failedFiles.length - 3} more)`; - showToast(`Failed moves:\n${failureMessage}`, 'warning', 6000); - } - } else { - showToast(`Successfully moved ${result.success_count} LoRAs`, 'success'); - } - successFilePaths = result.results - .filter(r => r.success) - .map(r => r.path); - } else { - throw new Error(result.message || 'Failed to move LoRAs'); - } - return successFilePaths; - } - /** * Get LoRA notes */