Skip to content

Commit 82d14f1

Browse files
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
1 parent 21f723b commit 82d14f1

File tree

4 files changed

+223
-2
lines changed

4 files changed

+223
-2
lines changed

src/app/_components/ScriptDetailModal.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Script } from "~/types/script";
77
import { DiffViewer } from "./DiffViewer";
88
import { TextViewer } from "./TextViewer";
99
import { ExecutionModeModal } from "./ExecutionModeModal";
10+
import { ConfirmationModal } from "./ConfirmationModal";
1011
import { TypeBadge, UpdateableBadge, PrivilegedBadge, NoteBadge } from "./Badge";
1112
import { Button } from "./ui/button";
1213
import { useRegisterModal } from './modal/ModalStackProvider';
@@ -37,6 +38,8 @@ export function ScriptDetailModal({
3738
const [selectedDiffFile, setSelectedDiffFile] = useState<string | null>(null);
3839
const [textViewerOpen, setTextViewerOpen] = useState(false);
3940
const [executionModeOpen, setExecutionModeOpen] = useState(false);
41+
const [isDeleting, setIsDeleting] = useState(false);
42+
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
4043

4144
// Check if script files exist locally
4245
const {
@@ -83,6 +86,31 @@ export function ScriptDetailModal({
8386
},
8487
});
8588

89+
// Delete script mutation
90+
const deleteScriptMutation = api.scripts.deleteScript.useMutation({
91+
onSuccess: (data) => {
92+
setIsDeleting(false);
93+
if (data.success) {
94+
const message =
95+
"message" in data ? data.message : "Script deleted successfully";
96+
setLoadMessage(`[SUCCESS] ${message}`);
97+
// Refetch script files status and comparison data to update the UI
98+
void refetchScriptFiles();
99+
void refetchComparison();
100+
} else {
101+
const error = "error" in data ? data.error : "Failed to delete script";
102+
setLoadMessage(`[ERROR] ${error}`);
103+
}
104+
// Clear message after 5 seconds
105+
setTimeout(() => setLoadMessage(null), 5000);
106+
},
107+
onError: (error) => {
108+
setIsDeleting(false);
109+
setLoadMessage(`[ERROR] ${error.message}`);
110+
setTimeout(() => setLoadMessage(null), 5000);
111+
},
112+
});
113+
86114
if (!isOpen || !script) return null;
87115

88116
const handleImageError = () => {
@@ -130,6 +158,19 @@ export function ScriptDetailModal({
130158
setTextViewerOpen(true);
131159
};
132160

161+
const handleDeleteScript = () => {
162+
if (!script) return;
163+
setDeleteConfirmOpen(true);
164+
};
165+
166+
const handleConfirmDelete = () => {
167+
if (!script) return;
168+
setDeleteConfirmOpen(false);
169+
setIsDeleting(true);
170+
setLoadMessage(null);
171+
deleteScriptMutation.mutate({ slug: script.slug });
172+
};
173+
133174
return (
134175
<div
135176
className="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-sm bg-black/50"
@@ -373,6 +414,42 @@ export function ScriptDetailModal({
373414
);
374415
}
375416
})()}
417+
418+
{/* Delete Button - only show if script files exist */}
419+
{scriptFilesData?.success &&
420+
(scriptFilesData.ctExists || scriptFilesData.installExists) && (
421+
<Button
422+
onClick={handleDeleteScript}
423+
disabled={isDeleting}
424+
variant="destructive"
425+
size="default"
426+
className="w-full sm:w-auto flex items-center justify-center space-x-2"
427+
>
428+
{isDeleting ? (
429+
<>
430+
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-white"></div>
431+
<span>Deleting...</span>
432+
</>
433+
) : (
434+
<>
435+
<svg
436+
className="h-4 w-4"
437+
fill="none"
438+
stroke="currentColor"
439+
viewBox="0 0 24 24"
440+
>
441+
<path
442+
strokeLinecap="round"
443+
strokeLinejoin="round"
444+
strokeWidth={2}
445+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
446+
/>
447+
</svg>
448+
<span>Delete Script</span>
449+
</>
450+
)}
451+
</Button>
452+
)}
376453
</div>
377454

378455
{/* Content */}
@@ -736,6 +813,20 @@ export function ScriptDetailModal({
736813
onExecute={handleExecuteScript}
737814
/>
738815
)}
816+
817+
{/* Delete Confirmation Modal */}
818+
{script && (
819+
<ConfirmationModal
820+
isOpen={deleteConfirmOpen}
821+
onClose={() => setDeleteConfirmOpen(false)}
822+
onConfirm={handleConfirmDelete}
823+
title="Delete Script"
824+
message={`Are you sure you want to delete all downloaded files for "${script.name}"? This action cannot be undone.`}
825+
variant="simple"
826+
confirmButtonText="Delete"
827+
cancelButtonText="Cancel"
828+
/>
829+
)}
739830
</div>
740831
);
741832
}

src/server/api/routers/scripts.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,34 @@ export const scriptsRouter = createTRPCRouter({
363363
}
364364
}),
365365

