refactor: clarify image drag constant names
This commit is contained in:
1 parent
f0ed41c8dd
commit
9485f9bf40
6 files changed
+23
-20
No files matched your search
@@ -3,12 +3,14 @@ import { randomUUID } from 'node:crypto'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const IMAGE_EXTENSIONS: Readonly<Record<string, string>> = {
|
||||
// Keep supported MIME types in sync with IMAGE_MEDIA_TYPES in
|
||||
// packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx (extensions: adjacent index.ts).
|
||||
const DRAG_FILE_EXTENSION_BY_MIME: Readonly<Record<string, string>> = {
|
||||
'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp',
|
||||
'image/bmp': 'bmp', 'image/x-icon': 'ico', 'image/vnd.microsoft.icon': 'ico', 'image/svg+xml': 'svg',
|
||||
}
|
||||
// Bounds aggregate original bytes in preparation and retained temporary files.
|
||||
const MAX_IMAGE_BYTES = 64 * 1024 * 1024
|
||||
const MAX_TOTAL_DRAG_FILE_BYTES = 64 * 1024 * 1024
|
||||
|
||||
interface ImageDragFile {
|
||||
readonly directory: string
|
||||
@@ -36,16 +38,17 @@ export class ImageDragFiles {
|
||||
* @returns application-local opaque handle; failures retain no usable handle or partial file.
|
||||
*/
|
||||
prepare(bytes: unknown, name: unknown, mediaType: unknown): Promise<string> {
|
||||
const extension = typeof mediaType === 'string' && Object.hasOwn(IMAGE_EXTENSIONS, mediaType)
|
||||
? IMAGE_EXTENSIONS[mediaType] : undefined
|
||||
if (this.disposed || !(bytes instanceof Uint8Array) || bytes.byteLength === 0 || bytes.byteLength > MAX_IMAGE_BYTES - this.stagedBytes
|
||||
|| typeof name !== 'string' || typeof mediaType !== 'string' || extension === undefined) {
|
||||
const outputExtension = typeof mediaType === 'string' && Object.hasOwn(DRAG_FILE_EXTENSION_BY_MIME, mediaType)
|
||||
? DRAG_FILE_EXTENSION_BY_MIME[mediaType] : undefined
|
||||
if (this.disposed || !(bytes instanceof Uint8Array) || bytes.byteLength === 0
|
||||
|| bytes.byteLength > MAX_TOTAL_DRAG_FILE_BYTES - this.stagedBytes
|
||||
|| typeof name !== 'string' || typeof mediaType !== 'string' || outputExtension === undefined) {
|
||||
return Promise.reject(new Error('Invalid image transfer'))
|
||||
}
|
||||
let stem = name.replace(/\.[^.]*$/u, '').replace(/[<>:"/\\|?*\x00-\x1f]/gu, '_').replace(/^[. ]+|[. ]+$/gu, '').slice(0, 120) || 'image'
|
||||
if (/^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/iu.test(stem)) stem = `_${stem}`
|
||||
this.stagedBytes += bytes.byteLength
|
||||
const operation = this.write(bytes, `${stem}.${extension}`).catch((error: unknown) => {
|
||||
const operation = this.write(bytes, `${stem}.${outputExtension}`).catch((error: unknown) => {
|
||||
this.stagedBytes -= bytes.byteLength
|
||||
throw error
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Document drag-and-drop listeners owned by one mounted attachment view. */
|
||||
import { IMAGE_DRAG_TYPE } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { IMAGE_DRAG_TOKEN_TYPE } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ComposerAttachmentsProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
|
||||
/**
|
||||
@@ -38,7 +38,7 @@ export function installDocumentDropEvents(
|
||||
): () => void {
|
||||
const fileTransfer = (event: globalThis.DragEvent): DataTransfer | null => {
|
||||
const dataTransfer = event.dataTransfer
|
||||
if (dataTransfer === null || (!dataTransfer.types.includes('Files') && !dataTransfer.types.includes(IMAGE_DRAG_TYPE))) return null
|
||||
if (dataTransfer === null || (!dataTransfer.types.includes('Files') && !dataTransfer.types.includes(IMAGE_DRAG_TOKEN_TYPE))) return null
|
||||
return dataTransfer
|
||||
}
|
||||
const reset = (): void => {
|
||||
@@ -50,7 +50,7 @@ export function installDocumentDropEvents(
|
||||
if (dataTransfer === null) return
|
||||
event.preventDefault()
|
||||
dragDepth.current += 1
|
||||
if (!dataTransfer.types.includes(IMAGE_DRAG_TYPE)) setDragActive(true)
|
||||
if (!dataTransfer.types.includes(IMAGE_DRAG_TOKEN_TYPE)) setDragActive(true)
|
||||
}
|
||||
const onDragOver = (event: globalThis.DragEvent): void => {
|
||||
const dataTransfer = fileTransfer(event)
|
||||
@@ -72,7 +72,7 @@ export function installDocumentDropEvents(
|
||||
event.preventDefault()
|
||||
reset()
|
||||
if (canAcceptDrop) {
|
||||
const image = dataTransfer.types.includes(IMAGE_DRAG_TYPE) ? readImageDrag(dataTransfer) : undefined
|
||||
const image = dataTransfer.types.includes(IMAGE_DRAG_TOKEN_TYPE) ? readImageDrag(dataTransfer) : undefined
|
||||
const files = image === undefined ? [...dataTransfer.files] : [image]
|
||||
if (files.length > 0) onAddFiles(files, droppedDirectories(dataTransfer, files))
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
||||
import { Toast } from './Toast.tsx'
|
||||
import type { DesktopImageDrag } from './image-drag.ts'
|
||||
|
||||
/** Browser drag token; file bytes remain in the owning mounted application. */
|
||||
export const IMAGE_DRAG_TYPE = 'application/x-dsh-image'
|
||||
/** DataTransfer type for an application-local drag token; file bytes remain in the owning mounted application. */
|
||||
export const IMAGE_DRAG_TOKEN_TYPE = 'application/x-dsh-image'
|
||||
|
||||
interface ImageTransfer {
|
||||
desktop: DesktopImageDrag | undefined
|
||||
@@ -43,7 +43,7 @@ export function ImageDragProvider({ children }: { readonly children: ReactNode }
|
||||
}, [end])
|
||||
const read = useCallback((transfer: DataTransfer): File | undefined => {
|
||||
const current = active.current
|
||||
return current !== undefined && current.token === transfer.getData(IMAGE_DRAG_TYPE) ? current.file : undefined
|
||||
return current !== undefined && current.token === transfer.getData(IMAGE_DRAG_TOKEN_TYPE) ? current.file : undefined
|
||||
}, [])
|
||||
const value = useMemo(() => ({ desktop, report, begin, end, read }), [desktop, report, begin, end, read])
|
||||
return <ImageDragFeedback.Provider value={value}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Original image files supplied to browser and native drag sessions. */
|
||||
import { useCallback, useEffect, useRef, type DragEvent, type MouseEvent, type PointerEvent } from 'react'
|
||||
|
||||
import { IMAGE_DRAG_TYPE, useImageDragTransfer } from './ImageDragProvider.tsx'
|
||||
import { IMAGE_DRAG_TOKEN_TYPE, useImageDragTransfer } from './ImageDragProvider.tsx'
|
||||
|
||||
/** Application-owned native image transfer; prepared handles never expose host paths. */
|
||||
export interface DesktopImageDrag {
|
||||
@@ -106,7 +106,7 @@ export function useImageDrag(src: string, name: string, failureText: string, ori
|
||||
void desktop.start(prepared.handle).then((started) => { if (!started) failed() }, failed)
|
||||
return
|
||||
}
|
||||
event.dataTransfer.setData(IMAGE_DRAG_TYPE, transfer.begin(prepared.file))
|
||||
event.dataTransfer.setData(IMAGE_DRAG_TOKEN_TYPE, transfer.begin(prepared.file))
|
||||
event.dataTransfer.setData('DownloadURL', `${prepared.file.type}:${prepared.file.name}:${src}`)
|
||||
}, [src, desktop, prepare, reportFailure, failureText, transfer])
|
||||
const onPointerDown = useCallback((event: PointerEvent<HTMLImageElement>): void => {
|
||||
|
||||
@@ -67,7 +67,7 @@ export { pointerModality } from './input-modality.ts'
|
||||
export { Toast } from './Toast.tsx'
|
||||
export { fileSizeText } from './file-size.ts'
|
||||
export { writeClipboard } from './clipboard.ts'
|
||||
export { IMAGE_DRAG_TYPE, ImageDragProvider, ImageDrop } from './ImageDragProvider.tsx'
|
||||
export { IMAGE_DRAG_TOKEN_TYPE, ImageDragProvider, ImageDrop } from './ImageDragProvider.tsx'
|
||||
export { DraggableImage } from './DraggableImage.tsx'
|
||||
export type { DraggableImageProps } from './DraggableImage.tsx'
|
||||
export type { DesktopImageDrag } from './image-drag.ts'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect } from 'react'
|
||||
import { File as NodeFile } from 'node:buffer'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { IMAGE_DRAG_TYPE, ImageDragProvider, useImageDrop } from '../src/ImageDragProvider.tsx'
|
||||
import { IMAGE_DRAG_TOKEN_TYPE, ImageDragProvider, useImageDrop } from '../src/ImageDragProvider.tsx'
|
||||
import { DraggableImage } from '../src/DraggableImage.tsx'
|
||||
import { useImageDrag, type DesktopImageDrag } from '../src/image-drag.ts'
|
||||
|
||||
@@ -36,7 +36,7 @@ it('hands original bytes to the browser without alt text or an internal URI payl
|
||||
await act(async () => { fireEvent.dragStart(screen.getByRole('img'), { dataTransfer }) })
|
||||
expect(dataTransfer.items.add).not.toHaveBeenCalled()
|
||||
expect(dataTransfer.items.clear).toHaveBeenCalledOnce()
|
||||
expect(dataTransfer.setData).toHaveBeenCalledWith(IMAGE_DRAG_TYPE, expect.any(String))
|
||||
expect(dataTransfer.setData).toHaveBeenCalledWith(IMAGE_DRAG_TOKEN_TYPE, expect.any(String))
|
||||
expect(dataTransfer.effectAllowed).toBe('copy')
|
||||
expect(dataTransfer.setData).toHaveBeenCalledWith('DownloadURL', 'image/png:original.png:blob:original')
|
||||
fireEvent.click(screen.getByRole('img'), { detail: 1 })
|
||||
@@ -169,7 +169,7 @@ it('fetches a readable source once and rejects a foreign drag token', async () =
|
||||
expect(fetchImage).toHaveBeenCalledOnce()
|
||||
const dataTransfer = transfer()
|
||||
fireEvent.dragStart(screen.getByRole('img'), { dataTransfer })
|
||||
dataTransfer.setData(IMAGE_DRAG_TYPE, 'foreign')
|
||||
dataTransfer.setData(IMAGE_DRAG_TOKEN_TYPE, 'foreign')
|
||||
fireEvent.drop(screen.getByTestId('drop'), { dataTransfer })
|
||||
expect(receive).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user