bulk delete function (#1034)

- in the library/gallery modal, added a checkbox on top left of each
item
- when checkbox is checked it enters toiggle select mode which allows
multi selecting and bulk deleting objects via one delete button on the
bottom right of the modal.
- added trash can icon to lightbox delete button
This commit is contained in:
BFlat authored and GitHub committed 2026-02-10 23:52:39 +07:00
1 parent 98969a660e
commit 8e4ad8b3a1
3 files changed
+321 -184

No files matched your search

@@ -31,6 +31,9 @@ interface GalleryDraggableItemProps {
onDeleted?: (id: string) => void;
onEditClicked?: (url: string, media_id?: string) => Promise<void> | void;
maxSelections?: number;
bulkSelected?: boolean;
onBulkSelectToggle?: () => void;
bulkSelectionMode?: boolean;
}
export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
@@ -45,6 +48,9 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
onDeleted,
onEditClicked,
maxSelections,
bulkSelected = false,
onBulkSelectToggle,
bulkSelectionMode = false,
}) => {
const imgRef = useRef<HTMLImageElement>(null);
const dragStarted = useRef(false);
@@ -76,6 +82,11 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
// Disable dragging for video items, but allow clicks in select mode
if (item.mediaClass === "video" && mode !== "select") return;
// In bulk selection mode, skip drag — clicks toggle selection
if (bulkSelectionMode) {
dragStarted.current = false;
return;
}
dragStarted.current = false;
const moveListener = (moveEvent: PointerEvent) => {
const dx = moveEvent.pageX - event.pageX;
@@ -100,24 +111,83 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
};
const handlePointerUp = (event: React.PointerEvent) => {
if (!dragStarted.current && (mode === "select" || !disableTooltipAndBadge)) {
// In bulk selection mode, let only handleButtonClick fire onClick
// to avoid double-toggling the selection
if (bulkSelectionMode) return;
if (
!dragStarted.current &&
(mode === "select" || !disableTooltipAndBadge)
) {
onClick();
}
};
const handleButtonClick = (event: React.MouseEvent) => {
if (mode === "select" || !disableTooltipAndBadge) {
if (mode === "select" || !disableTooltipAndBadge || bulkSelectionMode) {
onClick();
}
};
return (
<div
// Shared button content — avoids duplicating the image/thumbnail rendering
const showTooltip = !disableTooltipAndBadge && !bulkSelectionMode;
const itemButton = (
<button
type="button"
tabIndex={-1}
className={twMerge(
"group relative w-full",
activeFilter === "video" ? "aspect-square" : "aspect-square"
"w-full group relative overflow-visible rounded-md border-[3px] transition-all focus:outline-none",
"aspect-square",
selected || bulkSelected
? "border-primary"
: disableTooltipAndBadge
? "border-transparent hover:border-primary/80"
: "border-transparent hover:border-primary",
mode === "select" || item.mediaClass === "video" || bulkSelectionMode
? "cursor-pointer"
: disableTooltipAndBadge
? "cursor-pointer"
: "cursor-grab hover:cursor-grab active:cursor-grabbing",
)}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onClick={handleButtonClick}
aria-label={item.label}
>
<div className="relative h-full w-full">
{!item.thumbnail ? (
<div className="flex h-full w-full items-center justify-center bg-black/30">
<span className="text-white/60">Image not available</span>
</div>
) : (
<img
data-gallery-draggable-1="true"
// NB: "loading=lazy" is necessary to prevent loading GIGABYTES of images!
// It is a bit finnicky, too: you must include this attribute
// BEFORE the `src` attribute, or it won't work.
loading="lazy"
ref={imgRef}
src={item.thumbnail}
alt={item.label}
className={twMerge(
"h-full w-full bg-black/30",
imageFit === "contain" ? "object-contain" : "object-cover",
)}
draggable={false}
onError={onImageError}
/>
)}
{selected && (
<div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary">
<FontAwesomeIcon icon={faCheck} className="text-sm" />
</div>
)}
</div>
</button>
);
return (
<div className={twMerge("group relative w-full", "aspect-square")}>
{/* dropdown menu */}
{mode !== "select" && (
<div
@@ -128,111 +198,88 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
}}
>
<PopoverMenu
position="bottom"
align="end"
mode="default"
triggerIcon={
<FontAwesomeIcon icon={faEllipsis} className="text-base-fg" />
}
buttonClassName="h-7 w-7 p-0 rounded-full bg-ui-controls/60 hover:bg-ui-controls/90 text-base-fg border border-ui-controls-border"
panelClassName="min-w-28 p-1"
closeOnUnhover
>
{(close) => (
<div className="flex flex-col">
{item.mediaClass === "image" && (
position="bottom"
align="end"
mode="default"
triggerIcon={
<FontAwesomeIcon icon={faEllipsis} className="text-base-fg" />
}
buttonClassName="h-7 w-7 p-0 rounded-full bg-ui-controls/60 hover:bg-ui-controls/90 text-base-fg border border-ui-controls-border"
panelClassName="min-w-28 p-1"
closeOnUnhover
>
{(close) => (
<div className="flex flex-col">
{item.mediaClass === "image" && (
<button
type="button"
className="flex items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-base-fg text-sm"
onClick={async (e) => {
e.stopPropagation();
if (onEditClicked && (item.fullImage || item.thumbnail)) {
await onEditClicked(
item.fullImage || item.thumbnail!,
item.id,
);
}
close();
}}
>
<FontAwesomeIcon icon={faPencil} className="text-base-fg" />
<span>Edit image</span>
</button>
)}
<button
type="button"
className="flex items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-base-fg text-sm"
onClick={async (e) => {
className="flex items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-sm"
onClick={(e) => {
e.stopPropagation();
if (onEditClicked && (item.fullImage || item.thumbnail)) {
await onEditClicked(
item.fullImage || item.thumbnail!,
item.id
);
}
handleDelete();
close();
}}
>
<FontAwesomeIcon icon={faPencil} className="text-base-fg" />
<span>Edit image</span>
<FontAwesomeIcon icon={faTrashCan} className="text-red" />
<span className="text-red">Delete</span>
</button>
)}
<button
type="button"
className="flex items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-sm"
onClick={(e) => {
e.stopPropagation();
handleDelete();
close();
}}
>
<FontAwesomeIcon icon={faTrashCan} className="text-red" />
<span className="text-red">Delete</span>
</button>
</div>
)}
</PopoverMenu>
</div>
)}
</PopoverMenu>
</div>
)}
{/* Media class badge on hover */}
{/* Bulk selection checkbox — top-left */}
{mode !== "select" && (
<div
className={twMerge(
"absolute left-2 top-2 z-30 flex h-5 w-5 items-center justify-center rounded border-2 cursor-pointer transition-all duration-100",
bulkSelected
? "bg-primary border-primary"
: "border-white/60 bg-black/40 hover:border-white",
bulkSelectionMode
? "opacity-100"
: "opacity-0 group-hover:opacity-100",
)}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
onBulkSelectToggle?.();
}}
>
{bulkSelected && (
<FontAwesomeIcon
icon={faCheck}
className="text-[10px] text-white"
/>
)}
</div>
)}
{/* Media class badge on hover — bottom-left */}
{!disableTooltipAndBadge && item.mediaClass && (
<div className="pointer-events-none absolute left-2 top-2 z-20 rounded-full bg-black/50 backdrop-blur-lg px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-white opacity-0 group-hover:opacity-100 transition-opacity duration-150">
<div className="pointer-events-none absolute left-2 bottom-2 z-20 rounded-full bg-black/50 backdrop-blur-lg px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-white opacity-0 group-hover:opacity-100 transition-opacity duration-150">
{item.mediaClass === "dimensional" ? "3D" : item.mediaClass}
</div>
)}
{/* Tooltip on hover */}
{disableTooltipAndBadge ? (
<button
type="button"
tabIndex={0}
className={twMerge(
"w-full group relative overflow-visible rounded-md border-transparent border-[3px] transition-all",
activeFilter === "video" ? "aspect-square" : "aspect-square",
selected
? "border-primary"
: "border-transparent hover:border-primary/80",
mode === "select" || item.mediaClass === "video"
? "cursor-pointer"
: "cursor-grab hover:cursor-grab active:cursor-grabbing"
)}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onClick={handleButtonClick}
aria-label={item.label}
>
<div className="relative h-full w-full">
{!item.thumbnail ? (
<div className="flex h-full w-full items-center justify-center bg-black/30">
<span className="text-white/60">Image not available</span>
</div>
) : (
<img
data-gallery-draggable-1="true"
// NB: "loading=lazy" is necessary to prevent loading GIGABYTES of images!
// It is a bit finnicky, too: you must include this attribute
// BEFORE the `src` attribute, or it won't work.
loading="lazy"
ref={imgRef}
src={item.thumbnail}
alt={item.label}
className={twMerge(
"h-full w-full bg-black/30",
imageFit === "contain" ? "object-contain" : "object-cover"
)}
draggable={false}
onError={onImageError}
/>
)}
{selected && (
<div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary">
<FontAwesomeIcon icon={faCheck} className="text-sm" />
</div>
)}
</div>
</button>
) : (
{/* Conditionally wrap with Tooltip — hidden when selecting or in bulk mode */}
{showTooltip ? (
<Tooltip
position="bottom"
delay={200}
@@ -256,55 +303,10 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
</div>
}
>
<button
type="button"
tabIndex={0}
className={twMerge(
"w-full group relative overflow-visible rounded-md border-[3px] transition-all focus:outline-none focus:ring-2 focus:ring-primary",
activeFilter === "video" ? "aspect-square" : "aspect-square",
selected
? "border-primary"
: "border-transparent hover:border-primary",
mode === "select" || item.mediaClass === "video"
? "cursor-pointer"
: "cursor-grab hover:cursor-grab active:cursor-grabbing"
)}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onClick={handleButtonClick}
aria-label={item.label}
>
<div className="relative h-full w-full">
{!item.thumbnail ? (
<div className="flex h-full w-full items-center justify-center bg-black/30">
<span className="text-white/60">Image not available</span>
</div>
) : (
<img
data-gallery-draggable-2="true"
// NB: "loading=lazy" is necessary to prevent loading GIGABYTES of images!
// It is a bit finnicky, too: you must include this attribute
// BEFORE the `src` attribute, or it won't work.
loading="lazy"
ref={imgRef}
src={item.thumbnail}
alt={item.label}
className={twMerge(
"h-full w-full bg-black/30",
imageFit === "contain" ? "object-contain" : "object-cover"
)}
draggable={false}
onError={onImageError}
/>
)}
{selected && (
<div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary">
<FontAwesomeIcon icon={faCheck} className="text-sm" />
</div>
)}
</div>
</button>
{itemButton}
</Tooltip>
) : (
itemButton
)}
</div>
);
@@ -43,10 +43,17 @@ import {
faExpand,
faCompress,
faArrowsRotate,
faTrashCan,
faXmark,
} from "@fortawesome/pro-solid-svg-icons";
import { PopoverMenu } from "@storyteller/ui-popover";
import { SliderV2 } from "@storyteller/ui-sliderv2";
import { Tooltip } from "@storyteller/ui-tooltip";
import {
showActionReminder,
isActionReminderOpen,
} from "@storyteller/ui-action-reminder-modal";
import { MediaFileDelete } from "@storyteller/tauri-api";
export interface GalleryItem {
id: string;
@@ -80,22 +87,22 @@ interface GalleryModalProps {
onDownloadClicked?: (url: string, mediaClass?: string) => Promise<void>;
onAddToSceneClicked?: (
url: string,
media_id: string | undefined
media_id: string | undefined,
) => Promise<void>;
isOpen?: boolean;
forceFilter?: string;
onEditClicked?: (url: string, media_id?: string) => Promise<void> | void;
onTurnIntoVideoClicked?: (
url: string,
media_id?: string
media_id?: string,
) => Promise<void> | void;
onRemoveBackgroundClicked?: (
url: string,
media_id?: string
media_id?: string,
) => Promise<void> | void;
onMake3DWorldClicked?: (
url: string,
media_id?: string
media_id?: string,
) => Promise<void> | void;
}
@@ -136,7 +143,7 @@ export const GalleryModal = React.memo(
// Default gridColumns to 5
const defaultGridColumns = 5;
const [sliderValue, setSliderValue] = useState(
maxColumns - (defaultGridColumns - minColumns)
maxColumns - (defaultGridColumns - minColumns),
);
const gridColumns = maxColumns - (sliderValue - minColumns);
const [imageFit, setImageFit] = useState<"cover" | "contain">("contain");
@@ -144,6 +151,17 @@ export const GalleryModal = React.memo(
const [pageIndex, setPageIndex] = useState(0);
const [hasMore, setHasMore] = useState(true);
// Bulk selection state (view mode only)
const [bulkSelectedIds, setBulkSelectedIds] = useState<Set<string>>(
new Set(),
);
const bulkSelectionMode = bulkSelectedIds.size > 0;
// Clear bulk selection when filter changes
useEffect(() => {
setBulkSelectedIds(new Set());
}, [activeFilter]);
// NB: We've got some kind of cursoring issue where subsequent pages are not requested.
// For now, let's set the pagination limit really high.
const pageSize = 100;
@@ -180,11 +198,11 @@ export const GalleryModal = React.memo(
Object.entries(grouped).sort(
(a, b) =>
new Date(b[1][0].createdAt).getTime() -
new Date(a[1][0].createdAt).getTime()
)
new Date(a[1][0].createdAt).getTime(),
),
);
},
[formatDate]
[formatDate],
);
const handleImageError = useCallback(
@@ -192,7 +210,7 @@ export const GalleryModal = React.memo(
console.error(`Failed to load gallery modal image: ${url}`);
failedImageUrls.add(url);
},
[failedImageUrls]
[failedImageUrls],
);
useEffect(() => {
@@ -280,7 +298,7 @@ export const GalleryModal = React.memo(
if (response.success && response.data) {
const newItems = response.data
.filter(
(item: any) => item.media_type !== FilterMediaType.SCENE_JSON
(item: any) => item.media_type !== FilterMediaType.SCENE_JSON,
)
.map((item: any) => ({
id: item.token,
@@ -289,10 +307,13 @@ export const GalleryModal = React.memo(
item.media_class === "video"
? item.media_links.maybe_video_previews?.animated
: item.media_class === "dimensional"
? item.cover_image?.maybe_cover_image_public_bucket_url
: getThumbnailUrl(item.media_links.maybe_thumbnail_template, {
width: THUMBNAIL_SIZES.MEDIUM,
}),
? item.cover_image?.maybe_cover_image_public_bucket_url
: getThumbnailUrl(
item.media_links.maybe_thumbnail_template,
{
width: THUMBNAIL_SIZES.MEDIUM,
},
),
thumbnailUrlTemplate: item.media_links.maybe_thumbnail_template,
fullImage: item.media_links.cdn_url,
createdAt: item.created_at,
@@ -338,8 +359,8 @@ export const GalleryModal = React.memo(
mode === "view"
? galleryModalVisibleViewMode.value
: typeof isOpen === "boolean"
? isOpen
: true;
? isOpen
: true;
if (modalIsOpen && username) {
refreshGallery();
}
@@ -356,13 +377,15 @@ export const GalleryModal = React.memo(
(item: GalleryItem) => {
if (mode === "select" && onSelectItem) {
onSelectItem(item.id);
} else if (bulkSelectionMode) {
toggleBulkSelect(item.id);
} else {
lightboxImageSignal.value = item;
lightboxVisibleSignal.value = true;
galleryModalLightboxMediaId.value = item.id;
}
},
[mode, onSelectItem]
[mode, onSelectItem, bulkSelectionMode],
);
const handleCloseLightbox = useCallback(() => {
@@ -384,15 +407,70 @@ export const GalleryModal = React.memo(
const handleItemDeleted = useCallback((id: string) => {
setAllItems((prev) => prev.filter((it) => it.id !== id));
setBulkSelectedIds((prev) => {
const next = new Set(prev);
next.delete(id);
return next;
});
}, []);
const toggleBulkSelect = useCallback((id: string) => {
setBulkSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
const clearBulkSelection = useCallback(() => {
setBulkSelectedIds(new Set());
}, []);
const bulkSelectedItems = useMemo(
() => allItems.filter((item) => bulkSelectedIds.has(item.id)),
[allItems, bulkSelectedIds],
);
const handleBulkDelete = useCallback(() => {
const count = bulkSelectedIds.size;
showActionReminder({
reminderType: "default",
title: `Delete ${count} item${count > 1 ? "s" : ""}?`,
message: (
<p className="text-sm text-white/70">
This will permanently remove {count} item{count > 1 ? "s" : ""} from
your library. This action cannot be undone.
</p>
),
primaryActionText: "Delete",
secondaryActionText: "Cancel",
primaryActionBtnClassName: "bg-red text-white hover:bg-red/90",
onPrimaryAction: async () => {
try {
const ids = Array.from(bulkSelectedIds);
await Promise.allSettled(ids.map((id) => MediaFileDelete(id)));
setAllItems((prev) =>
prev.filter((it) => !bulkSelectedIds.has(it.id)),
);
clearBulkSelection();
} finally {
isActionReminderOpen.value = false;
}
},
});
}, [bulkSelectedIds, clearBulkSelection]);
useSignals();
// Synchronize lightbox image when external media id signal is set
useEffect(() => {
if (galleryModalLightboxMediaId.value && allItems.length > 0) {
const target = allItems.find(
(it) => it.id === galleryModalLightboxMediaId.value
(it) => it.id === galleryModalLightboxMediaId.value,
);
if (target) {
lightboxImageSignal.value = target;
@@ -425,11 +503,12 @@ export const GalleryModal = React.memo(
? galleryModalVisibleViewMode.value &&
galleryModalVisibleDuringDrag.value
: typeof isOpen === "boolean"
? isOpen
: true
? isOpen
: true
}
onClose={() => {
if (mode === "view") {
clearBulkSelection();
onClose?.() || (galleryModalVisibleViewMode.value = false);
} else {
onClose?.();
@@ -438,7 +517,7 @@ export const GalleryModal = React.memo(
className={twMerge(
"h-[620px] max-w-4xl rounded-xl",
mode === "view" &&
"h-[640px] min-h-[640px] min-w-[56rem] w-[56rem] max-w-none"
"h-[640px] min-h-[640px] min-w-[56rem] w-[56rem] max-w-none",
)}
childPadding={false}
showClose={false}
@@ -462,18 +541,18 @@ export const GalleryModal = React.memo(
? "Select Video"
: "Select Videos"
: activeFilter === "3d"
? maxSelections === 1
? "Select 3D Object"
: "Select 3D Objects"
: activeFilter === "uploaded"
? maxSelections === 1
? "Select Upload"
: "Select Uploads"
: activeFilter === "all"
? "Select Media"
: maxSelections === 1
? "Select Image"
: "Select Images"
? maxSelections === 1
? "Select 3D Object"
: "Select 3D Objects"
: activeFilter === "uploaded"
? maxSelections === 1
? "Select Upload"
: "Select Uploads"
: activeFilter === "all"
? "Select Media"
: maxSelections === 1
? "Select Image"
: "Select Images"
: "My Library"}
</h2>
{mode === "view" && (
@@ -527,7 +606,7 @@ export const GalleryModal = React.memo(
variant="action"
onClick={() =>
setImageFit((fit) =>
fit === "cover" ? "contain" : "cover"
fit === "cover" ? "contain" : "cover",
)
}
className="relative z-[51] h-9 w-9 bg-ui-controls/60 hover:bg-ui-controls/90"
@@ -587,7 +666,7 @@ export const GalleryModal = React.memo(
// Only allow filter changes if no forceFilter was provided
if (!forceFilterRef.current) {
const filter = FILTERS.find(
(f) => f.label === item.label
(f) => f.label === item.label,
);
if (filter) setActiveFilter(filter.id);
}
@@ -604,6 +683,7 @@ export const GalleryModal = React.memo(
<CloseButton
onClick={() => {
if (mode === "view") {
clearBulkSelection();
galleryModalVisibleViewMode.value = false;
} else {
onClose?.();
@@ -674,7 +754,9 @@ export const GalleryModal = React.memo(
PLACEHOLDER_IMAGES.DEFAULT;
e.currentTarget.style.opacity = "0.3";
// Set the `data-brokenurl` property for debugging the broken images:
(e.currentTarget as HTMLImageElement).dataset.brokenurl = item.thumbnail || "";
(
e.currentTarget as HTMLImageElement
).dataset.brokenurl = item.thumbnail || "";
handleImageError(item.thumbnail!);
}}
disableTooltipAndBadge={mode === "select"}
@@ -682,12 +764,17 @@ export const GalleryModal = React.memo(
onDeleted={handleItemDeleted}
onEditClicked={onEditClicked}
maxSelections={maxSelections}
bulkSelected={bulkSelectedIds.has(item.id)}
onBulkSelectToggle={() =>
toggleBulkSelect(item.id)
}
bulkSelectionMode={bulkSelectionMode}
/>
))}
</div>
</div>
);
}
},
)}
{loading && allItems.length > 0 && (
<div className="flex justify-center py-4">
@@ -703,6 +790,52 @@ export const GalleryModal = React.memo(
)}
</div>
{mode === "view" && bulkSelectionMode && (
<div className="flex items-center justify-between border-t border-ui-panel-border bg-ui-background p-3 py-2 rounded-b-xl">
<div className="flex items-center gap-3">
{/* Thumbnail previews of selected items */}
<div className="flex -space-x-1.5">
{bulkSelectedItems.slice(0, 4).map((si) => (
<div
key={si.id}
className="h-8 w-8 rounded overflow-hidden border-2 border-ui-panel bg-black/30 flex-shrink-0"
>
{si.thumbnail ? (
<img
src={si.thumbnail}
alt=""
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-black/50" />
)}
</div>
))}
</div>
<span className="text-sm font-medium text-base-fg/80">
{bulkSelectedIds.size} selected
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="destructive"
onClick={handleBulkDelete}
className="px-3"
icon={faTrashCan}
>
Delete
</Button>
<button
onClick={clearBulkSelection}
className="flex h-8 w-8 items-center justify-center rounded-md bg-ui-controls/60 hover:bg-ui-controls/90 text-base-fg transition-colors"
aria-label="Clear selection"
>
<FontAwesomeIcon icon={faXmark} className="text-base" />
</button>
</div>
</div>
)}
{mode === "select" && (
<div className="flex items-center justify-between border-t border-ui-panel-border bg-ui-panel p-4 rounded-b-xl">
<div className="flex items-center gap-3">
@@ -761,7 +894,7 @@ export const GalleryModal = React.memo(
)}
</>
);
}
},
);
GalleryModal.displayName = "GalleryModal";
@@ -6,6 +6,7 @@ import {
faDownToLine,
faGlobe,
faPencil,
faTrashCan,
faVideo,
faWandMagicSparkles,
} from "@fortawesome/pro-solid-svg-icons";
@@ -864,6 +865,7 @@ export function LightboxModal({
{selectedMediaToken && onDeleteClicked && (
<Button
icon={faTrashCan}
className="w-full"
variant="destructive"
onClick={async (e) => {