fix(ui): polish issue detail timelines and attachments

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-02 11:51:40 -05:00
parent 36376968af
commit bd6d07d0b4
25 changed files with 2020 additions and 82 deletions

View file

@ -29,7 +29,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Check, ChevronDown, ChevronRight, Copy, Download, FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react";
import { Check, ChevronDown, ChevronRight, Copy, Download, FilePenLine, FileText, MoreHorizontal, Plus, Trash2, X } from "lucide-react";
type DraftState = {
key: string;
@ -106,6 +106,25 @@ function documentHasUnsavedChanges(doc: IssueDocument, draft: DraftState | null)
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,
@ -181,6 +200,36 @@ export function IssueDocumentsSection({
});
}, [issue.id, queryClient]);
const syncDocumentCaches = useCallback((document: IssueDocument) => {
queryClient.setQueryData<IssueDocument[] | undefined>(
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<Issue | undefined>(
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, {
@ -206,7 +255,8 @@ export function IssueDocumentsSection({
const restoreDocumentRevision = useMutation({
mutationFn: ({ key, revisionId }: { key: string; revisionId: string }) =>
issuesApi.restoreDocumentRevision(issue.id, key, revisionId),
onSuccess: (_document, variables) => {
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);
@ -369,6 +419,7 @@ export function IssueDocumentsSection({
isNew: false,
};
});
syncDocumentCaches(saved);
invalidateIssueDocuments();
};
@ -408,7 +459,7 @@ export function IssueDocumentsSection({
setError(err instanceof Error ? err.message : "Failed to save document");
return false;
}
}, [documentConflict, invalidateIssueDocuments, issue.id, resetAutosaveState, runSave, sortedDocuments, upsertDocument]);
}, [documentConflict, invalidateIssueDocuments, issue.id, resetAutosaveState, runSave, sortedDocuments, syncDocumentCaches, upsertDocument]);
const reloadDocumentFromServer = useCallback((key: string) => {
if (documentConflict?.key !== key) return;
@ -864,7 +915,14 @@ export function IssueDocumentsSection({
<MoreHorizontal className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent align="end">
{!isHistoricalPreview ? (
<DropdownMenuItem onClick={() => beginEdit(doc.key)}>
<FilePenLine className="h-3.5 w-3.5" />
Edit document
</DropdownMenuItem>
) : null}
{!isHistoricalPreview ? <DropdownMenuSeparator /> : null}
<DropdownMenuItem
onClick={() => downloadDocumentFile(doc.key, displayedBody)}
>
@ -889,13 +947,6 @@ export function IssueDocumentsSection({
{!isFolded ? (
<div
className="mt-3 space-y-3"
onFocusCapture={!isHistoricalPreview
? () => {
if (!activeDraft) {
beginEdit(doc.key);
}
}
: undefined}
onBlurCapture={!isHistoricalPreview
? async (event) => {
if (activeDraft) {
@ -1026,7 +1077,7 @@ export function IssueDocumentsSection({
<div className="rounded-md border border-amber-500/20 bg-background/50 p-3">
{renderBody(displayedBody, documentBodyContentClassName)}
</div>
) : (
) : activeDraft ? (
<MarkdownEditor
value={displayedBody}
onChange={(body) => {
@ -1035,13 +1086,7 @@ export function IssueDocumentsSection({
if (current && current.key === doc.key && !current.isNew) {
return { ...current, body };
}
return {
key: doc.key,
title: doc.title ?? "",
body,
baseRevisionId: doc.latestRevisionId,
isNew: false,
};
return current;
});
}}
placeholder="Markdown body"
@ -1052,6 +1097,10 @@ export function IssueDocumentsSection({
imageUploadHandler={imageUploadHandler}
onSubmit={() => void commitDraft(activeDraft ?? draft, { clearAfterSave: false, trackAutosave: true })}
/>
) : (
<div className="rounded-md border border-border/60 bg-background/40 p-3">
{renderBody(displayedBody, documentBodyContentClassName)}
</div>
)}
</div>
<div className="flex min-h-4 items-center justify-end px-1">