Merge pull request #283 from community-scripts/feat/delete_downloaded_scripts

Add delete script functionality
This commit is contained in:
Michel Roegl-Brunner
2025-11-07 13:12:38 +01:00
committed by GitHub
4 changed files with 223 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ import type { Script } from "~/types/script";
import { DiffViewer } from "./DiffViewer"; import { DiffViewer } from "./DiffViewer";
import { TextViewer } from "./TextViewer"; import { TextViewer } from "./TextViewer";
import { ExecutionModeModal } from "./ExecutionModeModal"; import { ExecutionModeModal } from "./ExecutionModeModal";
import { ConfirmationModal } from "./ConfirmationModal";
import { TypeBadge, UpdateableBadge, PrivilegedBadge, NoteBadge } from "./Badge"; import { TypeBadge, UpdateableBadge, PrivilegedBadge, NoteBadge } from "./Badge";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { useRegisterModal } from './modal/ModalStackProvider'; import { useRegisterModal } from './modal/ModalStackProvider';
@@ -37,6 +38,8 @@ export function ScriptDetailModal({
const [selectedDiffFile, setSelectedDiffFile] = useState<string | null>(null); const [selectedDiffFile, setSelectedDiffFile] = useState<string | null>(null);
const [textViewerOpen, setTextViewerOpen] = useState(false); const [textViewerOpen, setTextViewerOpen] = useState(false);
const [executionModeOpen, setExecutionModeOpen] = useState(false); const [executionModeOpen, setExecutionModeOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
// Check if script files exist locally // Check if script files exist locally
const { 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; if (!isOpen || !script) return null;
const handleImageError = () => { const handleImageError = () => {
@@ -130,6 +158,19 @@ export function ScriptDetailModal({
setTextViewerOpen(true); 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 ( return (
<div <div
className="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-sm bg-black/50" 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({
); );
} }
})()} })()}
{/* Delete Button - only show if script files exist */}
{scriptFilesData?.success &&
(scriptFilesData.ctExists || scriptFilesData.installExists) && (
<Button
onClick={handleDeleteScript}
disabled={isDeleting}
variant="destructive"
size="default"
className="w-full sm:w-auto flex items-center justify-center space-x-2"
>
{isDeleting ? (
<>
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-white"></div>
<span>Deleting...</span>
</>
) : (
<>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
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"
/>
</svg>
<span>Delete Script</span>
</>
)}
</Button>
)}
</div> </div>
{/* Content */} {/* Content */}
@@ -736,6 +813,20 @@ export function ScriptDetailModal({
onExecute={handleExecuteScript} onExecute={handleExecuteScript}
/> />
)} )}
{/* Delete Confirmation Modal */}
{script && (
<ConfirmationModal
isOpen={deleteConfirmOpen}
onClose={() => 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"
/>
)}
</div> </div>
); );
} }

View File

@@ -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 // Compare local and remote script content
compareScriptContent: publicProcedure compareScriptContent: publicProcedure
.input(z.object({ slug: z.string() })) .input(z.object({ slug: z.string() }))

View File

@@ -1,6 +1,6 @@
// Real JavaScript implementation for script downloading // Real JavaScript implementation for script downloading
import { join } from 'path'; import { join } from 'path';
import { writeFile, mkdir, access, readFile } from 'fs/promises'; import { writeFile, mkdir, access, readFile, unlink } from 'fs/promises';
export class ScriptDownloaderService { export class ScriptDownloaderService {
constructor() { 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) { async compareScriptContent(script) {
this.initializeConfig(); this.initializeConfig();
const differences = []; const differences = [];

View File

@@ -1,4 +1,4 @@
import { readFile, writeFile, mkdir } from 'fs/promises'; import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import { env } from '~/env.js'; import { env } from '~/env.js';
import type { Script } from '~/types/script'; 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[] }> { async compareScriptContent(script: Script): Promise<{ hasDifferences: boolean; differences: string[] }> {
this.initializeConfig(); this.initializeConfig();
const differences: string[] = []; const differences: string[] = [];