Merge pull request #861 from rowyio/multi-file-upload

Multi file upload
This commit is contained in:
Sidney Alcantara
2022-11-17 14:39:21 +11:00
committed by GitHub
7 changed files with 437 additions and 379 deletions

View File

@@ -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 theyre 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] : []
);
}

View File

@@ -1,11 +1,6 @@
import { useCallback, 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,
ButtonBase,
@@ -15,69 +10,33 @@ import {
Chip,
} from "@mui/material";
import { Upload as UploadIcon } from "@src/assets/icons";
import { FileIcon } from ".";
import { ISideDrawerFieldProps } from "@src/components/fields/types";
import CircularProgressOptical from "@src/components/CircularProgressOptical";
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 useFileUpload from "./useFileUpload";
import { FileIcon } from ".";
export default function File_({
column,
_rowy_ref,
value,
onChange,
onSubmit,
disabled,
}: ISideDrawerFieldProps) {
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const { loading, progress, handleDelete, localFiles, dropzoneState } =
useFileUpload(_rowy_ref, column.key, { multiple: true });
const { uploaderState, upload, deleteUpload } = useUploader();
// Store a preview image locally while uploading
const [localFile, setLocalFile] = useState<string>("");
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (_rowy_ref && file) {
upload({
docRef: _rowy_ref! as any,
fieldName: column.key,
files: [file],
previousValue: value ?? [],
onComplete: (newValue) => {
onChange(newValue);
onSubmit();
setLocalFile("");
},
});
setLocalFile(file.name);
}
},
[_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 { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
});
const { isDragActive, getRootProps, getInputProps } = dropzoneState;
return (
<>
{!disabled && (
<ButtonBase
disabled={loading}
sx={[
fieldSx,
{
@@ -101,13 +60,21 @@ export default function File_({
<Typography color="inherit" style={{ flexGrow: 1 }}>
Click to upload or drop file here
</Typography>
<UploadIcon sx={{ ml: 1, mr: 2 / 8 }} />
{loading ? (
<CircularProgressOptical
size={20}
variant={progress === 0 ? "indeterminate" : "determinate"}
value={progress}
/>
) : (
<UploadIcon sx={{ ml: 1, mr: 2 / 8 }} />
)}
</ButtonBase>
)}
<Grid container spacing={0.5} style={{ marginTop: 2 }}>
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
value.map((file: FileValue) => (
<Grid item key={file.name}>
<Tooltip
title={`File last modified ${format(
@@ -128,7 +95,7 @@ export default function File_({
body: "This file cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: () => handleDelete(i),
handleConfirm: () => handleDelete(file),
})
: undefined
}
@@ -138,15 +105,18 @@ export default function File_({
</Grid>
))}
{localFile && (
<Grid item>
<Chip
icon={<FileIcon />}
label={localFile}
deleteIcon={<CircularProgressOptical size={20} color="inherit" />}
/>
</Grid>
)}
{localFiles &&
localFiles.map((file) => (
<Grid item>
<Chip
icon={<FileIcon />}
label={file.name}
deleteIcon={
<CircularProgressOptical size={20} color="inherit" />
}
/>
</Grid>
))}
</Grid>
</>
);

View File

@@ -1,9 +1,6 @@
import { useCallback } 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";
import { alpha, Stack, Grid, Tooltip, Chip, IconButton } from "@mui/material";
@@ -12,64 +9,24 @@ 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 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 { uploaderState, upload, deleteUpload } = useUploader();
const { progress, isLoading } = uploaderState;
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (file) {
upload({
docRef: docRef! as any,
fieldName: column.key,
files: [file],
previousValue: value,
onComplete: (newValue) => {
updateField({
path: docRef.path,
fieldName: column.key,
value: newValue,
});
},
});
}
},
[value]
);
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,
multiple: false,
});
const { loading, progress, handleDelete, localFiles, dropzoneState } =
useFileUpload(docRef, column.key, { multiple: true });
const { isDragActive, getRootProps, getInputProps } = dropzoneState;
const dropzoneProps = getRootProps();
return (
<Stack
direction="row"
@@ -112,8 +69,13 @@ export default function File_({
)}`}
>
<Chip
icon={<FileIcon />}
label={file.name}
icon={<FileIcon />}
sx={{
"& .MuiChip-label": {
lineHeight: 5 / 3,
},
}}
onClick={(e) => {
window.open(file.downloadURL);
e.stopPropagation();
@@ -123,21 +85,32 @@ 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%" }}
/>
</Tooltip>
</Grid>
))}
{localFiles &&
localFiles.map((file) => (
<Grid item>
<Chip
icon={<FileIcon />}
label={file.name}
deleteIcon={
<CircularProgressOptical size={20} color="inherit" />
}
/>
</Grid>
))}
</ChipList>
{!isLoading ? (
{!loading ? (
!disabled && (
<IconButton
size="small"

View File

@@ -0,0 +1,87 @@
import { useCallback, useState } from "react";
import { useSetAtom } from "jotai";
import { some } from "lodash-es";
import { tableScope, updateFieldAtom } from "@src/atoms/tableScope";
import useUploader from "@src/hooks/useFirebaseStorageUploader";
import { FileValue } from "@src/types/table";
import { DropzoneOptions, useDropzone } from "react-dropzone";
export default function useFileUpload(
docRef: any,
fieldName: string,
dropzoneOptions: DropzoneOptions = {}
) {
const updateField = useSetAtom(updateFieldAtom, tableScope);
const { uploaderState, upload, deleteUpload } = useUploader();
const [localFiles, setLocalFiles] = useState<File[]>([]);
const dropzoneState = useDropzone({
onDrop: async (acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
setLocalFiles(acceptedFiles);
await handleUpload(acceptedFiles);
setLocalFiles([]);
}
},
...dropzoneOptions,
});
const uploadingFiles = Object.keys(uploaderState);
const progress =
uploadingFiles.length > 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 {
localFiles,
progress,
loading,
uploaderState,
handleUpload,
handleDelete,
dropzoneState,
};
}

View File

@@ -1,10 +1,7 @@
import { ISideDrawerFieldProps } from "@src/components/fields/types";
import { useCallback, useState } from "react";
import { useMemo } from "react";
import { useSetAtom } from "jotai";
import { useDropzone } from "react-dropzone";
// TODO: GENERALIZE
import useUploader from "@src/hooks/useFirebaseStorageUploader";
import { assignIn } from "lodash-es";
import {
alpha,
@@ -20,12 +17,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 useFileUpload from "@src/components/fields/File/useFileUpload";
import { IMAGE_MIME_TYPES } from ".";
const imgSx = {
position: "relative",
@@ -84,57 +83,37 @@ 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 [localImage, setLocalImage] = useState<string>("");
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const imageFile = acceptedFiles[0];
if (_rowy_ref && imageFile) {
upload({
docRef: _rowy_ref! as any,
fieldName: column.key,
files: [imageFile],
previousValue: value ?? [],
onComplete: (newValue) => {
onChange(newValue);
onSubmit();
setLocalImage("");
},
});
setLocalImage(URL.createObjectURL(imageFile));
}
},
[_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 { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
const {
loading,
progress,
handleDelete,
uploaderState,
localFiles,
dropzoneState,
} = useFileUpload(_rowy_ref, column.key, {
multiple: true,
accept: IMAGE_MIME_TYPES,
});
const localImages = useMemo(
() =>
localFiles.map((file) =>
assignIn(file, { localURL: URL.createObjectURL(file) })
),
[localFiles]
);
const { getRootProps, getInputProps, isDragActive } = dropzoneState;
return (
<>
{!disabled && (
<ButtonBase
disabled={loading}
sx={[
fieldSx,
{
@@ -160,14 +139,22 @@ export default function Image_({
? "Drop image here"
: "Click to upload or drop image here"}
</Typography>
<AddIcon sx={{ ml: 1, mr: 2 / 8 }} />
{loading ? (
<CircularProgressOptical
size={20}
variant={progress === 0 ? "indeterminate" : "determinate"}
value={progress}
/>
) : (
<AddIcon sx={{ ml: 1, mr: 2 / 8 }} />
)}
</ButtonBase>
)}
<Grid container spacing={1} style={{ marginTop: 0 }}>
{Array.isArray(value) &&
value.map((image, i) => (
<Grid item key={image.downloadURL}>
value.map((image: FileValue) => (
<Grid item key={image.name}>
{disabled ? (
<Tooltip title="Open">
<ButtonBase
@@ -214,7 +201,7 @@ export default function Image_({
body: "This image cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: () => handleDelete(i),
handleConfirm: () => handleDelete(image),
})
}
>
@@ -237,29 +224,38 @@ export default function Image_({
</Grid>
))}
{localImage && (
<Grid item>
<ButtonBase
sx={imgSx}
style={{ backgroundImage: `url("${localImage}")` }}
className="img"
>
<Grid
container
justifyContent="center"
alignItems="center"
sx={overlaySx}
{localImages &&
localImages.map((image) => (
<Grid item key={image.name}>
<ButtonBase
sx={imgSx}
style={{
backgroundImage: `url("${image.localURL}")`,
}}
className="img"
>
<CircularProgressOptical
color="inherit"
size={48}
variant={progress === 0 ? "indeterminate" : "determinate"}
value={progress}
/>
</Grid>
</ButtonBase>
</Grid>
)}
{uploaderState[image.name] && (
<Grid
container
justifyContent="center"
alignItems="center"
sx={overlaySx}
>
<CircularProgressOptical
color="inherit"
size={48}
variant={
uploaderState[image.name].progress === 0
? "indeterminate"
: "determinate"
}
value={uploaderState[image.name].progress}
/>
</Grid>
)}
</ButtonBase>
</Grid>
))}
</Grid>
</>
);

View File

@@ -1,9 +1,7 @@
import { useCallback, useState } from "react";
import { useMemo } from "react";
import { IHeavyCellProps } from "@src/components/fields/types";
import { useAtom, useSetAtom } from "jotai";
import { findIndex } from "lodash-es";
import { useDropzone } from "react-dropzone";
import { assignIn } from "lodash-es";
import {
alpha,
@@ -23,15 +21,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 useFileUpload from "@src/components/fields/File/useFileUpload";
import { IMAGE_MIME_TYPES } from "./index";
// MULTIPLE
const imgSx = (rowHeight: number) => ({
@@ -88,57 +82,27 @@ 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 [localImage, setLocalImage] = useState<string>("");
const { loading, progress, handleDelete, localFiles, dropzoneState } =
useFileUpload(docRef, column.key, {
multiple: true,
accept: IMAGE_MIME_TYPES,
});
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const imageFile = acceptedFiles[0];
if (imageFile) {
upload({
docRef: docRef! as any,
fieldName: column.key,
files: [imageFile],
previousValue: value,
onComplete: (newValue) => {
updateField({
path: docRef.path,
fieldName: column.key,
value: newValue,
});
setLocalImage("");
},
});
setLocalImage(URL.createObjectURL(imageFile));
}
},
[value]
const localImages = useMemo(
() =>
localFiles.map((file) =>
assignIn(file, { localURL: URL.createObjectURL(file) })
),
[localFiles]
);
const handleDelete = (index: number) => () => {
const newValue = [...value];
const toBeDeleted = newValue.splice(index, 1);
toBeDeleted.length && deleteUpload(toBeDeleted[0]);
onSubmit(newValue);
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
accept: IMAGE_MIME_TYPES,
});
const { getRootProps, getInputProps, isDragActive } = dropzoneState;
const dropzoneProps = getRootProps();
const rowHeight = tableSchema.rowHeight ?? DEFAULT_ROW_HEIGHT;
@@ -183,17 +147,17 @@ export default function Image_({
>
<Grid container spacing={0.5} wrap="nowrap">
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
<Grid item key={file.downloadURL}>
value.map((image: FileValue) => (
<Grid item key={image.downloadURL}>
{disabled ? (
<Tooltip title="Open">
<ButtonBase
sx={imgSx(rowHeight)}
className="img"
onClick={() => window.open(file.downloadURL, "_blank")}
onClick={() => window.open(image.downloadURL, "_blank")}
>
<Thumbnail
imageUrl={file.downloadURL}
imageUrl={image.downloadURL}
size={thumbnailSize}
objectFit="contain"
sx={thumbnailSx}
@@ -224,12 +188,12 @@ export default function Image_({
body: "This image cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleDelete(i),
handleConfirm: () => handleDelete(image),
});
}}
>
<Thumbnail
imageUrl={file.downloadURL}
imageUrl={image.downloadURL}
size={thumbnailSize}
objectFit="contain"
sx={thumbnailSx}
@@ -249,24 +213,27 @@ export default function Image_({
</Grid>
))}
{localImage && (
<Grid item>
<Box
sx={[
imgSx(rowHeight),
{
boxShadow: (theme) =>
`0 0 0 1px ${theme.palette.divider} inset`,
},
]}
style={{ backgroundImage: `url("${localImage}")` }}
/>
</Grid>
)}
{localImages &&
localImages.map((image) => (
<Grid item>
<Box
sx={[
imgSx(rowHeight),
{
boxShadow: (theme) =>
`0 0 0 1px ${theme.palette.divider} inset`,
},
]}
style={{
backgroundImage: `url("${image.localURL}")`,
}}
/>
</Grid>
))}
</Grid>
</div>
{!isLoading ? (
{!loading ? (
!disabled && (
<IconButton
size="small"

View File

@@ -16,25 +16,50 @@ import { projectScope } from "@src/atoms/projectScope";
import { firebaseStorageAtom } from "@src/sources/ProjectSourceFirebase";
import { WIKI_LINKS } from "@src/constants/externalLinks";
import { FileValue } from "@src/types/table";
import { generateId } from "@src/utils/table";
export type UploaderState = {
export type UploadState = {
progress: number;
isLoading: boolean;
loading: boolean;
error?: string;
};
const initialState: UploaderState = { progress: 0, isLoading: false };
export type UploaderState = {
[fileName: string]: UploadState;
};
const uploadReducer = (
prevState: UploaderState,
newProps: Partial<UploaderState>
) => ({ ...prevState, ...newProps });
action: {
type: "reset" | "file_update";
data?: { fileName: string; newProps: Partial<UploadState> };
}
) => {
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[];
previousValue?: FileValue[];
onComplete?: (values: FileValue[]) => void;
onComplete?: ({
uploads,
failures,
}: {
uploads: FileValue[];
failures: string[];
}) => void;
};
// TODO: GENERALIZE INTO ATOM
@@ -42,125 +67,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,
previousValue,
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);
return new Promise((resolve) =>
files.forEach((file) => {
uploaderDispatch({
type: "file_update",
data: {
fileName: file.name,
newProps: { loading: true, progress: 0 },
},
});
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");
const storageRef = ref(
firebaseStorage,
`${docRef.path}/${fieldName}/${generateId()}-${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 } },
});
},
switch (snapshot.state) {
case "paused":
console.log("Upload is paused");
break;
case "running":
console.log("Upload is running");
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<UploadState>,
};
switch (error.code) {
case "storage/unknown":
// Unknown error occurred, inspect error.serverResponse
enqueueSnackbar("Unknown error occurred", { variant: "error" });
errorAction.newProps.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
switch (error.code) {
case "storage/unknown":
// Unknown error occurred, inspect error.serverResponse
enqueueSnackbar("Unknown error occurred", { variant: "error" });
uploaderDispatch({ error: error.serverResponse });
break;
case "storage/unauthorized":
// User doesn't have permission to access the object
enqueueSnackbar("You dont have permissions to upload files", {
variant: "error",
action: (
<Paper elevation={0} sx={{ borderRadius: 1 }}>
<Button
color="primary"
href={
WIKI_LINKS.setupRoles +
"#write-firebase-storage-security-rules"
}
target="_blank"
rel="noopener noreferrer"
>
Fix
<InlineOpenInNewIcon />
</Button>
</Paper>
),
});
errorAction.newProps.error = error.code;
break;
case "storage/unauthorized":
// User doesn't have permission to access the object
enqueueSnackbar("You dont have permissions to upload files", {
variant: "error",
action: (
<Paper elevation={0} sx={{ borderRadius: 1 }}>
<Button
color="primary"
href={
WIKI_LINKS.setupRoles +
"#write-firebase-storage-security-rules"
}
target="_blank"
rel="noopener noreferrer"
>
Fix
<InlineOpenInNewIcon />
</Button>
</Paper>
),
});
uploaderDispatch({ 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) => {
const newValue: FileValue[] = Array.isArray(previousValue)
? [...previousValue]
: [];
newValue.push({
ref: uploadTask.snapshot.ref.fullPath,
downloadURL,
name: file.name,
type: file.type,
lastModifiedTS: file.lastModified,
});
// 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
if (onComplete) onComplete(newValue);
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 };
});
};