Keep issue composer as sole drop target

This commit is contained in:
Dotta committed 2026-04-24 13:39:29 -05:00
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);
+37 -27
View File
@@ -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>