From 8ca2b198ccee45e06550002a25b2eee2b40ebdaa Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Tue, 15 Nov 2022 08:30:43 +0300 Subject: [PATCH] enable multi files for image&file fields --- src/atoms/tableScope/rowActions.ts | 42 ++- .../SideDrawer/SideDrawerFields.tsx | 15 +- .../fields/File/SideDrawerField.tsx | 68 ++--- src/components/fields/File/TableCell.tsx | 70 ++--- src/components/fields/File/useFileUpload.ts | 61 +++++ .../fields/Image/SideDrawerField.tsx | 116 ++++---- src/components/fields/Image/TableCell.tsx | 87 +++--- src/hooks/useFirebaseStorageUploader.tsx | 256 ++++++++++-------- 8 files changed, 402 insertions(+), 313 deletions(-) create mode 100644 src/components/fields/File/useFileUpload.ts diff --git a/src/atoms/tableScope/rowActions.ts b/src/atoms/tableScope/rowActions.ts index 37d1f9de..eb462498 100644 --- a/src/atoms/tableScope/rowActions.ts +++ b/src/atoms/tableScope/rowActions.ts @@ -6,6 +6,7 @@ import { set as _set, isEqual, unset, + filter, } from "lodash-es"; import { currentUserAtom } from "@src/atoms/projectScope"; @@ -29,6 +30,7 @@ import { updateRowData, omitRowyFields, } from "@src/utils/table"; +import { arrayRemove, arrayUnion } from "firebase/firestore"; export interface IAddRowOptions { /** The row or array of rows to add */ @@ -306,6 +308,10 @@ export interface IUpdateFieldOptions { ignoreRequiredFields?: boolean; /** Optionally, disable checking if the updated value is equal to the current value. By default, we skip the update if they’re equal. */ disableCheckEquality?: boolean; + /** Optionally, uses firestore's arrayUnion with the given value. Appends given value items to the existing array */ + useArrayUnion?: boolean; + /** Optionally, uses firestore's arrayRemove with the given value. Removes given value items from the existing array */ + useArrayRemove?: boolean; } /** * Set function updates or deletes a field in a row. @@ -331,6 +337,8 @@ export const updateFieldAtom = atom( deleteField, ignoreRequiredFields, disableCheckEquality, + useArrayUnion, + useArrayRemove, }: IUpdateFieldOptions ) => { const updateRowDb = get(_updateRowDbAtom); @@ -367,8 +375,36 @@ export const updateFieldAtom = atom( _set(update, fieldName, value); } + const localUpdate = cloneDeep(update); + const dbUpdate = cloneDeep(update); + // apply arrayUnion + if (useArrayUnion) { + if (!Array.isArray(update[fieldName])) + throw new Error("Field must be an array"); + + // use basic array merge on local row value + localUpdate[fieldName] = [ + ...(row[fieldName] ?? []), + ...localUpdate[fieldName], + ]; + dbUpdate[fieldName] = arrayUnion(...dbUpdate[fieldName]); + } + + //apply arrayRemove + if (useArrayRemove) { + if (!Array.isArray(update[fieldName])) + throw new Error("Field must be an array"); + + // use basic array filter on local row value + localUpdate[fieldName] = filter( + row[fieldName] ?? [], + (el) => !find(localUpdate[fieldName], el) + ); + dbUpdate[fieldName] = arrayRemove(...dbUpdate[fieldName]); + } + // Check for required fields - const newRowValues = updateRowData(cloneDeep(row), update); + const newRowValues = updateRowData(cloneDeep(row), dbUpdate); const requiredFields = ignoreRequiredFields ? [] : tableColumnsOrdered @@ -383,7 +419,7 @@ export const updateFieldAtom = atom( set(tableRowsLocalAtom, { type: "update", path, - row: update, + row: localUpdate, deleteFields: deleteField ? [fieldName] : [], }); @@ -403,7 +439,7 @@ export const updateFieldAtom = atom( else { await updateRowDb( row._rowy_ref.path, - omitRowyFields(update), + omitRowyFields(dbUpdate), deleteField ? [fieldName] : [] ); } diff --git a/src/components/SideDrawer/SideDrawerFields.tsx b/src/components/SideDrawer/SideDrawerFields.tsx index 7b6a6578..937d5b44 100644 --- a/src/components/SideDrawer/SideDrawerFields.tsx +++ b/src/components/SideDrawer/SideDrawerFields.tsx @@ -23,7 +23,8 @@ import { sideDrawerShowHiddenFieldsAtom, } from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; -import { TableRow } from "@src/types/table"; +import { ColumnConfig, TableRow } from "@src/types/table"; +import { FieldType } from "@src/components/fields/types"; export interface ISideDrawerFieldsProps { row: TableRow; @@ -66,7 +67,17 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) { setSaveState("saving"); try { - await updateField({ path: selectedCell!.path, fieldName, value }); + const { type } = tableColumnsOrdered.find( + (c) => c.key === selectedCell.columnKey + ) as ColumnConfig; + const useArrayUnion = [FieldType.file, FieldType.image].includes(type); + + await updateField({ + path: selectedCell!.path, + fieldName, + value, + useArrayUnion, + }); setSaveState("saved"); } catch (e) { enqueueSnackbar((e as Error).message, { variant: "error" }); diff --git a/src/components/fields/File/SideDrawerField.tsx b/src/components/fields/File/SideDrawerField.tsx index cad34d9b..af3f6168 100644 --- a/src/components/fields/File/SideDrawerField.tsx +++ b/src/components/fields/File/SideDrawerField.tsx @@ -1,10 +1,9 @@ -import { useCallback, useState } from "react"; +import { useState } from "react"; import { ISideDrawerFieldProps } from "@src/components/fields/types"; import { useSetAtom } from "jotai"; import { format } from "date-fns"; import { useDropzone } from "react-dropzone"; -import useUploader from "@src/hooks/useFirebaseStorageUploader"; import { alpha, @@ -23,52 +22,30 @@ import { DATE_TIME_FORMAT } from "@src/constants/dates"; import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope"; import { FileValue } from "@src/types/table"; -import { arrayUnion } from "firebase/firestore"; +import useFileUpload from "./useFileUpload"; export default function File_({ column, _rowy_ref, value, - onChange, - onSubmit, disabled, }: ISideDrawerFieldProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); - - const { uploaderState, upload, deleteUpload } = useUploader(); - - // Store a preview image locally while uploading - const [localFiles, setLocalFiles] = useState([]); - - const onDrop = useCallback( - (acceptedFiles: File[]) => { - if (_rowy_ref && acceptedFiles.length > 0) { - upload({ - docRef: _rowy_ref! as any, - fieldName: column.key, - files: acceptedFiles, - onComplete: (newUploads) => { - onChange(arrayUnion(newUploads)); - onSubmit(); - setLocalFiles([]); - }, - }); - setLocalFiles(acceptedFiles.map((file) => file.name)); - } - }, - [_rowy_ref, value] + const { loading, progress, handleUpload, handleDelete } = useFileUpload( + _rowy_ref, + column.key ); - const handleDelete = (index: number) => { - const newValue = [...value]; - const toBeDeleted = newValue.splice(index, 1); - toBeDeleted.length && deleteUpload(toBeDeleted[0]); - onChange(newValue); - onSubmit(); - }; + const [localFiles, setLocalFiles] = useState([]); const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, + onDrop: async (acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + setLocalFiles(acceptedFiles); + await handleUpload(acceptedFiles); + setLocalFiles([]); + } + }, multiple: true, }); @@ -76,6 +53,7 @@ export default function File_({ <> {!disabled && ( Click to upload or drop file here - + {loading ? ( + + ) : ( + + )} )} {Array.isArray(value) && - value.map((file: FileValue, i) => ( + value.map((file: FileValue) => ( handleDelete(i), + handleConfirm: () => handleDelete(file), }) : undefined } @@ -137,11 +123,11 @@ export default function File_({ ))} {localFiles && - localFiles.map((fileName) => ( + localFiles.map((file) => ( } - label={fileName} + label={file.name} deleteIcon={ } diff --git a/src/components/fields/File/TableCell.tsx b/src/components/fields/File/TableCell.tsx index a4f96c0a..eee9a330 100644 --- a/src/components/fields/File/TableCell.tsx +++ b/src/components/fields/File/TableCell.tsx @@ -1,7 +1,6 @@ -import { useCallback, useState } from "react"; +import { useState } from "react"; import { IHeavyCellProps } from "@src/components/fields/types"; import { useSetAtom } from "jotai"; -import { findIndex } from "lodash-es"; import { useDropzone } from "react-dropzone"; import { format } from "date-fns"; @@ -12,58 +11,33 @@ import ChipList from "@src/components/Table/formatters/ChipList"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope"; -import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; -import useUploader from "@src/hooks/useFirebaseStorageUploader"; import { FileIcon } from "."; import { DATE_TIME_FORMAT } from "@src/constants/dates"; import { FileValue } from "@src/types/table"; -import { arrayUnion } from "firebase/firestore"; +import useFileUpload from "./useFileUpload"; export default function File_({ column, - row, value, - onSubmit, disabled, docRef, }: IHeavyCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); - const updateField = useSetAtom(updateFieldAtom, tableScope); - const [localFiles, setLocalFiles] = useState([]); - const { uploaderState, upload, deleteUpload } = useUploader(); - const { progress, isLoading } = uploaderState; - const onDrop = useCallback( - (acceptedFiles: File[]) => { - if (acceptedFiles.length > 0) { - upload({ - docRef: docRef! as any, - fieldName: column.key, - files: acceptedFiles, - onComplete: (newUploads) => { - updateField({ - path: docRef.path, - fieldName: column.key, - value: arrayUnion(newUploads), - }); - setLocalFiles([]); - }, - }); - setLocalFiles(acceptedFiles.map((file) => file.name)); - } - }, - [value] + const [localFiles, setLocalFiles] = useState([]); + + const { loading, progress, handleUpload, handleDelete } = useFileUpload( + docRef, + column.key ); - const handleDelete = (ref: string) => { - const newValue = [...value]; - const index = findIndex(newValue, ["ref", ref]); - const toBeDeleted = newValue.splice(index, 1); - toBeDeleted.length && deleteUpload(toBeDeleted[0]); - onSubmit(newValue); - }; - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, + onDrop: async (acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + setLocalFiles(acceptedFiles); + await handleUpload(acceptedFiles); + setLocalFiles([]); + } + }, multiple: true, }); @@ -111,8 +85,13 @@ export default function File_({ )}`} > } label={file.name} + icon={} + sx={{ + "& .MuiChip-label": { + lineHeight: 5 / 3, + }, + }} onClick={(e) => { window.open(file.downloadURL); e.stopPropagation(); @@ -122,24 +101,23 @@ export default function File_({ ? undefined : () => confirm({ - handleConfirm: () => handleDelete(file.ref), + handleConfirm: () => handleDelete(file), title: "Delete file?", body: "This file cannot be recovered after", confirm: "Delete", confirmColor: "error", }) } - style={{ width: "100%" }} /> ))} {localFiles && - localFiles.map((fileName) => ( + localFiles.map((file) => ( } - label={fileName} + label={file.name} deleteIcon={ } @@ -148,7 +126,7 @@ export default function File_({ ))} - {!isLoading ? ( + {!loading ? ( !disabled && ( 0 + ? uploadingFiles.reduce((sum, fileName) => { + const fileState = uploaderState[fileName]; + return sum + fileState.progress; + }, 0) / uploadingFiles.length + : 0; + + const loading = some( + uploadingFiles, + (fileName) => uploaderState[fileName].loading + ); + + const handleUpload = useCallback( + async (files: File[]) => { + const { uploads, failures } = await upload({ + docRef, + fieldName, + files, + }); + updateField({ + path: docRef.path, + fieldName, + value: uploads, + useArrayUnion: true, + }); + return { uploads, failures }; + }, + [docRef, fieldName, updateField, upload] + ); + + const handleDelete = useCallback( + (file: FileValue) => { + updateField({ + path: docRef.path, + fieldName, + value: [file], + useArrayRemove: true, + disableCheckEquality: true, + }); + deleteUpload(file); + }, + [deleteUpload, docRef, fieldName, updateField] + ); + + return { progress, loading, uploaderState, handleUpload, handleDelete }; +} diff --git a/src/components/fields/Image/SideDrawerField.tsx b/src/components/fields/Image/SideDrawerField.tsx index 304e0555..531ba7e7 100644 --- a/src/components/fields/Image/SideDrawerField.tsx +++ b/src/components/fields/Image/SideDrawerField.tsx @@ -1,10 +1,9 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { useSetAtom } from "jotai"; +import { assignIn } from "lodash-es"; import { useDropzone } from "react-dropzone"; -// TODO: GENERALIZE -import useUploader from "@src/hooks/useFirebaseStorageUploader"; import { alpha, @@ -20,13 +19,14 @@ import AddIcon from "@mui/icons-material/AddAPhotoOutlined"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import OpenIcon from "@mui/icons-material/OpenInNewOutlined"; +import { FileValue } from "@src/types/table"; import Thumbnail from "@src/components/Thumbnail"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope"; -import { IMAGE_MIME_TYPES } from "."; import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; -import { arrayUnion } from "firebase/firestore"; +import useFileUpload from "@src/components/fields/File/useFileUpload"; +import { IMAGE_MIME_TYPES } from "."; const imgSx = { position: "relative", @@ -85,46 +85,29 @@ export default function Image_({ column, _rowy_ref, value, - onChange, - onSubmit, disabled, }: ISideDrawerFieldProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); - const { uploaderState, upload, deleteUpload } = useUploader(); - const { progress } = uploaderState; - // Store a preview image locally while uploading - const [localImages, setLocalImages] = useState([]); + const { loading, progress, handleUpload, handleDelete, uploaderState } = + useFileUpload(_rowy_ref, column.key); - const onDrop = useCallback( - (acceptedFiles: File[]) => { - if (_rowy_ref && acceptedFiles.length > 0) { - upload({ - docRef: _rowy_ref! as any, - fieldName: column.key, - files: acceptedFiles, - onComplete: (newUploads) => { - onChange(arrayUnion(newUploads)); - onSubmit(); - setLocalImages([]); - }, - }); - setLocalImages(acceptedFiles.map((file) => URL.createObjectURL(file))); - } - }, - [_rowy_ref, value] - ); - - const handleDelete = (index: number) => { - const newValue = [...value]; - const toBeDeleted = newValue.splice(index, 1); - toBeDeleted.length && deleteUpload(toBeDeleted[0]); - onChange(newValue); - onSubmit(); - }; + const [localImages, setLocalImages] = useState< + (File & { localURL: string })[] + >([]); const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, + onDrop: async (acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + setLocalImages( + acceptedFiles.map((file) => + assignIn(file, { localURL: URL.createObjectURL(file) }) + ) + ); + await handleUpload(acceptedFiles); + setLocalImages([]); + } + }, multiple: true, accept: IMAGE_MIME_TYPES, }); @@ -133,6 +116,7 @@ export default function Image_({ <> {!disabled && ( - + {loading ? ( + + ) : ( + + )} )} {Array.isArray(value) && - value.map((image, i) => ( - + value.map((image: FileValue) => ( + {disabled ? ( handleDelete(i), + handleConfirm: () => handleDelete(image), }) } > @@ -236,26 +228,34 @@ export default function Image_({ ))} {localImages && - localImages.map((image, i) => ( - + localImages.map((image) => ( + - - - + {uploaderState[image.name] && ( + + + + )} ))} diff --git a/src/components/fields/Image/TableCell.tsx b/src/components/fields/Image/TableCell.tsx index 471a6f37..f8f31861 100644 --- a/src/components/fields/Image/TableCell.tsx +++ b/src/components/fields/Image/TableCell.tsx @@ -1,8 +1,7 @@ -import { useCallback, useState } from "react"; +import { useState } from "react"; import { IHeavyCellProps } from "@src/components/fields/types"; import { useAtom, useSetAtom } from "jotai"; -import { findIndex } from "lodash-es"; - +import { assignIn } from "lodash-es"; import { useDropzone } from "react-dropzone"; import { @@ -23,16 +22,11 @@ import Thumbnail from "@src/components/Thumbnail"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope"; -import { - tableSchemaAtom, - tableScope, - updateFieldAtom, -} from "@src/atoms/tableScope"; -import useUploader from "@src/hooks/useFirebaseStorageUploader"; -import { IMAGE_MIME_TYPES } from "./index"; +import { tableSchemaAtom, tableScope } from "@src/atoms/tableScope"; import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; import { FileValue } from "@src/types/table"; -import { arrayUnion } from "firebase/firestore"; +import useFileUpload from "@src/components/fields/File/useFileUpload"; +import { IMAGE_MIME_TYPES } from "./index"; // MULTIPLE const imgSx = (rowHeight: number) => ({ @@ -89,50 +83,33 @@ const deleteImgHoverSx = { export default function Image_({ column, value, - onSubmit, disabled, docRef, }: IHeavyCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); - const updateField = useSetAtom(updateFieldAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); - const { uploaderState, upload, deleteUpload } = useUploader(); - const { progress, isLoading } = uploaderState; - // Store a preview image locally while uploading - const [localImages, setLocalImages] = useState([]); - - const onDrop = useCallback( - (acceptedFiles: File[]) => { - if (acceptedFiles.length > 0) { - upload({ - docRef: docRef! as any, - fieldName: column.key, - files: acceptedFiles, - onComplete: (newUploads) => { - updateField({ - path: docRef.path, - fieldName: column.key, - value: arrayUnion(newUploads), - }); - setLocalImages([]); - }, - }); - setLocalImages(acceptedFiles.map((file) => URL.createObjectURL(file))); - } - }, - [value] + const { loading, progress, handleUpload, handleDelete } = useFileUpload( + docRef, + column.key ); - const handleDelete = (index: number) => () => { - const newValue = [...value]; - const toBeDeleted = newValue.splice(index, 1); - toBeDeleted.length && deleteUpload(toBeDeleted[0]); - onSubmit(newValue); - }; + const [localImages, setLocalImages] = useState< + (File & { localURL: string })[] + >([]); const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, + onDrop: async (acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + setLocalImages( + acceptedFiles.map((file) => + assignIn(file, { localURL: URL.createObjectURL(file) }) + ) + ); + await handleUpload(acceptedFiles); + setLocalImages([]); + } + }, multiple: true, accept: IMAGE_MIME_TYPES, }); @@ -181,17 +158,17 @@ export default function Image_({ > {Array.isArray(value) && - value.map((file: FileValue, i) => ( - + value.map((image: FileValue) => ( + {disabled ? ( window.open(file.downloadURL, "_blank")} + onClick={() => window.open(image.downloadURL, "_blank")} > handleDelete(image), }); }} > ( + localImages.map((image) => ( ))} - {!isLoading ? ( + {!loading ? ( !disabled && ( -) => ({ ...prevState, ...newProps }); + action: { + type: "reset" | "file_update"; + data?: { fileName: string; newProps: Partial }; + } +) => { + switch (action.type) { + case "reset": + return {}; + case "file_update": + const { fileName, newProps } = action.data!; + const nextState = { ...prevState }; + nextState[fileName] = { + ...nextState[fileName], + ...newProps, + }; + return nextState; + } +}; export type UploadProps = { docRef: DocumentReference; fieldName: string; files: File[]; - onComplete?: (value: FileValue) => void; + onComplete?: ({ + uploads, + failures, + }: { + uploads: FileValue[]; + failures: string[]; + }) => void; }; // TODO: GENERALIZE INTO ATOM @@ -41,116 +66,129 @@ const useFirebaseStorageUploader = () => { const [firebaseStorage] = useAtom(firebaseStorageAtom, projectScope); const { enqueueSnackbar } = useSnackbar(); - const [uploaderState, uploaderDispatch] = useReducer(uploadReducer, { - ...initialState, - }); + const [uploaderState, uploaderDispatch] = useReducer(uploadReducer, {}); - const upload = ({ docRef, fieldName, files, onComplete }: UploadProps) => { - uploaderDispatch({ isLoading: true }); + const upload = ({ docRef, fieldName, files }: UploadProps) => { + const uploads = [] as FileValue[]; + const failures = [] as string[]; + const isCompleted = () => uploads.length + failures.length === files.length; - files.forEach((file) => { - const storageRef = ref( - firebaseStorage, - `${docRef.path}/${fieldName}/${file.name}` - ); - const uploadTask = uploadBytesResumable(storageRef, file, { - cacheControl: "public, max-age=31536000", - }); - uploadTask.on( - // event - "state_changed", - // observer - (snapshot) => { - // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded - const progress = - (snapshot.bytesTransferred / snapshot.totalBytes) * 100; - uploaderDispatch({ progress }); - console.log("Upload is " + progress + "% done"); + return new Promise((resolve) => + files.forEach((file) => { + uploaderDispatch({ + type: "file_update", + data: { + fileName: file.name, + newProps: { loading: true, progress: 0 }, + }, + }); - switch (snapshot.state) { - case "paused": - console.log("Upload is paused"); - break; - case "running": - console.log("Upload is running"); - break; - } - }, + const storageRef = ref( + firebaseStorage, + `${docRef.path}/${fieldName}/${file.name}` + ); + const uploadTask = uploadBytesResumable(storageRef, file, { + cacheControl: "public, max-age=31536000", + }); + uploadTask.on( + // event + "state_changed", + // observer + (snapshot) => { + // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded + const progress = + (snapshot.bytesTransferred / snapshot.totalBytes) * 100; + uploaderDispatch({ + type: "file_update", + data: { fileName: file.name, newProps: { progress } }, + }); + }, - // error – must be any to access error.code - (error: any) => { - // A full list of error codes is available at - // https://firebase.google.com/docs/storage/web/handle-errors - switch (error.code) { - case "storage/unknown": - // Unknown error occurred, inspect error.serverResponse - enqueueSnackbar("Unknown error occurred", { variant: "error" }); - uploaderDispatch({ error: error.serverResponse }); - break; + // error – must be any to access error.code + (error: any) => { + // A full list of error codes is available at + // https://firebase.google.com/docs/storage/web/handle-errors + const errorAction = { + fileName: file.name, + newProps: { loading: false } as Partial, + }; + switch (error.code) { + case "storage/unknown": + // Unknown error occurred, inspect error.serverResponse + enqueueSnackbar("Unknown error occurred", { variant: "error" }); + errorAction.newProps.error = error.serverResponse; + break; - case "storage/unauthorized": - // User doesn't have permission to access the object - enqueueSnackbar("You don’t have permissions to upload files", { - variant: "error", - action: ( - - - - ), - }); - uploaderDispatch({ error: error.code }); - break; + case "storage/unauthorized": + // User doesn't have permission to access the object + enqueueSnackbar("You don’t have permissions to upload files", { + variant: "error", + action: ( + + + + ), + }); + errorAction.newProps.error = error.code; + break; - case "storage/canceled": - // User canceled the upload - uploaderDispatch({ error: error.code }); - break; - - default: - uploaderDispatch({ error: error.code }); - // Unknown error occurred, inspect error.serverResponse - break; - } - - uploaderDispatch({ isLoading: false }); - }, - - // complete - () => { - uploaderDispatch({ isLoading: false }); - - // Upload completed successfully, now we can get the download URL - getDownloadURL(uploadTask.snapshot.ref).then( - (downloadURL: string) => { - // STore in the document if docRef provided - // if (docRef && docRef.update)docRef.update({ [fieldName]: newValue }); - // Also call callback if it exists - // IMPORTANT: SideDrawer form may not update its local values after this - // function updates the doc, so you MUST update it manually - // using this callback - const obj = { - ref: uploadTask.snapshot.ref.fullPath, - downloadURL, - name: file.name, - type: file.type, - lastModifiedTS: file.lastModified, - }; - if (onComplete) onComplete(obj); + case "storage/canceled": + default: + errorAction.newProps.error = error.code; + break; } - ); - } - ); + failures.push(file.name); + uploaderDispatch({ type: "file_update", data: errorAction }); + if (isCompleted()) resolve(true); + }, + + // complete + () => { + uploaderDispatch({ + type: "file_update", + data: { + fileName: file.name, + newProps: { loading: false }, + }, + }); + + // Upload completed successfully, now we can get the download URL + getDownloadURL(uploadTask.snapshot.ref).then( + (downloadURL: string) => { + // Store in the document if docRef provided + // if (docRef && docRef.update)docRef.update({ [fieldName]: newValue }); + // Also call callback if it exists + // IMPORTANT: SideDrawer form may not update its local values after this + // function updates the doc, so you MUST update it manually + // using this callback + const obj = { + ref: uploadTask.snapshot.ref.fullPath, + downloadURL, + name: file.name, + type: file.type, + lastModifiedTS: file.lastModified, + }; + uploads.push(obj); + if (isCompleted()) resolve(true); + } + ); + } + ); + }) + ).then(() => { + uploaderDispatch({ type: "reset" }); + return { uploads, failures }; }); };