From 60467f1d7fdc63e8ec3b10ad0d183248cebac4ee Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Sun, 17 Mar 2024 15:38:03 +0000 Subject: feature(web): A better tags editor using react select with auto complete and auto create --- .../components/dashboard/bookmarks/TagsEditor.tsx | 233 +++++++++++---------- 1 file changed, 118 insertions(+), 115 deletions(-) (limited to 'apps/web/components/dashboard') diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx index 12c0dcd0..38f01bdd 100644 --- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx @@ -1,134 +1,137 @@ -import type { KeyboardEvent } from "react"; -import { useEffect, useState } from "react"; -import { Input } from "@/components/ui/input"; +import type { ActionMeta } from "react-select"; import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; import { cn } from "@/lib/utils"; -import { Sparkles, X } from "lucide-react"; +import { Sparkles } from "lucide-react"; +import CreateableSelect from "react-select/creatable"; import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; import type { ZAttachedByEnum } from "@hoarder/trpc/types/tags"; interface EditableTag { attachedBy: ZAttachedByEnum; - id?: string; - name: string; -} - -function TagAddInput({ addTag }: { addTag: (tag: string) => void }) { - const onKeyUp = (e: KeyboardEvent) => { - if (e.key === "Enter") { - addTag(e.currentTarget.value); - e.currentTarget.value = ""; - } - }; - return ( - - ); -} - -function TagPill({ - tag, - deleteCB, -}: { - tag: { attachedBy: ZAttachedByEnum; id?: string; name: string }; - deleteCB: () => void; -}) { - const isAttachedByAI = tag.attachedBy == "ai"; - return ( -
- {isAttachedByAI && } -

{tag.name}

- -
- ); + value?: string; + label: string; } export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) { - const [tags, setTags] = useState>(new Map()); - useEffect(() => { - const m = new Map(); - for (const t of bookmark.tags) { - m.set(t.name, { attachedBy: t.attachedBy, id: t.id, name: t.name }); - } - setTags(m); - }, [bookmark.tags]); - const bookmarkInvalidationFunction = api.useUtils().bookmarks.getBookmark.invalidate; - const { mutate } = api.bookmarks.updateTags.useMutation({ - onSuccess: () => { - toast({ - description: "Tags has been updated!", - }); - bookmarkInvalidationFunction({ bookmarkId: bookmark.id }); - // TODO(bug) Invalidate the tag views as well - }, - onError: () => { - toast({ - variant: "destructive", - title: "Something went wrong", - description: "There was a problem with your request.", - }); - }, - }); + const { mutate, isPending: isMutating } = + api.bookmarks.updateTags.useMutation({ + onSuccess: () => { + toast({ + description: "Tags has been updated!", + }); + bookmarkInvalidationFunction({ bookmarkId: bookmark.id }); + // TODO(bug) Invalidate the tag views as well + }, + 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(); + + const onChange = ( + _option: readonly EditableTag[], + actionMeta: ActionMeta, + ) => { + switch (actionMeta.action) { + case "remove-value": { + if (actionMeta.removedValue.value) { + mutate({ + bookmarkId: bookmark.id, + attach: [], + detach: [{ tagId: actionMeta.removedValue.value }], + }); + } + break; + } + case "create-option": { + mutate({ + bookmarkId: bookmark.id, + attach: [{ tag: actionMeta.option.label }], + detach: [], + }); + break; + } + case "select-option": { + if (actionMeta.option) { + mutate({ + bookmarkId: bookmark.id, + attach: [ + { tag: actionMeta.option.label, tagId: actionMeta.option?.value }, + ], + detach: [], + }); + } + break; + } + } + }; return ( -
- {[...tags.values()].map((t) => ( - { - setTags((m) => { - const newMap = new Map(m); - newMap.delete(t.name); - if (t.id) { - mutate({ - bookmarkId: bookmark.id, - attach: [], - detach: [{ tagId: t.id }], - }); - } - return newMap; - }); - }} - /> - ))} -
- { - setTags((m) => { - if (m.has(val)) { - // Tag already exists - // Do nothing - return m; - } - const newMap = new Map(m); - newMap.set(val, { attachedBy: "human", name: val }); - mutate({ - bookmarkId: bookmark.id, - attach: [{ tag: val }], - detach: [], - }); - return newMap; - }); - }} - /> -
-
+ ({ + label: t.name, + value: t.id, + attachedBy: "human" as const, + })) ?? [] + } + value={bookmark.tags.map((t) => ({ + label: t.name, + value: t.id, + attachedBy: t.attachedBy, + }))} + isMulti + closeMenuOnSelect={false} + isClearable={false} + isLoading={isExistingTagsLoading || isMutating} + styles={{ + multiValueRemove: () => ({ + "background-color": "transparent", + }), + valueContainer: (styles) => ({ + ...styles, + padding: "0.5rem", + }), + }} + components={{ + MultiValueContainer: ({ children, data }) => ( +
+ {children} +
+ ), + MultiValueLabel: ({ children, data }) => ( +
+ {(data as { attachedBy: string }).attachedBy == "ai" && ( + + )} + {children} +
+ ), + }} + classNames={{ + multiValueRemove: () => "my-auto", + valueContainer: () => "gap-2", + menuList: () => "text-sm", + }} + /> ); } -- cgit v1.2.3-70-g09d2