mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix(feedback): improve attachment failure feedback
This commit is contained in:
@@ -112,6 +112,7 @@ describe('SidebarFeedbackDialog image submission', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => expect((send as HTMLButtonElement).disabled).toBe(false))
|
||||
expect(screen.getByRole('button', { name: 'Remove shot.png' })).toBeTruthy()
|
||||
fireEvent.click(send)
|
||||
await waitFor(() => expect(mocks.submit).toHaveBeenCalledTimes(1))
|
||||
expect(mocks.submit.mock.calls[0]?.[0].images).toEqual([
|
||||
|
||||
@@ -84,7 +84,8 @@ export function SidebarFeedbackImageAttachments({
|
||||
type="button"
|
||||
aria-label={translate(
|
||||
'auto.components.sidebar.SidebarFeedbackImageAttachments.removeImage',
|
||||
'Remove image'
|
||||
'Remove {{fileName}}',
|
||||
{ fileName: image.name }
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={() => onRemove(image.id)}
|
||||
|
||||
@@ -658,6 +658,18 @@
|
||||
},
|
||||
"pluginCommandKeybindings": {
|
||||
"group": "Plugins"
|
||||
},
|
||||
"feedback": {
|
||||
"image": {
|
||||
"attachments": {
|
||||
"fallbackName": "Image attachment",
|
||||
"unsupportedType": "{{fileName}} is not a supported image type.",
|
||||
"empty": "{{fileName}} is empty.",
|
||||
"tooLarge": "{{fileName}} is larger than {{maxSize}}.",
|
||||
"tooMany": "You can attach up to {{maxCount}} images.",
|
||||
"additionalErrors": "{{count}} additional images could not be attached."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
@@ -5025,7 +5037,7 @@
|
||||
"SidebarFeedbackImageAttachments": {
|
||||
"screenshotsHint": "Attach up to {count} screenshots",
|
||||
"attachImages": "Attach",
|
||||
"removeImage": "Remove image"
|
||||
"removeImage": "Remove {{fileName}}"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -635,6 +635,18 @@
|
||||
},
|
||||
"pluginCommandKeybindings": {
|
||||
"group": "Plugins"
|
||||
},
|
||||
"feedback": {
|
||||
"image": {
|
||||
"attachments": {
|
||||
"fallbackName": "Image attachment",
|
||||
"unsupportedType": "{{fileName}} is not a supported image type.",
|
||||
"empty": "{{fileName}} is empty.",
|
||||
"tooLarge": "{{fileName}} is larger than {{maxSize}}.",
|
||||
"tooMany": "You can attach up to {{maxCount}} images.",
|
||||
"additionalErrors": "{{count}} additional images could not be attached."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
@@ -5002,7 +5014,7 @@
|
||||
"SidebarFeedbackImageAttachments": {
|
||||
"screenshotsHint": "Attach up to {count} screenshots",
|
||||
"attachImages": "Attach",
|
||||
"removeImage": "Remove image"
|
||||
"removeImage": "Remove {{fileName}}"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -635,6 +635,18 @@
|
||||
},
|
||||
"pluginCommandKeybindings": {
|
||||
"group": "Plugins"
|
||||
},
|
||||
"feedback": {
|
||||
"image": {
|
||||
"attachments": {
|
||||
"fallbackName": "Image attachment",
|
||||
"unsupportedType": "{{fileName}} is not a supported image type.",
|
||||
"empty": "{{fileName}} is empty.",
|
||||
"tooLarge": "{{fileName}} is larger than {{maxSize}}.",
|
||||
"tooMany": "You can attach up to {{maxCount}} images.",
|
||||
"additionalErrors": "{{count}} additional images could not be attached."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
@@ -5002,7 +5014,7 @@
|
||||
"SidebarFeedbackImageAttachments": {
|
||||
"screenshotsHint": "Attach up to {count} screenshots",
|
||||
"attachImages": "Attach",
|
||||
"removeImage": "Remove image"
|
||||
"removeImage": "Remove {{fileName}}"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -635,6 +635,18 @@
|
||||
},
|
||||
"pluginCommandKeybindings": {
|
||||
"group": "Plugins"
|
||||
},
|
||||
"feedback": {
|
||||
"image": {
|
||||
"attachments": {
|
||||
"fallbackName": "Image attachment",
|
||||
"unsupportedType": "{{fileName}} is not a supported image type.",
|
||||
"empty": "{{fileName}} is empty.",
|
||||
"tooLarge": "{{fileName}} is larger than {{maxSize}}.",
|
||||
"tooMany": "You can attach up to {{maxCount}} images.",
|
||||
"additionalErrors": "{{count}} additional images could not be attached."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
@@ -5002,7 +5014,7 @@
|
||||
"SidebarFeedbackImageAttachments": {
|
||||
"screenshotsHint": "Attach up to {count} screenshots",
|
||||
"attachImages": "Attach",
|
||||
"removeImage": "Remove image"
|
||||
"removeImage": "Remove {{fileName}}"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -635,6 +635,18 @@
|
||||
},
|
||||
"pluginCommandKeybindings": {
|
||||
"group": "Plugins"
|
||||
},
|
||||
"feedback": {
|
||||
"image": {
|
||||
"attachments": {
|
||||
"fallbackName": "Image attachment",
|
||||
"unsupportedType": "{{fileName}} is not a supported image type.",
|
||||
"empty": "{{fileName}} is empty.",
|
||||
"tooLarge": "{{fileName}} is larger than {{maxSize}}.",
|
||||
"tooMany": "You can attach up to {{maxCount}} images.",
|
||||
"additionalErrors": "{{count}} additional images could not be attached."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
@@ -5002,7 +5014,7 @@
|
||||
"SidebarFeedbackImageAttachments": {
|
||||
"screenshotsHint": "Attach up to {count} screenshots",
|
||||
"attachImages": "Attach",
|
||||
"removeImage": "Remove image"
|
||||
"removeImage": "Remove {{fileName}}"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
|
||||
@@ -68,6 +68,19 @@ describe('readFeedbackImageFiles', () => {
|
||||
expect(errors).toEqual(['notes.pdf is not a supported image type.'])
|
||||
})
|
||||
|
||||
it('caps rejection detail so a large drop cannot mount one toast per file', async () => {
|
||||
const files = Array.from(
|
||||
{ length: 100 },
|
||||
(_, index) => new File(['x'], `image-${index}.svg`, { type: 'image/svg+xml' })
|
||||
)
|
||||
|
||||
const { images, errors } = await readFeedbackImageFiles(files, 0)
|
||||
|
||||
expect(images).toEqual([])
|
||||
expect(errors).toHaveLength(5)
|
||||
expect(errors.at(-1)).toBe('96 additional images could not be attached.')
|
||||
})
|
||||
|
||||
it('reports an oversized image instead of skipping it', async () => {
|
||||
const { images, errors } = await readFeedbackImageFiles(
|
||||
[pngFile('huge.png', MAX_FEEDBACK_IMAGE_BYTES + 1)],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { createBrowserUuid } from './browser-uuid'
|
||||
|
||||
export const MAX_FEEDBACK_IMAGE_COUNT = 4
|
||||
@@ -10,6 +11,7 @@ export const SUPPORTED_FEEDBACK_IMAGE_TYPES = [
|
||||
] as const
|
||||
|
||||
export const FEEDBACK_IMAGE_FILE_ACCEPT = SUPPORTED_FEEDBACK_IMAGE_TYPES.join(',')
|
||||
const MAX_FEEDBACK_IMAGE_DETAIL_ERRORS = 4
|
||||
|
||||
export type FeedbackImageDraft = {
|
||||
id: string
|
||||
@@ -50,6 +52,12 @@ export function formatFeedbackImageSize(bytes: number): string {
|
||||
: `${Math.max(1, Math.round(bytes / 1024))} KB`
|
||||
}
|
||||
|
||||
function feedbackImageDisplayName(file: File): string {
|
||||
return (
|
||||
file.name || translate('auto.lib.feedback.image.attachments.fallbackName', 'Image attachment')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts picked/pasted/dropped files into drafts. Rejections come back as
|
||||
* messages rather than being skipped, because silently dropping an attachment
|
||||
@@ -62,25 +70,57 @@ export async function readFeedbackImageFiles(
|
||||
const images: FeedbackImageDraft[] = []
|
||||
const errors: string[] = []
|
||||
let remaining = MAX_FEEDBACK_IMAGE_COUNT - existingCount
|
||||
let omittedErrorCount = 0
|
||||
const addError = (createMessage: () => string): void => {
|
||||
if (errors.length < MAX_FEEDBACK_IMAGE_DETAIL_ERRORS) {
|
||||
errors.push(createMessage())
|
||||
} else {
|
||||
omittedErrorCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
const fileName = feedbackImageDisplayName(file)
|
||||
if (!isSupportedType(file.type)) {
|
||||
errors.push(`${file.name || 'Image'} is not a supported image type.`)
|
||||
addError(() =>
|
||||
translate(
|
||||
'auto.lib.feedback.image.attachments.unsupportedType',
|
||||
'{{fileName}} is not a supported image type.',
|
||||
{ fileName }
|
||||
)
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (file.size === 0) {
|
||||
errors.push(`${file.name || 'Image'} is empty.`)
|
||||
addError(() =>
|
||||
translate('auto.lib.feedback.image.attachments.empty', '{{fileName}} is empty.', {
|
||||
fileName
|
||||
})
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (file.size > MAX_FEEDBACK_IMAGE_BYTES) {
|
||||
errors.push(
|
||||
`${file.name || 'Image'} is larger than ${formatFeedbackImageSize(MAX_FEEDBACK_IMAGE_BYTES)}.`
|
||||
addError(() =>
|
||||
translate(
|
||||
'auto.lib.feedback.image.attachments.tooLarge',
|
||||
'{{fileName}} is larger than {{maxSize}}.',
|
||||
{
|
||||
fileName,
|
||||
maxSize: formatFeedbackImageSize(MAX_FEEDBACK_IMAGE_BYTES)
|
||||
}
|
||||
)
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (remaining <= 0) {
|
||||
errors.push(`You can attach up to ${MAX_FEEDBACK_IMAGE_COUNT} images.`)
|
||||
addError(() =>
|
||||
translate(
|
||||
'auto.lib.feedback.image.attachments.tooMany',
|
||||
'You can attach up to {{maxCount}} images.',
|
||||
{ maxCount: MAX_FEEDBACK_IMAGE_COUNT }
|
||||
)
|
||||
)
|
||||
break
|
||||
}
|
||||
remaining -= 1
|
||||
@@ -88,7 +128,7 @@ export async function readFeedbackImageFiles(
|
||||
// Why: crypto.randomUUID is undefined in non-secure browser contexts (LAN
|
||||
// web client over plain HTTP); createBrowserUuid falls back safely.
|
||||
id: `${file.name}-${file.size}-${createBrowserUuid()}`,
|
||||
name: file.name || 'pasted-image',
|
||||
name: fileName,
|
||||
contentType: file.type,
|
||||
bytes: file.size,
|
||||
data: new Uint8Array(await file.arrayBuffer()),
|
||||
@@ -102,6 +142,16 @@ export async function readFeedbackImageFiles(
|
||||
throw error
|
||||
}
|
||||
|
||||
if (omittedErrorCount > 0) {
|
||||
errors.push(
|
||||
translate(
|
||||
'auto.lib.feedback.image.attachments.additionalErrors',
|
||||
'{{count}} additional images could not be attached.',
|
||||
{ count: omittedErrorCount }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return { images, errors }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user