From 2150aca1545cc2bbca028ca3658ce715d985c875 Mon Sep 17 00:00:00 2001 From: Varun Raj Date: Sun, 19 Jan 2025 09:31:35 +0530 Subject: [PATCH] feat: bulk delete in find page and find page clean up Signed-off-by: Varun Raj --- src/components/albums/AlbumSelectorDialog.tsx | 1 - .../assets-options/AssetOffsetDialog.tsx | 2 +- .../TagMissingLocationDialog.tsx | 7 +- src/components/shared/AssetGrid.tsx | 53 +++++++++++++-- .../shared/AssetsBulkDeleteButton.tsx | 36 +++++++++++ src/components/ui/alert-dialog.tsx | 6 +- src/pages/404.tsx | 10 +++ src/pages/albums/potential-albums.tsx | 6 +- src/pages/api/find/search.ts | 13 +++- src/pages/api/share-link/generate.ts | 1 - src/pages/assets/missing-locations.tsx | 6 +- src/pages/find/index.tsx | 64 ++++++++++--------- 12 files changed, 156 insertions(+), 49 deletions(-) create mode 100644 src/components/shared/AssetsBulkDeleteButton.tsx create mode 100644 src/pages/404.tsx diff --git a/src/components/albums/AlbumSelectorDialog.tsx b/src/components/albums/AlbumSelectorDialog.tsx index 6e53b1b..c8c006f 100644 --- a/src/components/albums/AlbumSelectorDialog.tsx +++ b/src/components/albums/AlbumSelectorDialog.tsx @@ -55,7 +55,6 @@ export default function AlbumSelectorDialog({ onSelected, onCreated, onSubmit }: const handleSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); - console.log(formData); if (!onSubmit) return; setLoading(true); onSubmit(formData) diff --git a/src/components/assets/assets-options/AssetOffsetDialog.tsx b/src/components/assets/assets-options/AssetOffsetDialog.tsx index 9e29cf2..bc21585 100644 --- a/src/components/assets/assets-options/AssetOffsetDialog.tsx +++ b/src/components/assets/assets-options/AssetOffsetDialog.tsx @@ -68,7 +68,7 @@ export default function AssetOffsetDialog({ assets: _assets, onComplete }: IProp return ( - diff --git a/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog.tsx b/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog.tsx index af77728..d100f3b 100644 --- a/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog.tsx +++ b/src/components/assets/missing-location/TagMissingLocationDialog/TagMissingLocationDialog.tsx @@ -13,6 +13,7 @@ import React, { useState } from "react"; import TagMissingLocationSearchAndAdd from "./TagMissingLocationSearchAndAdd"; import TagMissingLocationSearchLatLong from "./TagMissingLocationSearchLatLong"; import dynamic from "next/dynamic"; +import { useMissingLocationContext } from "@/contexts/MissingLocationContext"; const LazyMap = dynamic(() => import("./Map"), { ssr: false @@ -24,7 +25,7 @@ interface ITagMissingLocationDialogProps { export default function TagMissingLocationDialog({ onSubmit, }: ITagMissingLocationDialogProps) { - + const {selectedIds} = useMissingLocationContext(); const [open, setOpen] = useState(false); const [mapPosition,setMapPosition] = useState({ latitude: 48.0, @@ -35,8 +36,8 @@ export default function TagMissingLocationDialog({ return ( - - + + diff --git a/src/components/shared/AssetGrid.tsx b/src/components/shared/AssetGrid.tsx index 2405c10..bd5a74b 100644 --- a/src/components/shared/AssetGrid.tsx +++ b/src/components/shared/AssetGrid.tsx @@ -1,7 +1,7 @@ import "yet-another-react-lightbox/styles.css"; import { IAsset } from '@/types/asset'; -import React, { useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import Lightbox from 'yet-another-react-lightbox'; import { Gallery } from "react-grid-gallery"; import LazyGridImage from "../ui/lazy-grid-image"; @@ -12,10 +12,40 @@ interface AssetGridProps { assets: IAsset[]; isInternal?: boolean; selectable?: boolean; + onSelectionChange?: (ids: string[]) => void; } -export default function AssetGrid({ assets, isInternal = true, selectable = false }: AssetGridProps) { +export default function AssetGrid({ assets, isInternal = true, selectable = false, onSelectionChange }: AssetGridProps) { const [index, setIndex] = useState(-1); + const [selectedIds, setSelectedIds] = useState([]); + const [lastSelectedIndex, setLastSelectedIndex] = useState(-1); + + const handleSelect = (_idx: number, asset: IAsset, event: React.MouseEvent) => { + + event.stopPropagation(); + const isPresent = selectedIds.includes(asset.id); + if (isPresent) { + setSelectedIds(selectedIds.filter((id) => id !== asset.id)); + onSelectionChange?.(selectedIds.filter((id) => id !== asset.id)); + } else { + const clickedIndex = images.findIndex((image) => { + return image.id === asset.id; + }); + if (event.shiftKey) { + const startIndex = Math.min(clickedIndex, lastSelectedIndex); + const endIndex = Math.max(clickedIndex, lastSelectedIndex); + const newSelectedIds = images.slice(startIndex, endIndex + 1).map((image) => image.id); + const allSelectedIds = [...selectedIds, ...newSelectedIds]; + const uniqueSelectedIds = [...new Set(allSelectedIds)]; + setSelectedIds(uniqueSelectedIds); + onSelectionChange?.(uniqueSelectedIds); + } else { + setSelectedIds([...selectedIds, asset.id]); + onSelectionChange?.([...selectedIds, asset.id]); + } + setLastSelectedIndex(clickedIndex); + } + }; const slides = useMemo(() => { return assets.map((asset) => ({ @@ -45,9 +75,23 @@ export default function AssetGrid({ assets, isInternal = true, selectable = fals original: p.previewUrl as string, width: p.exifImageWidth / 10 as number, height: p.exifImageHeight / 10 as number, - orientation: 1 + orientation: 1, + isSelected: selectedIds.includes(p.id), })); - }, [assets]); + }, [assets, selectedIds]); + + const handleEsc = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setSelectedIds([]); + onSelectionChange?.([]); + } + }; + + useEffect(() => { + // Listen for esc key press and unselect all images + window.addEventListener("keydown", handleEsc); + return () => window.removeEventListener("keydown", handleEsc); + }, [images]); return (
@@ -63,6 +107,7 @@ export default function AssetGrid({ assets, isInternal = true, selectable = fals onClick={setIndex} enableImageSelection={selectable} thumbnailImageComponent={LazyGridImage} + onSelect={handleSelect} />
); diff --git a/src/components/shared/AssetsBulkDeleteButton.tsx b/src/components/shared/AssetsBulkDeleteButton.tsx new file mode 100644 index 0000000..85418b6 --- /dev/null +++ b/src/components/shared/AssetsBulkDeleteButton.tsx @@ -0,0 +1,36 @@ +import React, { useMemo } from 'react' +import { Button } from '../ui/button'; +import { AlertDialog } from '../ui/alert-dialog'; +import { deleteAssets } from '@/handlers/api/asset.handler'; + +interface AssetsBulkDeleteButtonProps { + selectedIds: string[]; + onDelete: (ids: string[]) => void; +} + +export default function AssetsBulkDeleteButton({ selectedIds, onDelete }: AssetsBulkDeleteButtonProps) { + + const ids = useMemo(() => { + return selectedIds + }, [selectedIds]); + + + const handleDelete = () => { + return deleteAssets(ids).then(() => { + onDelete(ids); + }) + } + + return ( + + + + ) +} diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index d898f67..35c8942 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -139,6 +139,7 @@ interface AlertDialogProps extends AlertDialogPrimitive.AlertDialogProps { onCancel: () => void | Promise variant?: ButtonProps['variant'] asChild?: boolean + disabled?: boolean } const AlertDialog = React.forwardRef>(({ @@ -149,6 +150,7 @@ const AlertDialog = React.forwardRef { const [loading, setLoading] = useState(false) @@ -169,7 +171,7 @@ const AlertDialog = React.forwardRef - {children} + {children} {title} @@ -178,7 +180,7 @@ const AlertDialog = React.forwardRef Cancel diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..5343444 --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +export default function FourOFour() { + return ( +
+

404

+

Page not found

+
+ ) +} \ No newline at end of file diff --git a/src/pages/albums/potential-albums.tsx b/src/pages/albums/potential-albums.tsx index bc3893a..8cbb392 100644 --- a/src/pages/albums/potential-albums.tsx +++ b/src/pages/albums/potential-albums.tsx @@ -161,8 +161,10 @@ export default function PotentialAlbums() { - diff --git a/src/pages/api/find/search.ts b/src/pages/api/find/search.ts index 07c1668..471bc40 100644 --- a/src/pages/api/find/search.ts +++ b/src/pages/api/find/search.ts @@ -1,6 +1,7 @@ import { db } from '@/config/db'; import { ENV } from '@/config/environment'; import { getCurrentUser } from '@/handlers/serverUtils/user.utils'; +import { cleanUpAsset, cleanUpAssets, isFlipped } from '@/helpers/asset.helper'; import { parseFindQuery } from '@/helpers/gemini.helper'; import { assetFaces, assets, person } from '@/schema'; import { Person } from '@/schema/person.schema'; @@ -23,7 +24,7 @@ export default async function search(req: NextApiRequest, res: NextApiResponse) return fetch(url, { method: 'POST', - body: JSON.stringify(parsedQuery), + body: JSON.stringify({...parsedQuery, withExif: true }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${currentUser?.accessToken}` @@ -40,8 +41,16 @@ export default async function search(req: NextApiRequest, res: NextApiResponse) return response.json(); }) .then(data => { + const items = data.assets.items.map((item: any) => { + return { + ...item, + exifImageHeight: isFlipped(item?.exifInfo?.orientation) ? item?.exifInfo?.exifImageWidth : item?.exifInfo?.exifImageHeight, + exifImageWidth: isFlipped(item?.exifInfo?.orientation) ? item?.exifInfo?.exifImageHeight : item?.exifInfo?.exifImageWidth, + orientation: item?.exifInfo?.orientation, + } + }).map(cleanUpAsset) res.status(200).json({ - assets: data.assets.items, + assets: items, filters: { ...parsedQuery, personIds: dbPeople.map((person) => person.name) diff --git a/src/pages/api/share-link/generate.ts b/src/pages/api/share-link/generate.ts index 5c4affb..ca87aa1 100644 --- a/src/pages/api/share-link/generate.ts +++ b/src/pages/api/share-link/generate.ts @@ -10,7 +10,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const currentUser = await getCurrentUser(req); const filters = req.body; - console.log(filters); if (!currentUser) { return res.status(401).json({ message: "Unauthorized" }); } diff --git a/src/pages/assets/missing-locations.tsx b/src/pages/assets/missing-locations.tsx index 23fc94a..70d15a1 100644 --- a/src/pages/assets/missing-locations.tsx +++ b/src/pages/assets/missing-locations.tsx @@ -141,8 +141,10 @@ export default function MissingLocations() { - diff --git a/src/pages/find/index.tsx b/src/pages/find/index.tsx index 83aaa13..a714348 100644 --- a/src/pages/find/index.tsx +++ b/src/pages/find/index.tsx @@ -13,6 +13,9 @@ import React, { useMemo, useState } from 'react' import Lightbox from 'yet-another-react-lightbox'; import { Button } from "@/components/ui/button"; import Link from "next/link"; +import AssetGrid from "@/components/shared/AssetGrid"; +import FloatingBar from "@/components/shared/FloatingBar"; +import AssetsBulkDeleteButton from "@/components/shared/AssetsBulkDeleteButton"; interface IFindFilters { [key: string]: string; @@ -30,7 +33,7 @@ const FILTER_KEY_MAP = { } export default function FindPage() { - const [index, setIndex] = useState(-1); + const [selectedIds, setSelectedIds] = useState([]); const { geminiEnabled, exImmichUrl } = useConfig(); const [query, setQuery] = useState(''); const [assets, setAssets] = useState([]); @@ -52,7 +55,12 @@ export default function FindPage() { value: string; }[] = useMemo(() => { return Object.entries(filters) - .filter(([_key, value]) => value !== undefined) + .filter(([_key, value]) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return value !== undefined && value !== null && value !== ''; + }) .map(([key, value]) => ({ label: key, value: Array.isArray(value) ? value.join(', ') : value, @@ -73,6 +81,16 @@ export default function FindPage() { }); } + const handleSelectionChange = (ids: string[]) => { + setSelectedIds(ids); + } + + const handleDelete = (ids: string[]) => { + setSelectedIds([]); + setAssets(assets.filter((asset) => !ids.includes(asset.id))); + + } + const renderFilters = () => { if (appliedFilters.length === 0) return null; return ( @@ -114,41 +132,25 @@ export default function FindPage() {

{renderFilters()}

- } + return ( <> - = 0} - index={index} - close={() => setIndex(-1)} - /> {renderFilters()} -
- {assets.map((asset, idx) => ( -
- - Open - - - {asset.id} setIndex(idx)} +
+ + {selectedIds.length > 0 && ( + +

+ {selectedIds.length} Selected +

+ -
- ))} + + )}
)