@contember/bindx-uploader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/UploaderError.d.ts +6 -0
- package/dist/UploaderError.d.ts.map +1 -0
- package/dist/components/MultiUploader.d.ts +43 -0
- package/dist/components/MultiUploader.d.ts.map +1 -0
- package/dist/components/Uploader.d.ts +58 -0
- package/dist/components/Uploader.d.ts.map +1 -0
- package/dist/components/UploaderDropzoneArea.d.ts +6 -0
- package/dist/components/UploaderDropzoneArea.d.ts.map +1 -0
- package/dist/components/UploaderDropzoneRoot.d.ts +8 -0
- package/dist/components/UploaderDropzoneRoot.d.ts.map +1 -0
- package/dist/components/UploaderEachFile.d.ts +14 -0
- package/dist/components/UploaderEachFile.d.ts.map +1 -0
- package/dist/components/UploaderFileStateSwitch.d.ts +24 -0
- package/dist/components/UploaderFileStateSwitch.d.ts.map +1 -0
- package/dist/components/UploaderHasFile.d.ts +14 -0
- package/dist/components/UploaderHasFile.d.ts.map +1 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/contexts.d.ts +32 -0
- package/dist/contexts.d.ts.map +1 -0
- package/dist/extractors/getAudioFileDataExtractor.d.ts +9 -0
- package/dist/extractors/getAudioFileDataExtractor.d.ts.map +1 -0
- package/dist/extractors/getFileUrlDataExtractor.d.ts +9 -0
- package/dist/extractors/getFileUrlDataExtractor.d.ts.map +1 -0
- package/dist/extractors/getGenericFileMetadataExtractor.d.ts +12 -0
- package/dist/extractors/getGenericFileMetadataExtractor.d.ts.map +1 -0
- package/dist/extractors/getImageFileDataExtractor.d.ts +10 -0
- package/dist/extractors/getImageFileDataExtractor.d.ts.map +1 -0
- package/dist/extractors/getVideoFileDataExtractor.d.ts +11 -0
- package/dist/extractors/getVideoFileDataExtractor.d.ts.map +1 -0
- package/dist/extractors/index.d.ts +6 -0
- package/dist/extractors/index.d.ts.map +1 -0
- package/dist/extractors/types.d.ts +13 -0
- package/dist/extractors/types.d.ts.map +1 -0
- package/dist/fileTypes/createAnyFileType.d.ts +27 -0
- package/dist/fileTypes/createAnyFileType.d.ts.map +1 -0
- package/dist/fileTypes/createAudioFileType.d.ts +29 -0
- package/dist/fileTypes/createAudioFileType.d.ts.map +1 -0
- package/dist/fileTypes/createImageFileType.d.ts +47 -0
- package/dist/fileTypes/createImageFileType.d.ts.map +1 -0
- package/dist/fileTypes/createVideoFileType.d.ts +33 -0
- package/dist/fileTypes/createVideoFileType.d.ts.map +1 -0
- package/dist/fileTypes/index.d.ts +5 -0
- package/dist/fileTypes/index.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useS3Client.d.ts +7 -0
- package/dist/hooks/useS3Client.d.ts.map +1 -0
- package/dist/hooks/useUploaderStateFiles.d.ts +8 -0
- package/dist/hooks/useUploaderStateFiles.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/internal/hooks/index.d.ts +5 -0
- package/dist/internal/hooks/index.d.ts.map +1 -0
- package/dist/internal/hooks/useFillEntity.d.ts +17 -0
- package/dist/internal/hooks/useFillEntity.d.ts.map +1 -0
- package/dist/internal/hooks/useGetPreviewUrls.d.ts +6 -0
- package/dist/internal/hooks/useGetPreviewUrls.d.ts.map +1 -0
- package/dist/internal/hooks/useUploadState.d.ts +12 -0
- package/dist/internal/hooks/useUploadState.d.ts.map +1 -0
- package/dist/internal/hooks/useUploaderDoUpload.d.ts +7 -0
- package/dist/internal/hooks/useUploaderDoUpload.d.ts.map +1 -0
- package/dist/internal/utils/executeExtractors.d.ts +16 -0
- package/dist/internal/utils/executeExtractors.d.ts.map +1 -0
- package/dist/internal/utils/index.d.ts +4 -0
- package/dist/internal/utils/index.d.ts.map +1 -0
- package/dist/internal/utils/resolveAccept.d.ts +7 -0
- package/dist/internal/utils/resolveAccept.d.ts.map +1 -0
- package/dist/internal/utils/uploaderErrorHandler.d.ts +7 -0
- package/dist/internal/utils/uploaderErrorHandler.d.ts.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/uploadClient/S3UploadClient.d.ts +19 -0
- package/dist/uploadClient/S3UploadClient.d.ts.map +1 -0
- package/dist/uploadClient/index.d.ts +3 -0
- package/dist/uploadClient/index.d.ts.map +1 -0
- package/dist/uploadClient/types.d.ts +29 -0
- package/dist/uploadClient/types.d.ts.map +1 -0
- package/dist/utils/attrAccept.d.ts +16 -0
- package/dist/utils/attrAccept.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/urlSigner.d.ts +8 -0
- package/dist/utils/urlSigner.d.ts.map +1 -0
- package/package.json +35 -0
- package/src/UploaderError.ts +7 -0
- package/src/components/MultiUploader.tsx +210 -0
- package/src/components/Uploader.tsx +174 -0
- package/src/components/UploaderDropzoneArea.tsx +29 -0
- package/src/components/UploaderDropzoneRoot.tsx +32 -0
- package/src/components/UploaderEachFile.tsx +46 -0
- package/src/components/UploaderFileStateSwitch.tsx +66 -0
- package/src/components/UploaderHasFile.tsx +35 -0
- package/src/components/index.ts +7 -0
- package/src/contexts.ts +76 -0
- package/src/extractors/getAudioFileDataExtractor.ts +44 -0
- package/src/extractors/getFileUrlDataExtractor.ts +22 -0
- package/src/extractors/getGenericFileMetadataExtractor.ts +40 -0
- package/src/extractors/getImageFileDataExtractor.ts +50 -0
- package/src/extractors/getVideoFileDataExtractor.ts +60 -0
- package/src/extractors/index.ts +5 -0
- package/src/extractors/types.ts +14 -0
- package/src/fileTypes/createAnyFileType.ts +56 -0
- package/src/fileTypes/createAudioFileType.ts +67 -0
- package/src/fileTypes/createImageFileType.ts +87 -0
- package/src/fileTypes/createVideoFileType.ts +75 -0
- package/src/fileTypes/index.ts +4 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useS3Client.ts +26 -0
- package/src/hooks/useUploaderStateFiles.ts +22 -0
- package/src/index.ts +126 -0
- package/src/internal/hooks/index.ts +4 -0
- package/src/internal/hooks/useFillEntity.ts +89 -0
- package/src/internal/hooks/useGetPreviewUrls.ts +26 -0
- package/src/internal/hooks/useUploadState.ts +159 -0
- package/src/internal/hooks/useUploaderDoUpload.ts +129 -0
- package/src/internal/utils/executeExtractors.ts +46 -0
- package/src/internal/utils/index.ts +3 -0
- package/src/internal/utils/resolveAccept.ts +26 -0
- package/src/internal/utils/uploaderErrorHandler.ts +15 -0
- package/src/types.ts +242 -0
- package/src/uploadClient/S3UploadClient.ts +119 -0
- package/src/uploadClient/index.ts +2 -0
- package/src/uploadClient/types.ts +30 -0
- package/src/utils/attrAccept.ts +87 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/urlSigner.ts +117 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
// Upload client
|
|
4
|
+
UploadClient,
|
|
5
|
+
FileUploadResult,
|
|
6
|
+
FileUploadProgress,
|
|
7
|
+
UploadClientUploadArgs,
|
|
8
|
+
// File
|
|
9
|
+
FileWithMeta,
|
|
10
|
+
// State
|
|
11
|
+
UploaderFileState,
|
|
12
|
+
UploaderFileStateInitial,
|
|
13
|
+
UploaderFileStateUploading,
|
|
14
|
+
UploaderFileStateFinalizing,
|
|
15
|
+
UploaderFileStateSuccess,
|
|
16
|
+
UploaderFileStateError,
|
|
17
|
+
UploaderState,
|
|
18
|
+
// Events
|
|
19
|
+
BeforeUploadEvent,
|
|
20
|
+
StartUploadEvent,
|
|
21
|
+
ProgressEvent,
|
|
22
|
+
SuccessEvent,
|
|
23
|
+
AfterUploadEvent,
|
|
24
|
+
ErrorEvent,
|
|
25
|
+
UploaderEvents,
|
|
26
|
+
// Extractors
|
|
27
|
+
FileDataExtractor,
|
|
28
|
+
FileDataExtractorPopulator,
|
|
29
|
+
// File type
|
|
30
|
+
FileType,
|
|
31
|
+
DiscriminatedFileType,
|
|
32
|
+
DiscriminatedFileTypeMap,
|
|
33
|
+
// Field name utility type
|
|
34
|
+
FieldName,
|
|
35
|
+
// Options
|
|
36
|
+
UploaderOptions,
|
|
37
|
+
// Errors
|
|
38
|
+
UploaderErrorType,
|
|
39
|
+
UploaderErrorOptions,
|
|
40
|
+
} from './types.js'
|
|
41
|
+
|
|
42
|
+
// Error class
|
|
43
|
+
export { UploaderError } from './UploaderError.js'
|
|
44
|
+
|
|
45
|
+
// Contexts
|
|
46
|
+
export {
|
|
47
|
+
UploaderUploadFilesContext,
|
|
48
|
+
useUploaderUploadFiles,
|
|
49
|
+
UploaderStateContext,
|
|
50
|
+
useUploaderState,
|
|
51
|
+
UploaderOptionsContext,
|
|
52
|
+
useUploaderOptions,
|
|
53
|
+
UploaderFileStateContext,
|
|
54
|
+
useUploaderFileState,
|
|
55
|
+
UploaderClientContext,
|
|
56
|
+
useUploaderClient,
|
|
57
|
+
UploaderDropzoneStateContext,
|
|
58
|
+
useUploaderDropzoneState,
|
|
59
|
+
} from './contexts.js'
|
|
60
|
+
|
|
61
|
+
// Upload client
|
|
62
|
+
export {
|
|
63
|
+
S3UploadClient,
|
|
64
|
+
type S3UploadClientOptions,
|
|
65
|
+
type S3FileOptions,
|
|
66
|
+
type S3Acl,
|
|
67
|
+
type S3FileParameters,
|
|
68
|
+
type S3SignedUrlResponse,
|
|
69
|
+
type S3UrlSigner,
|
|
70
|
+
} from './uploadClient/index.js'
|
|
71
|
+
|
|
72
|
+
// Utils
|
|
73
|
+
export { createContentApiS3Signer, attrAccept, acceptToString } from './utils/index.js'
|
|
74
|
+
|
|
75
|
+
// Extractors
|
|
76
|
+
export {
|
|
77
|
+
getFileUrlDataExtractor,
|
|
78
|
+
type FileUrlDataExtractorProps,
|
|
79
|
+
getGenericFileMetadataExtractor,
|
|
80
|
+
type GenericFileMetadataExtractorProps,
|
|
81
|
+
getImageFileDataExtractor,
|
|
82
|
+
type ImageFileDataExtractorProps,
|
|
83
|
+
getVideoFileDataExtractor,
|
|
84
|
+
type VideoFileDataExtractorProps,
|
|
85
|
+
getAudioFileDataExtractor,
|
|
86
|
+
type AudioFileDataExtractorProps,
|
|
87
|
+
} from './extractors/index.js'
|
|
88
|
+
|
|
89
|
+
// File type creators
|
|
90
|
+
export {
|
|
91
|
+
createImageFileType,
|
|
92
|
+
type CreateImageFileTypeProps,
|
|
93
|
+
createVideoFileType,
|
|
94
|
+
type CreateVideoFileTypeProps,
|
|
95
|
+
createAudioFileType,
|
|
96
|
+
type CreateAudioFileTypeProps,
|
|
97
|
+
createAnyFileType,
|
|
98
|
+
type CreateAnyFileTypeProps,
|
|
99
|
+
} from './fileTypes/index.js'
|
|
100
|
+
|
|
101
|
+
// Hooks
|
|
102
|
+
export {
|
|
103
|
+
useUploaderStateFiles,
|
|
104
|
+
type StateFilter,
|
|
105
|
+
useS3Client,
|
|
106
|
+
} from './hooks/index.js'
|
|
107
|
+
|
|
108
|
+
// Components
|
|
109
|
+
export {
|
|
110
|
+
Uploader,
|
|
111
|
+
UploaderWithMeta,
|
|
112
|
+
type UploaderProps,
|
|
113
|
+
MultiUploader,
|
|
114
|
+
MultiUploaderWithMeta,
|
|
115
|
+
type MultiUploaderProps,
|
|
116
|
+
UploaderEachFile,
|
|
117
|
+
type UploaderEachFileProps,
|
|
118
|
+
UploaderHasFile,
|
|
119
|
+
type UploaderHasFileProps,
|
|
120
|
+
UploaderFileStateSwitch,
|
|
121
|
+
type UploaderFileStateSwitchProps,
|
|
122
|
+
UploaderDropzoneRoot,
|
|
123
|
+
type UploaderDropzoneRootProps,
|
|
124
|
+
UploaderDropzoneArea,
|
|
125
|
+
type UploaderDropzoneAreaProps,
|
|
126
|
+
} from './components/index.js'
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { useUploadState, type UseUploadStateResult } from './useUploadState.js'
|
|
2
|
+
export { useUploaderDoUpload } from './useUploaderDoUpload.js'
|
|
3
|
+
export { useGetPreviewUrls } from './useGetPreviewUrls.js'
|
|
4
|
+
export { useFillEntity, type UseFillEntityArgs } from './useFillEntity.js'
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import type { EntityRef, HasOneRef } from '@contember/bindx'
|
|
3
|
+
import type { FileType, StartUploadEvent, UploaderEvents } from '../../types.js'
|
|
4
|
+
import { resolveAcceptingSingleType } from '../utils/resolveAccept.js'
|
|
5
|
+
import { executeExtractors } from '../utils/executeExtractors.js'
|
|
6
|
+
|
|
7
|
+
export interface UseFillEntityArgs<TEntity = Record<string, unknown>> extends UploaderEvents {
|
|
8
|
+
/**
|
|
9
|
+
* The entity to fill. Can be:
|
|
10
|
+
* - EntityRef: fill the entity directly
|
|
11
|
+
* - HasOneRef: fill the related entity (disconnect first on upload start)
|
|
12
|
+
*/
|
|
13
|
+
entity: EntityRef<TEntity> | HasOneRef<TEntity>
|
|
14
|
+
fileType: FileType<TEntity>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if the entity is a HasOneRef (has $disconnect method)
|
|
19
|
+
*/
|
|
20
|
+
const isHasOneRef = <TEntity>(entity: EntityRef<TEntity> | HasOneRef<TEntity>): entity is HasOneRef<TEntity> => {
|
|
21
|
+
return '$disconnect' in entity && typeof entity.$disconnect === 'function'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the target entity for filling.
|
|
26
|
+
* For HasOneRef, returns the related entity. For EntityRef, returns the entity itself.
|
|
27
|
+
*/
|
|
28
|
+
const getTargetEntity = <TEntity>(entity: EntityRef<TEntity> | HasOneRef<TEntity>): EntityRef<TEntity> => {
|
|
29
|
+
if (isHasOneRef(entity)) {
|
|
30
|
+
return entity.$entity
|
|
31
|
+
}
|
|
32
|
+
return entity
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hook that connects upload events to entity field population.
|
|
37
|
+
* Handles both direct EntityRef and HasOneRef (for has-one relations).
|
|
38
|
+
*/
|
|
39
|
+
export const useFillEntity = <TEntity extends Record<string, unknown>>({
|
|
40
|
+
entity,
|
|
41
|
+
fileType,
|
|
42
|
+
...events
|
|
43
|
+
}: UseFillEntityArgs<TEntity>): UploaderEvents => {
|
|
44
|
+
const handleBeforeUpload = useCallback(
|
|
45
|
+
async (event: Parameters<UploaderEvents['onBeforeUpload']>[0]): Promise<FileType | undefined> => {
|
|
46
|
+
if (!(await resolveAcceptingSingleType(event.file, fileType as FileType))) {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
return (await events.onBeforeUpload?.(event)) ?? (fileType as FileType)
|
|
50
|
+
},
|
|
51
|
+
[events, fileType],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const handleStartUpload = useCallback(
|
|
55
|
+
(event: StartUploadEvent) => {
|
|
56
|
+
// Disconnect existing relation before upload
|
|
57
|
+
if (isHasOneRef(entity)) {
|
|
58
|
+
entity.$disconnect()
|
|
59
|
+
}
|
|
60
|
+
events.onStartUpload?.(event)
|
|
61
|
+
},
|
|
62
|
+
[entity, events],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const handleAfterUpload = useCallback(
|
|
66
|
+
async (event: Parameters<UploaderEvents['onAfterUpload']>[0]) => {
|
|
67
|
+
await Promise.all([
|
|
68
|
+
(async () => {
|
|
69
|
+
const targetEntity = getTargetEntity(entity)
|
|
70
|
+
const extractionResult = await executeExtractors({
|
|
71
|
+
fileType: fileType as FileType,
|
|
72
|
+
result: event.result,
|
|
73
|
+
file: event.file,
|
|
74
|
+
})
|
|
75
|
+
extractionResult?.({ entity: targetEntity as EntityRef<unknown> })
|
|
76
|
+
})(),
|
|
77
|
+
events.onAfterUpload?.(event),
|
|
78
|
+
])
|
|
79
|
+
},
|
|
80
|
+
[entity, events, fileType],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...events,
|
|
85
|
+
onBeforeUpload: handleBeforeUpload,
|
|
86
|
+
onStartUpload: handleStartUpload,
|
|
87
|
+
onAfterUpload: handleAfterUpload,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook that manages preview URL creation and cleanup.
|
|
5
|
+
* Returns a function that creates object URLs for file preview.
|
|
6
|
+
*/
|
|
7
|
+
export const useGetPreviewUrls = (): ((file: File) => string) => {
|
|
8
|
+
const urlsRef = useRef<Set<string>>(new Set())
|
|
9
|
+
|
|
10
|
+
// Cleanup URLs on unmount
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const urls = urlsRef.current
|
|
13
|
+
return () => {
|
|
14
|
+
for (const url of urls) {
|
|
15
|
+
URL.revokeObjectURL(url)
|
|
16
|
+
}
|
|
17
|
+
urls.clear()
|
|
18
|
+
}
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
return useCallback((file: File): string => {
|
|
22
|
+
const url = URL.createObjectURL(file)
|
|
23
|
+
urlsRef.current.add(url)
|
|
24
|
+
return url
|
|
25
|
+
}, [])
|
|
26
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
AfterUploadEvent,
|
|
4
|
+
BeforeUploadEvent,
|
|
5
|
+
ErrorEvent,
|
|
6
|
+
ProgressEvent,
|
|
7
|
+
StartUploadEvent,
|
|
8
|
+
SuccessEvent,
|
|
9
|
+
UploaderEvents,
|
|
10
|
+
UploaderFileState,
|
|
11
|
+
UploaderState,
|
|
12
|
+
} from '../../types.js'
|
|
13
|
+
|
|
14
|
+
export interface UseUploadStateResult extends UploaderEvents {
|
|
15
|
+
files: UploaderState
|
|
16
|
+
purgeFinal: () => void
|
|
17
|
+
purgeAll: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook that manages the upload state machine.
|
|
22
|
+
* Tracks file states through the upload lifecycle: initial -> uploading -> finalizing -> success/error
|
|
23
|
+
*/
|
|
24
|
+
export const useUploadState = ({
|
|
25
|
+
onBeforeUpload,
|
|
26
|
+
onStartUpload,
|
|
27
|
+
onSuccess,
|
|
28
|
+
onError,
|
|
29
|
+
onProgress,
|
|
30
|
+
onAfterUpload,
|
|
31
|
+
}: UploaderEvents): UseUploadStateResult => {
|
|
32
|
+
const [files, setFiles] = useState<Record<string, UploaderFileState>>({})
|
|
33
|
+
|
|
34
|
+
const purgeFinal = useCallback(() => {
|
|
35
|
+
setFiles(files => {
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
Object.entries(files).filter(
|
|
38
|
+
([, file]) => file.state !== 'success' && file.state !== 'error',
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
const purgeAll = useCallback(() => {
|
|
45
|
+
setFiles({})
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const handleBeforeUpload = useCallback(
|
|
49
|
+
async (event: BeforeUploadEvent) => {
|
|
50
|
+
setFiles(files => ({
|
|
51
|
+
...files,
|
|
52
|
+
[event.file.id]: { state: 'initial', file: event.file },
|
|
53
|
+
}))
|
|
54
|
+
return await onBeforeUpload?.(event)
|
|
55
|
+
},
|
|
56
|
+
[onBeforeUpload],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const handleStartUpload = useCallback(
|
|
60
|
+
(event: StartUploadEvent) => {
|
|
61
|
+
setFiles(files => ({
|
|
62
|
+
...files,
|
|
63
|
+
[event.file.id]: {
|
|
64
|
+
state: 'uploading',
|
|
65
|
+
file: event.file,
|
|
66
|
+
progress: {
|
|
67
|
+
progress: 0,
|
|
68
|
+
uploadedBytes: 0,
|
|
69
|
+
totalBytes: event.file.file.size,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}))
|
|
73
|
+
onStartUpload?.(event)
|
|
74
|
+
},
|
|
75
|
+
[onStartUpload],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const handleProgress = useCallback(
|
|
79
|
+
(event: ProgressEvent) => {
|
|
80
|
+
setFiles(files => ({
|
|
81
|
+
...files,
|
|
82
|
+
[event.file.id]: {
|
|
83
|
+
state: 'uploading',
|
|
84
|
+
file: event.file,
|
|
85
|
+
progress: event.progress,
|
|
86
|
+
},
|
|
87
|
+
}))
|
|
88
|
+
onProgress?.(event)
|
|
89
|
+
},
|
|
90
|
+
[onProgress],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const handleAfterUpload = useCallback(
|
|
94
|
+
async (event: AfterUploadEvent) => {
|
|
95
|
+
setFiles(files => ({
|
|
96
|
+
...files,
|
|
97
|
+
[event.file.id]: {
|
|
98
|
+
state: 'finalizing',
|
|
99
|
+
file: event.file,
|
|
100
|
+
result: event.result,
|
|
101
|
+
},
|
|
102
|
+
}))
|
|
103
|
+
return await onAfterUpload?.(event)
|
|
104
|
+
},
|
|
105
|
+
[onAfterUpload],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const handleSuccess = useCallback(
|
|
109
|
+
(event: SuccessEvent) => {
|
|
110
|
+
setFiles(files => ({
|
|
111
|
+
...files,
|
|
112
|
+
[event.file.id]: {
|
|
113
|
+
state: 'success',
|
|
114
|
+
file: event.file,
|
|
115
|
+
result: event.result,
|
|
116
|
+
dismiss: () =>
|
|
117
|
+
setFiles(files => {
|
|
118
|
+
const { [event.file.id]: _, ...rest } = files
|
|
119
|
+
return rest
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
}))
|
|
123
|
+
onSuccess?.(event)
|
|
124
|
+
},
|
|
125
|
+
[onSuccess],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const handleError = useCallback(
|
|
129
|
+
(event: ErrorEvent) => {
|
|
130
|
+
setFiles(files => ({
|
|
131
|
+
...files,
|
|
132
|
+
[event.file.id]: {
|
|
133
|
+
state: 'error',
|
|
134
|
+
file: event.file,
|
|
135
|
+
error: event.error,
|
|
136
|
+
dismiss: () =>
|
|
137
|
+
setFiles(files => {
|
|
138
|
+
const { [event.file.id]: _, ...rest } = files
|
|
139
|
+
return rest
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
}))
|
|
143
|
+
onError?.(event)
|
|
144
|
+
},
|
|
145
|
+
[onError],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
files: useMemo(() => Object.values(files), [files]),
|
|
150
|
+
purgeFinal,
|
|
151
|
+
purgeAll,
|
|
152
|
+
onBeforeUpload: handleBeforeUpload,
|
|
153
|
+
onStartUpload: handleStartUpload,
|
|
154
|
+
onProgress: handleProgress,
|
|
155
|
+
onAfterUpload: handleAfterUpload,
|
|
156
|
+
onSuccess: handleSuccess,
|
|
157
|
+
onError: handleError,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import type { FileWithMeta, UploaderEvents } from '../../types.js'
|
|
3
|
+
import { UploaderError } from '../../UploaderError.js'
|
|
4
|
+
import { useUploaderClient } from '../../contexts.js'
|
|
5
|
+
import { useGetPreviewUrls } from './useGetPreviewUrls.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook that orchestrates the file upload process.
|
|
9
|
+
* Handles file preparation, validation, and upload execution.
|
|
10
|
+
*/
|
|
11
|
+
export const useUploaderDoUpload = ({
|
|
12
|
+
onBeforeUpload,
|
|
13
|
+
onError,
|
|
14
|
+
onProgress,
|
|
15
|
+
onSuccess,
|
|
16
|
+
onStartUpload,
|
|
17
|
+
onAfterUpload,
|
|
18
|
+
}: UploaderEvents): ((files: File[]) => Promise<void>) => {
|
|
19
|
+
const getPreviewUrl = useGetPreviewUrls()
|
|
20
|
+
const defaultUploader = useUploaderClient()
|
|
21
|
+
|
|
22
|
+
return useCallback(
|
|
23
|
+
async (files: File[]) => {
|
|
24
|
+
// Prepare files with metadata
|
|
25
|
+
const fileWithMeta = files.map((file): FileWithMeta => {
|
|
26
|
+
const abortController = new AbortController()
|
|
27
|
+
const previewUrl = getPreviewUrl(file)
|
|
28
|
+
abortController.signal.addEventListener('abort', () => {
|
|
29
|
+
URL.revokeObjectURL(previewUrl)
|
|
30
|
+
}, { once: true })
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id: Math.random().toString(36).substring(7),
|
|
34
|
+
file,
|
|
35
|
+
previewUrl,
|
|
36
|
+
abortController,
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Validate and prepare files
|
|
41
|
+
const preparePromises = await Promise.allSettled(
|
|
42
|
+
fileWithMeta.map(async file => {
|
|
43
|
+
try {
|
|
44
|
+
const result = await onBeforeUpload?.({
|
|
45
|
+
file,
|
|
46
|
+
reject: (reason: string): never => {
|
|
47
|
+
throw new UploaderError({
|
|
48
|
+
type: 'fileRejected',
|
|
49
|
+
endUserMessage: reason,
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
if (!result) {
|
|
54
|
+
throw new UploaderError({ type: 'fileRejected' })
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
file,
|
|
58
|
+
fileType: result,
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
onError?.({ file, error: e })
|
|
62
|
+
return Promise.reject(e)
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// Filter successful preparations
|
|
68
|
+
const preparedFiles = preparePromises
|
|
69
|
+
.filter(
|
|
70
|
+
<T extends PromiseFulfilledResult<unknown>>(
|
|
71
|
+
p: T | PromiseRejectedResult,
|
|
72
|
+
): p is T => p.status === 'fulfilled',
|
|
73
|
+
)
|
|
74
|
+
.map(p => p.value)
|
|
75
|
+
|
|
76
|
+
// Upload files
|
|
77
|
+
await Promise.allSettled(
|
|
78
|
+
preparedFiles.map(async ({ file, fileType }) => {
|
|
79
|
+
try {
|
|
80
|
+
if (file.abortController.signal.aborted) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onStartUpload?.({ file, fileType })
|
|
85
|
+
|
|
86
|
+
const uploader = fileType.uploader ?? defaultUploader
|
|
87
|
+
if (!uploader) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'No uploader. Please specify the uploader in FileType or using UploaderClientContext.',
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await uploader.upload({
|
|
94
|
+
file: file.file,
|
|
95
|
+
signal: file.abortController.signal,
|
|
96
|
+
onProgress: progress => {
|
|
97
|
+
onProgress?.({ file, progress, fileType })
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (file.abortController.signal.aborted) {
|
|
102
|
+
throw new UploaderError({ type: 'aborted' })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await onAfterUpload?.({ file, result, fileType })
|
|
106
|
+
|
|
107
|
+
if (file.abortController.signal.aborted) {
|
|
108
|
+
throw new UploaderError({ type: 'aborted' })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onSuccess?.({ file, result, fileType })
|
|
112
|
+
} catch (e) {
|
|
113
|
+
onError?.({ file, error: e, fileType })
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
},
|
|
118
|
+
[
|
|
119
|
+
defaultUploader,
|
|
120
|
+
getPreviewUrl,
|
|
121
|
+
onAfterUpload,
|
|
122
|
+
onBeforeUpload,
|
|
123
|
+
onError,
|
|
124
|
+
onProgress,
|
|
125
|
+
onStartUpload,
|
|
126
|
+
onSuccess,
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { EntityRef } from '@contember/bindx'
|
|
2
|
+
import type { FileType, FileWithMeta, FileUploadResult } from '../../types.js'
|
|
3
|
+
|
|
4
|
+
interface ExecuteExtractorsArgs {
|
|
5
|
+
fileType: FileType
|
|
6
|
+
file: FileWithMeta
|
|
7
|
+
result: FileUploadResult
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Executes all extractors for a file type and returns a populator function
|
|
12
|
+
* that can be used to fill entity fields.
|
|
13
|
+
*/
|
|
14
|
+
export const executeExtractors = async ({
|
|
15
|
+
fileType,
|
|
16
|
+
file,
|
|
17
|
+
result,
|
|
18
|
+
}: ExecuteExtractorsArgs): Promise<((options: { entity: EntityRef<unknown> }) => void) | undefined> => {
|
|
19
|
+
const extractorsResult =
|
|
20
|
+
fileType.extractors?.map(it => {
|
|
21
|
+
return it.extractFileData?.(file)
|
|
22
|
+
}) ?? []
|
|
23
|
+
|
|
24
|
+
const extractionResult = await Promise.allSettled(extractorsResult)
|
|
25
|
+
|
|
26
|
+
if (file.abortController.signal.aborted) {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return ({ entity }) => {
|
|
31
|
+
// Cast entity to the expected type - extractors use runtime field access via $fields proxy
|
|
32
|
+
const typedEntity = entity as EntityRef<Record<string, unknown>>
|
|
33
|
+
|
|
34
|
+
// First populate fields synchronously
|
|
35
|
+
fileType.extractors?.forEach(it => {
|
|
36
|
+
it.populateFields?.({ entity: typedEntity, result })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Then populate from async extractors
|
|
40
|
+
extractionResult.forEach(it => {
|
|
41
|
+
if (it.status === 'fulfilled' && it.value) {
|
|
42
|
+
it.value({ entity: typedEntity, result })
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FileType, FileWithMeta } from '../../types.js'
|
|
2
|
+
import { attrAccept } from '../../utils/attrAccept.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Checks if a file is accepted by a single file type.
|
|
6
|
+
* Returns false if rejected, true if accepted.
|
|
7
|
+
*/
|
|
8
|
+
export const resolveAcceptingSingleType = async (
|
|
9
|
+
file: FileWithMeta,
|
|
10
|
+
fileType: FileType,
|
|
11
|
+
): Promise<boolean> => {
|
|
12
|
+
// Check MIME type
|
|
13
|
+
if (fileType.accept && !attrAccept(file.file, fileType.accept)) {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Run custom validator
|
|
18
|
+
if (fileType.acceptFile) {
|
|
19
|
+
const result = await fileType.acceptFile(file)
|
|
20
|
+
if (result === false) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ErrorEvent } from '../../types.js'
|
|
2
|
+
import { UploaderError } from '../../UploaderError.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default error handler that filters out expected errors (fileRejected, aborted)
|
|
6
|
+
* and logs unexpected errors.
|
|
7
|
+
*/
|
|
8
|
+
export const uploaderErrorHandler = (event: ErrorEvent): void => {
|
|
9
|
+
if (event.error instanceof UploaderError) {
|
|
10
|
+
if (event.error.options.type === 'fileRejected' || event.error.options.type === 'aborted') {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
console.error('Upload error:', event.error)
|
|
15
|
+
}
|