From a50a60088394e958ebfbed6008a50c3af3924c2b Mon Sep 17 00:00:00 2001 From: Lawrence Wagerfield Date: Mon, 18 Sep 2023 14:34:04 +0100 Subject: [PATCH] Add "pendingFiles" and "uploadedFiles" to OnUpdate callback --- MIGRATE.md | 7 ++- README.md | 7 ++- examples/src/index.ts | 9 ++- .../modal/UploadWidgetPendingFile.ts | 5 ++ .../widgets/uploadWidget/UploadWidget.tsx | 61 ++++++++++--------- .../uploadWidget/model/SubmittedFile.ts | 11 +++- lib/src/config/UploadWidgetConfig.ts | 14 ++--- ...lt.ts => UploadWidgetOnPreUploadResult.ts} | 2 +- lib/src/config/UploadWidgetOnUpdateEvent.ts | 7 +++ lib/src/index.ts | 2 +- 10 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 lib/src/components/modal/UploadWidgetPendingFile.ts rename lib/src/config/{OnPreUploadResult.ts => UploadWidgetOnPreUploadResult.ts} (51%) create mode 100644 lib/src/config/UploadWidgetOnUpdateEvent.ts diff --git a/MIGRATE.md b/MIGRATE.md index f9388b9..11970a7 100644 --- a/MIGRATE.md +++ b/MIGRATE.md @@ -10,9 +10,10 @@ Steps: 4. Replace `uploader` with `upload-widget` in all CSS class name overrides (if you have any). 5. Replace `Uploader({apiKey}).open(params)` with `UploadWidget.open({apiKey, ...params })` 1. As such `.open(...)` now takes a mandatory configuration object, with `apiKey` being the only required field. -6. `beginAuthSession` and `endAuthSession` are now static methods on `AuthManager` from the [`@bytescale/sdk` NPM package](https://www.bytescale.com/docs/sdks/javascript). -7. `url` is now a static method on `UrlBuilder` from the [`@bytescale/sdk` NPM package](https://www.bytescale.com/docs/sdks/javascript). -8. `onValidate` has been replaced with `onPreUpload`: you should return an object of `{errorMessage: "your error message"}` instead of `"your error message"`. (This can also be a promise.) +6. Replace `onUpdate: (files) => {}` with `onUpdate: ({uploadedFiles}) => {}`. +7. `beginAuthSession` and `endAuthSession` are now static methods on `AuthManager` from the [`@bytescale/sdk` NPM package](https://www.bytescale.com/docs/sdks/javascript). +8. `url` is now a static method on `UrlBuilder` from the [`@bytescale/sdk` NPM package](https://www.bytescale.com/docs/sdks/javascript). +9. `onValidate` has been replaced with `onPreUpload`: you should return an object of `{errorMessage: "your error message"}` instead of `"your error message"`. (This can also be a promise.) ### Before diff --git a/README.md b/README.md index 3db2e97..1eda831 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Bytescale.UploadWidget.open({ multi: true, // Full Config: https://www.bytescale.com/docs/upload-widget#configuration layout: "inline", // Specifies dropzone behaviour. container: "#example_div_id", // Replace with the ID of an existing DOM element (to render the dropzone inside). - onUpdate: (files) => console.log(files) + onUpdate: ({ uploadedFiles }) => console.log(uploadedFiles) }) ``` @@ -226,7 +226,10 @@ Bytescale.UploadWidget.open({ reset, // Resets the widget when called. updateConfig // Updates the widget's config by passing a new config }) => {}, // object to the method's first parameter. - onUpdate: files => {}, // Called each time the list of uploaded files change. + onUpdate: (event) => { // Called each time the Upload Widget's list of files change. + // event.pendingFiles // Array of files that are either uploading or queued. + // event.uploadedFiles // Array of files that have been uploaded and not removed. + }, onPreUpload: async file => ({ errorMessage: "Uh oh!", // Displays this validation error to the user (if set). transformedFile: file // Uploads 'transformedFile' instead of 'file' (if set). diff --git a/examples/src/index.ts b/examples/src/index.ts index 1289358..48e47e5 100644 --- a/examples/src/index.ts +++ b/examples/src/index.ts @@ -6,9 +6,11 @@ const apiKey: string = (window as any).UPLOAD_JS_API_KEY ?? "free"; const openUploader = (): void => { UploadWidget.open({ apiKey, - multi: true, - mimeTypes: ["image/jpeg", "image/webp", "image/png", "image/heic", "image/svg+xml"], - maxFileCount: 10, + // multi: true, + // mimeTypes: ["image/jpeg", "image/webp", "image/png", "image/heic", "image/svg+xml"], + maxFileCount: 1, + showFinishButton: false, + onUpdate: x => console.log(JSON.stringify(x)), editor: { images: { cropShape: "circ", cropRatio: 1 / 1 } }, styles: { colors: { @@ -50,6 +52,7 @@ const dropZoneInitialConfig: UploadWidgetConfig = { primary: "#8b63f1" } }, + onUpdate: x => console.log(JSON.stringify(x)), onInit: x => { dropzoneMethods = x; } diff --git a/lib/src/components/modal/UploadWidgetPendingFile.ts b/lib/src/components/modal/UploadWidgetPendingFile.ts new file mode 100644 index 0000000..3bb81b6 --- /dev/null +++ b/lib/src/components/modal/UploadWidgetPendingFile.ts @@ -0,0 +1,5 @@ +import { FileLike } from "@bytescale/upload-widget"; + +export interface UploadWidgetPendingFile { + file: FileLike; +} diff --git a/lib/src/components/widgets/uploadWidget/UploadWidget.tsx b/lib/src/components/widgets/uploadWidget/UploadWidget.tsx index 9f9eed5..132dd44 100644 --- a/lib/src/components/widgets/uploadWidget/UploadWidget.tsx +++ b/lib/src/components/widgets/uploadWidget/UploadWidget.tsx @@ -7,6 +7,7 @@ import { UploaderWelcomeScreen } from "@bytescale/upload-widget/components/widge import { UploaderMainScreen } from "@bytescale/upload-widget/components/widgets/uploadWidget/screens/UploaderMainScreen"; import { ErroneousFile, + isPendingFile, isUploadedFile, SubmittedFile, SubmittedFileMap, @@ -24,9 +25,10 @@ import { UploadWidgetResult } from "@bytescale/upload-widget/components/modal/Up import { UploaderImageListEditor } from "@bytescale/upload-widget/components/widgets/uploadWidget/screens/UploaderImageListEditor"; import { useShowImageEditor } from "@bytescale/upload-widget/components/widgets/uploadWidget/screens/modules/UseShowImageEditor"; import { isEditableImage, isReadOnlyImage } from "@bytescale/upload-widget/modules/MimeUtils"; -import { OnPreUploadResult } from "@bytescale/upload-widget/config/OnPreUploadResult"; +import { UploadWidgetOnPreUploadResult } from "@bytescale/upload-widget/config/UploadWidgetOnPreUploadResult"; import { UploadTracker } from "@bytescale/upload-widget/modules/UploadTracker"; import { UploadedFile } from "@bytescale/upload-widget/modules/UploadedFile"; +import { UploadWidgetPendingFile } from "@bytescale/upload-widget/components/modal/UploadWidgetPendingFile"; interface Props { options: UploadWidgetConfigRequired; @@ -57,21 +59,25 @@ export const UploadWidget = ({ resolve, options, upload }: Props): JSX.Element = const [submittedFiles, setSubmittedFiles] = useState({}); const submittedFileList: SubmittedFile[] = Object.values(submittedFiles).filter(isDefined); const uploadedFiles = submittedFileList.filter(isUploadedFile); + const pendingFiles = submittedFileList.filter(isPendingFile); + const makeDeps = (fileLists: SubmittedFile[][]): number[] => [ + ...fileLists.map(x => x.length), + ...fileLists.flatMap(x => x.map(y => y.fileIndex)) + ]; const onFileUploadDelay = progressWheelDelay + (progressWheelVanish - 100); // Allows the animation to finish before closing modal. We add some time to allow the wheel to fade out. const { multi, tags, metadata, path } = options; - const uploadWidgetResult = uploadedFiles.map(x => UploadWidgetResult.from(x.uploadedFile, x.editedFile)); + const uploadedFilesReady = uploadedFiles.filter(x => x.isReady); + const uploadedFilesNotReady = uploadedFiles.filter(x => !x.isReady); // These will be previewable or editable media files. + const uploadedFilesReadyResult = uploadedFilesReady.map(x => UploadWidgetResult.from(x.uploadedFile, x.editedFile)); const canEditImages = options.editor.images.crop; const canPreviewImages = options.editor.images.preview; - const pendingImages = uploadedFiles.filter( - x => - !x.isSubmitted && - (((canEditImages || canPreviewImages) && isEditableImage(x.uploadedFile)) || - (canPreviewImages && isReadOnlyImage(x.uploadedFile))) - ); - const showImageEditor = useShowImageEditor(pendingImages, onFileUploadDelay); + const fileRequiresUserInteraction = (uploadedFile: UploadedFile): boolean => + ((canEditImages || canPreviewImages) && isEditableImage(uploadedFile)) || + (canPreviewImages && isReadOnlyImage(uploadedFile)); + const showImageEditor = useShowImageEditor(uploadedFilesNotReady, onFileUploadDelay); - const onImageSubmitted = (keep: boolean, editedFile: UploadedFile | undefined, sparseFileIndex: number): void => { - if (!keep) { + const onFileReady = (keepFile: boolean, editedFile: UploadedFile | undefined, sparseFileIndex: number): void => { + if (!keepFile) { removeSubmittedFile(sparseFileIndex); } else { updateFile( @@ -80,41 +86,39 @@ export const UploadWidget = ({ resolve, options, upload }: Props): JSX.Element = (file): UploadedFileContainer => ({ ...file, editedFile, - isSubmitted: true + isReady: true }) ); } }; const finalize = (): void => { - resolve(uploadWidgetResult); + resolve(uploadedFilesReadyResult); }; // We want to use a 'layout effect' since if the cropper has just been closed in 'single file mode', we want to // immediately resolve the Upload Widget, rather than momentarily showing the main screen. useLayoutEffect(() => { - if (pendingImages.length > 0) { - // Do not raise update events until after the images have finished editing. - return; - } - if (isInitialUpdate) { setIsInitialUpdate(false); return; } - options.onUpdate(uploadWidgetResult); + options.onUpdate({ + uploadedFiles: uploadedFilesReadyResult, + pendingFiles: [...pendingFiles, ...uploadedFilesNotReady].map(({ file }): UploadWidgetPendingFile => ({ file })) + }); // For inline layouts, if in single-file mode, we never resolve (there is no terminal state): we just allow the // user to add/remove their file, and the caller should instead rely on the 'onUpdate' method above. const shouldCloseModalImmediatelyAfterUpload = - !multi && uploadedFiles.length > 0 && !options.showFinishButton && options.layout === "modal"; + !multi && uploadedFilesReady.length > 0 && !options.showFinishButton && options.layout === "modal"; if (shouldCloseModalImmediatelyAfterUpload) { // Just in case the user dragged-and-dropped multiple files. - const firstUploadedFile = uploadWidgetResult.slice(0, 1); + const firstUploadedFile = uploadedFilesReadyResult.slice(0, 1); const doResolve = (): void => resolve(firstUploadedFile); - const previousScreenWasEditor = uploadedFiles[0].isSubmitted; + const previousScreenWasEditor = fileRequiresUserInteraction(uploadedFiles[0].uploadedFile); if (previousScreenWasEditor) { doResolve(); @@ -123,7 +127,7 @@ export const UploadWidget = ({ resolve, options, upload }: Props): JSX.Element = return () => clearTimeout(timeout); } } - }, [pendingImages.length, ...uploadedFiles.map(x => x.uploadedFile.fileUrl)]); + }, makeDeps([pendingFiles, uploadedFilesReady, uploadedFilesNotReady])); const removeSubmittedFile = (fileIndex: number): void => { setSubmittedFiles( @@ -192,7 +196,7 @@ export const UploadWidget = ({ resolve, options, upload }: Props): JSX.Element = type: "preprocessing" }); - let preUploadResult: OnPreUploadResult | undefined; + let preUploadResult: UploadWidgetOnPreUploadResult | undefined; try { preUploadResult = (await onPreUpload(file)) ?? undefined; // incase the user returns 'null' instead of undefined. @@ -253,10 +257,11 @@ export const UploadWidget = ({ resolve, options, upload }: Props): JSX.Element = fileIndex, "uploading", (): UploadedFileContainer => ({ + file, fileIndex, uploadedFile, editedFile: undefined, - isSubmitted: false, + isReady: !fileRequiresUserInteraction(uploadedFile), type: "uploaded" }) ); @@ -291,10 +296,10 @@ export const UploadWidget = ({ resolve, options, upload }: Props): JSX.Element = multi={options.multi}> {submittedFileList.length === 0 ? ( - ) : showImageEditor && pendingImages.length > 0 ? ( + ) : showImageEditor && uploadedFilesNotReady.length > 0 ? ( diff --git a/lib/src/components/widgets/uploadWidget/model/SubmittedFile.ts b/lib/src/components/widgets/uploadWidget/model/SubmittedFile.ts index 2647a74..f3a39e9 100644 --- a/lib/src/components/widgets/uploadWidget/model/SubmittedFile.ts +++ b/lib/src/components/widgets/uploadWidget/model/SubmittedFile.ts @@ -23,18 +23,25 @@ export interface ErroneousFile { export interface UploadedFileContainer { editedFile: UploadedFile | undefined; + file: File; fileIndex: number; - isSubmitted: boolean; // True if the image has been 'passed' by the user, i.e. successfully edited/left unedited, or has been accepted in the preview screen. + isReady: boolean; // False if the file still requires some action performing before it's considered fully uploaded, i.e. editing, or having 'accept' clicked in the preview screen. type: "uploaded"; uploadedFile: UploadedFile; } -export type SubmittedFile = UploadingFile | PreprocessingFile | UploadedFileContainer | ErroneousFile; +export type PendingFile = PreprocessingFile | UploadingFile; + +export type SubmittedFile = PendingFile | UploadedFileContainer | ErroneousFile; export function isUploadedFile(file: SubmittedFile): file is UploadedFileContainer { return file.type === "uploaded"; } +export function isPendingFile(file: SubmittedFile): file is PendingFile { + return file.type === "preprocessing" || file.type === "uploading"; +} + export interface SubmittedFileMap { [sparseFileIndex: number]: SubmittedFile | undefined; } diff --git a/lib/src/config/UploadWidgetConfig.ts b/lib/src/config/UploadWidgetConfig.ts index 32760cb..c03b0f3 100644 --- a/lib/src/config/UploadWidgetConfig.ts +++ b/lib/src/config/UploadWidgetConfig.ts @@ -2,13 +2,13 @@ import { UploadWidgetLocale } from "@bytescale/upload-widget/modules/locales/Upl import { UploaderLocaleEnUs } from "@bytescale/upload-widget/modules/locales/EN_US"; import { UploadWidgetLayout } from "@bytescale/upload-widget/config/UploadWidgetLayout"; import { UploadWidgetEditor, UploadWidgetEditorRequired } from "@bytescale/upload-widget/config/UploadWidgetEditor"; -import { UploadWidgetResult } from "@bytescale/upload-widget/components/modal/UploadWidgetResult"; import { UploadWidgetStyles, UploadWidgetStylesRequired } from "@bytescale/upload-widget/config/UploadWidgetStyles"; import { UploadWidgetMethods } from "@bytescale/upload-widget/config/UploadWidgetMethods"; -import { OnPreUploadResult } from "@bytescale/upload-widget/config/OnPreUploadResult"; +import { UploadWidgetOnPreUploadResult } from "@bytescale/upload-widget/config/UploadWidgetOnPreUploadResult"; import { Resolvable } from "@bytescale/upload-widget/modules/common/Resolvable"; import { FilePathDefinition } from "@bytescale/sdk"; import { BytescaleApiClientConfig } from "@bytescale/sdk/dist/types/public/shared/generated/runtime"; +import { UploadWidgetOnUpdateEvent } from "@bytescale/upload-widget/config/UploadWidgetOnUpdateEvent"; export interface UploadWidgetConfig extends BytescaleApiClientConfig { container?: string | HTMLElement; @@ -21,8 +21,8 @@ export interface UploadWidgetConfig extends BytescaleApiClientConfig { mimeTypes?: string[]; multi?: boolean; onInit?: (methods: UploadWidgetMethods) => void; - onPreUpload?: ((file: File) => Resolvable) | undefined; - onUpdate?: (files: UploadWidgetResult[]) => void; + onPreUpload?: ((file: File) => Resolvable) | undefined; + onUpdate?: (event: UploadWidgetOnUpdateEvent) => void; path?: FilePathDefinition; showFinishButton?: boolean; showRemoveButton?: boolean; @@ -41,8 +41,8 @@ export interface UploadWidgetConfigRequired extends BytescaleApiClientConfig { mimeTypes: string[] | undefined; multi: boolean; onInit: (methods: UploadWidgetMethods) => void; - onPreUpload: (file: File) => Promise; - onUpdate: (files: UploadWidgetResult[]) => void; + onPreUpload: (file: File) => Promise; + onUpdate: (event: UploadWidgetOnUpdateEvent) => void; path: FilePathDefinition | undefined; showFinishButton: boolean; showRemoveButton: boolean; @@ -76,7 +76,7 @@ export namespace UploadWidgetConfigRequired { multi, onInit: options.onInit ?? (() => {}), onUpdate: options.onUpdate ?? (() => {}), - onPreUpload: async (file): Promise => { + onPreUpload: async (file): Promise => { const { onPreUpload } = options; return onPreUpload === undefined ? undefined : await onPreUpload(file); }, diff --git a/lib/src/config/OnPreUploadResult.ts b/lib/src/config/UploadWidgetOnPreUploadResult.ts similarity index 51% rename from lib/src/config/OnPreUploadResult.ts rename to lib/src/config/UploadWidgetOnPreUploadResult.ts index 81c456e..fc9323e 100644 --- a/lib/src/config/OnPreUploadResult.ts +++ b/lib/src/config/UploadWidgetOnPreUploadResult.ts @@ -1,4 +1,4 @@ -export interface OnPreUploadResult { +export interface UploadWidgetOnPreUploadResult { errorMessage?: string; transformedFile?: File; } diff --git a/lib/src/config/UploadWidgetOnUpdateEvent.ts b/lib/src/config/UploadWidgetOnUpdateEvent.ts new file mode 100644 index 0000000..06916a9 --- /dev/null +++ b/lib/src/config/UploadWidgetOnUpdateEvent.ts @@ -0,0 +1,7 @@ +import { UploadWidgetResult } from "@bytescale/upload-widget"; +import { UploadWidgetPendingFile } from "@bytescale/upload-widget/components/modal/UploadWidgetPendingFile"; + +export interface UploadWidgetOnUpdateEvent { + pendingFiles: UploadWidgetPendingFile[]; + uploadedFiles: UploadWidgetResult[]; +} diff --git a/lib/src/index.ts b/lib/src/index.ts index 629bd73..fe16c53 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -3,7 +3,7 @@ export { UploadedFile } from "@bytescale/upload-widget/modules/UploadedFile"; export { UploadWidget } from "@bytescale/upload-widget/UploadWidget"; export { UploadWidgetResult } from "@bytescale/upload-widget/components/modal/UploadWidgetResult"; export { UploadWidgetLocale } from "@bytescale/upload-widget/modules/locales/UploadWidgetLocale"; -export { OnPreUploadResult } from "@bytescale/upload-widget/config/OnPreUploadResult"; +export { UploadWidgetOnPreUploadResult } from "@bytescale/upload-widget/config/UploadWidgetOnPreUploadResult"; export { UploadWidgetColors } from "@bytescale/upload-widget/config/UploadWidgetColors"; export { UploadWidgetLayout } from "@bytescale/upload-widget/config/UploadWidgetLayout"; export { UploadWidgetFontFamily } from "@bytescale/upload-widget/config/UploadWidgetFontFamily";