aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-04-09 15:49:24 +0100
committerMohamedBassem <me@mbassem.com>2024-04-09 15:49:24 +0100
commitfe13408831dce4bdae4911098d6079a097cae9e8 (patch)
tree228bbb192b3a0f3417a4526c382b0a3ddf7e04ff /packages
parent994691b02515dfb579a5c3618631065bd76b9e4b (diff)
downloadkarakeep-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.tsx27
-rw-r--r--packages/shared-react/hooks/bookmarks.ts91
-rw-r--r--packages/shared-react/hooks/lists.ts15
-rw-r--r--packages/trpc/routers/bookmarks.test.ts17
-rw-r--r--packages/trpc/routers/bookmarks.ts56
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),
+ };
});
}),
});