fix(ui): collapse empty document and attachment states

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-14 06:02:46 -05:00
parent 501ab4ffa9
commit c8cd950a03
2 changed files with 100 additions and 84 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { Issue } from "@paperclipai/shared"; import type { Issue } from "@paperclipai/shared";
import { issuesApi } from "../api/issues"; import { issuesApi } from "../api/issues";
@@ -46,11 +46,13 @@ export function IssueDocumentsSection({
canDeleteDocuments, canDeleteDocuments,
mentions, mentions,
imageUploadHandler, imageUploadHandler,
extraActions,
}: { }: {
issue: Issue; issue: Issue;
canDeleteDocuments: boolean; canDeleteDocuments: boolean;
mentions?: MentionOption[]; mentions?: MentionOption[];
imageUploadHandler?: (file: File) => Promise<string>; imageUploadHandler?: (file: File) => Promise<string>;
extraActions?: ReactNode;
}) { }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [confirmDeleteKey, setConfirmDeleteKey] = useState<string | null>(null); const [confirmDeleteKey, setConfirmDeleteKey] = useState<string | null>(null);
@@ -106,6 +108,7 @@ export function IssueDocumentsSection({
}, [documents]); }, [documents]);
const hasRealPlan = sortedDocuments.some((doc) => doc.key === "plan"); const hasRealPlan = sortedDocuments.some((doc) => doc.key === "plan");
const isEmpty = sortedDocuments.length === 0 && !issue.legacyPlanDocument;
const newDocumentKeyError = const newDocumentKeyError =
draft?.isNew && draft.key.trim().length > 0 && !DOCUMENT_KEY_PATTERN.test(draft.key.trim()) 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." ? "Use lowercase letters, numbers, -, or _, and start with a letter or number."
@@ -302,13 +305,26 @@ export function IssueDocumentsSection({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between gap-2"> {isEmpty && !draft?.isNew ? (
<h3 className="text-sm font-medium text-muted-foreground">Documents</h3> <div className="flex items-center justify-end gap-2">
<Button variant="outline" size="sm" onClick={beginNewDocument}> {extraActions}
<Plus className="mr-1.5 h-3.5 w-3.5" /> <Button variant="outline" size="sm" onClick={beginNewDocument}>
New document <Plus className="mr-1.5 h-3.5 w-3.5" />
</Button> New document
</div> </Button>
</div>
) : (
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium text-muted-foreground">Documents</h3>
<div className="flex items-center gap-2">
{extraActions}
<Button variant="outline" size="sm" onClick={beginNewDocument}>
<Plus className="mr-1.5 h-3.5 w-3.5" />
New document
</Button>
</div>
</div>
)}
{error && <p className="text-xs text-destructive">{error}</p>} {error && <p className="text-xs text-destructive">{error}</p>}
@@ -367,10 +383,6 @@ export function IssueDocumentsSection({
</div> </div>
)} )}
{sortedDocuments.length === 0 && !issue.legacyPlanDocument ? (
<p className="text-xs text-muted-foreground">No documents yet.</p>
) : null}
{!hasRealPlan && issue.legacyPlanDocument ? ( {!hasRealPlan && issue.legacyPlanDocument ? (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3"> <div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3">
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">

View File

@@ -606,6 +606,38 @@ export function IssueDetail() {
}; };
const isImageAttachment = (attachment: IssueAttachment) => attachment.contentType.startsWith("image/"); const isImageAttachment = (attachment: IssueAttachment) => attachment.contentType.startsWith("image/");
const attachmentList = attachments ?? [];
const hasAttachments = attachmentList.length > 0;
const attachmentUploadButton = (
<div
className={cn(
"rounded-md border border-dashed border-border p-1 transition-colors",
attachmentDragActive && "border-primary bg-primary/5",
)}
>
<input
ref={fileInputRef}
type="file"
accept="image/*,application/pdf,text/plain,text/markdown,application/json,text/csv,text/html,.md,.markdown"
className="hidden"
onChange={handleFilePicked}
multiple
/>
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={uploadAttachment.isPending || importMarkdownDocument.isPending}
className={cn(
"border-transparent bg-transparent shadow-none",
attachmentDragActive && "bg-transparent",
)}
>
<Paperclip className="h-3.5 w-3.5 mr-1.5" />
{uploadAttachment.isPending || importMarkdownDocument.isPending ? "Uploading..." : "Upload attachment"}
</Button>
</div>
);
return ( return (
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
@@ -774,9 +806,11 @@ export function IssueDetail() {
const attachment = await uploadAttachment.mutateAsync(file); const attachment = await uploadAttachment.mutateAsync(file);
return attachment.contentPath; return attachment.contentPath;
}} }}
extraActions={!hasAttachments ? attachmentUploadButton : undefined}
/> />
<div {hasAttachments ? (
<div
className={cn( className={cn(
"space-y-3 rounded-lg transition-colors", "space-y-3 rounded-lg transition-colors",
)} )}
@@ -796,84 +830,54 @@ export function IssueDetail() {
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium text-muted-foreground">Attachments</h3> <h3 className="text-sm font-medium text-muted-foreground">Attachments</h3>
<div {attachmentUploadButton}
className={cn(
"rounded-md border border-dashed border-border p-1 transition-colors",
attachmentDragActive && "border-primary bg-primary/5",
)}
>
<input
ref={fileInputRef}
type="file"
accept="image/*,application/pdf,text/plain,text/markdown,application/json,text/csv,text/html,.md,.markdown"
className="hidden"
onChange={handleFilePicked}
multiple
/>
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={uploadAttachment.isPending || importMarkdownDocument.isPending}
className={cn(
"border-transparent bg-transparent shadow-none",
attachmentDragActive && "bg-transparent",
)}
>
<Paperclip className="h-3.5 w-3.5 mr-1.5" />
{uploadAttachment.isPending || importMarkdownDocument.isPending ? "Uploading..." : "Upload attachment"}
</Button>
</div>
</div> </div>
{attachmentError && ( {attachmentError && (
<p className="text-xs text-destructive">{attachmentError}</p> <p className="text-xs text-destructive">{attachmentError}</p>
)} )}
{(!attachments || attachments.length === 0) ? ( <div className="space-y-2">
<p className="text-xs text-muted-foreground">No attachments yet.</p> {attachmentList.map((attachment) => (
) : ( <div key={attachment.id} className="border border-border rounded-md p-2">
<div className="space-y-2"> <div className="flex items-center justify-between gap-2">
{attachments.map((attachment) => ( <a
<div key={attachment.id} className="border border-border rounded-md p-2"> href={attachment.contentPath}
<div className="flex items-center justify-between gap-2"> target="_blank"
<a rel="noreferrer"
href={attachment.contentPath} className="text-xs hover:underline truncate"
target="_blank" title={attachment.originalFilename ?? attachment.id}
rel="noreferrer" >
className="text-xs hover:underline truncate" {attachment.originalFilename ?? attachment.id}
title={attachment.originalFilename ?? attachment.id} </a>
> <button
{attachment.originalFilename ?? attachment.id} type="button"
</a> className="text-muted-foreground hover:text-destructive"
<button onClick={() => deleteAttachment.mutate(attachment.id)}
type="button" disabled={deleteAttachment.isPending}
className="text-muted-foreground hover:text-destructive" title="Delete attachment"
onClick={() => deleteAttachment.mutate(attachment.id)} >
disabled={deleteAttachment.isPending} <Trash2 className="h-3.5 w-3.5" />
title="Delete attachment" </button>
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
<p className="text-[11px] text-muted-foreground">
{attachment.contentType} · {(attachment.byteSize / 1024).toFixed(1)} KB
</p>
{isImageAttachment(attachment) && (
<a href={attachment.contentPath} target="_blank" rel="noreferrer">
<img
src={attachment.contentPath}
alt={attachment.originalFilename ?? "attachment"}
className="mt-2 max-h-56 rounded border border-border object-contain bg-accent/10"
loading="lazy"
/>
</a>
)}
</div> </div>
))} <p className="text-[11px] text-muted-foreground">
</div> {attachment.contentType} · {(attachment.byteSize / 1024).toFixed(1)} KB
)} </p>
</div> {isImageAttachment(attachment) && (
<a href={attachment.contentPath} target="_blank" rel="noreferrer">
<img
src={attachment.contentPath}
alt={attachment.originalFilename ?? "attachment"}
className="mt-2 max-h-56 rounded border border-border object-contain bg-accent/10"
loading="lazy"
/>
</a>
)}
</div>
))}
</div>
</div>
) : null}
<Separator /> <Separator />