Skip to content

Commit

Permalink
Add "pendingFiles" and "uploadedFiles" to OnUpdate callback
Browse files Browse the repository at this point in the history
  • Loading branch information
ljwagerfield committed Sep 18, 2023
1 parent 2bfb909 commit a50a600
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 47 deletions.
7 changes: 4 additions & 3 deletions MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
```

Expand Down Expand Up @@ -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).
Expand Down
9 changes: 6 additions & 3 deletions examples/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -50,6 +52,7 @@ const dropZoneInitialConfig: UploadWidgetConfig = {
primary: "#8b63f1"
}
},
onUpdate: x => console.log(JSON.stringify(x)),
onInit: x => {
dropzoneMethods = x;
}
Expand Down
5 changes: 5 additions & 0 deletions lib/src/components/modal/UploadWidgetPendingFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FileLike } from "@bytescale/upload-widget";

export interface UploadWidgetPendingFile {
file: FileLike;
}
61 changes: 33 additions & 28 deletions lib/src/components/widgets/uploadWidget/UploadWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -57,21 +59,25 @@ export const UploadWidget = ({ resolve, options, upload }: Props): JSX.Element =
const [submittedFiles, setSubmittedFiles] = useState<SubmittedFileMap>({});
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<UploadedFileContainer>(
Expand All @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
})
);
Expand Down Expand Up @@ -291,10 +296,10 @@ export const UploadWidget = ({ resolve, options, upload }: Props): JSX.Element =
multi={options.multi}>
{submittedFileList.length === 0 ? (
<UploaderWelcomeScreen options={options} addFiles={addFiles} isImageUploader={isImageUploader} />
) : showImageEditor && pendingImages.length > 0 ? (
) : showImageEditor && uploadedFilesNotReady.length > 0 ? (
<UploaderImageListEditor
images={pendingImages}
onImageEdited={onImageSubmitted}
images={uploadedFilesNotReady}
onImageEdited={onFileReady}
upload={upload}
options={options}
/>
Expand Down
11 changes: 9 additions & 2 deletions lib/src/components/widgets/uploadWidget/model/SubmittedFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
14 changes: 7 additions & 7 deletions lib/src/config/UploadWidgetConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,8 +21,8 @@ export interface UploadWidgetConfig extends BytescaleApiClientConfig {
mimeTypes?: string[];
multi?: boolean;
onInit?: (methods: UploadWidgetMethods) => void;
onPreUpload?: ((file: File) => Resolvable<OnPreUploadResult | undefined>) | undefined;
onUpdate?: (files: UploadWidgetResult[]) => void;
onPreUpload?: ((file: File) => Resolvable<UploadWidgetOnPreUploadResult | undefined>) | undefined;
onUpdate?: (event: UploadWidgetOnUpdateEvent) => void;
path?: FilePathDefinition;
showFinishButton?: boolean;
showRemoveButton?: boolean;
Expand All @@ -41,8 +41,8 @@ export interface UploadWidgetConfigRequired extends BytescaleApiClientConfig {
mimeTypes: string[] | undefined;
multi: boolean;
onInit: (methods: UploadWidgetMethods) => void;
onPreUpload: (file: File) => Promise<OnPreUploadResult | undefined>;
onUpdate: (files: UploadWidgetResult[]) => void;
onPreUpload: (file: File) => Promise<UploadWidgetOnPreUploadResult | undefined>;
onUpdate: (event: UploadWidgetOnUpdateEvent) => void;
path: FilePathDefinition | undefined;
showFinishButton: boolean;
showRemoveButton: boolean;
Expand Down Expand Up @@ -76,7 +76,7 @@ export namespace UploadWidgetConfigRequired {
multi,
onInit: options.onInit ?? (() => {}),
onUpdate: options.onUpdate ?? (() => {}),
onPreUpload: async (file): Promise<OnPreUploadResult | undefined> => {
onPreUpload: async (file): Promise<UploadWidgetOnPreUploadResult | undefined> => {
const { onPreUpload } = options;
return onPreUpload === undefined ? undefined : await onPreUpload(file);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface OnPreUploadResult {
export interface UploadWidgetOnPreUploadResult {
errorMessage?: string;
transformedFile?: File;
}
7 changes: 7 additions & 0 deletions lib/src/config/UploadWidgetOnUpdateEvent.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
2 changes: 1 addition & 1 deletion lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down

0 comments on commit a50a600

Please sign in to comment.