"use client"; export const dynamic = "force-dynamic"; import { useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { X } from "lucide-react"; import { ApiError } from "@/api/mutator"; import { type getBoardApiV1BoardsBoardIdGetResponse, useGetBoardApiV1BoardsBoardIdGet, useUpdateBoardApiV1BoardsBoardIdPatch, } from "@/api/generated/boards/boards"; import { type listBoardGroupsApiV1BoardGroupsGetResponse, useListBoardGroupsApiV1BoardGroupsGet, } from "@/api/generated/board-groups/board-groups"; import { type listGatewaysApiV1GatewaysGetResponse, useListGatewaysApiV1GatewaysGet, } from "@/api/generated/gateways/gateways"; import { type getMyMembershipApiV1OrganizationsMeMemberGetResponse, useGetMyMembershipApiV1OrganizationsMeMemberGet, } from "@/api/generated/organizations/organizations"; import type { BoardGroupRead, BoardRead, BoardUpdate, } from "@/api/generated/model"; import { BoardOnboardingChat } from "@/components/BoardOnboardingChat"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogContent } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import SearchableSelect from "@/components/ui/searchable-select"; import { Textarea } from "@/components/ui/textarea"; import { localDateInputToUtcIso, toLocalDateInput } from "@/lib/datetime"; const slugify = (value: string) => value .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, "") || "board"; export default function EditBoardPage() { const { isSignedIn } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); const params = useParams(); const boardIdParam = params?.boardId; const boardId = Array.isArray(boardIdParam) ? boardIdParam[0] : boardIdParam; const membershipQuery = useGetMyMembershipApiV1OrganizationsMeMemberGet< getMyMembershipApiV1OrganizationsMeMemberGetResponse, ApiError >({ query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", retry: false, }, }); const member = membershipQuery.data?.status === 200 ? membershipQuery.data.data : null; const isAdmin = member ? ["owner", "admin"].includes(member.role) : false; const mainRef = useRef(null); const [board, setBoard] = useState(null); const [name, setName] = useState(undefined); const [gatewayId, setGatewayId] = useState(undefined); const [boardGroupId, setBoardGroupId] = useState( undefined, ); const [boardType, setBoardType] = useState(undefined); const [objective, setObjective] = useState(undefined); const [successMetrics, setSuccessMetrics] = useState( undefined, ); const [targetDate, setTargetDate] = useState(undefined); const [error, setError] = useState(null); const [metricsError, setMetricsError] = useState(null); const onboardingParam = searchParams.get("onboarding"); const searchParamsString = searchParams.toString(); const shouldAutoOpenOnboarding = onboardingParam !== null && onboardingParam !== "" && onboardingParam !== "0" && onboardingParam.toLowerCase() !== "false"; const [isOnboardingOpen, setIsOnboardingOpen] = useState( shouldAutoOpenOnboarding, ); useEffect(() => { if (!isOnboardingOpen) return; const mainEl = mainRef.current; const previousMainOverflow = mainEl?.style.overflow ?? ""; const previousHtmlOverflow = document.documentElement.style.overflow; const previousBodyOverflow = document.body.style.overflow; if (mainEl) { mainEl.style.overflow = "hidden"; } document.documentElement.style.overflow = "hidden"; document.body.style.overflow = "hidden"; return () => { if (mainEl) { mainEl.style.overflow = previousMainOverflow; } document.documentElement.style.overflow = previousHtmlOverflow; document.body.style.overflow = previousBodyOverflow; }; }, [isOnboardingOpen]); useEffect(() => { if (!boardId) return; if (!shouldAutoOpenOnboarding) return; // Remove the flag from the URL so refreshes don't constantly reopen it. const nextParams = new URLSearchParams(searchParamsString); nextParams.delete("onboarding"); const qs = nextParams.toString(); router.replace( qs ? `/boards/${boardId}/edit?${qs}` : `/boards/${boardId}/edit`, ); }, [boardId, router, searchParamsString, shouldAutoOpenOnboarding]); const gatewaysQuery = useListGatewaysApiV1GatewaysGet< listGatewaysApiV1GatewaysGetResponse, ApiError >(undefined, { query: { enabled: Boolean(isSignedIn && isAdmin), refetchOnMount: "always", retry: false, }, }); const groupsQuery = useListBoardGroupsApiV1BoardGroupsGet< listBoardGroupsApiV1BoardGroupsGetResponse, ApiError >(undefined, { query: { enabled: Boolean(isSignedIn && isAdmin), refetchOnMount: "always", retry: false, }, }); const boardQuery = useGetBoardApiV1BoardsBoardIdGet< getBoardApiV1BoardsBoardIdGetResponse, ApiError >(boardId ?? "", { query: { enabled: Boolean(isSignedIn && isAdmin && boardId), refetchOnMount: "always", retry: false, }, }); const updateBoardMutation = useUpdateBoardApiV1BoardsBoardIdPatch({ mutation: { onSuccess: (result) => { if (result.status === 200) { router.push(`/boards/${result.data.id}`); } }, onError: (err) => { setError(err.message || "Something went wrong."); }, }, }); const gateways = useMemo(() => { if (gatewaysQuery.data?.status !== 200) return []; return gatewaysQuery.data.data.items ?? []; }, [gatewaysQuery.data]); const loadedBoard: BoardRead | null = boardQuery.data?.status === 200 ? boardQuery.data.data : null; const baseBoard = board ?? loadedBoard; const resolvedName = name ?? baseBoard?.name ?? ""; const resolvedGatewayId = gatewayId ?? baseBoard?.gateway_id ?? ""; const resolvedBoardGroupId = boardGroupId ?? baseBoard?.board_group_id ?? "none"; const resolvedBoardType = boardType ?? baseBoard?.board_type ?? "goal"; const resolvedObjective = objective ?? baseBoard?.objective ?? ""; const resolvedSuccessMetrics = successMetrics ?? (baseBoard?.success_metrics ? JSON.stringify(baseBoard.success_metrics, null, 2) : ""); const resolvedTargetDate = targetDate ?? toLocalDateInput(baseBoard?.target_date); const displayGatewayId = resolvedGatewayId || gateways[0]?.id || ""; const isLoading = gatewaysQuery.isLoading || groupsQuery.isLoading || boardQuery.isLoading || updateBoardMutation.isPending; const errorMessage = error ?? gatewaysQuery.error?.message ?? groupsQuery.error?.message ?? boardQuery.error?.message ?? null; const isFormReady = Boolean(resolvedName.trim() && displayGatewayId); const gatewayOptions = useMemo( () => gateways.map((gateway) => ({ value: gateway.id, label: gateway.name })), [gateways], ); const groups = useMemo(() => { if (groupsQuery.data?.status !== 200) return []; return groupsQuery.data.data.items ?? []; }, [groupsQuery.data]); const groupOptions = useMemo( () => [ { value: "none", label: "No group" }, ...groups.map((group) => ({ value: group.id, label: group.name })), ], [groups], ); const handleOnboardingConfirmed = (updated: BoardRead) => { setBoard(updated); setBoardType(updated.board_type ?? "goal"); setObjective(updated.objective ?? ""); setSuccessMetrics( updated.success_metrics ? JSON.stringify(updated.success_metrics, null, 2) : "", ); setTargetDate(toLocalDateInput(updated.target_date)); setBoardGroupId(updated.board_group_id ?? "none"); setIsOnboardingOpen(false); }; const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); if (!isSignedIn || !boardId) return; const trimmedName = resolvedName.trim(); if (!trimmedName) { setError("Board name is required."); return; } const resolvedGatewayId = displayGatewayId; if (!resolvedGatewayId) { setError("Select a gateway before saving."); return; } setError(null); setMetricsError(null); let parsedMetrics: Record | null = null; if (resolvedSuccessMetrics.trim()) { try { parsedMetrics = JSON.parse(resolvedSuccessMetrics) as Record< string, unknown >; } catch { setMetricsError("Success metrics must be valid JSON."); return; } } const payload: BoardUpdate = { name: trimmedName, slug: slugify(trimmedName), gateway_id: resolvedGatewayId || null, board_group_id: resolvedBoardGroupId === "none" ? null : resolvedBoardGroupId, board_type: resolvedBoardType, objective: resolvedObjective.trim() || null, success_metrics: parsedMetrics, target_date: localDateInputToUtcIso(resolvedTargetDate), }; updateBoardMutation.mutate({ boardId, data: payload }); }; return ( <>

Sign in to edit boards.

Edit board

Update board settings and gateway.

)}
{!isAdmin ? (
Only organization owners and admins can edit board settings.
) : (
{resolvedBoardType !== "general" && baseBoard && !(baseBoard.goal_confirmed ?? false) ? (

Goal needs confirmation

Start onboarding to draft an objective and success metrics.

) : null}
setName(event.target.value)} placeholder="Board name" disabled={isLoading || !baseBoard} />

Boards in the same group can share cross-board context for agents.

setTargetDate(event.target.value)} disabled={isLoading} />