Skip to content

Commit 98158c7

Browse files
committed
refactor(core): refactor input upload support
This will streamline how inputs handle drag and drop + paste files. It will add asset source support to array of object uploads as well.
1 parent c762f46 commit 98158c7

35 files changed

+868
-929
lines changed

packages/sanity/src/core/components/previews/portableText/BlockPreview.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Box, Flex, rem, Stack, Text} from '@sanity/ui'
22
import {styled} from 'styled-components'
33
import {getDevicePixelRatio} from 'use-device-pixel-ratio'
44

5+
import {LinearProgress} from '../../progress/LinearProgress'
56
import {Media} from '../_common/Media'
67
import {PREVIEW_SIZES} from '../constants'
78
import {renderPreviewNode} from '../helpers'
@@ -29,10 +30,13 @@ export function BlockPreview(props: Omit<PreviewProps<'block'>, 'renderDefault'>
2930
description,
3031
mediaDimensions = DEFAULT_MEDIA_DIMENSIONS,
3132
media,
33+
progress,
3234
status,
3335
children,
3436
} = props
3537

38+
const isUploading = typeof progress === 'number' && progress > -1
39+
3640
return (
3741
<Stack data-testid="block-preview" space={1}>
3842
<HeaderFlex data-testid="block-preview__header">
@@ -58,6 +62,12 @@ export function BlockPreview(props: Omit<PreviewProps<'block'>, 'renderDefault'>
5862
</Text>
5963
</Box>
6064
)}
65+
66+
{isUploading && (
67+
<Box marginTop={3}>
68+
<LinearProgress value={progress} />
69+
</Box>
70+
)}
6171
</Box>
6272

6373
<Flex gap={1} paddingLeft={1}>

packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {EMPTY_ARRAY} from '../../../util'
1818
import {ActivateOnFocus} from '../../components/ActivateOnFocus/ActivateOnFocus'
1919
import {type ArrayOfObjectsInputProps, type RenderCustomMarkers} from '../../types'
2020
import {type RenderBlockActionsCallback} from '../../types/_transitional'
21-
import {UploadTargetCard} from '../arrays/common/UploadTargetCard'
21+
import {UploadTargetCard} from '../files/common/uploadTarget/UploadTargetCard'
2222
import {ExpandedLayer, Root, StringDiffContainer} from './Compositor.styles'
2323
import {useSetPortableTextMemberItemElementRef} from './contexts/PortableTextMemberItemElementRefsProvider'
2424
import {Editor} from './Editor'
@@ -67,6 +67,7 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
6767
onItemRemove,
6868
onPaste,
6969
onPathFocus,
70+
onSelectFile,
7071
onToggleFullscreen,
7172
onUpload,
7273
path,
@@ -81,7 +82,6 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
8182
renderInput,
8283
renderItem,
8384
renderPreview,
84-
resolveUploader,
8585
value,
8686
} = props
8787

