import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { DocumentRevision, FeedbackDataSharingPreference, FeedbackVote, FeedbackVoteValue, Issue, IssueDocument, } from "@paperclipai/shared"; import { useLocation } from "@/lib/router"; import { ApiError } from "../api/client"; import { issuesApi } from "../api/issues"; import { useAutosaveIndicator } from "../hooks/useAutosaveIndicator"; import { queryKeys } from "../lib/queryKeys"; import { cn, relativeTime } from "../lib/utils"; import { MarkdownBody } from "./MarkdownBody"; import { MarkdownEditor, type MentionOption } from "./MarkdownEditor"; import { OutputFeedbackButtons } from "./OutputFeedbackButtons"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Check, ChevronDown, ChevronRight, Copy, Download, FilePenLine, FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react"; type DraftState = { key: string; title: string; body: string; baseRevisionId: string | null; isNew: boolean; }; type DocumentConflictState = { key: string; serverDocument: IssueDocument; localDraft: DraftState; showRemote: boolean; }; const DOCUMENT_AUTOSAVE_DEBOUNCE_MS = 900; const DOCUMENT_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]*$/; const getFoldedDocumentsStorageKey = (issueId: string) => `paperclip:issue-document-folds:${issueId}`; function loadFoldedDocumentKeys(issueId: string) { if (typeof window === "undefined") return []; try { const raw = window.localStorage.getItem(getFoldedDocumentsStorageKey(issueId)); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.filter((value): value is string => typeof value === "string") : []; } catch { return []; } } function saveFoldedDocumentKeys(issueId: string, keys: string[]) { if (typeof window === "undefined") return; window.localStorage.setItem(getFoldedDocumentsStorageKey(issueId), JSON.stringify(keys)); } function renderBody(body: string, className?: string) { return {body}; } function isPlanKey(key: string) { return key.trim().toLowerCase() === "plan"; } function titlesMatchKey(title: string | null | undefined, key: string) { return (title ?? "").trim().toLowerCase() === key.trim().toLowerCase(); } function isDocumentConflictError(error: unknown) { return error instanceof ApiError && error.status === 409; } function downloadDocumentFile(key: string, body: string) { const blob = new Blob([body], { type: "text/markdown;charset=utf-8" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `${key}.md`; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); } function getRevisionActorLabel(revision: DocumentRevision) { if (revision.createdByUserId) return "board"; if (revision.createdByAgentId) return "agent"; return "system"; } function documentHasUnsavedChanges(doc: IssueDocument, draft: DraftState | null) { if (!draft || draft.isNew || draft.key !== doc.key) return false; return draft.body !== doc.body || (doc.title ?? "") !== draft.title; } function toDocumentSummary(document: IssueDocument) { return { id: document.id, companyId: document.companyId, issueId: document.issueId, key: document.key, title: document.title, format: document.format, latestRevisionId: document.latestRevisionId, latestRevisionNumber: document.latestRevisionNumber, createdByAgentId: document.createdByAgentId, createdByUserId: document.createdByUserId, updatedByAgentId: document.updatedByAgentId, updatedByUserId: document.updatedByUserId, createdAt: document.createdAt, updatedAt: document.updatedAt, }; } export function IssueDocumentsSection({ issue, canDeleteDocuments, feedbackVotes = [], feedbackDataSharingPreference = "prompt", feedbackTermsUrl = null, mentions, imageUploadHandler, onVote, extraActions, }: { issue: Issue; canDeleteDocuments: boolean; feedbackVotes?: FeedbackVote[]; feedbackDataSharingPreference?: FeedbackDataSharingPreference; feedbackTermsUrl?: string | null; mentions?: MentionOption[]; imageUploadHandler?: (file: File) => Promise; onVote?: ( revisionId: string, vote: FeedbackVoteValue, options?: { allowSharing?: boolean; reason?: string }, ) => Promise; extraActions?: ReactNode; }) { const queryClient = useQueryClient(); const location = useLocation(); const [confirmDeleteKey, setConfirmDeleteKey] = useState(null); const [error, setError] = useState(null); const [draft, setDraft] = useState(null); const [documentConflict, setDocumentConflict] = useState(null); const [foldedDocumentKeys, setFoldedDocumentKeys] = useState(() => loadFoldedDocumentKeys(issue.id)); const [autosaveDocumentKey, setAutosaveDocumentKey] = useState(null); const [copiedDocumentKey, setCopiedDocumentKey] = useState(null); const [highlightDocumentKey, setHighlightDocumentKey] = useState(null); const [revisionMenuOpenKey, setRevisionMenuOpenKey] = useState(null); const [selectedRevisionIds, setSelectedRevisionIds] = useState>({}); const autosaveDebounceRef = useRef | null>(null); const copiedDocumentTimerRef = useRef | null>(null); const hasScrolledToHashRef = useRef(false); const { state: autosaveState, markDirty, reset, runSave, } = useAutosaveIndicator(); const { data: documents } = useQuery({ queryKey: queryKeys.issues.documents(issue.id), queryFn: () => issuesApi.listDocuments(issue.id), }); const { data: activeDocumentRevisions, isFetching: isFetchingDocumentRevisions } = useQuery({ queryKey: revisionMenuOpenKey ? queryKeys.issues.documentRevisions(issue.id, revisionMenuOpenKey) : ["issues", "document-revisions", issue.id, "__idle__"], queryFn: async () => { if (!revisionMenuOpenKey) return []; return issuesApi.listDocumentRevisions(issue.id, revisionMenuOpenKey); }, enabled: Boolean(revisionMenuOpenKey), }); const invalidateIssueDocuments = useCallback(() => { queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issue.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.documents(issue.id) }); queryClient.invalidateQueries({ predicate: (query) => Array.isArray(query.queryKey) && query.queryKey[0] === "issues" && query.queryKey[1] === "document-revisions" && query.queryKey[2] === issue.id, }); }, [issue.id, queryClient]); const syncDocumentCaches = useCallback((document: IssueDocument) => { queryClient.setQueryData( queryKeys.issues.documents(issue.id), (current) => { if (!current) return [document]; const existingIndex = current.findIndex((entry) => entry.key === document.key); if (existingIndex === -1) return [...current, document]; return current.map((entry, index) => index === existingIndex ? document : entry); }, ); queryClient.setQueryData( queryKeys.issues.detail(issue.id), (current) => { if (!current) return current; const nextSummaries = (() => { const summary = toDocumentSummary(document); const existingIndex = (current.documentSummaries ?? []).findIndex((entry) => entry.key === document.key); if (existingIndex === -1) return [...(current.documentSummaries ?? []), summary]; return (current.documentSummaries ?? []).map((entry, index) => index === existingIndex ? summary : entry); })(); return { ...current, planDocument: document.key === "plan" ? document : current.planDocument ?? null, documentSummaries: nextSummaries, legacyPlanDocument: document.key === "plan" ? null : current.legacyPlanDocument ?? null, }; }, ); }, [issue.id, queryClient]); const upsertDocument = useMutation({ mutationFn: async (nextDraft: DraftState) => issuesApi.upsertDocument(issue.id, nextDraft.key, { title: isPlanKey(nextDraft.key) ? null : nextDraft.title.trim() || null, format: "markdown", body: nextDraft.body, baseRevisionId: nextDraft.baseRevisionId, }), }); const deleteDocument = useMutation({ mutationFn: (key: string) => issuesApi.deleteDocument(issue.id, key), onSuccess: () => { setError(null); setConfirmDeleteKey(null); invalidateIssueDocuments(); }, onError: (err) => { setError(err instanceof Error ? err.message : "Failed to delete document"); }, }); const restoreDocumentRevision = useMutation({ mutationFn: ({ key, revisionId }: { key: string; revisionId: string }) => issuesApi.restoreDocumentRevision(issue.id, key, revisionId), onSuccess: (document, variables) => { syncDocumentCaches(document); setSelectedRevisionIds((current) => ({ ...current, [variables.key]: null })); setDraft((current) => current?.key === variables.key ? null : current); setDocumentConflict((current) => current?.key === variables.key ? null : current); resetAutosaveState(); setError(null); invalidateIssueDocuments(); }, onError: (err) => { setError(err instanceof Error ? err.message : "Failed to restore document revision"); }, }); const sortedDocuments = useMemo(() => { return [...(documents ?? [])].sort((a, b) => { if (a.key === "plan" && b.key !== "plan") return -1; if (a.key !== "plan" && b.key === "plan") return 1; return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); }); }, [documents]); const feedbackVoteByTargetId = useMemo(() => { const map = new Map(); for (const feedbackVote of feedbackVotes) { if (feedbackVote.targetType !== "issue_document_revision") continue; map.set(feedbackVote.targetId, feedbackVote.vote); } return map; }, [feedbackVotes]); const hasRealPlan = sortedDocuments.some((doc) => doc.key === "plan"); const isEmpty = sortedDocuments.length === 0 && !issue.legacyPlanDocument; const newDocumentKeyError = draft?.isNew && draft.key.trim().length > 0 && !DOCUMENT_KEY_PATTERN.test(draft.key.trim()) ? "Use lowercase letters, numbers, -, or _, and start with a letter or number." : null; const resetAutosaveState = useCallback(() => { setAutosaveDocumentKey(null); reset(); }, [reset]); const markDocumentDirty = useCallback((key: string) => { setAutosaveDocumentKey(key); markDirty(); }, [markDirty]); const beginNewDocument = () => { resetAutosaveState(); setDocumentConflict(null); setDraft({ key: "", title: "", body: "", baseRevisionId: null, isNew: true, }); setError(null); }; const beginEdit = (key: string) => { const doc = sortedDocuments.find((entry) => entry.key === key); if (!doc) return; const conflictedDraft = documentConflict?.key === key ? documentConflict.localDraft : null; setFoldedDocumentKeys((current) => current.filter((entry) => entry !== key)); resetAutosaveState(); setDocumentConflict((current) => current?.key === key ? current : null); setDraft({ key: conflictedDraft?.key ?? doc.key, title: conflictedDraft?.title ?? doc.title ?? "", body: conflictedDraft?.body ?? doc.body, baseRevisionId: conflictedDraft?.baseRevisionId ?? doc.latestRevisionId, isNew: false, }); setError(null); }; const cancelDraft = () => { if (autosaveDebounceRef.current) { clearTimeout(autosaveDebounceRef.current); } resetAutosaveState(); setDocumentConflict(null); setDraft(null); setError(null); }; const commitDraft = useCallback(async ( currentDraft: DraftState | null, options?: { clearAfterSave?: boolean; trackAutosave?: boolean; overrideConflict?: boolean }, ) => { if (!currentDraft || upsertDocument.isPending) return false; const normalizedKey = currentDraft.key.trim().toLowerCase(); const normalizedBody = currentDraft.body.trim(); const normalizedTitle = currentDraft.title.trim(); const activeConflict = documentConflict?.key === normalizedKey ? documentConflict : null; if (activeConflict && !options?.overrideConflict) { if (options?.trackAutosave) { resetAutosaveState(); } return false; } if (!normalizedKey || !normalizedBody) { if (currentDraft.isNew) { setError("Document key and body are required"); } else if (!normalizedBody) { setError("Document body cannot be empty"); } if (options?.trackAutosave) { resetAutosaveState(); } return false; } if (!DOCUMENT_KEY_PATTERN.test(normalizedKey)) { setError("Document key must start with a letter or number and use only lowercase letters, numbers, -, or _."); if (options?.trackAutosave) { resetAutosaveState(); } return false; } const existing = sortedDocuments.find((doc) => doc.key === normalizedKey); if ( !currentDraft.isNew && existing && existing.body === currentDraft.body && (existing.title ?? "") === currentDraft.title ) { if (options?.clearAfterSave) { setDraft((value) => (value?.key === normalizedKey ? null : value)); } if (options?.trackAutosave) { resetAutosaveState(); } return true; } const save = async () => { const saved = await upsertDocument.mutateAsync({ ...currentDraft, key: normalizedKey, title: isPlanKey(normalizedKey) ? "" : normalizedTitle, body: currentDraft.body, baseRevisionId: options?.overrideConflict ? activeConflict?.serverDocument.latestRevisionId ?? currentDraft.baseRevisionId : currentDraft.baseRevisionId, }); setError(null); setDocumentConflict((current) => current?.key === normalizedKey ? null : current); setDraft((value) => { if (!value || value.key !== normalizedKey) return value; if (options?.clearAfterSave) return null; return { key: saved.key, title: saved.title ?? "", body: saved.body, baseRevisionId: saved.latestRevisionId, isNew: false, }; }); syncDocumentCaches(saved); invalidateIssueDocuments(); }; try { if (options?.trackAutosave) { setAutosaveDocumentKey(normalizedKey); await runSave(save); } else { await save(); } return true; } catch (err) { if (isDocumentConflictError(err)) { try { const latestDocument = await issuesApi.getDocument(issue.id, normalizedKey); setDocumentConflict({ key: normalizedKey, serverDocument: latestDocument, localDraft: { key: normalizedKey, title: isPlanKey(normalizedKey) ? "" : normalizedTitle, body: currentDraft.body, baseRevisionId: currentDraft.baseRevisionId, isNew: false, }, showRemote: true, }); setFoldedDocumentKeys((current) => current.filter((key) => key !== normalizedKey)); setError(null); resetAutosaveState(); return false; } catch { setError("Document changed remotely and the latest version could not be loaded"); return false; } } setError(err instanceof Error ? err.message : "Failed to save document"); return false; } }, [documentConflict, invalidateIssueDocuments, issue.id, resetAutosaveState, runSave, sortedDocuments, syncDocumentCaches, upsertDocument]); const reloadDocumentFromServer = useCallback((key: string) => { if (documentConflict?.key !== key) return; const serverDocument = documentConflict.serverDocument; setDraft({ key: serverDocument.key, title: serverDocument.title ?? "", body: serverDocument.body, baseRevisionId: serverDocument.latestRevisionId, isNew: false, }); setDocumentConflict(null); resetAutosaveState(); setError(null); }, [documentConflict, resetAutosaveState]); const overwriteDocumentFromDraft = useCallback(async (key: string) => { if (documentConflict?.key !== key) return; const sourceDraft = draft && draft.key === key && !draft.isNew ? draft : documentConflict.localDraft; await commitDraft( { ...sourceDraft, baseRevisionId: documentConflict.serverDocument.latestRevisionId, }, { clearAfterSave: false, trackAutosave: true, overrideConflict: true, }, ); }, [commitDraft, documentConflict, draft]); const keepConflictedDraft = useCallback((key: string) => { if (documentConflict?.key !== key) return; setDraft(documentConflict.localDraft); setDocumentConflict((current) => current?.key === key ? { ...current, showRemote: false } : current, ); setError(null); }, [documentConflict]); const copyDocumentBody = useCallback(async (key: string, body: string) => { try { await navigator.clipboard.writeText(body); setCopiedDocumentKey(key); if (copiedDocumentTimerRef.current) { clearTimeout(copiedDocumentTimerRef.current); } copiedDocumentTimerRef.current = setTimeout(() => { setCopiedDocumentKey((current) => current === key ? null : current); }, 1400); } catch { setError("Could not copy document"); } }, []); const getDocumentRevisions = useCallback((key: string) => { const cached = queryClient.getQueryData(queryKeys.issues.documentRevisions(issue.id, key)); if (cached) return cached; if (revisionMenuOpenKey === key) return activeDocumentRevisions ?? []; return []; }, [activeDocumentRevisions, issue.id, queryClient, revisionMenuOpenKey]); const returnToLatestRevision = useCallback((key: string) => { setSelectedRevisionIds((current) => ({ ...current, [key]: null })); setError(null); }, []); const previewRevision = useCallback((doc: IssueDocument, revisionId: string) => { const revisions = getDocumentRevisions(doc.key); const selectedRevision = revisions.find((revision) => revision.id === revisionId); if (!selectedRevision) return; if (selectedRevision.id === doc.latestRevisionId) { returnToLatestRevision(doc.key); return; } if (documentConflict?.key === doc.key || documentHasUnsavedChanges(doc, draft)) { setError("Save or cancel your local changes before viewing an older revision."); return; } resetAutosaveState(); setDraft((current) => current?.key === doc.key ? null : current); setDocumentConflict((current) => current?.key === doc.key ? null : current); setFoldedDocumentKeys((current) => current.filter((entry) => entry !== doc.key)); setSelectedRevisionIds((current) => ({ ...current, [doc.key]: selectedRevision.id })); setError(null); }, [documentConflict, draft, getDocumentRevisions, resetAutosaveState, returnToLatestRevision]); const handleDraftBlur = async (event: React.FocusEvent) => { if (event.currentTarget.contains(event.relatedTarget as Node | null)) return; if (autosaveDebounceRef.current) { clearTimeout(autosaveDebounceRef.current); } await commitDraft(draft, { clearAfterSave: true, trackAutosave: true }); }; const handleDraftKeyDown = async (event: React.KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); cancelDraft(); return; } if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { event.preventDefault(); if (autosaveDebounceRef.current) { clearTimeout(autosaveDebounceRef.current); } await commitDraft(draft, { clearAfterSave: false, trackAutosave: true }); } }; useEffect(() => { setFoldedDocumentKeys(loadFoldedDocumentKeys(issue.id)); }, [issue.id]); useEffect(() => { hasScrolledToHashRef.current = false; }, [issue.id, location.hash]); useEffect(() => { const validKeys = new Set(sortedDocuments.map((doc) => doc.key)); setFoldedDocumentKeys((current) => { const next = current.filter((key) => validKeys.has(key)); if (next.length !== current.length) { saveFoldedDocumentKeys(issue.id, next); } return next; }); }, [issue.id, sortedDocuments]); useEffect(() => { saveFoldedDocumentKeys(issue.id, foldedDocumentKeys); }, [foldedDocumentKeys, issue.id]); useEffect(() => { if (!documentConflict) return; const latest = sortedDocuments.find((doc) => doc.key === documentConflict.key); if (!latest || latest.latestRevisionId === documentConflict.serverDocument.latestRevisionId) return; setDocumentConflict((current) => current?.key === latest.key ? { ...current, serverDocument: latest } : current, ); }, [documentConflict, sortedDocuments]); useEffect(() => { const hash = location.hash; if (!hash.startsWith("#document-")) return; const documentKey = decodeURIComponent(hash.slice("#document-".length)); const targetExists = sortedDocuments.some((doc) => doc.key === documentKey) || (documentKey === "plan" && Boolean(issue.legacyPlanDocument)); if (!targetExists || hasScrolledToHashRef.current) return; setFoldedDocumentKeys((current) => current.filter((key) => key !== documentKey)); const element = document.getElementById(`document-${documentKey}`); if (!element) return; hasScrolledToHashRef.current = true; setHighlightDocumentKey(documentKey); element.scrollIntoView({ behavior: "smooth", block: "center" }); const timer = setTimeout(() => setHighlightDocumentKey((current) => current === documentKey ? null : current), 3000); return () => clearTimeout(timer); }, [issue.legacyPlanDocument, location.hash, sortedDocuments]); useEffect(() => { return () => { if (autosaveDebounceRef.current) { clearTimeout(autosaveDebounceRef.current); } if (copiedDocumentTimerRef.current) { clearTimeout(copiedDocumentTimerRef.current); } }; }, []); useEffect(() => { if (!draft || draft.isNew) return; if (documentConflict?.key === draft.key) return; const existing = sortedDocuments.find((doc) => doc.key === draft.key); if (!existing) return; const hasChanges = existing.body !== draft.body || (existing.title ?? "") !== draft.title; if (!hasChanges) { if (autosaveState !== "saved") { resetAutosaveState(); } return; } markDocumentDirty(draft.key); if (autosaveDebounceRef.current) { clearTimeout(autosaveDebounceRef.current); } autosaveDebounceRef.current = setTimeout(() => { void commitDraft(draft, { clearAfterSave: false, trackAutosave: true }); }, DOCUMENT_AUTOSAVE_DEBOUNCE_MS); return () => { if (autosaveDebounceRef.current) { clearTimeout(autosaveDebounceRef.current); } }; }, [autosaveState, commitDraft, documentConflict, draft, markDocumentDirty, resetAutosaveState, sortedDocuments]); const documentBodyShellClassName = "mt-3 overflow-hidden rounded-md"; const documentBodyPaddingClassName = ""; const documentBodyContentClassName = "paperclip-edit-in-place-content min-h-[220px] text-[15px] leading-7"; const toggleFoldedDocument = (key: string) => { setFoldedDocumentKeys((current) => current.includes(key) ? current.filter((entry) => entry !== key) : [...current, key], ); }; return (
{isEmpty && !draft?.isNew ? (
{extraActions}
) : (

Documents

{extraActions}
)} {error &&

{error}

} {draft?.isNew && (
setDraft((current) => current ? { ...current, key: event.target.value.toLowerCase() } : current) } placeholder="Document key" /> {newDocumentKeyError && (

{newDocumentKeyError}

)} {!isPlanKey(draft.key) && ( setDraft((current) => current ? { ...current, title: event.target.value } : current) } placeholder="Optional title" /> )} setDraft((current) => current ? { ...current, body } : current) } placeholder="Markdown body" bordered={false} className="bg-transparent" contentClassName="min-h-[220px] text-[15px] leading-7" mentions={mentions} imageUploadHandler={imageUploadHandler} onSubmit={() => void commitDraft(draft, { clearAfterSave: false, trackAutosave: false })} />
)} {!hasRealPlan && issue.legacyPlanDocument ? (
PLAN
{renderBody(issue.legacyPlanDocument.body, documentBodyContentClassName)}
) : null}
{sortedDocuments.map((doc) => { const activeDraft = draft?.key === doc.key && !draft.isNew ? draft : null; const activeConflict = documentConflict?.key === doc.key ? documentConflict : null; const isFolded = foldedDocumentKeys.includes(doc.key); const revisionHistory = getDocumentRevisions(doc.key); const selectedRevisionId = selectedRevisionIds[doc.key] ?? null; const selectedHistoricalRevision = selectedRevisionId ? revisionHistory.find((revision) => revision.id === selectedRevisionId) ?? null : null; const isHistoricalPreview = Boolean(selectedHistoricalRevision); const displayedTitle = selectedHistoricalRevision ? selectedHistoricalRevision.title ?? "" : activeDraft?.title ?? doc.title ?? ""; const displayedBody = selectedHistoricalRevision?.body ?? activeDraft?.body ?? doc.body; const displayedRevisionNumber = selectedHistoricalRevision?.revisionNumber ?? doc.latestRevisionNumber; const displayedUpdatedAt = selectedHistoricalRevision?.createdAt ?? doc.updatedAt; const showTitle = !isPlanKey(doc.key) && !!displayedTitle.trim() && !titlesMatchKey(displayedTitle, doc.key); const canVoteOnDocument = Boolean(doc.latestRevisionId && doc.updatedByAgentId && !doc.updatedByUserId && onVote); return (
{doc.key} setRevisionMenuOpenKey(open ? doc.key : null)} > Revision history {revisionMenuOpenKey === doc.key && isFetchingDocumentRevisions && revisionHistory.length === 0 ? ( Loading revisions... ) : revisionHistory.length > 0 ? ( {revisionHistory.map((revision) => { const isCurrentRevision = revision.id === doc.latestRevisionId; return ( previewRevision(doc, revision.id)} className="items-start" >
rev {revision.revisionNumber} {isCurrentRevision ? ( Current ) : null}
{relativeTime(revision.createdAt)} • {getRevisionActorLabel(revision)}
); })}
) : ( No revisions yet )}
updated {relativeTime(displayedUpdatedAt)}
{showTitle &&

{displayedTitle}

}
{!isHistoricalPreview ? ( beginEdit(doc.key)}> Edit document ) : null} {!isHistoricalPreview ? : null} downloadDocumentFile(doc.key, displayedBody)} > Download document {canDeleteDocuments ? : null} {canDeleteDocuments ? ( setConfirmDeleteKey(doc.key)} > Delete document ) : null}
{!isFolded ? (
{ if (activeDraft) { await handleDraftBlur(event); } } : undefined} onKeyDown={!isHistoricalPreview ? async (event) => { if (activeDraft) { await handleDraftKeyDown(event); } } : undefined} > {isHistoricalPreview && selectedHistoricalRevision && (

Viewing revision {selectedHistoricalRevision.revisionNumber}

This is a historical preview. Restoring it creates a new latest revision and keeps history append-only.

)} {activeConflict && !isHistoricalPreview && (

Out of date

This document changed while you were editing. Your local draft is preserved and autosave is paused.

{activeConflict.showRemote && (
Remote revision {activeConflict.serverDocument.latestRevisionNumber} updated {relativeTime(activeConflict.serverDocument.updatedAt)}
{!isPlanKey(doc.key) && activeConflict.serverDocument.title ? (

{activeConflict.serverDocument.title}

) : null} {renderBody(activeConflict.serverDocument.body, "text-[14px] leading-7")}
)}
)} {activeDraft && !isPlanKey(doc.key) && !isHistoricalPreview && ( { markDocumentDirty(doc.key); setDraft((current) => current ? { ...current, title: event.target.value } : current); }} placeholder="Optional title" /> )}
{isHistoricalPreview ? (
{renderBody(displayedBody, documentBodyContentClassName)}
) : activeDraft ? ( { markDocumentDirty(doc.key); setDraft((current) => { if (current && current.key === doc.key && !current.isNew) { return { ...current, body }; } return current; }); }} placeholder="Markdown body" bordered={false} className="bg-transparent" contentClassName={documentBodyContentClassName} mentions={mentions} imageUploadHandler={imageUploadHandler} onSubmit={() => void commitDraft(activeDraft ?? draft, { clearAfterSave: false, trackAutosave: true })} /> ) : (
{renderBody(displayedBody, documentBodyContentClassName)}
)}
{isHistoricalPreview ? "Viewing historical revision" : activeDraft ? activeConflict ? "Out of date" : autosaveDocumentKey === doc.key ? autosaveState === "saving" ? "Autosaving..." : autosaveState === "saved" ? "Saved" : autosaveState === "error" ? "Could not save" : "" : "" : ""}
{canVoteOnDocument && doc.latestRevisionId ? ( onVote?.(doc.latestRevisionId!, vote, options) ?? Promise.resolve() } /> ) : null}
) : null} {confirmDeleteKey === doc.key && (

Delete this document? This cannot be undone.

)}
); })}
); }