From 82d14f1fb1451cbc2be1633ce5f6a43937b0dbd2 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 7 Nov 2025 13:10:37 +0100 Subject: [PATCH] Add delete script functionality - Add deleteScript method to ScriptDownloaderService (both .ts and .js) - Add deleteScript API endpoint to scripts router - Add delete button to ScriptDetailModal with confirmation modal - Use ConfirmationModal component instead of plain window.confirm - Delete button only shows when script files exist locally - Includes proper error handling and success/error messages --- src/app/_components/ScriptDetailModal.tsx | 91 +++++++++++++++++++++++ src/server/api/routers/scripts.ts | 28 +++++++ src/server/services/scriptDownloader.js | 53 ++++++++++++- src/server/services/scriptDownloader.ts | 53 ++++++++++++- 4 files changed, 223 insertions(+), 2 deletions(-) diff --git a/src/app/_components/ScriptDetailModal.tsx b/src/app/_components/ScriptDetailModal.tsx index 5f3f840..4bd36dd 100644 --- a/src/app/_components/ScriptDetailModal.tsx +++ b/src/app/_components/ScriptDetailModal.tsx @@ -7,6 +7,7 @@ import type { Script } from "~/types/script"; import { DiffViewer } from "./DiffViewer"; import { TextViewer } from "./TextViewer"; import { ExecutionModeModal } from "./ExecutionModeModal"; +import { ConfirmationModal } from "./ConfirmationModal"; import { TypeBadge, UpdateableBadge, PrivilegedBadge, NoteBadge } from "./Badge"; import { Button } from "./ui/button"; import { useRegisterModal } from './modal/ModalStackProvider'; @@ -37,6 +38,8 @@ export function ScriptDetailModal({ const [selectedDiffFile, setSelectedDiffFile] = useState(null); const [textViewerOpen, setTextViewerOpen] = useState(false); const [executionModeOpen, setExecutionModeOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); // Check if script files exist locally const { @@ -83,6 +86,31 @@ export function ScriptDetailModal({ }, }); + // Delete script mutation + const deleteScriptMutation = api.scripts.deleteScript.useMutation({ + onSuccess: (data) => { + setIsDeleting(false); + if (data.success) { + const message = + "message" in data ? data.message : "Script deleted successfully"; + setLoadMessage(`[SUCCESS] ${message}`); + // Refetch script files status and comparison data to update the UI + void refetchScriptFiles(); + void refetchComparison(); + } else { + const error = "error" in data ? data.error : "Failed to delete script"; + setLoadMessage(`[ERROR] ${error}`); + } + // Clear message after 5 seconds + setTimeout(() => setLoadMessage(null), 5000); + }, + onError: (error) => { + setIsDeleting(false); + setLoadMessage(`[ERROR] ${error.message}`); + setTimeout(() => setLoadMessage(null), 5000); + }, + }); + if (!isOpen || !script) return null; const handleImageError = () => { @@ -130,6 +158,19 @@ export function ScriptDetailModal({ setTextViewerOpen(true); }; + const handleDeleteScript = () => { + if (!script) return; + setDeleteConfirmOpen(true); + }; + + const handleConfirmDelete = () => { + if (!script) return; + setDeleteConfirmOpen(false); + setIsDeleting(true); + setLoadMessage(null); + deleteScriptMutation.mutate({ slug: script.slug }); + }; + return (
+ {isDeleting ? ( + <> +
+ Deleting... + + ) : ( + <> + + + + Delete Script + + )} + + )}
{/* Content */} @@ -736,6 +813,20 @@ export function ScriptDetailModal({ onExecute={handleExecuteScript} /> )} + + {/* Delete Confirmation Modal */} + {script && ( + setDeleteConfirmOpen(false)} + onConfirm={handleConfirmDelete} + title="Delete Script" + message={`Are you sure you want to delete all downloaded files for "${script.name}"? This action cannot be undone.`} + variant="simple" + confirmButtonText="Delete" + cancelButtonText="Cancel" + /> + )} ); } diff --git a/src/server/api/routers/scripts.ts b/src/server/api/routers/scripts.ts index 0c394b2..5ee0fbd 100644 --- a/src/server/api/routers/scripts.ts +++ b/src/server/api/routers/scripts.ts @@ -363,6 +363,34 @@ export const scriptsRouter = createTRPCRouter({ } }), + // Delete script files + deleteScript: publicProcedure + .input(z.object({ slug: z.string() })) + .mutation(async ({ input }) => { + try { + // Get the script details + const script = await localScriptsService.getScriptBySlug(input.slug); + if (!script) { + return { + success: false, + error: 'Script not found', + deletedFiles: [] + }; + } + + // Delete the script files + const result = await scriptDownloaderService.deleteScript(script); + return result; + } catch (error) { + console.error('Error in deleteScript:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete script', + deletedFiles: [] + }; + } + }), + // Compare local and remote script content compareScriptContent: publicProcedure .input(z.object({ slug: z.string() })) diff --git a/src/server/services/scriptDownloader.js b/src/server/services/scriptDownloader.js index c596d96..ad49e58 100644 --- a/src/server/services/scriptDownloader.js +++ b/src/server/services/scriptDownloader.js @@ -1,6 +1,6 @@ // Real JavaScript implementation for script downloading import { join } from 'path'; -import { writeFile, mkdir, access, readFile } from 'fs/promises'; +import { writeFile, mkdir, access, readFile, unlink } from 'fs/promises'; export class ScriptDownloaderService { constructor() { @@ -293,6 +293,57 @@ export class ScriptDownloaderService { } } + async deleteScript(script) { + this.initializeConfig(); + const deletedFiles = []; + + try { + // Get the list of files that exist for this script + const fileCheck = await this.checkScriptExists(script); + + if (fileCheck.files.length === 0) { + return { + success: false, + message: 'No script files found to delete', + deletedFiles: [] + }; + } + + // Delete all files + for (const filePath of fileCheck.files) { + try { + const fullPath = join(this.scriptsDirectory, filePath); + await unlink(fullPath); + deletedFiles.push(filePath); + } catch (error) { + // Log error but continue deleting other files + console.error(`Error deleting file ${filePath}:`, error); + } + } + + if (deletedFiles.length === 0) { + return { + success: false, + message: 'Failed to delete any script files', + deletedFiles: [] + }; + } + + return { + success: true, + message: `Successfully deleted ${deletedFiles.length} file(s) for ${script.name}`, + deletedFiles + }; + } catch (error) { + console.error('Error deleting script:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Failed to delete script', + deletedFiles + }; + } + } + async compareScriptContent(script) { this.initializeConfig(); const differences = []; diff --git a/src/server/services/scriptDownloader.ts b/src/server/services/scriptDownloader.ts index 90fb0e7..ff88b99 100644 --- a/src/server/services/scriptDownloader.ts +++ b/src/server/services/scriptDownloader.ts @@ -1,4 +1,4 @@ -import { readFile, writeFile, mkdir } from 'fs/promises'; +import { readFile, writeFile, mkdir, unlink } from 'fs/promises'; import { join } from 'path'; import { env } from '~/env.js'; import type { Script } from '~/types/script'; @@ -461,6 +461,57 @@ export class ScriptDownloaderService { } } + async deleteScript(script: Script): Promise<{ success: boolean; message: string; deletedFiles: string[] }> { + this.initializeConfig(); + const deletedFiles: string[] = []; + + try { + // Get the list of files that exist for this script + const fileCheck = await this.checkScriptExists(script); + + if (fileCheck.files.length === 0) { + return { + success: false, + message: 'No script files found to delete', + deletedFiles: [] + }; + } + + // Delete all files + for (const filePath of fileCheck.files) { + try { + const fullPath = join(this.scriptsDirectory!, filePath); + await unlink(fullPath); + deletedFiles.push(filePath); + } catch (error) { + // Log error but continue deleting other files + console.error(`Error deleting file ${filePath}:`, error); + } + } + + if (deletedFiles.length === 0) { + return { + success: false, + message: 'Failed to delete any script files', + deletedFiles: [] + }; + } + + return { + success: true, + message: `Successfully deleted ${deletedFiles.length} file(s) for ${script.name}`, + deletedFiles + }; + } catch (error) { + console.error('Error deleting script:', error); + return { + success: false, + message: error instanceof Error ? error.message : 'Failed to delete script', + deletedFiles + }; + } + } + async compareScriptContent(script: Script): Promise<{ hasDifferences: boolean; differences: string[] }> { this.initializeConfig(); const differences: string[] = [];