diff options
| author | MohamedBassem <me@mbassem.com> | 2024-04-09 15:49:24 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-04-09 15:49:24 +0100 |
| commit | fe13408831dce4bdae4911098d6079a097cae9e8 (patch) | |
| tree | 228bbb192b3a0f3417a4526c382b0a3ddf7e04ff /packages | |
| parent | 994691b02515dfb579a5c3618631065bd76b9e4b (diff) | |
| download | karakeep-fe13408831dce4bdae4911098d6079a097cae9e8.tar.zst | |
feature(web): Allow uploading directly into lists/tags. Fixes #69
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/shared-react/hooks/bookmark-grid-context.tsx | 27 | ||||
| -rw-r--r-- | packages/shared-react/hooks/bookmarks.ts | 91 | ||||
| -rw-r--r-- | packages/shared-react/hooks/lists.ts | 15 | ||||
| -rw-r--r-- | packages/trpc/routers/bookmarks.test.ts | 17 | ||||
| -rw-r--r-- | packages/trpc/routers/bookmarks.ts | 56 |
5 files changed, 183 insertions, 23 deletions
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 ( + <BookmarkGridContext.Provider value={query}> + {children} + </BookmarkGridContext.Provider> + ); +} + +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<typeof api.bookmarks.createBookmark.useMutation> +) { + 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<typeof api.bookmarks.deleteBookmark.useMutation> @@ -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<typeof api.bookmarks.updateTags.useMutation> +) { + 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<typeof api.lists.removeFromList.useMutation> +) { + 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<typeof api.lists.removeFromList.useMutation> ) { @@ -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<CustomTestContext>("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), + }; }); }), }); |