366+
// Delete script files
367+
deleteScript: publicProcedure
368+
.input(z.object({ slug: z.string() }))
369+
.mutation(async ({ input }) => {
370+
try {
371+
// Get the script details
372+
const script = await localScriptsService.getScriptBySlug(input.slug);
373+
if (!script) {
374+
return {
375+
success: false,
376+
error: 'Script not found',
377+
deletedFiles: []
378+
};
379+
}
380+
381+
// Delete the script files
382+
const result = await scriptDownloaderService.deleteScript(script);
383+
return result;
384+
} catch (error) {
385+
console.error('Error in deleteScript:', error);
386+
return {
387+
success: false,
388+
error: error instanceof Error ? error.message : 'Failed to delete script',
389+
deletedFiles: []
390+
};
391+
}
392+
}),
393+
366394
// Compare local and remote script content
367395
compareScriptContent: publicProcedure
368396
.input(z.object({ slug: z.string() }))

src/server/services/scriptDownloader.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Real JavaScript implementation for script downloading
22
import { join } from 'path';
3-
import { writeFile, mkdir, access, readFile } from 'fs/promises';
3+
import { writeFile, mkdir, access, readFile, unlink } from 'fs/promises';
44

55
export class ScriptDownloaderService {
66
constructor() {
@@ -293,6 +293,57 @@ export class ScriptDownloaderService {
293293
}
294294
}
295295

296+
async deleteScript(script) {
297+
this.initializeConfig();
298+
const deletedFiles = [];
299+
300+
try {
301+
// Get the list of files that exist for this script
302+
const fileCheck = await this.checkScriptExists(script);
303+
304+
if (fileCheck.files.length === 0) {
305+
return {
306+
success: false,
307+
message: 'No script files found to delete',
308+
deletedFiles: []
309+
};
310+
}
311+
312+
// Delete all files
313+
for (const filePath of fileCheck.files) {
314+
try {
315+
const fullPath = join(this.scriptsDirectory, filePath);
316+
await unlink(fullPath);
317+
deletedFiles.push(filePath);
318+
} catch (error) {
319+
// Log error but continue deleting other files
320+
console.error(`Error deleting file ${filePath}:`, error);
321+
}
322+
}
323+
324+
if (deletedFiles.length === 0) {
325+
return {
326+
success: false,
327+
message: 'Failed to delete any script files',
328+
deletedFiles: []
329+
};
330+
}
331+
332+
return {
333+
success: true,
334+
message: `Successfully deleted ${deletedFiles.length} file(s) for ${script.name}`,
335+
deletedFiles
336+
};
337+
} catch (error) {
338+
console.error('Error deleting script:', error);
339+
return {
340+
success: false,
341+
message: error instanceof Error ? error.message : 'Failed to delete script',
342+
deletedFiles
343+
};
344+
}
345+
}
346+
296347
async compareScriptContent(script) {
297348
this.initializeConfig();
298349
const differences = [];

src/server/services/scriptDownloader.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readFile, writeFile, mkdir } from 'fs/promises';
1+
import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
22
import { join } from 'path';
33
import { env } from '~/env.js';
44
import type { Script } from '~/types/script';
@@ -461,6 +461,57 @@ export class ScriptDownloaderService {
461461
}
462462
}
463463

464+
async deleteScript(script: Script): Promise<{ success: boolean; message: string; deletedFiles: string[] }> {
465+
this.initializeConfig();
466+
const deletedFiles: string[] = [];
467+
468+
try {
469+
// Get the list of files that exist for this script
470+
const fileCheck = await this.checkScriptExists(script);
471+
472+
if (fileCheck.files.length === 0) {
473+
return {
474+
success: false,
475+
message: 'No script files found to delete',
476+
deletedFiles: []
477+
};
478+
}
479+
480+
// Delete all files
481+
for (const filePath of fileCheck.files) {
482+
try {
483+
const fullPath = join(this.scriptsDirectory!, filePath);
484+
await unlink(fullPath);
485+
deletedFiles.push(filePath);
486+
} catch (error) {
487+
// Log error but continue deleting other files
488+
console.error(`Error deleting file ${filePath}:`, error);
489+
}
490+
}
491+
492+
if (deletedFiles.length === 0) {
493+
return {
494+
success: false,
495+
message: 'Failed to delete any script files',
496+
deletedFiles: []
497+
};
498+
}
499+
500+
return {
501+
success: true,
502+
message: `Successfully deleted ${deletedFiles.length} file(s) for ${script.name}`,
503+
deletedFiles
504+
};
505+
} catch (error) {
506+
console.error('Error deleting script:', error);
507+
return {
508+
success: false,
509+
message: error instanceof Error ? error.message : 'Failed to delete script',
510+
deletedFiles
511+
};
512+
}
513+
}
514+
464515
async compareScriptContent(script: Script): Promise<{ hasDifferences: boolean; differences: string[] }> {
465516
this.initializeConfig();
466517
const differences: string[] = [];

0 commit comments

Comments
 (0)