From ed46407c41405014dda4cbd3dc7a39d04d1a15ac Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Sun, 31 Mar 2024 17:39:56 +0100 Subject: feature: Split the tags in AllTags page by who used them --- apps/web/app/dashboard/tags/page.tsx | 75 ++++++++++++++++++++++++++--------- packages/trpc/routers/tags.ts | 77 ++++++++++++++++++++++++++---------- 2 files changed, 112 insertions(+), 40 deletions(-) diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx index f87b0fcc..2ba1074c 100644 --- a/apps/web/app/dashboard/tags/page.tsx +++ b/apps/web/app/dashboard/tags/page.tsx @@ -1,8 +1,13 @@ import Link from "next/link"; -import { redirect } from "next/navigation"; import { Separator } from "@/components/ui/separator"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { api } from "@/server/api/client"; -import { getServerAuthSession } from "@/server/auth"; +import { Info } from "lucide-react"; function TagPill({ name, count }: { name: string; count: number }) { return ( @@ -16,30 +21,62 @@ function TagPill({ name, count }: { name: string; count: number }) { } export default async function TagsPage() { - const session = await getServerAuthSession(); - if (!session) { - redirect("/"); - } - - let tags = (await api.tags.list()).tags; + let allTags = (await api.tags.list()).tags; // Sort tags by usage desc - tags = tags.sort((a, b) => b.count - a.count); + 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); - let tagPill; - if (tags.length) { - tagPill = tags.map((t) => ( - - )); - } else { - tagPill = "No Tags"; - } + const tagsToPill = (tags: typeof allTags) => { + let tagPill; + if (tags.length) { + tagPill = tags.map((t) => ( + + )); + } else { + tagPill = "No Tags"; + } + return tagPill; + }; return ( -
+
All Tags -
{tagPill}
+ + +

Your Tags

+ + + + + + +

Tags that were attached at least once by you

+
+
+
+
+
{tagsToPill(humanTags)}
+ + + + +

AI Tags

+ + + + + + +

Tags that were only attached automatically (by AI)

+
+
+
+
+
{tagsToPill(aiTags)}
); } diff --git a/packages/trpc/routers/tags.ts b/packages/trpc/routers/tags.ts index f6ce752d..e84f52ac 100644 --- a/packages/trpc/routers/tags.ts +++ b/packages/trpc/routers/tags.ts @@ -5,7 +5,9 @@ import { z } from "zod"; import { bookmarks, 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"; function conditionFromInput( input: { tagName: string } | { tagId: string }, @@ -55,6 +57,13 @@ 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( @@ -68,20 +77,14 @@ export const tagsAppRouter = router({ }), ), ) - .output( - z.object({ - id: z.string(), - name: z.string(), - bookmarks: z.array(z.string()), - }), - ) + .output(zTagSchema) .use(ensureTagOwnership) .query(async ({ input, ctx }) => { const res = await ctx.db .select({ id: bookmarkTags.id, name: bookmarkTags.name, - bookmarkId: bookmarks.id, + attachedBy: tagsOnBookmarks.attachedBy, }) .from(bookmarkTags) .leftJoin(tagsOnBookmarks, eq(bookmarkTags.id, tagsOnBookmarks.tagId)) @@ -98,10 +101,21 @@ export const tagsAppRouter = router({ throw new TRPCError({ code: "NOT_FOUND" }); } + const countAttachedBy = res.reduce>( + (acc, curr) => { + if (curr.attachedBy) { + acc[curr.attachedBy]++; + } + return acc; + }, + { ai: 0, human: 0 }, + ); + return { id: res[0].id, name: res[0].name, - bookmarks: res.flatMap((t) => (t.bookmarkId ? [t.bookmarkId] : [])), + count: Object.values(countAttachedBy).reduce((s, a) => s + a, 0), + countAttachedBy, }; }), delete: authedProcedure @@ -133,26 +147,47 @@ export const tagsAppRouter = router({ list: authedProcedure .output( z.object({ - tags: z.array( - z.object({ - id: z.string(), - name: z.string(), - count: z.number(), - }), - ), + tags: z.array(zTagSchema), }), ) .query(async ({ ctx }) => { - const tags = await ctx.db + const res = await ctx.db .select({ id: tagsOnBookmarks.tagId, name: bookmarkTags.name, + attachedBy: tagsOnBookmarks.attachedBy, count: count(), }) .from(tagsOnBookmarks) - .where(eq(bookmarkTags.userId, ctx.user.id)) - .groupBy(tagsOnBookmarks.tagId) - .innerJoin(bookmarkTags, eq(bookmarkTags.id, tagsOnBookmarks.tagId)); - return { tags }; + .groupBy(tagsOnBookmarks.tagId, tagsOnBookmarks.attachedBy) + .innerJoin(bookmarkTags, eq(bookmarkTags.id, tagsOnBookmarks.tagId)) + .leftJoin(bookmarks, eq(tagsOnBookmarks.bookmarkId, bookmarks.id)) + .where( + and( + eq(bookmarkTags.userId, ctx.user.id), + eq(bookmarks.archived, false), + ), + ); + + const tags = res.reduce>>( + (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++; + acc[row.id].countAttachedBy[row.attachedBy]!++; + return acc; + }, + {}, + ); + return { tags: Object.values(tags) }; }), }); -- cgit v1.2.3-70-g09d2