From fe13408831dce4bdae4911098d6079a097cae9e8 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Tue, 9 Apr 2024 15:49:24 +0100 Subject: [PATCH] feature(web): Allow uploading directly into lists/tags. Fixes #69 --- apps/web/app/dashboard/archive/page.tsx | 1 + apps/web/app/dashboard/bookmarks/page.tsx | 21 ++--- apps/web/app/dashboard/favourites/page.tsx | 1 + apps/web/app/dashboard/layout.tsx | 2 +- .../web/app/dashboard/lists/[listId]/page.tsx | 28 +++--- .../web/app/dashboard/tags/[tagName]/page.tsx | 1 + .../components/dashboard/UploadDropzone.tsx | 25 ++--- .../dashboard/bookmarks/BookmarkOptions.tsx | 6 +- .../bookmarks/BookmarkedTextEditor.tsx | 38 ++------ .../dashboard/bookmarks/BookmarksGrid.tsx | 21 ++--- .../dashboard/bookmarks/EditorCard.tsx | 7 +- .../dashboard/bookmarks/TagsEditor.tsx | 14 +-- .../bookmarks/UpdatableBookmarksGrid.tsx | 10 +- apps/web/lib/hooks/list-context.tsx | 21 ----- .../hooks/bookmark-grid-context.tsx | 27 ++++++ packages/shared-react/hooks/bookmarks.ts | 91 ++++++++++++++++++- packages/shared-react/hooks/lists.ts | 15 ++- packages/trpc/routers/bookmarks.test.ts | 17 +++- packages/trpc/routers/bookmarks.ts | 56 ++++++++---- 19 files changed, 258 insertions(+), 144 deletions(-) delete mode 100644 apps/web/lib/hooks/list-context.tsx create mode 100644 packages/shared-react/hooks/bookmark-grid-context.tsx diff --git a/apps/web/app/dashboard/archive/page.tsx b/apps/web/app/dashboard/archive/page.tsx index b2b4df4f..26ec45e9 100644 --- a/apps/web/app/dashboard/archive/page.tsx +++ b/apps/web/app/dashboard/archive/page.tsx @@ -14,6 +14,7 @@ export default async function ArchivedBookmarkPage() { } query={{ archived: true }} showDivider={true} + showEditorCard={true} /> ); } diff --git a/apps/web/app/dashboard/bookmarks/page.tsx b/apps/web/app/dashboard/bookmarks/page.tsx index 9907df4d..90f4f2cb 100644 --- a/apps/web/app/dashboard/bookmarks/page.tsx +++ b/apps/web/app/dashboard/bookmarks/page.tsx @@ -1,23 +1,20 @@ import React from "react"; import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; import TopNav from "@/components/dashboard/bookmarks/TopNav"; -import UploadDropzone from "@/components/dashboard/UploadDropzone"; import { Separator } from "@/components/ui/separator"; export default async function BookmarksPage() { return (
- - - -
- Bookmarks

} - query={{ archived: false }} - showEditorCard={true} - /> -
-
+ + +
+ Bookmarks

} + query={{ archived: false }} + showEditorCard={true} + /> +
); } diff --git a/apps/web/app/dashboard/favourites/page.tsx b/apps/web/app/dashboard/favourites/page.tsx index 13d793c6..423a8e69 100644 --- a/apps/web/app/dashboard/favourites/page.tsx +++ b/apps/web/app/dashboard/favourites/page.tsx @@ -6,6 +6,7 @@ export default async function FavouritesBookmarkPage() { header={

⭐️ Favourites

} query={{ favourited: true }} showDivider={true} + showEditorCard={true} /> ); } diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx index 628c3a34..3b6908f8 100644 --- a/apps/web/app/dashboard/layout.tsx +++ b/apps/web/app/dashboard/layout.tsx @@ -21,7 +21,7 @@ export default async function Dashboard({ -
{children}
+
{children}
); diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx index 2b8025e5..49bf77f7 100644 --- a/apps/web/app/dashboard/lists/[listId]/page.tsx +++ b/apps/web/app/dashboard/lists/[listId]/page.tsx @@ -1,7 +1,6 @@ import { notFound } from "next/navigation"; import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; import DeleteListButton from "@/components/dashboard/lists/DeleteListButton"; -import { BookmarkListContextProvider } from "@/lib/hooks/list-context"; import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; @@ -23,19 +22,18 @@ export default async function ListPage({ } return ( - - - - {list.icon} {list.name} - - - - } - /> - + + + {list.icon} {list.name} + + + + } + /> ); } diff --git a/apps/web/app/dashboard/tags/[tagName]/page.tsx b/apps/web/app/dashboard/tags/[tagName]/page.tsx index 6bbb5234..b8bf351d 100644 --- a/apps/web/app/dashboard/tags/[tagName]/page.tsx +++ b/apps/web/app/dashboard/tags/[tagName]/page.tsx @@ -32,6 +32,7 @@ export default async function TagPage({ } query={{ tagId: tag.id }} + showEditorCard={true} /> ); } diff --git a/apps/web/components/dashboard/UploadDropzone.tsx b/apps/web/components/dashboard/UploadDropzone.tsx index 70e6483a..bd08d2cf 100644 --- a/apps/web/components/dashboard/UploadDropzone.tsx +++ b/apps/web/components/dashboard/UploadDropzone.tsx @@ -1,12 +1,12 @@ "use client"; import React, { useState } from "react"; -import { api } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import DropZone from "react-dropzone"; +import { useCreateBookmarkWithPostHook } from "@hoarder/shared-react/hooks/bookmarks"; import { zUploadErrorSchema, zUploadResponseSchema, @@ -16,20 +16,15 @@ import LoadingSpinner from "../ui/spinner"; import { toast } from "../ui/use-toast"; function useUploadAsset({ onComplete }: { onComplete: () => void }) { - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; - - const { mutateAsync: createBookmark } = - api.bookmarks.createBookmark.useMutation({ - onSuccess: () => { - toast({ description: "Bookmark uploaded" }); - invalidateAllBookmarks(); - onComplete(); - }, - onError: () => { - toast({ description: "Something went wrong", variant: "destructive" }); - }, - }); + const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook({ + onSuccess: () => { + toast({ description: "Bookmark uploaded" }); + onComplete(); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); const { mutateAsync: runUpload } = useMutation({ mutationFn: async (file: File) => { diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx index e3cfc796..a8ec1ab5 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx @@ -1,6 +1,6 @@ "use client"; -import { useContext, useState } from "react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -10,7 +10,6 @@ import { } from "@/components/ui/dropdown-menu"; import { useToast } from "@/components/ui/use-toast"; import { useClientConfig } from "@/lib/clientConfig"; -import { BookmarkListContext } from "@/lib/hooks/list-context"; import { Link, List, @@ -29,6 +28,7 @@ import { useUpdateBookmark, } from "@hoarder/shared-react/hooks//bookmarks"; import { useRemoveBookmarkFromList } from "@hoarder/shared-react/hooks//lists"; +import { useBookmarkGridContext } from "@hoarder/shared-react/hooks/bookmark-grid-context"; import { useAddToListModal } from "./AddToListModal"; import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; @@ -48,7 +48,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const [isTextEditorOpen, setTextEditorOpen] = useState(false); - const { listId } = useContext(BookmarkListContext); + const { listId } = useBookmarkGridContext() ?? {}; const onError = () => { toast({ diff --git a/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx b/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx index eb618474..294f2b5a 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx @@ -21,7 +21,7 @@ export function BookmarkedTextEditor({ open, setOpen, }: { - bookmark?: ZBookmark; + bookmark: ZBookmark; open: boolean; setOpen: (open: boolean) => void; }) { @@ -30,30 +30,14 @@ export function BookmarkedTextEditor({ bookmark && bookmark.content.type == "text" ? bookmark.content.text : "", ); - const invalidateAllBookmarksCache = - api.useUtils().bookmarks.getBookmarks.invalidate; const invalidateOneBookmarksCache = api.useUtils().bookmarks.getBookmark.invalidate; - const { mutate: createBookmarkMutator, isPending: isCreationPending } = - api.bookmarks.createBookmark.useMutation({ - onSuccess: () => { - invalidateAllBookmarksCache(); - toast({ - description: "Note created!", - }); - setOpen(false); - setNoteText(""); - }, - onError: () => { - toast({ description: "Something went wrong", variant: "destructive" }); - }, - }); - const { mutate: updateBookmarkMutator, isPending: isUpdatePending } = + const { mutate: updateBookmarkMutator, isPending } = api.bookmarks.updateBookmarkText.useMutation({ onSuccess: () => { invalidateOneBookmarksCache({ - bookmarkId: bookmark!.id, + bookmarkId: bookmark.id, }); toast({ description: "Note updated!", @@ -64,20 +48,12 @@ export function BookmarkedTextEditor({ toast({ description: "Something went wrong", variant: "destructive" }); }, }); - const isPending = isCreationPending || isUpdatePending; const onSave = () => { - if (isNewBookmark) { - createBookmarkMutator({ - type: "text", - text: noteText, - }); - } else { - updateBookmarkMutator({ - bookmarkId: bookmark.id, - text: noteText, - }); - } + updateBookmarkMutator({ + bookmarkId: bookmark.id, + text: noteText, + }); }; return ( diff --git a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx index 048dab85..bace3435 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx @@ -1,5 +1,3 @@ -"use client"; - import { useMemo } from "react"; import { ActionButton } from "@/components/ui/action-button"; import tailwindConfig from "@/tailwind.config"; @@ -78,15 +76,16 @@ export default function BookmarksGrid({ {bookmarks.map((b) => renderBookmark(b))} {hasNextPage && ( - fetchNextPage()} - className="mx-auto w-min" - variant="ghost" - > - Load More - +
+ fetchNextPage()} + variant="ghost" + > + Load More + +
)} ); diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx index b9e46a30..10ad1f13 100644 --- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx +++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx @@ -7,12 +7,13 @@ import { Separator } from "@/components/ui/separator"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "@/components/ui/use-toast"; import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { useCreateBookmarkWithPostHook } from "@hoarder/shared-react/hooks/bookmarks"; + function useFocusOnKeyPress(inputRef: React.RefObject) { useEffect(() => { function handleKeyPress(e: KeyboardEvent) { @@ -47,10 +48,8 @@ export default function EditorCard({ className }: { className?: string }) { useImperativeHandle(ref, () => inputRef.current); useFocusOnKeyPress(inputRef); - const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate; - const { mutate, isPending } = api.bookmarks.createBookmark.useMutation({ + const { mutate, isPending } = useCreateBookmarkWithPostHook({ onSuccess: () => { - invalidateBookmarksCache(); form.reset(); }, onError: () => { diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx index 91294b2e..c1a4cc70 100644 --- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx @@ -8,6 +8,7 @@ import CreateableSelect from "react-select/creatable"; import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; import type { ZAttachedByEnum } from "@hoarder/trpc/types/tags"; +import { useUpdateBookmarkTags } from "@hoarder/shared-react/hooks/bookmarks"; interface EditableTag { attachedBy: ZAttachedByEnum; @@ -17,16 +18,12 @@ interface EditableTag { export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) { const demoMode = !!useClientConfig().demoMode; - const apiUtils = api.useUtils(); - const { mutate } = api.bookmarks.updateTags.useMutation({ + const { mutate } = useUpdateBookmarkTags({ onSuccess: () => { toast({ description: "Tags has been updated!", }); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: bookmark.id }); - apiUtils.tags.list.invalidate(); - apiUtils.tags.get.invalidate(); }, onError: () => { toast({ @@ -58,7 +55,7 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) { case "create-option": { mutate({ bookmarkId: bookmark.id, - attach: [{ tag: actionMeta.option.label }], + attach: [{ tagName: actionMeta.option.label }], detach: [], }); break; @@ -68,7 +65,10 @@ export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) { mutate({ bookmarkId: bookmark.id, attach: [ - { tag: actionMeta.option.label, tagId: actionMeta.option?.value }, + { + tagName: actionMeta.option.label, + tagId: actionMeta.option?.value, + }, ], detach: [], }); diff --git a/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx index a344320e..fe69201c 100644 --- a/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx +++ b/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx @@ -1,11 +1,13 @@ "use client"; +import UploadDropzone from "@/components/dashboard/UploadDropzone"; import { api } from "@/lib/trpc"; import type { ZGetBookmarksRequest, ZGetBookmarksResponse, } from "@hoarder/trpc/types/bookmarks"; +import { BookmarkGridContextProvider } from "@hoarder/shared-react/hooks/bookmark-grid-context"; import BookmarksGrid from "./BookmarksGrid"; @@ -29,7 +31,7 @@ export default function UpdatableBookmarksGrid({ getNextPageParam: (lastPage) => lastPage.nextCursor, }); - return ( + const grid = ( b.bookmarks)} hasNextPage={hasNextPage} @@ -38,4 +40,10 @@ export default function UpdatableBookmarksGrid({ showEditorCard={showEditorCard} /> ); + + return ( + + {showEditorCard ? {grid} : grid} + + ); } diff --git a/apps/web/lib/hooks/list-context.tsx b/apps/web/lib/hooks/list-context.tsx deleted file mode 100644 index cb8a20b2..00000000 --- a/apps/web/lib/hooks/list-context.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { createContext } from "react"; - -export const BookmarkListContext = createContext<{ - listId: string | undefined; -}>({ listId: undefined }); - -export function BookmarkListContextProvider({ - listId, - children, -}: { - listId: string; - children: React.ReactNode; -}) { - return ( - - {children} - - ); -} diff --git a/packages/shared-react/hooks/bookmark-grid-context.tsx b/packages/shared-react/hooks/bookmark-grid-context.tsx new file mode 100644 index 00000000..5814da12 --- /dev/null +++ b/packages/shared-react/hooks/bookmark-grid-context.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { createContext, useContext } from "react"; + +import type { ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks"; + +export const BookmarkGridContext = createContext< + ZGetBookmarksRequest | undefined +>(undefined); + +export function BookmarkGridContextProvider({ + query, + children, +}: { + query: ZGetBookmarksRequest; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} + +export function useBookmarkGridContext() { + return useContext(BookmarkGridContext); +} diff --git a/packages/shared-react/hooks/bookmarks.ts b/packages/shared-react/hooks/bookmarks.ts index 7349e680..5f246b38 100644 --- a/packages/shared-react/hooks/bookmarks.ts +++ b/packages/shared-react/hooks/bookmarks.ts @@ -1,4 +1,22 @@ import { api } from "../trpc"; +import { useBookmarkGridContext } from "./bookmark-grid-context"; +import { useAddBookmarkToList } from "./lists"; + +export function useCreateBookmarkWithPostHook( + ...opts: Parameters +) { + const apiUtils = api.useUtils(); + const postCreationCB = useBookmarkPostCreationHook(); + return api.bookmarks.createBookmark.useMutation({ + ...opts, + onSuccess: async (res, req, meta) => { + apiUtils.bookmarks.getBookmarks.invalidate(); + apiUtils.bookmarks.searchBookmarks.invalidate(); + await postCreationCB(res.id); + return opts[0]?.onSuccess?.(res, req, meta); + }, + }); +} export function useDeleteBookmark( ...opts: Parameters @@ -9,7 +27,7 @@ export function useDeleteBookmark( onSuccess: (res, req, meta) => { apiUtils.bookmarks.getBookmarks.invalidate(); apiUtils.bookmarks.searchBookmarks.invalidate(); - opts[0]?.onSuccess?.(res, req, meta); + return opts[0]?.onSuccess?.(res, req, meta); }, }); } @@ -24,7 +42,7 @@ export function useUpdateBookmark( apiUtils.bookmarks.getBookmarks.invalidate(); apiUtils.bookmarks.searchBookmarks.invalidate(); apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - opts[0]?.onSuccess?.(res, req, meta); + return opts[0]?.onSuccess?.(res, req, meta); }, }); } @@ -37,7 +55,74 @@ export function useRecrawlBookmark( ...opts, onSuccess: (res, req, meta) => { apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - opts[0]?.onSuccess?.(res, req, meta); + return opts[0]?.onSuccess?.(res, req, meta); + }, + }); +} + +export function useUpdateBookmarkTags( + ...opts: Parameters +) { + const apiUtils = api.useUtils(); + return api.bookmarks.updateTags.useMutation({ + ...opts, + onSuccess: (res, req, meta) => { + apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); + + [...res.attached, ...res.detached].forEach((id) => { + apiUtils.tags.get.invalidate({ tagId: id }); + apiUtils.bookmarks.getBookmarks.invalidate({ tagId: id }); + }); + apiUtils.tags.list.invalidate(); + return opts[0]?.onSuccess?.(res, req, meta); }, }); } + +/** + * Checks the grid query context to know if we need to augment the bookmark post creation to fit the grid context + */ +export function useBookmarkPostCreationHook() { + const gridQueryCtx = useBookmarkGridContext(); + const { mutateAsync: updateBookmark } = useUpdateBookmark(); + const { mutateAsync: addToList } = useAddBookmarkToList(); + const { mutateAsync: updateTags } = useUpdateBookmarkTags(); + + return async (bookmarkId: string) => { + if (!gridQueryCtx) { + return; + } + + const promises = []; + if (gridQueryCtx.favourited ?? gridQueryCtx.archived) { + promises.push( + updateBookmark({ + bookmarkId, + favourited: gridQueryCtx.favourited, + archived: gridQueryCtx.archived, + }), + ); + } + + if (gridQueryCtx.listId) { + promises.push( + addToList({ + bookmarkId, + listId: gridQueryCtx.listId, + }), + ); + } + + if (gridQueryCtx.tagId) { + promises.push( + updateTags({ + bookmarkId, + attach: [{ tagId: gridQueryCtx.tagId }], + detach: [], + }), + ); + } + + return Promise.all(promises); + }; +} diff --git a/packages/shared-react/hooks/lists.ts b/packages/shared-react/hooks/lists.ts index 5cfcd194..f4b19c3c 100644 --- a/packages/shared-react/hooks/lists.ts +++ b/packages/shared-react/hooks/lists.ts @@ -1,5 +1,18 @@ import { api } from "../trpc"; +export function useAddBookmarkToList( + ...opts: Parameters +) { + const apiUtils = api.useUtils(); + return api.lists.addToList.useMutation({ + ...opts, + onSuccess: (res, req, meta) => { + apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId }); + return opts[0]?.onSuccess?.(res, req, meta); + }, + }); +} + export function useRemoveBookmarkFromList( ...opts: Parameters ) { @@ -8,7 +21,7 @@ export function useRemoveBookmarkFromList( ...opts, onSuccess: (res, req, meta) => { apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId }); - opts[0]?.onSuccess?.(res, req, meta); + return opts[0]?.onSuccess?.(res, req, meta); }, }); } diff --git a/packages/trpc/routers/bookmarks.test.ts b/packages/trpc/routers/bookmarks.test.ts index 58f4739d..603c18fd 100644 --- a/packages/trpc/routers/bookmarks.test.ts +++ b/packages/trpc/routers/bookmarks.test.ts @@ -123,7 +123,7 @@ describe("Bookmark Routes", () => { await api.updateTags({ bookmarkId: bookmark.id, - attach: [{ tag: "tag1" }, { tag: "tag2" }], + attach: [{ tagName: "tag1" }, { tagName: "tag2" }], detach: [], }); @@ -134,12 +134,25 @@ describe("Bookmark Routes", () => { await api.updateTags({ bookmarkId: bookmark.id, - attach: [{ tag: "tag3" }], + attach: [{ tagName: "tag3" }], detach: [{ tagId: tag1Id }], }); bookmark = await api.getBookmark({ bookmarkId: bookmark.id }); expect(bookmark.tags.map((t) => t.name).sort()).toEqual(["tag2", "tag3"]); + + await api.updateTags({ + bookmarkId: bookmark.id, + attach: [{ tagId: tag1Id }, { tagName: "tag4" }], + detach: [], + }); + bookmark = await api.getBookmark({ bookmarkId: bookmark.id }); + expect(bookmark.tags.map((t) => t.name).sort()).toEqual([ + "tag1", + "tag2", + "tag3", + "tag4", + ]); }); test("update bookmark text", async ({ apiCallers }) => { diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts index 9611829f..c042d3a1 100644 --- a/packages/trpc/routers/bookmarks.ts +++ b/packages/trpc/routers/bookmarks.ts @@ -1,5 +1,5 @@ import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; -import { and, desc, eq, exists, inArray, lte } from "drizzle-orm"; +import { and, desc, eq, exists, inArray, lte, or } from "drizzle-orm"; import invariant from "tiny-invariant"; import { z } from "zod"; @@ -523,17 +523,24 @@ export const bookmarksAppRouter = router({ bookmarkId: z.string(), attach: z.array( z.object({ - tagId: z.string().optional(), // If the tag already exists and we know its id - tag: z.string(), + // At least one of the two must be set + tagId: z.string().optional(), // If the tag already exists and we know its id we should pass it + tagName: z.string().optional(), }), ), // Detach by tag ids detach: z.array(z.object({ tagId: z.string() })), }), ) + .output( + z.object({ + attached: z.array(z.string()), + detached: z.array(z.string()), + }), + ) .use(ensureBookmarkOwnership) .mutation(async ({ input, ctx }) => { - await ctx.db.transaction(async (tx) => { + return await ctx.db.transaction(async (tx) => { // Detaches if (input.detach.length > 0) { await tx.delete(tagsOnBookmarks).where( @@ -548,21 +555,27 @@ export const bookmarksAppRouter = router({ } if (input.attach.length == 0) { - return; + return { + bookmarkId: input.bookmarkId, + attached: [], + detached: input.detach.map((t) => t.tagId), + }; } + const toAddTagNames = input.attach.flatMap((i) => + i.tagName ? [i.tagName] : [], + ); + const toAddTagIds = input.attach.flatMap((i) => + i.tagId ? [i.tagId] : [], + ); + // New Tags - const toBeCreatedTags = input.attach - .filter((i) => i.tagId === undefined) - .map((i) => ({ - name: i.tag, - userId: ctx.user.id, - })); - - if (toBeCreatedTags.length > 0) { + if (toAddTagNames.length > 0) { await tx .insert(bookmarkTags) - .values(toBeCreatedTags) + .values( + toAddTagNames.map((name) => ({ name, userId: ctx.user.id })), + ) .onConflictDoNothing() .returning(); } @@ -571,9 +584,13 @@ export const bookmarksAppRouter = router({ await tx.query.bookmarkTags.findMany({ where: and( eq(bookmarkTags.userId, ctx.user.id), - inArray( - bookmarkTags.name, - input.attach.map((t) => t.tag), + or( + toAddTagIds.length > 0 + ? inArray(bookmarkTags.id, toAddTagIds) + : undefined, + toAddTagNames.length > 0 + ? inArray(bookmarkTags.name, toAddTagNames) + : undefined, ), ), columns: { @@ -593,6 +610,11 @@ export const bookmarksAppRouter = router({ })), ) .onConflictDoNothing(); + return { + bookmarkId: input.bookmarkId, + attached: allIds, + detached: input.detach.map((t) => t.tagId), + }; }); }), });