From d193d9bf89e8a88bf70b673ea5e438d73cf40c0c Mon Sep 17 00:00:00 2001 From: Md Saban <45597394+mdsaban@users.noreply.github.com> Date: Tue, 2 Jul 2024 03:51:23 +0530 Subject: feat: Add bulk edit option for bookmarks. Fixes #84 (#259) * feat: add bulk edit option for bookmarks * fix: resolve comments * fix: resolve comments * fix: resolve comments * fix: resolve comments * rename bulk action store, simplify the bulk action toolbar --------- Co-authored-by: MohamedBassem --- .../components/dashboard/BulkBookmarksAction.tsx | 171 +++++++++++++++++++++ apps/web/components/dashboard/ChangeLayout.tsx | 12 +- apps/web/components/dashboard/GlobalActions.tsx | 13 ++ .../dashboard/bookmarks/BookmarkActionBar.tsx | 2 +- .../bookmarks/BookmarkLayoutAdaptingCard.tsx | 62 +++++++- apps/web/components/dashboard/bookmarks/icons.tsx | 12 +- apps/web/components/dashboard/lists/ListHeader.tsx | 6 +- 7 files changed, 263 insertions(+), 15 deletions(-) create mode 100644 apps/web/components/dashboard/BulkBookmarksAction.tsx create mode 100644 apps/web/components/dashboard/GlobalActions.tsx (limited to 'apps/web/components/dashboard') diff --git a/apps/web/components/dashboard/BulkBookmarksAction.tsx b/apps/web/components/dashboard/BulkBookmarksAction.tsx new file mode 100644 index 00000000..b78071ee --- /dev/null +++ b/apps/web/components/dashboard/BulkBookmarksAction.tsx @@ -0,0 +1,171 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { + ActionButton, + ActionButtonWithTooltip, +} from "@/components/ui/action-button"; +import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; +import { useToast } from "@/components/ui/use-toast"; +import useBulkActionsStore from "@/lib/bulkActions"; +import { Pencil, Trash2, X } from "lucide-react"; + +import { + useDeleteBookmark, + useUpdateBookmark, +} from "@hoarder/shared-react/hooks/bookmarks"; + +import { ArchivedActionIcon, FavouritedActionIcon } from "./bookmarks/icons"; + +export default function BulkBookmarksAction() { + const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore(); + const setIsBulkEditEnabled = useBulkActionsStore( + (state) => state.setIsBulkEditEnabled, + ); + const { toast } = useToast(); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + useEffect(() => { + setIsBulkEditEnabled(false); // turn off toggle + clear selected bookmarks on mount + }, []); + + const onError = () => { + toast({ + variant: "destructive", + title: "Something went wrong", + description: "There was a problem with your request.", + }); + }; + + const deleteBookmarkMutator = useDeleteBookmark({ + onSuccess: () => { + setIsBulkEditEnabled(false); + }, + onError, + }); + + const updateBookmarkMutator = useUpdateBookmark({ + onSuccess: () => { + setIsBulkEditEnabled(false); + }, + onError, + }); + + interface UpdateBookmarkProps { + favourited?: boolean; + archived?: boolean; + } + + const updateBookmarks = async ({ + favourited, + archived, + }: UpdateBookmarkProps) => { + await Promise.all( + selectedBookmarks.map((item) => + updateBookmarkMutator.mutateAsync({ + bookmarkId: item.id, + favourited, + archived, + }), + ), + ); + toast({ + description: `${selectedBookmarks.length} bookmarks have been updated!`, + }); + }; + + const deleteBookmarks = async () => { + await Promise.all( + selectedBookmarks.map((item) => + deleteBookmarkMutator.mutateAsync({ bookmarkId: item.id }), + ), + ); + toast({ + description: `${selectedBookmarks.length} bookmarks have been deleted!`, + }); + }; + + const alreadyFavourited = + selectedBookmarks.length && + selectedBookmarks.every((item) => item.favourited === true); + + const alreadyArchived = + selectedBookmarks.length && + selectedBookmarks.every((item) => item.archived === true); + + const actionList = [ + { + name: alreadyFavourited ? "Unfavourite" : "Favourite", + icon: , + action: () => updateBookmarks({ favourited: !alreadyFavourited }), + isPending: updateBookmarkMutator.isPending, + hidden: !isBulkEditEnabled, + }, + { + name: alreadyArchived ? "Un-arhcive" : "Archive", + icon: , + action: () => updateBookmarks({ archived: !alreadyArchived }), + isPending: updateBookmarkMutator.isPending, + hidden: !isBulkEditEnabled, + }, + { + name: "Delete", + icon: , + action: () => setIsDeleteDialogOpen(true), + hidden: !isBulkEditEnabled, + }, + { + name: "Close bulk edit", + icon: , + action: () => setIsBulkEditEnabled(false), + alwaysEnable: true, + hidden: !isBulkEditEnabled, + }, + { + name: "Bulk Edit", + icon: , + action: () => setIsBulkEditEnabled(true), + alwaysEnable: true, + hidden: isBulkEditEnabled, + }, + ]; + + return ( +
+ Are you sure you want to delete these bookmarks?

} + actionButton={() => ( + deleteBookmarks()} + > + Delete + + )} + /> +
+ {actionList.map( + ({ name, icon: Icon, action, isPending, hidden, alwaysEnable }) => ( + + {Icon} + + ), + )} +
+
+ ); +} diff --git a/apps/web/components/dashboard/ChangeLayout.tsx b/apps/web/components/dashboard/ChangeLayout.tsx index 59acb6bd..7449bd2d 100644 --- a/apps/web/components/dashboard/ChangeLayout.tsx +++ b/apps/web/components/dashboard/ChangeLayout.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import { Button } from "@/components/ui/button"; +import { ButtonWithTooltip } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, @@ -20,15 +20,19 @@ const iconMap = { list: LayoutList, }; -export default function SidebarProfileOptions() { +export default function ChangeLayout() { const layout = useBookmarkLayout(); return ( - + {Object.keys(iconMap).map((key) => ( diff --git a/apps/web/components/dashboard/GlobalActions.tsx b/apps/web/components/dashboard/GlobalActions.tsx new file mode 100644 index 00000000..e09f92a2 --- /dev/null +++ b/apps/web/components/dashboard/GlobalActions.tsx @@ -0,0 +1,13 @@ +"use client"; + +import BulkBookmarksAction from "@/components/dashboard/BulkBookmarksAction"; +import ChangeLayout from "@/components/dashboard/ChangeLayout"; + +export default function GlobalActions() { + return ( +
+ + +
+ ); +} diff --git a/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx b/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx index 6cc8e44e..299f47eb 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx @@ -22,7 +22,7 @@ export default function BookmarkActionBar({ href={`/dashboard/preview/${bookmark.id}`} className={cn(buttonVariants({ variant: "ghost" }), "px-2")} > - + diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx index e1cc1f7c..33b65108 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx @@ -1,12 +1,15 @@ import type { BookmarksLayoutTypes } from "@/lib/userLocalSettings/types"; -import React from "react"; +import React, { useEffect, useState } from "react"; import Link from "next/link"; +import useBulkActionsStore from "@/lib/bulkActions"; import { bookmarkLayoutSwitch, useBookmarkLayout, } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; import dayjs from "dayjs"; +import { Check } from "lucide-react"; +import { useTheme } from "next-themes"; import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; import { isBookmarkStillTagging } from "@hoarder/shared-react/utils/bookmarkUtils"; @@ -45,6 +48,57 @@ function BottomRow({ ); } +function MultiBookmarkSelector({ bookmark }: { bookmark: ZBookmark }) { + const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore(); + const toggleBookmark = useBulkActionsStore((state) => state.toggleBookmark); + const [isSelected, setIsSelected] = useState(false); + const { theme } = useTheme(); + + useEffect(() => { + setIsSelected(selectedBookmarks.some((item) => item.id === bookmark.id)); + }, [selectedBookmarks]); + + if (!isBulkEditEnabled) return null; + + const getIconColor = () => { + if (theme === "dark") { + return isSelected ? "black" : "white"; + } + return isSelected ? "white" : "black"; + }; + + const getIconBackgroundColor = () => { + if (theme === "dark") { + return isSelected ? "bg-white" : "bg-white bg-opacity-10"; + } + return isSelected ? "bg-black" : "bg-white bg-opacity-40"; + }; + + return ( + + + ); +} + function ListView({ bookmark, image, @@ -56,10 +110,11 @@ function ListView({ return (
+
{image("list", "object-cover rounded-lg size-32")}
@@ -100,11 +155,12 @@ function GridView({ return (
+ {img &&
{img}
}
diff --git a/apps/web/components/dashboard/bookmarks/icons.tsx b/apps/web/components/dashboard/bookmarks/icons.tsx index d899f19d..04e3ff32 100644 --- a/apps/web/components/dashboard/bookmarks/icons.tsx +++ b/apps/web/components/dashboard/bookmarks/icons.tsx @@ -3,27 +3,31 @@ import { Archive, ArchiveRestore, Star } from "lucide-react"; export function FavouritedActionIcon({ favourited, className, + size, }: { favourited: boolean; className?: string; + size?: number; }) { return favourited ? ( - + ) : ( - + ); } export function ArchivedActionIcon({ archived, className, + size, }: { archived: boolean; className?: string; + size?: number; }) { return archived ? ( - + ) : ( - + ); } diff --git a/apps/web/components/dashboard/lists/ListHeader.tsx b/apps/web/components/dashboard/lists/ListHeader.tsx index 2f69203e..1655a80b 100644 --- a/apps/web/components/dashboard/lists/ListHeader.tsx +++ b/apps/web/components/dashboard/lists/ListHeader.tsx @@ -1,7 +1,7 @@ "use client"; import { useRouter } from "next/navigation"; -import ChangeLayout from "@/components/dashboard/ChangeLayout"; +import GlobalActions from "@/components/dashboard/GlobalActions"; import { Button } from "@/components/ui/button"; import { MoreHorizontal } from "lucide-react"; @@ -37,13 +37,13 @@ export default function ListHeader({ {list.icon} {list.name} -
+
- +
); -- cgit v1.2.3-70-g09d2