diff --git a/api/src/services/identity.service.ts b/api/src/services/identity.service.ts
index 11bfe31d..6d44b4ea 100644
--- a/api/src/services/identity.service.ts
+++ b/api/src/services/identity.service.ts
@@ -319,6 +319,7 @@ export type ProfileActivityRow = {
base_slug: string | null;
review_case_id: string | null;
revision_id: string | null;
+ subjects: Array<{ slug: string; name: string }>;
};
// Earliest revision id per parent, so a row can be tagged as the "creation"
@@ -463,6 +464,7 @@ export async function getProfileActivity(
base_slug: guide ? (slugByBase.get(guide.guide_base_id) ?? null) : null,
review_case_id: caseId,
revision_id: rev.id,
+ subjects: [],
});
}
}
@@ -509,6 +511,7 @@ export async function getProfileActivity(
base_slug: null,
review_case_id: null,
revision_id: rev.id,
+ subjects: [],
});
}
}
@@ -615,11 +618,47 @@ export async function getProfileActivity(
? caseId
: null,
revision_id: rev ?? null,
+ subjects: [],
});
}
}
}
+ // Batch-fetch subject tags for all guide revision rows in one query.
+ const guideRevisionIds = rows
+ .filter((r) => r.content_kind === "guide" && r.revision_id !== null)
+ .map((r) => r.revision_id as string);
+
+ if (guideRevisionIds.length > 0) {
+ const { data: tagData, error: tagError } = await supabase
+ .from("guide_revision_subjects")
+ .select("guide_revision_id, subjects(slug, name)")
+ .in("guide_revision_id", guideRevisionIds);
+
+ if (tagError) fail(tagError);
+
+ // Build a map from revision_id -> subject list, then hydrate the rows.
+ const subjectsByRev = new Map<
+ string,
+ Array<{ slug: string; name: string }>
+ >();
+ for (const tag of tagData ?? []) {
+ const subj = tag.subjects;
+ if (!subj || typeof subj !== "object" || Array.isArray(subj)) continue;
+ const { slug, name } = subj as { slug: string | null; name: string };
+ if (slug === null) continue;
+ const list = subjectsByRev.get(tag.guide_revision_id) ?? [];
+ list.push({ slug, name });
+ subjectsByRev.set(tag.guide_revision_id, list);
+ }
+
+ for (const row of rows) {
+ if (row.content_kind === "guide" && row.revision_id !== null) {
+ row.subjects = subjectsByRev.get(row.revision_id) ?? [];
+ }
+ }
+ }
+
rows.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
return rows;
}
diff --git a/api/tests/identity.test.ts b/api/tests/identity.test.ts
index df125df7..d7f7f72e 100644
--- a/api/tests/identity.test.ts
+++ b/api/tests/identity.test.ts
@@ -14,6 +14,7 @@ import {
createGuideRevision,
createPublishedGuide,
} from "./factories/guides";
+import { createSubject, tagGuideRevision } from "./factories/subjects";
import { expectToMatchSpec } from "./openapi";
describe("GET /me", () => {
@@ -197,4 +198,46 @@ describe("GET /profiles/{username}", () => {
expect(res.status).toBe(404);
await expectToMatchSpec(res, "GET", "/profiles/{username}");
});
+
+ it("includes subject tags on guide revision activity rows", async () => {
+ const { token, userId } = await makeUser();
+ const username = await getUsername(userId);
+ const subject = await createSubject();
+ const { revision } = await createPublishedGuide({ authorId: userId });
+ await tagGuideRevision(revision.id, subject.id);
+
+ const res = await app.request(`/profiles/${username}`, auth(token), env);
+
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as {
+ activity: Array<{
+ revision_id: string | null;
+ subjects: Array<{ slug: string; name: string }>;
+ }>;
+ };
+ const row = body.activity.find((r) => r.revision_id === revision.id);
+ expect(row).toBeDefined();
+ expect(row?.subjects).toEqual(
+ expect.arrayContaining([{ slug: subject.slug, name: subject.name }])
+ );
+ });
+
+ it("returns an empty subjects array for untagged guide revisions", async () => {
+ const { token, userId } = await makeUser();
+ const username = await getUsername(userId);
+ const { revision } = await createPublishedGuide({ authorId: userId });
+
+ const res = await app.request(`/profiles/${username}`, auth(token), env);
+
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as {
+ activity: Array<{
+ revision_id: string | null;
+ subjects: Array<{ slug: string; name: string }>;
+ }>;
+ };
+ const row = body.activity.find((r) => r.revision_id === revision.id);
+ expect(row).toBeDefined();
+ expect(row?.subjects).toEqual([]);
+ });
});
diff --git a/app/src/components/ActivityColumnFilters.tsx b/app/src/components/ActivityColumnFilters.tsx
index f4ba599e..83ac898e 100644
--- a/app/src/components/ActivityColumnFilters.tsx
+++ b/app/src/components/ActivityColumnFilters.tsx
@@ -191,9 +191,9 @@ export function ChoiceColumnFilter({
setFilters,
}: {
label: string;
- field: "type" | "status";
+ field: "type" | "status" | "subject";
options: ReadonlyArray<{
- value: ActivityTypeFilter | ActivityStatusFilter;
+ value: ActivityTypeFilter | ActivityStatusFilter | string;
label: string;
}>;
search: ActivityFilters;
diff --git a/app/src/components/profile/ActivityTable.tsx b/app/src/components/profile/ActivityTable.tsx
index cf2b7899..cfb96ef7 100644
--- a/app/src/components/profile/ActivityTable.tsx
+++ b/app/src/components/profile/ActivityTable.tsx
@@ -7,6 +7,7 @@ import {
activityStatusLabel,
activityTypeLabel,
filterActivity,
+ getActivitySubjectOptions,
} from "@/lib/profile";
import { formatDate } from "@/lib/guideUtils";
import { cn } from "@/lib/utils";
@@ -95,6 +96,7 @@ export function ActivityTable({
const hasFilters = Boolean(
search.type?.length ||
search.status?.length ||
+ search.subject?.length ||
search.title ||
search.summary ||
search.from ||
@@ -120,6 +122,10 @@ export function ActivityTable({
? "No activity matches these filters."
: "No activity available yet.";
+ // Derive subject options from the full activity (before filtering) so all
+ // available subject choices are always visible, even when other filters are active.
+ const subjectOptions = getActivitySubjectOptions(activity);
+
return (
<>
@@ -151,6 +157,15 @@ export function ActivityTable({
search={search}
setFilters={setFilters}
/>
+ {subjectOptions.length > 0 && (
+
+ )}
{pageRows.length === 0 ? (
@@ -188,6 +203,20 @@ export function ActivityTable({
)}
+ {row.subjects.length > 0 && (
+
+ {row.subjects.map((s) => (
+
+ {s.name}
+
+ ))}
+
+ )}
+
{formatDate(new Date(row.created_at))}
@@ -256,6 +285,19 @@ export function ActivityTable({
setFilters={setFilters}
/>
+
+ {subjectOptions.length > 0 ? (
+
+ ) : (
+ "Subject"
+ )}
+
Review Case
@@ -265,7 +307,7 @@ export function ActivityTable({
{pageRows.length === 0 ? (
{emptyMessage}
@@ -303,6 +345,24 @@ export function ActivityTable({
+
+ {row.subjects.length > 0 ? (
+
+ {row.subjects.map((s) => (
+
+ {s.name}
+
+ ))}
+
+ ) : (
+ —
+ )}
+
+
{row.review_case_id ? (