import Link from "next/link"; import { notFound } from "next/navigation"; import { and, desc, eq, inArray, isNull, or } from "drizzle-orm"; import { auth } from "@/auth"; import { db } from "@/lib/db/client"; import { memories, projects, projectShares, groups, users } from "@/lib/db/schema"; import { updateMemoryAction, deleteMemoryAction } from "@/lib/memory-actions"; import { getProjectAccess, getUserGroupNames, readableProjectIds } from "@/lib/access"; import { Container, PageHeader } from "@/app/_components/ui/container"; import { Card, CardBody, CardHeader } from "@/app/_components/ui/card"; import { Input, Textarea, Label } from "@/app/_components/ui/input"; import { Button } from "@/app/_components/ui/button"; import { Badge } from "@/app/_components/ui/badge"; export const dynamic = "force-dynamic"; export default async function MemoryDetailPage({ params, searchParams, }: { params: Promise<{ id: string }>; searchParams: Promise<{ edit?: string }>; }) { const session = await auth(); const userId = session!.user.id; const groupNames = await getUserGroupNames(userId); const { id } = await params; const { edit } = await searchParams; // Widen the visibility predicate: a user can see a memory they own, // or any memory whose project is shared with them. Project_id filter // uses the precomputed accessible-id list for parity with the search // / list paths. const accessibleProjectIds = await readableProjectIds(userId, groupNames); const visibility = accessibleProjectIds.length > 0 ? or( eq(memories.userId, userId), inArray(memories.projectId, accessibleProjectIds), ) : eq(memories.userId, userId); const rows = await db .select({ id: memories.id, scope: memories.scope, content: memories.content, tags: memories.tags, version: memories.version, lastEditedBy: memories.lastEditedBy, createdAt: memories.createdAt, updatedAt: memories.updatedAt, projectKey: projects.key, projectId: memories.projectId, ownerUserId: memories.userId, }) .from(memories) .leftJoin(projects, eq(memories.projectId, projects.id)) .where(and(eq(memories.id, id), isNull(memories.deletedAt), visibility!)) .limit(1); const m = rows[0]; if (!m) notFound(); // Determine the viewer's write permission. user-scope memories = // owner-only; project-scope = canWriteProject. Used to gate the // Edit / Delete affordances. let canWrite: boolean; if (m.scope === "user") { canWrite = m.ownerUserId === userId; } else if (m.projectId) { const access = await getProjectAccess(userId, groupNames, m.projectId); canWrite = access === "owner" || access === "rw"; } else { canWrite = false; } const isEditing = edit === "1" && canWrite; // Shares on this project drive the "Shared" chip plus an editor-name // lookup (we want to display who last edited, even if they're another // member of the same group). const shareRows = m.projectId ? await db .select({ groupName: groups.name }) .from(projectShares) .innerJoin(groups, eq(groups.id, projectShares.groupId)) .where(eq(projectShares.projectId, m.projectId)) : []; const editorRow = m.lastEditedBy ? await db .select({ name: users.name, email: users.email }) .from(users) .where(eq(users.id, m.lastEditedBy)) .limit(1) : []; const editorLabel = editorRow[0] ? editorRow[0].name ?? editorRow[0].email ?? "unknown" : null; const projectList = isEditing ? await db .select({ key: projects.key, displayName: projects.displayName }) .from(projects) .where(eq(projects.userId, userId)) .orderBy(desc(projects.updatedAt)) .limit(50) : []; return ( {m.id}} actions={ <> {!isEditing && canWrite ? ( ) : null} } /> {m.scope} {m.projectKey ? {m.projectKey} : null} {shareRows.length > 0 ? ( s.groupName).join(", ")}`} > Shared ) : null} · Created {new Date(m.createdAt).toLocaleString()} {m.updatedAt.getTime() !== m.createdAt.getTime() ? ( · Updated {new Date(m.updatedAt).toLocaleString()} ) : null} {editorLabel && m.lastEditedBy !== m.ownerUserId ? ( · Last edited by {editorLabel} ) : null} {isEditing ? (
{projectList.length > 0 ? ( {projectList.map((p) => ( ))} ) : null}