aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-09-01 17:44:09 +0000
committerMohamedBassem <me@mbassem.com>2024-09-01 17:44:27 +0000
commitddc7e5dbfc31b3e5b189c61883e1cc737fefd2ee (patch)
treebba661440260b9615ebdf7b00eb2da8a537ce317 /apps/web
parentdbdbd4902796a1cb6a3a382ccbbf8338954e7431 (diff)
downloadkarakeep-ddc7e5dbfc31b3e5b189c61883e1cc737fefd2ee.tar.zst
feature(web): Allow adding to lists in bulk actions. #368
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/components/dashboard/BulkBookmarksAction.tsx16
-rw-r--r--apps/web/components/dashboard/bookmarks/BulkManageListsModal.tsx138
2 files changed, 153 insertions, 1 deletions
diff --git a/apps/web/components/dashboard/BulkBookmarksAction.tsx b/apps/web/components/dashboard/BulkBookmarksAction.tsx
index bed1bda8..5dd31fa5 100644
--- a/apps/web/components/dashboard/BulkBookmarksAction.tsx
+++ b/apps/web/components/dashboard/BulkBookmarksAction.tsx
@@ -8,13 +8,14 @@ import {
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 { List, Pencil, Trash2, X } from "lucide-react";
import {
useDeleteBookmark,
useUpdateBookmark,
} from "@hoarder/shared-react/hooks/bookmarks";
+import BulkManageListsModal from "./bookmarks/BulkManageListsModal";
import { ArchivedActionIcon, FavouritedActionIcon } from "./bookmarks/icons";
export default function BulkBookmarksAction() {
@@ -24,6 +25,7 @@ export default function BulkBookmarksAction() {
);
const { toast } = useToast();
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+ const [manageListsModal, setManageListsModalOpen] = useState(false);
useEffect(() => {
setIsBulkEditEnabled(false); // turn off toggle + clear selected bookmarks on mount
@@ -96,6 +98,13 @@ export default function BulkBookmarksAction() {
const actionList = [
{
+ name: "Add to List",
+ icon: <List size={18} />,
+ action: () => setManageListsModalOpen(true),
+ isPending: false,
+ hidden: !isBulkEditEnabled,
+ },
+ {
name: alreadyFavourited ? "Unfavourite" : "Favourite",
icon: <FavouritedActionIcon favourited={!!alreadyFavourited} size={18} />,
action: () => updateBookmarks({ favourited: !alreadyFavourited }),
@@ -149,6 +158,11 @@ export default function BulkBookmarksAction() {
</ActionButton>
)}
/>
+ <BulkManageListsModal
+ bookmarkIds={selectedBookmarks.map((b) => b.id)}
+ open={manageListsModal}
+ setOpen={setManageListsModalOpen}
+ />
<div className="flex">
{actionList.map(
({ name, icon: Icon, action, isPending, hidden, alwaysEnable }) => (
diff --git a/apps/web/components/dashboard/bookmarks/BulkManageListsModal.tsx b/apps/web/components/dashboard/bookmarks/BulkManageListsModal.tsx
new file mode 100644
index 00000000..9c1f05d2
--- /dev/null
+++ b/apps/web/components/dashboard/bookmarks/BulkManageListsModal.tsx
@@ -0,0 +1,138 @@
+import { ActionButton } from "@/components/ui/action-button";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "@/components/ui/form";
+import { toast } from "@/components/ui/use-toast";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { useAddBookmarkToList } from "@hoarder/shared-react/hooks/lists";
+
+import { BookmarkListSelector } from "../lists/BookmarkListSelector";
+
+export default function BulkManageListsModal({
+ bookmarkIds,
+ open,
+ setOpen,
+}: {
+ bookmarkIds: string[];
+ open: boolean;
+ setOpen: (open: boolean) => void;
+}) {
+ const formSchema = z.object({
+ listId: z.string({
+ required_error: "Please select a list",
+ }),
+ });
+ const form = useForm<z.infer<typeof formSchema>>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ listId: undefined,
+ },
+ });
+
+ const { mutateAsync: addToList, isPending: isAddingToListPending } =
+ useAddBookmarkToList({
+ onSettled: () => {
+ form.resetField("listId");
+ },
+ onError: (e) => {
+ if (e.data?.code == "BAD_REQUEST") {
+ toast({
+ variant: "destructive",
+ description: e.message,
+ });
+ } else {
+ toast({
+ variant: "destructive",
+ title: "Something went wrong",
+ });
+ }
+ },
+ });
+
+ const onSubmit = async (value: z.infer<typeof formSchema>) => {
+ const results = await Promise.allSettled(
+ bookmarkIds.map((bookmarkId) =>
+ addToList({
+ bookmarkId,
+ listId: value.listId,
+ }),
+ ),
+ );
+
+ const successes = results.filter((r) => r.status == "fulfilled").length;
+ if (successes > 0) {
+ toast({
+ description: `${successes} bookmarks have been added to the list!`,
+ });
+ }
+
+ setOpen(false);
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogContent>
+ <Form {...form}>
+ <form
+ className="flex w-full flex-col gap-4"
+ onSubmit={form.handleSubmit(onSubmit)}
+ >
+ <DialogHeader>
+ <DialogTitle>
+ Add {bookmarkIds.length} bookmarks to List
+ </DialogTitle>
+ </DialogHeader>
+
+ <FormField
+ control={form.control}
+ name="listId"
+ render={({ field }) => {
+ return (
+ <FormItem>
+ <FormControl>
+ <BookmarkListSelector
+ value={field.value}
+ onChange={field.onChange}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ );
+ }}
+ />
+ <DialogFooter className="sm:justify-end">
+ <DialogClose asChild>
+ <Button type="button" variant="secondary">
+ Close
+ </Button>
+ </DialogClose>
+ <ActionButton
+ type="submit"
+ loading={isAddingToListPending}
+ disabled={isAddingToListPending}
+ >
+ Add
+ </ActionButton>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ );
+}