From 0c7f4c73fbbffb57592a97160b7335c28d4d1855 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Tue, 28 Jan 2025 23:00:01 +1100 Subject: [PATCH] Handle side bar updates --- .../projects/[projectId]/documents/actions.ts | 20 +++++--- .../projects/[projectId]/tasklists/actions.ts | 32 +++++++------ app/(dashboard)/[tenant]/settings/actions.ts | 7 ++- components/nav-main.tsx | 47 ++++++++++++++----- lib/utils/cable-server.ts | 12 ++--- 5 files changed, 79 insertions(+), 39 deletions(-) diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/documents/actions.ts b/app/(dashboard)/[tenant]/projects/[projectId]/documents/actions.ts index f30a056..e514b78 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/documents/actions.ts +++ b/app/(dashboard)/[tenant]/projects/[projectId]/documents/actions.ts @@ -3,6 +3,7 @@ import { blob, comment, document, documentFolder } from "@/drizzle/schema"; import { generateObjectDiffMessage, logActivity } from "@/lib/activity"; import { deleteFile } from "@/lib/blobStore"; +import { broadcastEvent } from "@/lib/utils/cable-server"; import { database } from "@/lib/utils/useDatabase"; import { deleteFilesInMarkdown } from "@/lib/utils/useMarkdown"; import { getOwner } from "@/lib/utils/useOwner"; @@ -124,7 +125,7 @@ export async function updateDocument(payload: FormData) { } export async function createDocumentFolder(payload: FormData) { - const { userId, orgSlug } = await getOwner(); + const { userId, orgSlug, ownerId } = await getOwner(); const name = payload.get("name") as string; const description = payload.get("description") as string; const projectId = payload.get("projectId") as string; @@ -135,8 +136,7 @@ export async function createDocumentFolder(payload: FormData) { }); const db = await database(); - await db - .insert(documentFolder) + db.insert(documentFolder) .values({ ...data, projectId: +projectId, @@ -153,13 +153,15 @@ export async function createDocumentFolder(payload: FormData) { projectId: +projectId, }); + await broadcastEvent("update_sidebar", ownerId); + revalidatePath(`/${orgSlug}/projects/${projectId}`); revalidatePath(`/${orgSlug}/projects/${projectId}/documents`); redirect(`/${orgSlug}/projects/${projectId}/documents`); } export async function updateDocumentFolder(payload: FormData) { - const { orgSlug } = await getOwner(); + const { orgSlug, ownerId } = await getOwner(); const name = payload.get("name") as string; const description = payload.get("description") as string; const id = payload.get("id") as string; @@ -175,7 +177,7 @@ export async function updateDocumentFolder(payload: FormData) { .findFirst({ where: eq(documentFolder.id, +id) }) .execute(); - const folderDetails = await db + const folderDetails = db .update(documentFolder) .set({ ...data, @@ -195,13 +197,15 @@ export async function updateDocumentFolder(payload: FormData) { projectId: +projectId, }); + await broadcastEvent("update_sidebar", ownerId); + revalidatePath(`/${orgSlug}/projects/${projectId}`); revalidatePath(`/${orgSlug}/projects/${projectId}/documents/folders/${id}`); redirect(`/${orgSlug}/projects/${projectId}/documents/folders/${id}`); } export async function deleteDocumentFolder(payload: FormData) { - const { orgSlug } = await getOwner(); + const { orgSlug, ownerId } = await getOwner(); const id = payload.get("id") as string; const projectId = payload.get("projectId") as string; const currentPath = payload.get("currentPath") as string; @@ -226,6 +230,8 @@ export async function deleteDocumentFolder(payload: FormData) { projectId: +projectId, }); + await broadcastEvent("update_sidebar", ownerId); + revalidatePath(currentPath); redirect(`/${orgSlug}/projects/${projectId}/documents`); } @@ -295,7 +301,7 @@ export async function deleteBlob( await deleteFile(file.key); const db = await database(); - const blobDetails = await db + const blobDetails = db .delete(blob) .where(eq(blob.id, file.id)) .returning() diff --git a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions.ts b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions.ts index e9b134a..4fe30d2 100644 --- a/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions.ts +++ b/app/(dashboard)/[tenant]/projects/[projectId]/tasklists/actions.ts @@ -2,6 +2,7 @@ import { task, taskList } from "@/drizzle/schema"; import { generateObjectDiffMessage, logActivity } from "@/lib/activity"; +import { broadcastEvent } from "@/lib/utils/cable-server"; import { database } from "@/lib/utils/useDatabase"; import { getOwner } from "@/lib/utils/useOwner"; import { and, desc, eq } from "drizzle-orm"; @@ -42,7 +43,7 @@ const taskSchema = z.object({ }); export async function createTaskList(payload: FormData) { - const { userId, orgSlug } = await getOwner(); + const { userId, orgSlug, ownerId } = await getOwner(); const name = payload.get("name") as string; const description = payload.get("description") as string; const dueDate = payload.get("dueDate") as string; @@ -74,12 +75,14 @@ export async function createTaskList(payload: FormData) { projectId: +projectId, }); + await broadcastEvent("update_sidebar", ownerId); + revalidatePath(`/${orgSlug}/projects/${projectId}/tasklists`); redirect(`/${orgSlug}/projects/${projectId}/tasklists`); } export async function updateTaskList(payload: FormData) { - const { orgSlug } = await getOwner(); + const { orgSlug, ownerId } = await getOwner(); const id = payload.get("id") as string; const name = payload.get("name") as string; const description = payload.get("description") as string; @@ -100,8 +103,7 @@ export async function updateTaskList(payload: FormData) { }) .execute(); - await db - .update(taskList) + db.update(taskList) .set({ ...data, updatedAt: new Date(), @@ -120,6 +122,8 @@ export async function updateTaskList(payload: FormData) { projectId: +projectId, }); + await broadcastEvent("update_sidebar", ownerId); + revalidatePath(`/${orgSlug}/projects/${projectId}/tasklists`); redirect(`/${orgSlug}/projects/${projectId}/tasklists`); } @@ -136,7 +140,7 @@ export async function partialUpdateTaskList( }) .execute(); - const updated = await db + const updated = db .update(taskList) .set({ ...data, @@ -164,7 +168,7 @@ export async function deleteTaskList(payload: FormData) { const id = payload.get("id") as string; const projectId = payload.get("projectId") as string; - const { orgSlug } = await getOwner(); + const { orgSlug, ownerId } = await getOwner(); const db = await database(); const taskListDetails = db .delete(taskList) @@ -179,6 +183,7 @@ export async function deleteTaskList(payload: FormData) { projectId: +projectId, }); + await broadcastEvent("update_sidebar", ownerId); revalidatePath(`/${orgSlug}/projects/${projectId}/tasklists`); } @@ -217,8 +222,7 @@ export async function createTask({ ? lastPosition?.position + POSITION_INCREMENT : 1; - await db - .insert(task) + db.insert(task) .values({ ...data, position, @@ -335,7 +339,7 @@ export async function repositionTask( } export async function forkTaskList(taskListId: number, projectId: number) { - const { orgSlug } = await getOwner(); + const { orgSlug, ownerId } = await getOwner(); const db = await database(); const taskListDetails = await db.query.taskList @@ -347,8 +351,7 @@ export async function forkTaskList(taskListId: number, projectId: number) { throw new Error("Task list not found"); } - await db - .update(taskList) + db.update(taskList) .set({ status: "archived", updatedAt: new Date(), @@ -356,7 +359,7 @@ export async function forkTaskList(taskListId: number, projectId: number) { .where(eq(taskList.id, +taskListId)) .run(); - const newTaskList = await db + const newTaskList = db .insert(taskList) .values({ name: taskListDetails.name, @@ -371,8 +374,7 @@ export async function forkTaskList(taskListId: number, projectId: number) { .returning() .get(); - await db - .update(task) + db.update(task) .set({ taskListId: newTaskList.id, updatedAt: new Date(), @@ -387,6 +389,8 @@ export async function forkTaskList(taskListId: number, projectId: number) { projectId: +projectId, }); + await broadcastEvent("update_sidebar", ownerId); + revalidatePath(`/${orgSlug}/projects/${projectId}/tasklists`); } diff --git a/app/(dashboard)/[tenant]/settings/actions.ts b/app/(dashboard)/[tenant]/settings/actions.ts index f206804..85d727e 100644 --- a/app/(dashboard)/[tenant]/settings/actions.ts +++ b/app/(dashboard)/[tenant]/settings/actions.ts @@ -3,7 +3,7 @@ import { logtoConfig } from "@/app/logto"; import { notification, user } from "@/drizzle/schema"; import { updateUser } from "@/lib/ops/auth"; -import { getStreamFor, getToken } from "@/lib/utils/cable-server"; +import { getStreamFor } from "@/lib/utils/cable-server"; import { database } from "@/lib/utils/useDatabase"; import { getOwner } from "@/lib/utils/useOwner"; import { signOut } from "@logto/next/server-actions"; @@ -63,6 +63,11 @@ export async function getNotificationsStream() { return getStreamFor("notifications", userId); } +export async function getSidebarStream() { + const { ownerId } = await getOwner(); + return getStreamFor("update_sidebar", ownerId); +} + export async function logout() { await signOut(logtoConfig); } diff --git a/components/nav-main.tsx b/components/nav-main.tsx index 8de6fb2..631cd3f 100644 --- a/components/nav-main.tsx +++ b/components/nav-main.tsx @@ -1,5 +1,6 @@ "use client"; +import { getSidebarStream } from "@/app/(dashboard)/[tenant]/settings/actions"; import { Collapsible, CollapsibleContent, @@ -18,9 +19,12 @@ import { } from "@/components/ui/sidebar"; import type { ProjectWithData } from "@/drizzle/types"; import { cn } from "@/lib/utils"; +import { useCable } from "@/lib/utils/cable-client"; import { getProjectById } from "@/lib/utils/useProjects"; +import type { Channel } from "@anycable/web"; import { CalendarCheck, + CalendarHeartIcon, ChevronRight, File, GaugeIcon, @@ -28,10 +32,9 @@ import { type LucideIcon, SettingsIcon, } from "lucide-react"; -import { CalendarHeartIcon } from "lucide-react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Notifications } from "./core/notifications"; type MainNavItem = { @@ -48,23 +51,45 @@ type MainNavItem = { export function NavMain() { const { setOpenMobile } = useSidebar(); + const cable = useCable(); const { tenant, projectId } = useParams(); const pathname = usePathname(); const [projectData, setProjectData] = useState(null); + const updateProjectData = useCallback(() => { + getProjectById(String(projectId), true) + .then((data) => { + setProjectData(data); + }) + .catch((error) => { + setProjectData(null); + console.error(error); + }); + }, [projectId]); + useEffect(() => { if (projectId) { - getProjectById(String(projectId), true) - .then((data) => { - setProjectData(data); - }) - .catch((error) => { - setProjectData(null); - console.error(error); - }); + updateProjectData(); } - }, [projectId]); + }, [updateProjectData, projectId]); + + useEffect(() => { + if (!cable) return; + + let channel: Channel | undefined; + + getSidebarStream().then((stream) => { + channel = cable.streamFromSigned(stream); + channel.on("message", (_) => { + updateProjectData(); + }); + }); + + return () => { + channel?.disconnect(); + }; + }, [cable, updateProjectData]); const navItems: MainNavItem[] = useMemo(() => { const items: MainNavItem[] = [ diff --git a/lib/utils/cable-server.ts b/lib/utils/cable-server.ts index 61502a7..64d7e78 100644 --- a/lib/utils/cable-server.ts +++ b/lib/utils/cable-server.ts @@ -7,7 +7,7 @@ const jwtTTL = "1h"; const broadcastURL = process.env.ANYCABLE_BROADCAST_URL!; const broadcastKey = process.env.ANYCABLE_BROADCAST_KEY!; -export type Event = "notifications"; +export type Event = "notifications" | "update_sidebar"; export async function getToken(userId: string) { if (!secret) { @@ -19,22 +19,22 @@ export async function getToken(userId: string) { return token; } -export async function getStreamFor(room: Event, userId: string) { +export async function getStreamFor(room: Event, actor: string) { if (!secret) { throw new Error("ANYCABLE_STREAMS_SECRET is not set"); } const sign = signer(secret); - const signedStreamName = sign(`${room}/${userId}`); + const signedStreamName = sign(`${room}/${actor}`); return signedStreamName; } export async function broadcastEvent( room: Event, - userId: string, - message: Record, + actor: string, + message: Record | null = null, ) { const broadcastTo = broadcaster(broadcastURL, broadcastKey); - await broadcastTo(`${room}/${userId}`, message); + await broadcastTo(`${room}/${actor}`, message); }