aboutsummaryrefslogtreecommitdiffstats
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
parent994691b02515dfb579a5c3618631065bd76b9e4b (diff)
downloadkarakeep-fe13408831dce4bdae4911098d6079a097cae9e8.tar.zst
feature(web): Allow uploading directly into lists/tags. Fixes #69
-rw-r--r--apps/web/app/dashboard/archive/page.tsx1
-rw-r--r--apps/web/app/dashboard/bookmarks/page.tsx21
-rw-r--r--apps/web/app/dashboard/favourites/page.tsx1
-rw-r--r--apps/web/app/dashboard/layout.tsx2
-rw-r--r--apps/web/app/dashboard/lists/[listId]/page.tsx28
-rw-r--r--apps/web/app/dashboard/tags/[tagName]/page.tsx1
-rw-r--r--apps/web/components/dashboard/UploadDropzone.tsx25
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx6
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx38
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx21
-rw-r--r--apps/web/components/dashboard/bookmarks/EditorCard.tsx7
-rw-r--r--apps/web/components/dashboard/bookmarks/TagsEditor.tsx14
-rw-r--r--apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx10
-rw-r--r--apps/web/lib/hooks/list-context.tsx21
-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
19 files changed, 258 insertions, 144 deletions
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 (
<div>
- <UploadDropzone>
- <TopNav />
- <Separator />
- <div className="my-4 flex-1">
- <Bookmarks
- header={<p className="text-2xl">Bookmarks</p>}
- query={{ archived: false }}
- showEditorCard={true}
- />
- </div>
- </UploadDropzone>
+ <TopNav />
+ <Separator />
+ <div className="my-4 flex-1">
+ <Bookmarks
+ header={<p className="text-2xl">Bookmarks</p>}
+ query={{ archived: false }}
+ showEditorCard={true}
+ />
+ </div>
</div>
);
}
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={<p className="text-2xl">⭐️ Favourites</p>}
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({
<MobileSidebar />
<Separator />
</div>
- <div className="container p-4">{children}</div>
+ <div className="container min-h-screen p-4">{children}</div>
</main>
</div>
);
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 (
- <BookmarkListContextProvider listId={list.id}>
- <Bookmarks
- query={{ listId: list.id }}
- showDivider={true}
- header={
- <div className="flex justify-between">
- <span className="text-2xl">
- {list.icon} {list.name}
- </span>
- <DeleteListButton list={list} />
- </div>
- }
- />
- </BookmarkListContextProvider>
+ <Bookmarks
+ query={{ listId: list.id }}
+ showDivider={true}
+ showEditorCard={true}
+ header={
+ <div className="flex justify-between">
+ <span className="text-2xl">
+ {list.icon} {list.name}
+ </span>
+ <DeleteListButton list={list} />
+ </div>
+ }
+ />
);
}
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({
</div>
}
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))}
</Masonry>
{hasNextPage && (
- <ActionButton
- ignoreDemoMode={true}
- loading={isFetchingNextPage}
- onClick={() => fetchNextPage()}
- className="mx-auto w-min"
- variant="ghost"
- >
- Load More
- </ActionButton>
+ <div className="flex justify-center">
+ <ActionButton
+ ignoreDemoMode={true}
+ loading={isFetchingNextPage}
+ onClick={() => fetchNextPage()}
+ variant="ghost"
+ >
+ Load More
+ </ActionButton>
+ </div>
)}
</>
);
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<HTMLTextAreaElement>) {
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 = (
<BookmarksGrid
bookmarks={data!.pages.flatMap((b) => b.bookmarks)}
hasNextPage={hasNextPage}
@@ -38,4 +40,10 @@ export default function UpdatableBookmarksGrid({
showEditorCard={showEditorCard}
/>
);
+
+ return (
+ <BookmarkGridContextProvider query={query}>
+ {showEditorCard ? <UploadDropzone>{grid}</UploadDropzone> : grid}
+ </BookmarkGridContextProvider>
+ );
}
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 (
- <BookmarkListContext.Provider value={{ listId }}>
- {children}
- </BookmarkListContext.Provider>
- );
-}
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),
+ };
});
}),
});