mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 00:30:19 +02:00
Keep issue composer as sole drop target
This commit is contained in:
1 parent
3313c8d6ce
commit
65879a8dee
2 files changed
+135
-27
No files matched your search
@@ -940,6 +940,104 @@ describe("IssueChatThread", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows only the outer composer drop overlay when dragging over the reply editor", () => {
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<MemoryRouter>
|
||||
<IssueChatThread
|
||||
comments={[]}
|
||||
linkedRuns={[]}
|
||||
timelineEvents={[]}
|
||||
liveRuns={[]}
|
||||
onAdd={async () => {}}
|
||||
imageUploadHandler={async () => "/api/attachments/image/content"}
|
||||
onAttachImage={async () => undefined}
|
||||
enableLiveTranscriptPolling={false}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
|
||||
const composer = container.querySelector('[data-testid="issue-chat-composer"]') as HTMLDivElement | null;
|
||||
const editor = container.querySelector('textarea[aria-label="Issue chat editor"]') as HTMLTextAreaElement | null;
|
||||
expect(composer).not.toBeNull();
|
||||
expect(editor).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
editor?.dispatchEvent(createFileDragEvent("dragenter", [
|
||||
new File(["hello"], "notes.txt", { type: "text/plain" }),
|
||||
]));
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-testid="issue-chat-composer-drop-overlay"]')).not.toBeNull();
|
||||
expect(container.textContent).toContain("Drop to upload");
|
||||
expect(container.textContent).not.toContain("Drop image to upload");
|
||||
expect(composer?.className).toContain("border-primary/45");
|
||||
|
||||
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement | null;
|
||||
expect(fileInput?.getAttribute("accept")).toBeNull();
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows non-image attachment upload state in the composer after a drop from the editor", async () => {
|
||||
const root = createRoot(container);
|
||||
const onAttachImage = vi.fn(async (file: File) => ({
|
||||
id: "attachment-1",
|
||||
companyId: "company-1",
|
||||
issueId: "issue-1",
|
||||
issueCommentId: null,
|
||||
assetId: "asset-1",
|
||||
provider: "local_disk",
|
||||
objectKey: "issues/issue-1/report.pdf",
|
||||
contentPath: "/api/attachments/attachment-1/content",
|
||||
originalFilename: file.name,
|
||||
contentType: file.type,
|
||||
byteSize: file.size,
|
||||
sha256: "abc123",
|
||||
createdByAgentId: null,
|
||||
createdByUserId: "user-1",
|
||||
createdAt: new Date("2026-04-24T12:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-24T12:00:00.000Z"),
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MemoryRouter>
|
||||
<IssueChatThread
|
||||
comments={[]}
|
||||
linkedRuns={[]}
|
||||
timelineEvents={[]}
|
||||
liveRuns={[]}
|
||||
onAdd={async () => {}}
|
||||
onAttachImage={onAttachImage}
|
||||
enableLiveTranscriptPolling={false}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
|
||||
const editor = container.querySelector('textarea[aria-label="Issue chat editor"]') as HTMLTextAreaElement | null;
|
||||
const file = new File(["report body"], "report.pdf", { type: "application/pdf" });
|
||||
|
||||
await act(async () => {
|
||||
editor?.dispatchEvent(createFileDragEvent("drop", [file]));
|
||||
});
|
||||
|
||||
expect(onAttachImage).toHaveBeenCalledWith(file);
|
||||
expect(container.querySelector('[data-testid="issue-chat-composer-attachments"]')).not.toBeNull();
|
||||
expect(container.textContent).toContain("report.pdf");
|
||||
expect(container.textContent).toContain("Attached to issue");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders the bottom spacer with zero height until the user has submitted", () => {
|
||||
const root = createRoot(container);
|
||||
|
||||
|
||||
@@ -2268,6 +2268,37 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
||||
setIsDragOver(false);
|
||||
}
|
||||
|
||||
function handleFileDragEnter(evt: ReactDragEvent<HTMLDivElement>) {
|
||||
if (!canAcceptFiles || !hasFilePayload(evt)) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
dragDepthRef.current += 1;
|
||||
setIsDragOver(true);
|
||||
}
|
||||
|
||||
function handleFileDragOver(evt: ReactDragEvent<HTMLDivElement>) {
|
||||
if (!canAcceptFiles || !hasFilePayload(evt)) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
evt.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
|
||||
function handleFileDragLeave(evt: ReactDragEvent<HTMLDivElement>) {
|
||||
if (!canAcceptFiles || !hasFilePayload(evt)) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
||||
if (dragDepthRef.current === 0) setIsDragOver(false);
|
||||
}
|
||||
|
||||
function handleFileDrop(evt: ReactDragEvent<HTMLDivElement>) {
|
||||
if (!canAcceptFiles || !hasFilePayload(evt)) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
resetDragState();
|
||||
void handleDroppedFiles(evt.dataTransfer?.files);
|
||||
}
|
||||
|
||||
const canSubmit = !submitting && !!body.trim();
|
||||
|
||||
if (composerDisabledReason) {
|
||||
@@ -2284,33 +2315,12 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
||||
data-testid="issue-chat-composer"
|
||||
className={cn(
|
||||
"relative rounded-md border border-border/70 bg-background/95 p-[15px] shadow-[0_-12px_28px_rgba(15,23,42,0.08)] backdrop-blur transition-[border-color,background-color,box-shadow] duration-150 supports-[backdrop-filter]:bg-background/85 dark:shadow-[0_-12px_28px_rgba(0,0,0,0.28)]",
|
||||
isDragOver && "border-primary/45 bg-background shadow-[0_-12px_28px_rgba(15,23,42,0.08),0_0_0_1px_hsl(var(--primary)/0.18)]",
|
||||
isDragOver && "border-primary/45 bg-background shadow-[0_-12px_28px_rgba(15,23,42,0.08),0_0_0_1px_hsl(var(--primary)/0.16)]",
|
||||
)}
|
||||
onDragEnter={(evt) => {
|
||||
if (!canAcceptFiles || !hasFilePayload(evt)) return;
|
||||
dragDepthRef.current += 1;
|
||||
setIsDragOver(true);
|
||||
}}
|
||||
onDragOver={(evt) => {
|
||||
if (!canAcceptFiles || !hasFilePayload(evt)) return;
|
||||
evt.preventDefault();
|
||||
evt.dataTransfer.dropEffect = "copy";
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
if (!canAcceptFiles) return;
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
||||
if (dragDepthRef.current === 0) setIsDragOver(false);
|
||||
}}
|
||||
onDrop={(evt) => {
|
||||
if (!canAcceptFiles) return;
|
||||
if (evt.defaultPrevented) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
evt.preventDefault();
|
||||
resetDragState();
|
||||
void handleDroppedFiles(evt.dataTransfer?.files);
|
||||
}}
|
||||
onDragEnterCapture={handleFileDragEnter}
|
||||
onDragOverCapture={handleFileDragOver}
|
||||
onDragLeaveCapture={handleFileDragLeave}
|
||||
onDropCapture={handleFileDrop}
|
||||
>
|
||||
{isDragOver && canAcceptFiles ? (
|
||||
<div
|
||||
@@ -2322,7 +2332,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">Drop files to attach</div>
|
||||
<div className="text-sm font-medium text-foreground">Drop to upload</div>
|
||||
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">
|
||||
Images insert into the reply. Other files are added to this issue.
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user