feat(web): add ability to delete unused attachments (#5272)

This commit is contained in:
Richard Szegh 2025-11-24 04:55:53 +01:00 committed by GitHub
parent 60d977c0bf
commit 455eef9fa3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 4 deletions

View file

@ -231,6 +231,8 @@
"delete-selected-resources": "Delete Selected Resources",
"delete-all-unused": "Delete all unused",
"delete-all-unused-confirm": "Are you sure you want to delete all unused resources? THIS ACTION IS IRREVERSIBLE",
"delete-all-unused-success": "Resources deleted successfully",
"delete-all-unused-error": "Failed to delete unused resources",
"fetching-data": "Fetching data…",
"file-drag-drop-prompt": "Drag and drop your file here to upload file",
"linked-amount": "Linked amount",

View file

@ -1,19 +1,22 @@
import dayjs from "dayjs";
import { includes } from "lodash-es";
import { PaperclipIcon, SearchIcon } from "lucide-react";
import { PaperclipIcon, SearchIcon, Trash } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import AttachmentIcon from "@/components/AttachmentIcon";
import ConfirmDialog from "@/components/ConfirmDialog";
import Empty from "@/components/Empty";
import MobileHeader from "@/components/MobileHeader";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { attachmentServiceClient } from "@/grpcweb";
import useDialog from "@/hooks/useDialog";
import useLoading from "@/hooks/useLoading";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import i18n from "@/i18n";
import { attachmentStore } from "@/store";
import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { useTranslate } from "@/utils/i18n";
@ -39,6 +42,7 @@ const Attachments = observer(() => {
const t = useTranslate();
const { md } = useResponsiveWidth();
const loadingState = useLoading();
const deleteUnusedAttachmentsDialog = useDialog();
const [state, setState] = useState<State>({
searchQuery: "",
});
@ -88,6 +92,37 @@ const Attachments = observer(() => {
}
};
const handleRefetch = async () => {
try {
loadingState.setLoading();
const { attachments: fetchedAttachments, nextPageToken } = await attachmentServiceClient.listAttachments({
pageSize: 50,
});
setAttachments(fetchedAttachments);
setNextPageToken(nextPageToken ?? "");
loadingState.setFinish();
} catch (error) {
console.error(error);
loadingState.setError();
}
};
const handleDeleteUnusedAttachments = async () => {
try {
await Promise.all(
unusedAttachments.map((attachment) => {
return attachmentStore.deleteAttachment(attachment.name);
}),
);
toast.success(t("resource.delete-all-unused-success"));
} catch (error) {
console.error(error);
toast.error(t("resource.delete-all-unused-error"));
} finally {
void handleRefetch();
}
};
return (
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
{!md && <MobileHeader />}
@ -158,9 +193,17 @@ const Attachments = observer(() => {
<div className="w-full flex flex-row justify-start items-start">
<div className="w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"></div>
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
<div className="w-full flex flex-row justify-start items-center gap-2">
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
<div className="w-full flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div className="flex flex-row items-center gap-2">
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
</div>
<div>
<Button variant="destructive" onClick={() => deleteUnusedAttachmentsDialog.open()} size="sm">
<Trash />
{t("resource.delete-all-unused")}
</Button>
</div>
</div>
{unusedAttachments.map((attachment) => {
return (
@ -193,6 +236,16 @@ const Attachments = observer(() => {
</div>
</div>
</div>
<ConfirmDialog
open={deleteUnusedAttachmentsDialog.isOpen}
onOpenChange={deleteUnusedAttachmentsDialog.setOpen}
title={t("resource.delete-all-unused-confirm")}
confirmLabel={t("common.delete")}
cancelLabel={t("common.cancel")}
onConfirm={handleDeleteUnusedAttachments}
confirmVariant="destructive"
/>
</section>
);
});