diff --git a/resources/js/components/commit-composer.tsx b/resources/js/components/commit-composer.tsx index 767b915fd..624d5e231 100644 --- a/resources/js/components/commit-composer.tsx +++ b/resources/js/components/commit-composer.tsx @@ -1,6 +1,6 @@ import { Form, Link, usePage } from '@inertiajs/react'; import { ChartBar, Clock3 } from 'lucide-react'; -import type { KeyboardEvent } from 'react'; +import type { DragEvent, KeyboardEvent } from 'react'; import { useEffect, useRef, useState } from 'react'; import CommitController from '@/actions/App/Http/Controllers/CommitController'; import ScheduledCommitController from '@/actions/App/Http/Controllers/ScheduledCommitController'; @@ -76,6 +76,13 @@ function focusBody(id: string): void { requestAnimationFrame(() => document.getElementById(id)?.focus()); } +function preventFileDrop(event: DragEvent): void { + if (Array.from(event.dataTransfer.types).includes('Files')) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'none'; + } +} + function useCommitSchedule() { const [isScheduling, setIsScheduling] = useState(false); const [scheduledAt, setScheduledAt] = useState(''); @@ -156,6 +163,7 @@ function CommitComposerBody({ const textBox = useRef(null); const preview = useComposerPreview({ body, textareaRef, isSubmittable }); const isIssue = issue.command !== null; + const acceptsImages = !isIssue && poll === null; // When the dialog's text box shrinks, as the keyboard opens or an image is added, it keeps the caret in view. useEffect(() => { @@ -183,9 +191,15 @@ function CommitComposerBody({ return (
- {attachments.isDraggingFiles && ( + {acceptsImages && attachments.isDraggingFiles && ( )} preview.handleShortcut(event) || diff --git a/tests/Unit/ComposerDraft.test.tsx b/tests/Unit/ComposerDraft.test.tsx index 41e169499..26bd9c065 100644 --- a/tests/Unit/ComposerDraft.test.tsx +++ b/tests/Unit/ComposerDraft.test.tsx @@ -599,6 +599,78 @@ test('picking a suggestion with Enter starts an empty issue, and the shortcut op } }); +for (const command of ['/bug', '/feature']) { + test(`${command} refuses image attachments and restores them when returning to a commit`, () => { + let view = render(); + const textarea = () => + view.fields.find((element) => element.type === MentionTextarea)!; + (textarea().props.onValueChange as (value: string) => void)( + `${command} A text-only issue`, + ); + draft.body = 'A text-only issue'; + view = render(); + + assert.equal(textarea().props.onPaste, undefined); + assert.equal( + view.fields.some((element) => element.type === ImagePickerButton), + false, + ); + + const dropzone = view.fields.find( + (element) => element.props.onDrop !== undefined, + )!; + const dropped = { + dataTransfer: { + types: ['Files'], + files: [image], + dropEffect: 'copy', + }, + preventDefault: vi.fn(), + }; + (dropzone.props.onDragOver as (event: unknown) => void)(dropped); + (dropzone.props.onDrop as (event: unknown) => void)(dropped); + assert.equal(dropped.preventDefault.mock.calls.length, 2); + assert.equal(dropped.dataTransfer.dropEffect, 'none'); + assert.deepEqual(draft.images, []); + assert.equal( + render().fields.some( + (element) => element.type === ImageDropOverlay, + ), + false, + ); + + const text = { + dataTransfer: { + types: ['text/plain'], + files: [], + dropEffect: 'copy', + }, + preventDefault: vi.fn(), + }; + (dropzone.props.onDrop as (event: unknown) => void)(text); + assert.equal(text.preventDefault.mock.calls.length, 0); + + const badge = view.fields.find( + (element) => element.type === IssueCommandBadge, + )!; + vi.stubGlobal('requestAnimationFrame', vi.fn()); + try { + (badge.props.onExit as () => void)(); + } finally { + vi.unstubAllGlobals(); + } + view = render(); + + const pasted = { + clipboardData: { files: [image] }, + preventDefault: vi.fn(), + }; + (textarea().props.onPaste as (event: unknown) => void)(pasted); + assert.equal(pasted.preventDefault.mock.calls.length, 1); + assert.deepEqual(draft.images, [image]); + }); +} + test('a draft that mentions a bug suggests /bug, keeps the text when used, and can be dismissed', () => { draft.body = 'The search has a bug when going back'; let hint = render().fields.find(