import { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Archive, Loader2, RefreshCw, RotateCcw } from "lucide-react"; import { Button } from "@/client/components/ui/button"; import { useCurrentWorkspace, useWorkspaceRole } from "./use-workspace-view"; import { replicaCommands } from "@/client/stores/db/workspace-replica"; import { useDocumentTitle } from "@/client/hooks/use-document-title"; import { api, toApiError } from "@/client/lib/api"; import { archivedPagesQueryKey, archivedPagesQueryOptions } from "@/client/lib/queries/archived-pages"; import { formatAbsoluteDate, formatRelativeDate } from "@/client/lib/format-date"; import { DEFAULT_PAGE_TITLE } from "@/shared/constants"; import type { ArchivedPage, Workspace } from "@/shared/types"; export function WorkspaceArchive() { const params = useParams({ strict: false }) as { workspaceSlug?: string }; const navigate = useNavigate(); const currentWorkspace = useCurrentWorkspace(); const role = useWorkspaceRole(); useDocumentTitle(currentWorkspace ? `Archive — ${currentWorkspace.name}` : "Archive"); // The layout's member-only route gating already keeps non-members off this // route, but guests are members and reach it like Settings does. Guests // cannot manage the archive, so funnel them back to the workspace. Key on // the concrete "guest" value — role is null while the replica resolves, and // redirecting on null would bounce a legitimate member mid-load. const isGuest = role === "guest"; useEffect(() => { if (isGuest && params.workspaceSlug) { navigate({ to: "/$workspaceSlug", params: { workspaceSlug: params.workspaceSlug }, replace: true }); } }, [isGuest, params.workspaceSlug, navigate]); if (!currentWorkspace || isGuest) return null; return (
Back to {currentWorkspace.name}

Archive

); } function ArchiveList({ workspace }: { workspace: Workspace }) { const queryClient = useQueryClient(); const archivedQuery = useQuery(archivedPagesQueryOptions(workspace.id)); const [restoreError, setRestoreError] = useState(null); const archivedPages = archivedQuery.data ?? []; const restoreMutation = useMutation({ mutationFn: (pageId: string) => api.pages.restore(workspace.id, pageId), onSuccess: async (restoredPages, pageId) => { await replicaCommands.upsertPages(workspace.id, restoredPages); queryClient.setQueryData(archivedPagesQueryKey(workspace.id), (prev) => prev ? prev.filter((page) => page.id !== pageId) : prev, ); setRestoreError(null); }, onError: (err) => { setRestoreError(toApiError(err).message); }, }); return (

{archivedQuery.isSuccess ? `${archivedPages.length} ${archivedPages.length === 1 ? "page" : "pages"}` : ""}

{restoreError &&

{restoreError}

} {archivedQuery.isLoading ? (
Loading archived pages
) : archivedQuery.isError ? (
{toApiError(archivedQuery.error).message}
) : archivedPages.length === 0 ? (
No archived pages.
) : (
{archivedPages.map((page) => ( restoreMutation.mutate(page.id)} /> ))}
)}
); } function ArchivePageRow({ page, restoring, onRestore, }: { page: ArchivedPage; restoring: boolean; onRestore: () => void; }) { const descendantText = page.archived_descendant_count === 1 ? "1 subpage" : `${page.archived_descendant_count} subpages`; const archivedAt = page.archived_at; const archivedLabel = archivedAt ? `Archived ${formatRelativeDate(archivedAt)}` : "Archived"; return (

{page.title || DEFAULT_PAGE_TITLE}

{descendantText} · {archivedLabel}

); }