mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(feedback): stop an unsupported pasted image from eating co-pasted text
The paste handler consumed the event whenever the clipboard held any image/* file, but only the four allow-listed types can actually attach. Pasting text alongside an SVG or BMP therefore lost the text and attached nothing — a silent loss of the user's own input, in the dialog where they are mid-sentence. Consume the paste only when something is attachable. Unsupported types still route through readFeedbackImageFiles for their rejection toast, so nothing is dropped silently; the difference is that the default paste is left alone when we have nothing to offer in exchange. Extraction deliberately stays broad. Narrowing it there (as suggested by review) would skip handleAddFiles entirely, and a file paste into a textarea does nothing visible — the image would vanish with no feedback. The drop path is untouched: it must keep cancelling every native file drop or the browser navigates the tab to the file.
This commit is contained in:
@@ -17,6 +17,7 @@ import type { GitHubViewer } from '../../../../shared/types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
extractImageFilesFromDataTransfer,
|
||||
hasAttachableFeedbackImage,
|
||||
readFeedbackImageFiles,
|
||||
releaseFeedbackImageDraft,
|
||||
type FeedbackImageDraft
|
||||
@@ -258,10 +259,16 @@ export function SidebarFeedbackDialog({
|
||||
// screenshot lands whether or not the caret is in the message box.
|
||||
onPaste={(event) => {
|
||||
const pasted = extractImageFilesFromDataTransfer(event.clipboardData)
|
||||
if (pasted.length > 0) {
|
||||
event.preventDefault()
|
||||
handleAddFiles(pasted)
|
||||
if (pasted.length === 0) {
|
||||
return
|
||||
}
|
||||
// Why: consume the paste only when something is actually attachable.
|
||||
// An unsupported image still routes through for its rejection toast,
|
||||
// but preventing default there would silently eat co-pasted text.
|
||||
if (hasAttachableFeedbackImage(pasted)) {
|
||||
event.preventDefault()
|
||||
}
|
||||
handleAddFiles(pasted)
|
||||
}}
|
||||
// Why: dragenter/leave fire per nested child; the hook counts depth so
|
||||
// the highlight only clears once the pointer leaves the dialog.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
MAX_FEEDBACK_IMAGE_BYTES,
|
||||
MAX_FEEDBACK_IMAGE_COUNT,
|
||||
hasAttachableFeedbackImage,
|
||||
readFeedbackImageFiles
|
||||
} from './feedback-image-attachments'
|
||||
|
||||
@@ -19,6 +20,26 @@ function pngFile(name: string, size = 8): File {
|
||||
return file
|
||||
}
|
||||
|
||||
describe('hasAttachableFeedbackImage', () => {
|
||||
it('is true when any file is an allow-listed type', () => {
|
||||
const svg = new File(['x'], 'a.svg', { type: 'image/svg+xml' })
|
||||
expect(hasAttachableFeedbackImage([svg, pngFile('a.png')])).toBe(true)
|
||||
})
|
||||
|
||||
// Why: the paste handler only consumes the event when this is true. An
|
||||
// image/* type outside the allow-list must still fall through so co-pasted
|
||||
// text is not swallowed, while readFeedbackImageFiles raises its toast.
|
||||
it('is false when every file is an unsupported image type', () => {
|
||||
const svg = new File(['x'], 'a.svg', { type: 'image/svg+xml' })
|
||||
const bmp = new File(['x'], 'a.bmp', { type: 'image/bmp' })
|
||||
expect(hasAttachableFeedbackImage([svg, bmp])).toBe(false)
|
||||
})
|
||||
|
||||
it('is false for an empty selection', () => {
|
||||
expect(hasAttachableFeedbackImage([])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readFeedbackImageFiles', () => {
|
||||
it('reads supported images into drafts with distinct ids', async () => {
|
||||
const { images, errors } = await readFeedbackImageFiles([pngFile('a.png'), pngFile('a.png')], 0)
|
||||
|
||||
@@ -25,6 +25,16 @@ function isSupportedType(contentType: string): boolean {
|
||||
return (SUPPORTED_FEEDBACK_IMAGE_TYPES as readonly string[]).includes(contentType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a paste should be consumed. Extraction stays broad so unsupported
|
||||
* image types still reach the rejection toast, but swallowing the paste when
|
||||
* nothing is attachable would also discard any text riding along on the
|
||||
* clipboard.
|
||||
*/
|
||||
export function hasAttachableFeedbackImage(files: readonly File[]): boolean {
|
||||
return files.some((file) => isSupportedType(file.type))
|
||||
}
|
||||
|
||||
export function releaseFeedbackImageDraft(draft: FeedbackImageDraft): void {
|
||||
URL.revokeObjectURL(draft.previewUrl)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user