From 07d13e173824d8a45ed99f9934bb8712b5c9b938 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 14 Mar 2026 06:13:07 -0500 Subject: [PATCH] fix(ui): streamline issue document chrome Co-Authored-By: Paperclip --- ui/src/components/IssueDocumentsSection.tsx | 206 +++++++++++++------- ui/src/pages/IssueDetail.tsx | 13 +- 2 files changed, 136 insertions(+), 83 deletions(-) diff --git a/ui/src/components/IssueDocumentsSection.tsx b/ui/src/components/IssueDocumentsSection.tsx index d6aa356f..a2685911 100644 --- a/ui/src/components/IssueDocumentsSection.tsx +++ b/ui/src/components/IssueDocumentsSection.tsx @@ -16,7 +16,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react"; +import { ChevronDown, ChevronRight, FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react"; type DraftState = { key: string; @@ -28,6 +28,24 @@ type DraftState = { 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}; @@ -58,6 +76,7 @@ export function IssueDocumentsSection({ const [confirmDeleteKey, setConfirmDeleteKey] = useState(null); const [error, setError] = useState(null); const [draft, setDraft] = useState(null); + const [foldedDocumentKeys, setFoldedDocumentKeys] = useState(() => loadFoldedDocumentKeys(issue.id)); const [autosaveDocumentKey, setAutosaveDocumentKey] = useState(null); const autosaveDebounceRef = useRef | null>(null); const { @@ -139,6 +158,7 @@ export function IssueDocumentsSection({ const beginEdit = (key: string) => { const doc = sortedDocuments.find((entry) => entry.key === key); if (!doc) return; + setFoldedDocumentKeys((current) => current.filter((entry) => entry !== key)); resetAutosaveState(); setDraft({ key: doc.key, @@ -263,6 +283,25 @@ export function IssueDocumentsSection({ } }; + useEffect(() => { + setFoldedDocumentKeys(loadFoldedDocumentKeys(issue.id)); + }, [issue.id]); + + 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(() => { return () => { if (autosaveDebounceRef.current) { @@ -302,6 +341,13 @@ export function IssueDocumentsSection({ 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 (
@@ -400,6 +446,7 @@ export function IssueDocumentsSection({
{sortedDocuments.map((doc) => { const activeDraft = draft?.key === doc.key && !draft.isNew ? draft : null; + const isFolded = foldedDocumentKeys.includes(doc.key); const showTitle = !isPlanKey(doc.key) && !!doc.title?.trim() && !titlesMatchKey(doc.title, doc.key); return ( @@ -407,6 +454,15 @@ export function IssueDocumentsSection({
+ {doc.key} @@ -442,83 +498,85 @@ export function IssueDocumentsSection({ )}
-
{ - if (!activeDraft) { - beginEdit(doc.key); - } - }} - onBlurCapture={async (event) => { - if (activeDraft) { - await handleDraftBlur(event); - } - }} - onKeyDown={async (event) => { - if (activeDraft) { - await handleDraftKeyDown(event); - } - }} - > - {activeDraft && !isPlanKey(doc.key) && ( - { - markDocumentDirty(doc.key); - setDraft((current) => current ? { ...current, title: event.target.value } : current); - }} - placeholder="Optional title" - /> - )} + {!isFolded ? (
{ + if (!activeDraft) { + beginEdit(doc.key); + } + }} + onBlurCapture={async (event) => { + if (activeDraft) { + await handleDraftBlur(event); + } + }} + onKeyDown={async (event) => { + if (activeDraft) { + await handleDraftKeyDown(event); + } + }} > - { - markDocumentDirty(doc.key); - setDraft((current) => { - if (current && current.key === doc.key && !current.isNew) { - return { ...current, body }; - } - return { - key: doc.key, - title: doc.title ?? "", - body, - baseRevisionId: doc.latestRevisionId, - isNew: false, - }; - }); - }} - placeholder="Markdown body" - bordered={false} - className="bg-transparent" - contentClassName={documentBodyContentClassName} - mentions={mentions} - imageUploadHandler={imageUploadHandler} - onSubmit={() => void commitDraft(activeDraft ?? draft, { clearAfterSave: false, trackAutosave: true })} - /> -
-
- { + markDocumentDirty(doc.key); + setDraft((current) => current ? { ...current, title: event.target.value } : current); + }} + placeholder="Optional title" + /> + )} +
- {activeDraft && autosaveDocumentKey === doc.key - ? autosaveState === "saving" - ? "Autosaving..." - : autosaveState === "saved" - ? "Saved" - : autosaveState === "error" - ? "Could not save" - : "" - : ""} - + { + markDocumentDirty(doc.key); + setDraft((current) => { + if (current && current.key === doc.key && !current.isNew) { + return { ...current, body }; + } + return { + key: doc.key, + title: doc.title ?? "", + body, + baseRevisionId: doc.latestRevisionId, + isNew: false, + }; + }); + }} + placeholder="Markdown body" + bordered={false} + className="bg-transparent" + contentClassName={documentBodyContentClassName} + mentions={mentions} + imageUploadHandler={imageUploadHandler} + onSubmit={() => void commitDraft(activeDraft ?? draft, { clearAfterSave: false, trackAutosave: true })} + /> +
+
+ + {activeDraft && autosaveDocumentKey === doc.key + ? autosaveState === "saving" + ? "Autosaving..." + : autosaveState === "saved" + ? "Saved" + : autosaveState === "error" + ? "Could not save" + : "" + : ""} + +
-
+ ) : null} {confirmDeleteKey === doc.key && (
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 5087bb73..b96ca7db 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -609,12 +609,7 @@ export function IssueDetail() { const attachmentList = attachments ?? []; const hasAttachments = attachmentList.length > 0; const attachmentUploadButton = ( -
+ <> fileInputRef.current?.click()} disabled={uploadAttachment.isPending || importMarkdownDocument.isPending} className={cn( - "border-transparent bg-transparent shadow-none", - attachmentDragActive && "bg-transparent", + "shadow-none", + attachmentDragActive && "border-primary bg-primary/5", )} > {uploadAttachment.isPending || importMarkdownDocument.isPending ? "Uploading..." : "Upload attachment"} -
+ ); return (