Skip to content

Commit

Permalink
Improve the CLI #209
Browse files Browse the repository at this point in the history
added the possibility to assign tags to bookmarks while creating
added the possibility to assign a newly created to a list right away
added the possibility to add and remove tags from bookmarks
  • Loading branch information
kamtschatka committed Jun 10, 2024
1 parent 219329b commit e15cf01
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 43 deletions.
4 changes: 2 additions & 2 deletions apps/browser-extension/src/components/TagsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ export function TagsSelector({ bookmarkId }: { bookmarkId: string }) {
const { mutate } = useUpdateBookmarkTags({
onMutate: (req) => {
req.attach.forEach((t) => currentlyUpdating.add(t.tagId ?? ""));
req.detach.forEach((t) => currentlyUpdating.add(t.tagId));
req.detach.forEach((t) => currentlyUpdating.add(t.tagId ?? ""));
},
onSettled: (_resp, _err, req) => {
if (!req) {
return;
}
req.attach.forEach((t) => currentlyUpdating.delete(t.tagId ?? ""));
req.detach.forEach((t) => currentlyUpdating.delete(t.tagId));
req.detach.forEach((t) => currentlyUpdating.delete(t.tagId ?? ""));
},
});

Expand Down
98 changes: 87 additions & 11 deletions apps/cli/src/commands/bookmarks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from "node:fs";
import { addToList } from "@/commands/lists";
import {
printError,
printObject,
Expand All @@ -20,7 +21,11 @@ function collect<T>(val: T, acc: T[]) {
return acc;
}

function normalizeBookmark(bookmark: ZBookmark) {
type Bookmark = Omit<ZBookmark, "tags"> & {
tags: string[];
};

function normalizeBookmark(bookmark: ZBookmark): Bookmark {
const ret = {
...bookmark,
tags: bookmark.tags.map((t) => t.name),
Expand Down Expand Up @@ -55,10 +60,17 @@ bookmarkCmd
[],
)
.option("--stdin", "reads the data from stdin and store it as a note")
.option("--list <id>", "if set, the bookmark(s) will be added to this list")
.option(
"--tag <tag>",
"if set, this tag will be added to the bookmark(s). Specify multiple times to add multiple tags",
collect<string>,
[],
)
.action(async (opts) => {
const api = getAPIClient();

const results: object[] = [];
const results: Bookmark[] = [];

const promises = [
...opts.link.map((url) =>
Expand Down Expand Up @@ -101,6 +113,12 @@ bookmarkCmd

await Promise.allSettled(promises);
printObject(results);
for (const bookmark of results) {
await updateTags(opts.tag, [], bookmark.id);
if (opts.list) {
await addToList(opts.list, bookmark.id);
}
}
});

bookmarkCmd
Expand All @@ -115,6 +133,48 @@ bookmarkCmd
.catch(printError(`Failed to get the bookmark with id "${id}"`));
});

function printTagMessage(
tags: { tagName: string }[],
bookmarkId: string,
action: "Added" | "Removed",
) {
tags.forEach((tag) => {
printStatusMessage(
true,
`${action} the tag ${tag.tagName} ${action === "Added" ? "to" : "from"} the bookmark with id ${bookmarkId}`,
);
});
}

async function updateTags(addTags: string[], removeTags: string[], id: string) {
const tagsToAdd = addTags.map((addTag) => {
return { tagName: addTag };
});

const tagsToRemove = removeTags.map((removeTag) => {
return { tagName: removeTag };
});

if (tagsToAdd.length > 0 || tagsToRemove.length > 0) {
const api = getAPIClient();
await api.bookmarks.updateTags
.mutate({
bookmarkId: id,
attach: tagsToAdd,
detach: tagsToRemove,
})
.then(() => {
printTagMessage(tagsToAdd, id, "Added");
printTagMessage(tagsToRemove, id, "Removed");
})
.catch(
printError(
`Failed to add/remove tags to/from bookmark with id "${id}"`,
),
);
}
}

bookmarkCmd
.command("update")
.description("update a bookmark")
Expand All @@ -124,18 +184,34 @@ bookmarkCmd
.option("--no-archive", "if set, the bookmark will be unarchived")
.option("--favourite", "if set, the bookmark will be favourited")
.option("--no-favourite", "if set, the bookmark will be unfavourited")
.option(
"--addtag <tag>",
"if set, this tag will be added to the bookmark. Specify multiple times to add multiple tags",
collect<string>,
[],
)
.option(
"--removetag <tag>",
"if set, this tag will be removed from the bookmark. Specify multiple times to remove multiple tags",
collect<string>,
[],
)
.argument("<id>", "the id of the bookmark to get")
.action(async (id, opts) => {
const api = getAPIClient();
await api.bookmarks.updateBookmark
.mutate({
bookmarkId: id,
archived: opts.archive,
favourited: opts.favourite,
title: opts.title,
})
.then(printObject)
.catch(printError(`Failed to update bookmark with id "${id}"`));
await updateTags(opts.addtag, opts.removetag, id);

if ("archive" in opts || "favourite" in opts || "title" in opts) {
await api.bookmarks.updateBookmark
.mutate({
bookmarkId: id,
archived: opts.archive,
favourited: opts.favourite,
title: opts.title,
})
.then(printObject)
.catch(printError(`Failed to update bookmark with id "${id}"`));
}
});

bookmarkCmd
Expand Down
38 changes: 21 additions & 17 deletions apps/cli/src/commands/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,29 +62,33 @@ listsCmd
.catch(printError(`Failed to delete list with id "${id}"`));
});

export async function addToList(listId: string, bookmarkId: string) {
const api = getAPIClient();

await api.lists.addToList
.mutate({
listId,
bookmarkId,
})
.then(
printSuccess(
`Successfully added bookmark "${bookmarkId}" to list with id "${listId}"`,
),
)
.catch(
printError(
`Failed to add bookmark "${bookmarkId}" to list with id "${listId}"`,
),
);
}

listsCmd
.command("add-bookmark")
.description("add a bookmark to list")
.requiredOption("--list <id>", "the id of the list")
.requiredOption("--bookmark <bookmark>", "the id of the bookmark")
.action(async (opts) => {
const api = getAPIClient();

await api.lists.addToList
.mutate({
listId: opts.list,
bookmarkId: opts.bookmark,
})
.then(
printSuccess(
`Successfully added bookmark "${opts.bookmark}" to list with id "${opts.list}"`,
),
)
.catch(
printError(
`Failed to add bookmark "${opts.bookmark}" to list with id "${opts.list}"`,
),
);
await addToList(opts.list, opts.bookmark);
});

listsCmd
Expand Down
63 changes: 63 additions & 0 deletions packages/trpc/routers/bookmarks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,69 @@ describe("Bookmark Routes", () => {
expect(res.favourited).toBeTruthy();
});

test<CustomTestContext>("update tags on bookmarks", async ({
apiCallers,
}) => {
const api = apiCallers[0].bookmarks;

// Create the bookmark
const bookmark = await api.createBookmark({
url: "https://google.com",
type: "link",
});

await api.updateTags({
bookmarkId: bookmark.id,
attach: [{ tagName: "asdf" }, { tagName: "qwer" }],
detach: [],
});

let res = await api.getBookmark({ bookmarkId: bookmark.id });
expect(res.tags.length).toBe(2);
for (const tag of res.tags) {
if (tag.name !== "qwer" && tag.name !== "asdf") {
throw new Error("tag.name is neither qwer nor asdf");
}
}

// Adding the same tags again, doesn't change anything
await api.updateTags({
bookmarkId: bookmark.id,
attach: [{ tagName: "asdf" }, { tagName: "qwer" }],
detach: [],
});
res = await api.getBookmark({ bookmarkId: bookmark.id });
expect(res.tags.length).toBe(2);

// Empty arrays don't do anything
await api.updateTags({
bookmarkId: bookmark.id,
attach: [],
detach: [],
});
res = await api.getBookmark({ bookmarkId: bookmark.id });
expect(res.tags.length).toBe(2);

await api.updateTags({
bookmarkId: bookmark.id,
attach: [],
detach: [{ tagName: "asdf" }, { tagName: "qwer" }],
});

res = await api.getBookmark({ bookmarkId: bookmark.id });
expect(res.tags.length).toBe(0);

// Removing the same tags again, does not do anything either
await api.updateTags({
bookmarkId: bookmark.id,
attach: [],
detach: [{ tagName: "asdf" }, { tagName: "qwer" }],
});

res = await api.getBookmark({ bookmarkId: bookmark.id });
expect(res.tags.length).toBe(0);
});

test<CustomTestContext>("list bookmarks", async ({ apiCallers }) => {
const api = apiCallers[0].bookmarks;
const emptyBookmarks = await api.getBookmarks({});
Expand Down
57 changes: 44 additions & 13 deletions packages/trpc/routers/bookmarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,8 +615,13 @@ export const bookmarksAppRouter = router({
tagName: z.string().optional(),
}),
),
// Detach by tag ids
detach: z.array(z.object({ tagId: z.string() })),
detach: z.array(
z.object({
// At least one of the two must be set
tagId: z.string().optional(),
tagName: z.string().optional(), // Also allow removing by tagName, to make CLI usage easier
}),
),
}),
)
.output(
Expand All @@ -627,25 +632,51 @@ export const bookmarksAppRouter = router({
)
.use(ensureBookmarkOwnership)
.mutation(async ({ input, ctx }) => {
return await ctx.db.transaction(async (tx) => {
return ctx.db.transaction(async (tx) => {
// Detaches
const idsToRemove: string[] = [];
if (input.detach.length > 0) {
await tx.delete(tagsOnBookmarks).where(
and(
eq(tagsOnBookmarks.bookmarkId, input.bookmarkId),
inArray(
tagsOnBookmarks.tagId,
input.detach.map((t) => t.tagId),
const namesToRemove: string[] = [];
input.detach.forEach((detachInfo) => {
if (detachInfo.tagId) {
idsToRemove.push(detachInfo.tagId);
}
if (detachInfo.tagName) {
namesToRemove.push(detachInfo.tagName);
}
});

if (namesToRemove.length > 0) {
(
await tx.query.bookmarkTags.findMany({
where: and(
eq(bookmarkTags.userId, ctx.user.id),
inArray(bookmarkTags.name, namesToRemove),
),
columns: {
id: true,
},
})
).forEach((tag) => {
idsToRemove.push(tag.id);
});
}

await tx
.delete(tagsOnBookmarks)
.where(
and(
eq(tagsOnBookmarks.bookmarkId, input.bookmarkId),
inArray(tagsOnBookmarks.tagId, idsToRemove),
),
),
);
);
}

if (input.attach.length == 0) {
return {
bookmarkId: input.bookmarkId,
attached: [],
detached: input.detach.map((t) => t.tagId),
detached: idsToRemove,
};
}

Expand Down Expand Up @@ -701,7 +732,7 @@ export const bookmarksAppRouter = router({
return {
bookmarkId: input.bookmarkId,
attached: allIds,
detached: input.detach.map((t) => t.tagId),
detached: idsToRemove,
};
});
}),
Expand Down

0 comments on commit e15cf01

Please sign in to comment.