Skip to content

Commit 4e0bb58

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 147c302 commit 4e0bb58

File tree

38 files changed

+903
-955
lines changed

38 files changed

+903
-955
lines changed

e2e/tests/inputs/array.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const image = readFileSync(
1212
path.join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'resources', fileName),
1313
)
1414

15-
test(`file drop event should not propagate to dialog parent`, async ({
15+
test.only(`file drop event should not propagate to dialog parent`, async ({
1616
page,
1717
createDraftDocument,
1818
}) => {
@@ -43,6 +43,9 @@ test(`file drop event should not propagate to dialog parent`, async ({
4343
// Drop the file.
4444
await list.dispatchEvent('drop', {dataTransfer})
4545

46+
// Select asset source
47+
await page.getByTestId('upload-destination-sanity-default').click()
48+
4649
// Ensure the list contains one item.
4750
await expect(item).toHaveCount(1)
4851

packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/copyPaste/CopyPaste.spec.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ test.describe('Portable Text Input', () => {
118118
const $pte = await getFocusedPortableTextEditor('field-body')
119119

120120
await pasteFileOverPortableTextEditor(imagePath, 'image/jpeg', $pte)
121-
121+
await page.getByTestId('upload-destination-sanity-default').click()
122122
await expect($pte.getByTestId('block-preview')).toBeVisible()
123123
})
124124
test(`Added dropped image as a block`, async ({mount, page}) => {
@@ -141,6 +141,8 @@ test.describe('Portable Text Input', () => {
141141

142142
await expect(page.getByText('Drop to upload 1 file')).not.toBeVisible()
143143

144+
await page.getByTestId('upload-destination-sanity-default').click()
145+
144146
await expect($pte.getByTestId('block-preview')).toBeVisible()
145147
})
146148
test(`Display error message on drag over if file is not accepted`, async ({mount, page}) => {

packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from 'sanity'
1919
import {styled} from 'styled-components'
2020

21+
import {AssetLimitUpsellProvider} from '../../../../src/core/limits/context/assets/AssetLimitUpsellProvider'
2122
import {PerspectiveProvider} from '../../../../src/core/perspective/PerspectiveProvider'
2223
import {route} from '../../../../src/router'
2324
import {RouterProvider} from '../../../../src/router/RouterProvider'
@@ -106,30 +107,32 @@ export const TestWrapperContents = (
106107
<WorkspaceProvider workspace={mockWorkspace}>
107108
<ResourceCacheProvider>
108109
<SourceProvider source={mockWorkspace.unstable_sources[0]}>
109-
<CopyPasteProvider>
110-
<ColorSchemeProvider>
111-
<UserColorManagerProvider>
112-
<StyledChangeConnectorRoot
113-
isReviewChangesOpen={false}
114-
onOpenReviewChanges={noop}
115-
onSetFocus={noop}
116-
>
117-
<PerspectiveProvider
118-
selectedPerspectiveName={undefined}
119-
excludedPerspectives={EMPTY_ARRAY}
110+
<AssetLimitUpsellProvider>
111+
<CopyPasteProvider>
112+
<ColorSchemeProvider>
113+
<UserColorManagerProvider>
114+
<StyledChangeConnectorRoot
115+
isReviewChangesOpen={false}
116+
onOpenReviewChanges={noop}
117+
onSetFocus={noop}
120118
>
121-
<PaneLayout height="fill">
122-
<Pane id="test-pane">
123-
<PaneContent>
124-
<Card padding={3}>{children}</Card>
125-
</PaneContent>
126-
</Pane>
127-
</PaneLayout>
128-
</PerspectiveProvider>
129-
</StyledChangeConnectorRoot>
130-
</UserColorManagerProvider>
131-
</ColorSchemeProvider>
132-
</CopyPasteProvider>
119+
<PerspectiveProvider
120+
selectedPerspectiveName={undefined}
121+
excludedPerspectives={EMPTY_ARRAY}
122+
>
123+
<PaneLayout height="fill">
124+
<Pane id="test-pane">
125+
<PaneContent>
126+
<Card padding={3}>{children}</Card>
127+
</PaneContent>
128+
</Pane>
129+
</PaneLayout>
130+
</PerspectiveProvider>
131+
</StyledChangeConnectorRoot>
132+
</UserColorManagerProvider>
133+
</ColorSchemeProvider>
134+
</CopyPasteProvider>
135+
</AssetLimitUpsellProvider>
133136
</SourceProvider>
134137
</ResourceCacheProvider>
135138
</WorkspaceProvider>

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: 18 additions & 15 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+
pasteTarget={wrapperElement || undefined}
410411
tabIndex={-1}
412+
types={editor.schemaTypes.portableText.of}
411413
>
412414
<StringDiffContainer>
413415
<Editor
@@ -439,27 +441,28 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
439441

440442
// Keep only stable ones here!
441443
[
442-
ariaDescribedBy,
444+
readOnly,
445+
onSelectFile,
446+
onUpload,
447+
wrapperElement,
443448
editor.schemaTypes.portableText.of,
444-
editorHotkeys,
445-
editorRenderAnnotation,
446-
editorRenderBlock,
447-
editorRenderChild,
449+
ariaDescribedBy,
448450
elementRef,
449-
handleToggleFullscreen,
450-
hideToolbar,
451451
initialSelection,
452+
hideToolbar,
453+
editorHotkeys,
452454
isActive,
453455
isFullscreen,
454456
isOneLineEditor,
455-
onCopy,
456457
onItemOpen,
458+
onCopy,
457459
onPaste,
460+
handleToggleFullscreen,
458461
path,
459-
onUpload,
460-
resolveUploader,
461462
rangeDecorations,
462-
readOnly,
463+
editorRenderAnnotation,
464+
editorRenderBlock,
465+
editorRenderChild,
463466
scrollElement,
464467
],
465468
)

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}

0 commit comments

Comments
 (0)