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 type { Issue } from "@paperclipai/shared";
import { issuesApi } from "../api/issues";
@@ -46,11 +46,13 @@ export function IssueDocumentsSection({
canDeleteDocuments,
mentions,
imageUploadHandler,
extraActions,
}: {
issue: Issue;
canDeleteDocuments: boolean;
mentions?: MentionOption[];
imageUploadHandler?: (file: File) => Promise<string>;
extraActions?: ReactNode;
}) {
const queryClient = useQueryClient();
const [confirmDeleteKey, setConfirmDeleteKey] = useState<string | null>(null);
@@ -106,6 +108,7 @@ export function IssueDocumentsSection({
}, [documents]);
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."
@@ -302,13 +305,26 @@ export function IssueDocumentsSection({
return (
<div className="space-y-3">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium text-muted-foreground">Documents</h3>
<Button variant="outline" size="sm" onClick={beginNewDocument}>
<Plus className="mr-1.5 h-3.5 w-3.5" />
New document
</Button>
</div>
{isEmpty && !draft?.isNew ? (
<div className="flex items-center justify-end 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 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>}
@@ -367,10 +383,6 @@ export function IssueDocumentsSection({
</div>
)}
{sortedDocuments.length === 0 && !issue.legacyPlanDocument ? (
<p className="text-xs text-muted-foreground">No documents yet.</p>
) : null}
{!hasRealPlan && issue.legacyPlanDocument ? (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3">
<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 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 (
<div className="max-w-2xl space-y-6">
@@ -774,9 +806,11 @@ export function IssueDetail() {
const attachment = await uploadAttachment.mutateAsync(file);
return attachment.contentPath;
}}
extraActions={!hasAttachments ? attachmentUploadButton : undefined}
/>
<div
{hasAttachments ? (
<div
className={cn(
"space-y-3 rounded-lg transition-colors",
)}
@@ -796,84 +830,54 @@ export function IssueDetail() {
>
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium text-muted-foreground">Attachments</h3>
<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>
{attachmentUploadButton}
</div>
{attachmentError && (
<p className="text-xs text-destructive">{attachmentError}</p>
)}
{(!attachments || attachments.length === 0) ? (
<p className="text-xs text-muted-foreground">No attachments yet.</p>
) : (
<div className="space-y-2">
{attachments.map((attachment) => (
<div key={attachment.id} className="border border-border rounded-md p-2">
<div className="flex items-center justify-between gap-2">
<a
href={attachment.contentPath}
target="_blank"
rel="noreferrer"
className="text-xs hover:underline truncate"
title={attachment.originalFilename ?? attachment.id}
>
{attachment.originalFilename ?? attachment.id}
</a>
<button
type="button"
className="text-muted-foreground hover:text-destructive"
onClick={() => deleteAttachment.mutate(attachment.id)}
disabled={deleteAttachment.isPending}
title="Delete attachment"
>
<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 className="space-y-2">
{attachmentList.map((attachment) => (
<div key={attachment.id} className="border border-border rounded-md p-2">
<div className="flex items-center justify-between gap-2">
<a
href={attachment.contentPath}
target="_blank"
rel="noreferrer"
className="text-xs hover:underline truncate"
title={attachment.originalFilename ?? attachment.id}
>
{attachment.originalFilename ?? attachment.id}
</a>
<button
type="button"
className="text-muted-foreground hover:text-destructive"
onClick={() => deleteAttachment.mutate(attachment.id)}
disabled={deleteAttachment.isPending}
title="Delete attachment"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
))}
</div>
)}
</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>
</div>
) : null}
<Separator />