Merge pull request #283 from community-scripts/feat/delete_downloaded_scripts
Add delete script functionality
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }))
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user