@@ -404,10 +404,12 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
404404
const editorNode = useMemo(
405405
() => (
406406
<UploadTargetCard
407-
types={editor.schemaTypes.portableText.of}
408-
resolveUploader={resolveUploader}
407+
isReadOnly={readOnly}
408+
onSelectFile={onSelectFile}
409409
onUpload={onUpload}
410+
pasteTargetRef={elementRef}
410411
tabIndex={-1}
412+
types={editor.schemaTypes.portableText.of}
411413
>
412414
<StringDiffContainer>
413415
<Editor
@@ -439,27 +441,27 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
439441

440442
// Keep only stable ones here!
441443
[
442-
ariaDescribedBy,
443444
editor.schemaTypes.portableText.of,
444-
editorHotkeys,
445-
editorRenderAnnotation,
446-
editorRenderBlock,
447-
editorRenderChild,
445+
onUpload,
446+
onSelectFile,
447+
ariaDescribedBy,
448448
elementRef,
449-
handleToggleFullscreen,
450-
hideToolbar,
451449
initialSelection,
450+
hideToolbar,
451+
editorHotkeys,
452452
isActive,
453453
isFullscreen,
454454
isOneLineEditor,
455-
onCopy,
456455
onItemOpen,
456+
onCopy,
457457
onPaste,
458+
handleToggleFullscreen,
458459
path,
459-
onUpload,
460-
resolveUploader,
461460
rangeDecorations,
462461
readOnly,
462+
editorRenderAnnotation,
463+
editorRenderBlock,
464+
editorRenderChild,
463465
scrollElement,
464466
],
465467
)

packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx

Lines changed: 2 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
EditorProvider,
55
type EditorSelection,
66
type InvalidValue,
7-
type OnPasteFn,
87
type Patch,
98
PortableTextEditor,
109
type RangeDecoration,
@@ -14,9 +13,8 @@ import {
1413
import {EventListenerPlugin} from '@portabletext/editor/plugins'
1514
import {useTelemetry} from '@sanity/telemetry/react'
1615
import {isKeySegment, type Path, type PortableTextBlock} from '@sanity/types'
17-
import {Box, Flex, Text, useToast} from '@sanity/ui'
16+
import {Box, useToast} from '@sanity/ui'
1817
import {randomKey} from '@sanity/util/content'
19-
import {sortBy} from 'lodash'
2018
import {
2119
forwardRef,
2220
type ReactNode,
@@ -29,7 +27,6 @@ import {
2927
useState,
3028
} from 'react'
3129

32-
import {useTranslation} from '../../../i18n'
3330
import {usePerspective} from '../../../perspective/usePerspective'
3431
import {EMPTY_ARRAY} from '../../../util'
3532
import {pathToString} from '../../../validation/util/pathToString'
@@ -40,9 +37,7 @@ import {
4037
import {SANITY_PATCH_TYPE} from '../../patch'
4138
import {type ArrayOfObjectsItemMember, type ObjectFormNode} from '../../store'
4239
import {immutableReconcile} from '../../store/utils/immutableReconcile'
43-
import {type ResolvedUploader} from '../../studio/uploads/types'
4440
import {type PortableTextInputProps} from '../../types'
45-
import {extractPastedFiles} from '../common/fileTarget/utils/extractFiles'
4641
import {Compositor} from './Compositor'
4742
import {useFullscreenPTE} from './contexts/fullscreen'
4843
import {PortableTextMarkersProvider} from './contexts/PortableTextMarkers'
@@ -58,14 +53,8 @@ import {
5853
type PresenceCursorDecorationsHookProps,
5954
usePresenceCursorDecorations,
6055
} from './presence-cursors'
61-
import {getUploadCandidates} from './upload/helpers'
6256
import {usePatches} from './usePatches'
6357

64-
interface UploadTask {
65-
file: File
66-
uploaderCandidates: ResolvedUploader[]
67-
}
68-
6958
function keyGenerator() {
7059
return randomKey(12)
7160
}
@@ -129,8 +118,6 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
129118
renderCustomMarkers,
130119
schemaType,
131120
value,
132-
resolveUploader,
133-
onUpload,
134121
displayInlineChanges,
135122
} = props
136123

@@ -157,7 +144,6 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
157144
displayInlineChanges,
158145
})
159146

160-
const {t} = useTranslation()
161147
const [ignoreValidationError, setIgnoreValidationError] = useState(false)
162148
const [invalidValue, setInvalidValue] = useState<InvalidValue | null>(null)
163149
const [isActive, setIsActive] = useState(initialActive ?? true)
@@ -354,85 +340,6 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
354340
return reconciled
355341
}, [diffRangeDecorations, displayInlineChanges, presenceCursorDecorations, rangeDecorationsProp])
356342

