aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard/bookmarks
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-09-01 19:15:24 +0000
committerMohamedBassem <me@mbassem.com>2024-09-01 19:15:24 +0000
commit67729c131c92a2fab6d1422db34aa000c348af07 (patch)
treefe3064beb40f40d3a19c5ec200f7afc2b0e1ff9e /apps/web/components/dashboard/bookmarks
parent6d4d1a6e574a99d86c46636ebc8f14a55b07e1b5 (diff)
downloadkarakeep-67729c131c92a2fab6d1422db34aa000c348af07.tar.zst
feature(web): Manage tags in bulk actions
Diffstat (limited to 'apps/web/components/dashboard/bookmarks')
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx48
-rw-r--r--apps/web/components/dashboard/bookmarks/BulkTagModal.tsx126
-rw-r--r--apps/web/components/dashboard/bookmarks/TagModal.tsx4
-rw-r--r--apps/web/components/dashboard/bookmarks/TagsEditor.tsx60
4 files changed, 197 insertions, 41 deletions
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx b/apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx
new file mode 100644
index 00000000..d6d60d22
--- /dev/null
+++ b/apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx
@@ -0,0 +1,48 @@
+import { toast } from "@/components/ui/use-toast";
+
+import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
+import { useUpdateBookmarkTags } from "@hoarder/shared-react/hooks/bookmarks";
+
+import { TagsEditor } from "./TagsEditor";
+
+export function BookmarkTagsEditor({ bookmark }: { bookmark: ZBookmark }) {
+ const { mutate } = useUpdateBookmarkTags({
+ onSuccess: () => {
+ toast({
+ description: "Tags has been updated!",
+ });
+ },
+ onError: () => {
+ toast({
+ variant: "destructive",
+ title: "Something went wrong",
+ description: "There was a problem with your request.",
+ });
+ },
+ });
+
+ return (
+ <TagsEditor
+ tags={bookmark.tags}
+ onAttach={({ tagName, tagId }) => {
+ mutate({
+ bookmarkId: bookmark.id,
+ attach: [
+ {
+ tagName,
+ tagId,
+ },
+ ],
+ detach: [],
+ });
+ }}
+ onDetach={({ tagId }) => {
+ mutate({
+ bookmarkId: bookmark.id,
+ attach: [],
+ detach: [{ tagId }],
+ });
+ }}
+ />
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx b/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx
new file mode 100644
index 00000000..3c8e75e7
--- /dev/null
+++ b/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx
@@ -0,0 +1,126 @@
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { toast } from "@/components/ui/use-toast";
+
+import { useUpdateBookmarkTags } from "@hoarder/shared-react/hooks/bookmarks";
+import { api } from "@hoarder/shared-react/trpc";
+import { ZBookmark } from "@hoarder/shared/types/bookmarks";
+
+import { TagsEditor } from "./TagsEditor";
+
+export default function BulkTagModal({
+ bookmarkIds,
+ open,
+ setOpen,
+}: {
+ bookmarkIds: string[];
+ open: boolean;
+ setOpen: (open: boolean) => void;
+}) {
+ const results = api.useQueries((t) =>
+ bookmarkIds.map((id) => t.bookmarks.getBookmark({ bookmarkId: id })),
+ );
+
+ const bookmarks = results
+ .map((r) => r.data)
+ .filter((b): b is ZBookmark => !!b);
+
+ const { mutateAsync } = useUpdateBookmarkTags({
+ onError: (err) => {
+ if (err.data?.code == "BAD_REQUEST") {
+ if (err.data.zodError) {
+ toast({
+ variant: "destructive",
+ description: Object.values(err.data.zodError.fieldErrors)
+ .flat()
+ .join("\n"),
+ });
+ } else {
+ toast({
+ variant: "destructive",
+ description: err.message,
+ });
+ }
+ } else {
+ toast({
+ variant: "destructive",
+ title: "Something went wrong",
+ });
+ }
+ },
+ });
+
+ const onAttach = async (tag: { tagName: string; tagId?: string }) => {
+ const results = await Promise.allSettled(
+ bookmarkIds.map((id) =>
+ mutateAsync({
+ bookmarkId: id,
+ attach: [tag],
+ detach: [],
+ }),
+ ),
+ );
+ const successes = results.filter((r) => r.status == "fulfilled").length;
+ toast({
+ description: `Tag "${tag.tagName}" has been added to ${successes} bookmarks!`,
+ });
+ };
+
+ const onDetach = async ({
+ tagId,
+ tagName,
+ }: {
+ tagId: string;
+ tagName: string;
+ }) => {
+ const results = await Promise.allSettled(
+ bookmarkIds.map((id) =>
+ mutateAsync({
+ bookmarkId: id,
+ attach: [],
+ detach: [{ tagId }],
+ }),
+ ),
+ );
+ const successes = results.filter((r) => r.status == "fulfilled").length;
+ toast({
+ description: `Tag "${tagName}" has been removed from ${successes} bookmarks!`,
+ });
+ };
+
+ // Get all the tags that are attached to all the bookmarks
+ let tags = bookmarks
+ .flatMap((b) => b.tags)
+ .filter((tag) =>
+ bookmarks.every((b) => b.tags.some((t) => tag.id == t.id)),
+ );
+ // Filter duplicates
+ tags = tags.filter(
+ (tag, index, self) => index === self.findIndex((t) => t.id == tag.id),
+ );
+
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Edit Tags of {bookmarks.length} Bookmarks</DialogTitle>
+ </DialogHeader>
+ <TagsEditor tags={tags} onAttach={onAttach} onDetach={onDetach} />
+ <DialogFooter className="sm:justify-end">
+ <DialogClose asChild>
+ <Button type="button" variant="secondary">
+ Close
+ </Button>
+ </DialogClose>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/TagModal.tsx b/apps/web/components/dashboard/bookmarks/TagModal.tsx
index 00cc40fc..c2f081be 100644
--- a/apps/web/components/dashboard/bookmarks/TagModal.tsx
+++ b/apps/web/components/dashboard/bookmarks/TagModal.tsx
@@ -11,7 +11,7 @@ import {
import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
-import { TagsEditor } from "./TagsEditor";
+import { BookmarkTagsEditor } from "./BookmarkTagsEditor";
export default function TagModal({
bookmark,
@@ -28,7 +28,7 @@ export default function TagModal({
<DialogHeader>
<DialogTitle>Edit Tags</DialogTitle>
</DialogHeader>
- <TagsEditor bookmark={bookmark} />
+ <BookmarkTagsEditor bookmark={bookmark} />
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
index 06d89aff..d50acc60 100644
--- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
+++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
@@ -1,14 +1,14 @@
import type { ActionMeta } from "react-select";
-import { toast } from "@/components/ui/use-toast";
import { useClientConfig } from "@/lib/clientConfig";
import { api } from "@/lib/trpc";
import { cn } from "@/lib/utils";
import { Sparkles } from "lucide-react";
import CreateableSelect from "react-select/creatable";
-import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
-import type { ZAttachedByEnum } from "@hoarder/shared/types/tags";
-import { useUpdateBookmarkTags } from "@hoarder/shared-react/hooks/bookmarks";
+import type {
+ ZAttachedByEnum,
+ ZBookmarkTags,
+} from "@hoarder/shared/types/tags";
interface EditableTag {
attachedBy: ZAttachedByEnum;
@@ -16,24 +16,17 @@ interface EditableTag {
label: string;
}
-export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
+export function TagsEditor({
+ tags,
+ onAttach,
+ onDetach,
+}: {
+ tags: ZBookmarkTags[];
+ onAttach: (tag: { tagName: string; tagId?: string }) => void;
+ onDetach: (tag: { tagName: string; tagId: string }) => void;
+}) {
const demoMode = !!useClientConfig().demoMode;
- const { mutate } = useUpdateBookmarkTags({
- onSuccess: () => {
- toast({
- description: "Tags has been updated!",
- });
- },
- onError: () => {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- description: "There was a problem with your request.",
- });
- },
- });
-
const { data: existingTags, isLoading: isExistingTagsLoading } =
api.tags.list.useQuery();
@@ -47,33 +40,22 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
case "pop-value":
case "remove-value": {
if (actionMeta.removedValue.value) {
- mutate({
- bookmarkId: bookmark.id,
- attach: [],
- detach: [{ tagId: actionMeta.removedValue.value }],
+ onDetach({
+ tagId: actionMeta.removedValue.value,
+ tagName: actionMeta.removedValue.label,
});
}
break;
}
case "create-option": {
- mutate({
- bookmarkId: bookmark.id,
- attach: [{ tagName: actionMeta.option.label }],
- detach: [],
- });
+ onAttach({ tagName: actionMeta.option.label });
break;
}
case "select-option": {
if (actionMeta.option) {
- mutate({
- bookmarkId: bookmark.id,
- attach: [
- {
- tagName: actionMeta.option.label,
- tagId: actionMeta.option?.value,
- },
- ],
- detach: [],
+ onAttach({
+ tagName: actionMeta.option.label,
+ tagId: actionMeta.option?.value,
});
}
break;
@@ -92,7 +74,7 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
attachedBy: "human" as const,
})) ?? []
}
- value={bookmark.tags.map((t) => ({
+ value={tags.map((t) => ({
label: t.name,
value: t.id,
attachedBy: t.attachedBy,