fix(ui): collapse empty document and attachment states
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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">
|
||||||
|
{extraActions}
|
||||||
<Button variant="outline" size="sm" onClick={beginNewDocument}>
|
<Button variant="outline" size="sm" onClick={beginNewDocument}>
|
||||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||||
New document
|
New document
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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">
|
||||||
|
|||||||
@@ -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,8 +806,10 @@ 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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{hasAttachments ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"space-y-3 rounded-lg transition-colors",
|
"space-y-3 rounded-lg transition-colors",
|
||||||
@@ -796,45 +830,15 @@ 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) ? (
|
|
||||||
<p className="text-xs text-muted-foreground">No attachments yet.</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{attachments.map((attachment) => (
|
{attachmentList.map((attachment) => (
|
||||||
<div key={attachment.id} className="border border-border rounded-md p-2">
|
<div key={attachment.id} className="border border-border rounded-md p-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<a
|
<a
|
||||||
@@ -872,8 +876,8 @@ export function IssueDetail() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user