357-
const uploadFile = useCallback(
358-
(file: File, resolvedUploader: ResolvedUploader) => {
359-
const {type, uploader} = resolvedUploader
360-
onUpload?.({file, schemaType: type, uploader})
361-
},
362-
[onUpload],
363-
)
364-
365-
const handleFiles = useCallback(
366-
(files: File[]) => {
367-
if (!resolveUploader) {
368-
return
369-
}
370-
const tasks: UploadTask[] = files.map((file) => ({
371-
file,
372-
uploaderCandidates: getUploadCandidates(schemaType.of, resolveUploader, file),
373-
}))
374-
const readyTasks = tasks.filter((task) => task.uploaderCandidates.length > 0)
375-
const rejected: UploadTask[] = tasks.filter((task) => task.uploaderCandidates.length === 0)
376-
377-
if (rejected.length > 0) {
378-
toast.push({
379-
closable: true,
380-
status: 'warning',
381-
title: t('inputs.array.error.cannot-upload-unable-to-convert', {
382-
count: rejected.length,
383-
}),
384-
description: rejected.map((task, i) => (
385-
// oxlint-disable-next-line no-array-index-key
386-
<Flex key={i} gap={2} padding={2}>
387-
<Box>
388-
<Text weight="medium">{task.file.name}</Text>
389-
</Box>
390-
<Box>
391-
<Text size={1}>({task.file.type})</Text>
392-
</Box>
393-
</Flex>
394-
)),
395-
})
396-
}
397-
398-
// todo: consider if we should to ask the user here
399-
// the list of candidates is sorted by their priority and the first one is selected
400-
readyTasks.forEach((task) => {
401-
uploadFile(
402-
task.file,
403-
sortBy(task.uploaderCandidates, (candidate) => candidate.uploader.priority)[0],
404-
)
405-
})
406-
},
407-
[toast, resolveUploader, schemaType, uploadFile, t],
408-
)
409-
410-
const handlePaste: OnPasteFn = useCallback(
411-
(input) => {
412-
const {event} = input
413-
414-
// Some applications may put both text and files on the clipboard when content is copied.
415-
// If we have both text and html on the clipboard, just ignore the files if this is a paste event.
416-
// Drop events will most probably be files so skip this test for those.
417-
const eventType = event.type === 'paste' ? 'paste' : 'drop'
418-
const hasHtml = !!event.clipboardData.getData('text/html')
419-
const hasText = !!event.clipboardData.getData('text/plain')
420-
if (eventType === 'paste' && hasHtml && hasText) {
421-
return onPaste?.(input)
422-
}
423-
424-
void extractPastedFiles(event.clipboardData)
425-
.then((files) => {
426-
return files.length > 0 ? files : []
427-
})
428-
.then((files) => {
429-
handleFiles(files)
430-
})
431-
return onPaste?.(input)
432-
},
433-
[handleFiles, onPaste],
434-
)
435-
436343
return (
437344
<Box>
438345
{!ignoreValidationError && respondToInvalidContent}
@@ -467,7 +374,7 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
467374
onItemRemove={onItemRemove}
468375
onCopy={onCopy}
469376
onInsert={onInsert}
470-
onPaste={handlePaste}
377+
onPaste={onPaste}
471378
onToggleFullscreen={handleToggleFullscreen}
472379
rangeDecorations={rangeDecorations}
473380
readOnly={readOnly || !ready}

packages/sanity/src/core/form/inputs/PortableText/object/BlockObject.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import {type EditorSelection, PortableTextEditor, usePortableTextEditor} from '@portabletext/editor'
2-
import {isImage, type ObjectSchemaType, type Path, type PortableTextBlock} from '@sanity/types'
2+
import {
3+
isImage,
4+
type ObjectSchemaType,
5+
type Path,
6+
type PortableTextBlock,
7+
type UploadState,
8+
} from '@sanity/types'
39
import {Box, Flex, type ResponsivePaddingProps} from '@sanity/ui'
410
import {isEqual} from '@sanity/util/paths'
511
import {
@@ -20,6 +26,7 @@ import {EMPTY_ARRAY} from '../../../../util'
2026
import {useFormCallbacks} from '../../../studio'
2127
import {useChildPresence} from '../../../studio/contexts/Presence'
2228
import {useEnhancedObjectDialog} from '../../../studio/tree-editing/context/enabled/useEnhancedObjectDialog'
29+
import {UPLOAD_STATUS_KEY} from '../../../studio/uploads/constants'
2330
import {
2431
type BlockProps,
2532
type RenderAnnotationCallback,
@@ -384,6 +391,10 @@ export const DefaultBlockObjectComponent = (
384391
const hasMarkers = Boolean(markers.length > 0)
385392
const tone = selected || focused ? 'primary' : 'default'
386393

394+
const uploadState = (value as any)[UPLOAD_STATUS_KEY] as UploadState | undefined
395+
const uploadProgress =
396+
typeof uploadState?.progress === 'number' ? uploadState?.progress : undefined
397+
387398
const handleDoubleClickToOpen = useCallback(
388399
(e: MouseEvent<Element, globalThis.MouseEvent>) => {
389400
e.preventDefault()
@@ -421,6 +432,7 @@ export const DefaultBlockObjectComponent = (
421432
value={value}
422433
/>
423434
),
435+
progress: uploadProgress,
424436
layout: isImagePreview ? 'blockImage' : 'block',
425437
schemaType,
426438
skipVisibilityCheck: true,

packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/Grid/GridArrayInput.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
type ObjectItem,
99
type ObjectItemProps,
1010
} from '../../../../types'
11+
import {UploadTargetCard} from '../../../files/common/uploadTarget/UploadTargetCard'
1112
import {Item, List} from '../../common/list'
12-
import {UploadTargetCard} from '../../common/UploadTargetCard'
1313
import {ArrayOfObjectsFunctions} from '../ArrayOfObjectsFunctions'
1414
import {createProtoArrayValue} from '../createProtoArrayValue'
1515
import {ErrorItem} from './ErrorItem'
@@ -26,6 +26,7 @@ export function GridArrayInput<Item extends ObjectItem>(props: ArrayOfObjectsInp
2626
onItemPrepend,
2727
onItemAppend,
2828
onItemMove,
29+
onSelectFile,
2930
onUpload,
3031
readOnly,
3132
renderAnnotation,
@@ -34,7 +35,6 @@ export function GridArrayInput<Item extends ObjectItem>(props: ArrayOfObjectsInp
3435
renderInlineBlock,
3536
renderInput,
3637
renderPreview,
37-
resolveUploader,
3838
schemaType,
3939
value = EMPTY,
4040
} = props
@@ -52,11 +52,12 @@ export function GridArrayInput<Item extends ObjectItem>(props: ArrayOfObjectsInp
5252
return (
5353
<Stack space={2}>
5454
<UploadTargetCard
55-
types={schemaType.of}
56-
resolveUploader={resolveUploader}
57-
onUpload={onUpload}
5855
{...elementProps}
56+
isReadOnly={readOnly}
57+
onSelectFile={onSelectFile}
58+
onUpload={onUpload}
5959
tabIndex={0}
60+
types={schemaType.of}
6061
>
6162
<Stack data-ui="ArrayInput__content" space={2}>
6263
{members?.length === 0 && (

0 commit comments

Comments
 (0)