aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-04-06 02:13:47 +0100
committerMohamedBassem <me@mbassem.com>2024-04-06 02:13:47 +0100
commit044659fd1ba2082491eed713dc72bafd696f0439 (patch)
treed597da0ae1165e6a2dc50387121ea079a607b8a3
parent4cf990816817c009a512356373fdb1c4baa5e63b (diff)
downloadkarakeep-044659fd1ba2082491eed713dc72bafd696f0439.tar.zst
fix: Refresh the all tags page automatically when a tag is modified
-rw-r--r--apps/web/app/dashboard/tags/page.tsx53
-rw-r--r--apps/web/components/dashboard/bookmarks/TagsEditor.tsx8
-rw-r--r--apps/web/components/dashboard/tags/AllTagsView.tsx67
-rw-r--r--packages/trpc/routers/tags.ts50
-rw-r--r--packages/trpc/types/tags.ts8
5 files changed, 103 insertions, 83 deletions
diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx
index 9f5038e7..6caea513 100644
--- a/apps/web/app/dashboard/tags/page.tsx
+++ b/apps/web/app/dashboard/tags/page.tsx
@@ -1,62 +1,15 @@
-import Link from "next/link";
-import InfoTooltip from "@/components/ui/info-tooltip";
+import AllTagsView from "@/components/dashboard/tags/AllTagsView";
import { Separator } from "@/components/ui/separator";
import { api } from "@/server/api/client";
-function TagPill({ name, count }: { name: string; count: number }) {
- return (
- <Link
- className="flex gap-2 rounded-md border border-border bg-background px-2 py-1 text-foreground hover:bg-foreground hover:text-background"
- href={`/dashboard/tags/${name}`}
- >
- {name} <Separator orientation="vertical" /> {count}
- </Link>
- );
-}
-
export default async function TagsPage() {
- let allTags = (await api.tags.list()).tags;
-
- // Sort tags by usage desc
- allTags = allTags.sort((a, b) => b.count - a.count);
-
- const humanTags = allTags.filter((t) => (t.countAttachedBy.human ?? 0) > 0);
- const aiTags = allTags.filter((t) => (t.countAttachedBy.human ?? 0) == 0);
-
- const tagsToPill = (tags: typeof allTags) => {
- let tagPill;
- if (tags.length) {
- tagPill = tags.map((t) => (
- <TagPill key={t.id} name={t.name} count={t.count} />
- ));
- } else {
- tagPill = "No Tags";
- }
- return tagPill;
- };
+ const allTags = (await api.tags.list()).tags;
return (
<div className="space-y-4 rounded-md border bg-background p-4">
<span className="text-2xl">All Tags</span>
<Separator />
-
- <span className="flex items-center gap-2">
- <p className="text-lg">Your Tags</p>
- <InfoTooltip size={15} className="my-auto" variant="explain">
- <p>Tags that were attached at least once by you</p>
- </InfoTooltip>
- </span>
- <div className="flex flex-wrap gap-3">{tagsToPill(humanTags)}</div>
-
- <Separator />
-
- <span className="flex items-center gap-2">
- <p className="text-lg">AI Tags</p>
- <InfoTooltip size={15} className="my-auto" variant="explain">
- <p>Tags that were only attached automatically (by AI)</p>
- </InfoTooltip>
- </span>
- <div className="flex flex-wrap gap-3">{tagsToPill(aiTags)}</div>
+ <AllTagsView initialData={allTags} />
</div>
);
}
diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
index ecd6d29c..91294b2e 100644
--- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
+++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx
@@ -17,16 +17,16 @@ interface EditableTag {
export function TagsEditor({ bookmark }: { bookmark: ZBookmark }) {
const demoMode = !!useClientConfig().demoMode;
- const bookmarkInvalidationFunction =
- api.useUtils().bookmarks.getBookmark.invalidate;
+ const apiUtils = api.useUtils();
const { mutate } = api.bookmarks.updateTags.useMutation({
onSuccess: () => {
toast({
description: "Tags has been updated!",
});
- bookmarkInvalidationFunction({ bookmarkId: bookmark.id });
- // TODO(bug) Invalidate the tag views as well
+ apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: bookmark.id });
+ apiUtils.tags.list.invalidate();
+ apiUtils.tags.get.invalidate();
},
onError: () => {
toast({
diff --git a/apps/web/components/dashboard/tags/AllTagsView.tsx b/apps/web/components/dashboard/tags/AllTagsView.tsx
new file mode 100644
index 00000000..0f9ee823
--- /dev/null
+++ b/apps/web/components/dashboard/tags/AllTagsView.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import Link from "next/link";
+import InfoTooltip from "@/components/ui/info-tooltip";
+import { Separator } from "@/components/ui/separator";
+import { api } from "@/lib/trpc";
+
+import type { ZGetTagResponse } from "@hoarder/trpc/types/tags";
+
+function TagPill({ name, count }: { name: string; count: number }) {
+ return (
+ <Link
+ className="flex gap-2 rounded-md border border-border bg-background px-2 py-1 text-foreground hover:bg-foreground hover:text-background"
+ href={`/dashboard/tags/${name}`}
+ >
+ {name} <Separator orientation="vertical" /> {count}
+ </Link>
+ );
+}
+
+export default function AllTagsView({
+ initialData,
+}: {
+ initialData: ZGetTagResponse[];
+}) {
+ const { data } = api.tags.list.useQuery(undefined, {
+ initialData: { tags: initialData },
+ });
+ // Sort tags by usage desc
+ const allTags = data.tags.sort((a, b) => b.count - a.count);
+
+ const humanTags = allTags.filter((t) => (t.countAttachedBy.human ?? 0) > 0);
+ const aiTags = allTags.filter((t) => (t.countAttachedBy.human ?? 0) == 0);
+
+ const tagsToPill = (tags: typeof allTags) => {
+ let tagPill;
+ if (tags.length) {
+ tagPill = tags.map((t) => (
+ <TagPill key={t.id} name={t.name} count={t.count} />
+ ));
+ } else {
+ tagPill = "No Tags";
+ }
+ return tagPill;
+ };
+ return (
+ <>
+ <span className="flex items-center gap-2">
+ <p className="text-lg">Your Tags</p>
+ <InfoTooltip size={15} className="my-auto" variant="explain">
+ <p>Tags that were attached at least once by you</p>
+ </InfoTooltip>
+ </span>
+ <div className="flex flex-wrap gap-3">{tagsToPill(humanTags)}</div>
+
+ <Separator />
+
+ <span className="flex items-center gap-2">
+ <p className="text-lg">AI Tags</p>
+ <InfoTooltip size={15} className="my-auto" variant="explain">
+ <p>Tags that were only attached automatically (by AI)</p>
+ </InfoTooltip>
+ </span>
+ <div className="flex flex-wrap gap-3">{tagsToPill(aiTags)}</div>
+ </>
+ );
+}
diff --git a/packages/trpc/routers/tags.ts b/packages/trpc/routers/tags.ts
index b69c98e8..53b72a23 100644
--- a/packages/trpc/routers/tags.ts
+++ b/packages/trpc/routers/tags.ts
@@ -7,7 +7,7 @@ import { bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema";
import type { Context } from "../index";
import type { ZAttachedByEnum } from "../types/tags";
import { authedProcedure, router } from "../index";
-import { zAttachedByEnumSchema } from "../types/tags";
+import { zGetTagResponseSchema } from "../types/tags";
function conditionFromInput(
input: { tagName: string } | { tagId: string },
@@ -57,13 +57,6 @@ const ensureTagOwnership = experimental_trpcMiddleware<{
return opts.next();
});
-const zTagSchema = z.object({
- id: z.string(),
- name: z.string(),
- count: z.number(),
- countAttachedBy: z.record(zAttachedByEnumSchema, z.number()),
-});
-
export const tagsAppRouter = router({
get: authedProcedure
.input(
@@ -77,7 +70,7 @@ export const tagsAppRouter = router({
}),
),
)
- .output(zTagSchema)
+ .output(zGetTagResponseSchema)
.use(ensureTagOwnership)
.query(async ({ input, ctx }) => {
const res = await ctx.db
@@ -145,7 +138,7 @@ export const tagsAppRouter = router({
list: authedProcedure
.output(
z.object({
- tags: z.array(zTagSchema),
+ tags: z.array(zGetTagResponseSchema),
}),
)
.query(async ({ ctx }) => {
@@ -161,25 +154,24 @@ export const tagsAppRouter = router({
.innerJoin(bookmarkTags, eq(bookmarkTags.id, tagsOnBookmarks.tagId))
.where(eq(bookmarkTags.userId, ctx.user.id));
- const tags = res.reduce<Record<string, z.infer<typeof zTagSchema>>>(
- (acc, row) => {
- if (!(row.id in acc)) {
- acc[row.id] = {
- id: row.id,
- name: row.name,
- count: 0,
- countAttachedBy: {
- ai: 0,
- human: 0,
- },
- };
- }
- acc[row.id].count += row.count;
- acc[row.id].countAttachedBy[row.attachedBy]! += row.count;
- return acc;
- },
- {},
- );
+ const tags = res.reduce<
+ Record<string, z.infer<typeof zGetTagResponseSchema>>
+ >((acc, row) => {
+ if (!(row.id in acc)) {
+ acc[row.id] = {
+ id: row.id,
+ name: row.name,
+ count: 0,
+ countAttachedBy: {
+ ai: 0,
+ human: 0,
+ },
+ };
+ }
+ acc[row.id].count += row.count;
+ acc[row.id].countAttachedBy[row.attachedBy]! += row.count;
+ return acc;
+ }, {});
return { tags: Object.values(tags) };
}),
});
diff --git a/packages/trpc/types/tags.ts b/packages/trpc/types/tags.ts
index 7a99dad4..c9fe2a93 100644
--- a/packages/trpc/types/tags.ts
+++ b/packages/trpc/types/tags.ts
@@ -8,3 +8,11 @@ export const zBookmarkTagSchema = z.object({
attachedBy: zAttachedByEnumSchema,
});
export type ZBookmarkTags = z.infer<typeof zBookmarkTagSchema>;
+
+export const zGetTagResponseSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ count: z.number(),
+ countAttachedBy: z.record(zAttachedByEnumSchema, z.number()),
+});
+export type ZGetTagResponse = z.infer<typeof zGetTagResponseSchema>;