aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/shared-react/hooks/lists.ts15
-rw-r--r--packages/shared/types/lists.ts13
-rw-r--r--packages/trpc/models/lists.ts46
-rw-r--r--packages/trpc/routers/lists.ts13
4 files changed, 87 insertions, 0 deletions
diff --git a/packages/shared-react/hooks/lists.ts b/packages/shared-react/hooks/lists.ts
index 4dd9bc2b..1a98cac5 100644
--- a/packages/shared-react/hooks/lists.ts
+++ b/packages/shared-react/hooks/lists.ts
@@ -36,6 +36,21 @@ export function useEditBookmarkList(
});
}
+export function useMergeLists(
+ ...opts: Parameters<typeof api.lists.merge.useMutation>
+) {
+ const apiUtils = api.useUtils();
+ return api.lists.merge.useMutation({
+ ...opts[0],
+ onSuccess: (res, req, meta) => {
+ apiUtils.lists.list.invalidate();
+ apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.targetId });
+ apiUtils.lists.stats.invalidate();
+ return opts[0]?.onSuccess?.(res, req, meta);
+ },
+ });
+}
+
export function useAddBookmarkToList(
...opts: Parameters<typeof api.lists.addToList.useMutation>
) {
diff --git a/packages/shared/types/lists.ts b/packages/shared/types/lists.ts
index 474405ee..7ef5687c 100644
--- a/packages/shared/types/lists.ts
+++ b/packages/shared/types/lists.ts
@@ -85,3 +85,16 @@ export const zEditBookmarkListSchemaWithValidation = zEditBookmarkListSchema
"Smart lists cannot have unqualified terms (aka full text search terms) in the query",
path: ["query"],
});
+
+export const zMergeListSchema = z
+ .object({
+ sourceId: z.string(),
+ targetId: z.string(),
+ deleteSourceAfterMerge: z.boolean(),
+ })
+ .refine((val) => val.sourceId !== val.targetId, {
+ message: "Cannot merge a list into itself",
+ path: ["targetId"],
+ });
+
+export type ZMergeList = z.infer<typeof zMergeListSchema>;
diff --git a/packages/trpc/models/lists.ts b/packages/trpc/models/lists.ts
index a6b1720c..8072060f 100644
--- a/packages/trpc/models/lists.ts
+++ b/packages/trpc/models/lists.ts
@@ -145,6 +145,10 @@ export abstract class List implements PrivacyAware {
abstract getSize(ctx: AuthedContext): Promise<number>;
abstract addBookmark(bookmarkId: string): Promise<void>;
abstract removeBookmark(bookmarkId: string): Promise<void>;
+ abstract mergeInto(
+ targetList: List,
+ deleteSourceAfterMerge: boolean,
+ ): Promise<void>;
}
export class SmartList extends List {
@@ -200,6 +204,16 @@ export class SmartList extends List {
message: "Smart lists cannot be removed from",
});
}
+
+ mergeInto(
+ _targetList: List,
+ _deleteSourceAfterMerge: boolean,
+ ): Promise<void> {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Smart lists cannot be merged",
+ });
+ }
}
export class ManualList extends List {
@@ -276,4 +290,36 @@ export class ManualList extends List {
}
return super.update(input);
}
+
+ async mergeInto(
+ targetList: List,
+ deleteSourceAfterMerge: boolean,
+ ): Promise<void> {
+ if (targetList.type !== "manual") {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "You can only merge into a manual list",
+ });
+ }
+
+ const bookmarkIds = await this.getBookmarkIds();
+
+ await this.ctx.db.transaction(async (tx) => {
+ await tx
+ .insert(bookmarksInLists)
+ .values(
+ bookmarkIds.map((id) => ({
+ bookmarkId: id,
+ listId: targetList.list.id,
+ })),
+ )
+ .onConflictDoNothing();
+
+ if (deleteSourceAfterMerge) {
+ await tx
+ .delete(bookmarkLists)
+ .where(eq(bookmarkLists.id, this.list.id));
+ }
+ });
+ }
}
diff --git a/packages/trpc/routers/lists.ts b/packages/trpc/routers/lists.ts
index f988eb8b..12960316 100644
--- a/packages/trpc/routers/lists.ts
+++ b/packages/trpc/routers/lists.ts
@@ -4,6 +4,7 @@ import { z } from "zod";
import {
zBookmarkListSchema,
zEditBookmarkListSchemaWithValidation,
+ zMergeListSchema,
zNewBookmarkListSchema,
} from "@karakeep/shared/types/lists";
@@ -39,6 +40,18 @@ export const listsAppRouter = router({
.mutation(async ({ input, ctx }) => {
return await ctx.list.update(input);
}),
+ merge: authedProcedure
+ .input(zMergeListSchema)
+ .mutation(async ({ input, ctx }) => {
+ const [sourceList, targetList] = await Promise.all([
+ List.fromId(ctx, input.sourceId),
+ List.fromId(ctx, input.targetId),
+ ]);
+ return await sourceList.mergeInto(
+ targetList,
+ input.deleteSourceAfterMerge,
+ );
+ }),
delete: authedProcedure
.input(
z.object({