From 1bd38ad4c7d84020829ff24b3f66e1eb0112f081 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:23:46 +0530 Subject: [PATCH] refactor: image upload modals, file size limit added to config (#2868) * chore: add file size limit as config in the config api * refactor: image upload modals --------- Co-authored-by: Aaryan Khandelwal --- apiserver/plane/app/views/config.py | 2 + web/components/core/image-picker-popover.tsx | 12 +- web/components/core/modals/index.ts | 3 +- .../core/modals/user-image-upload-modal.tsx | 199 ++++++++++++++++++ ...l.tsx => workspace-image-upload-modal.tsx} | 102 ++++----- .../issues/attachment/attachment-upload.tsx | 25 ++- web/components/onboarding/user-details.tsx | 47 ++--- .../workspace/settings/workspace-details.tsx | 60 ++++-- web/components/workspace/sidebar-dropdown.tsx | 5 +- web/constants/common.ts | 1 + .../settings-layout/profile/sidebar.tsx | 111 +++++++++- web/pages/profile/index.tsx | 32 +-- web/types/app.d.ts | 1 + 13 files changed, 470 insertions(+), 130 deletions(-) create mode 100644 web/components/core/modals/user-image-upload-modal.tsx rename web/components/core/modals/{image-upload-modal.tsx => workspace-image-upload-modal.tsx} (81%) create mode 100644 web/constants/common.ts diff --git a/apiserver/plane/app/views/config.py b/apiserver/plane/app/views/config.py index a585fc82e..f42c853e2 100644 --- a/apiserver/plane/app/views/config.py +++ b/apiserver/plane/app/views/config.py @@ -102,4 +102,6 @@ class ConfigurationEndpoint(BaseAPIView): ) ) + data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880)) + return Response(data, status=status.HTTP_200_OK) diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index 123eee6d2..eea18ce43 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -14,6 +14,8 @@ import { FileService } from "services/file.service"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { Button, Input, Loader } from "@plane/ui"; +// constants +import { MAX_FILE_SIZE } from "constants/common"; const tabOptions = [ { @@ -58,8 +60,10 @@ export const ImagePickerPopover: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { workspace: workspaceStore } = useMobxStore(); - const { currentWorkspace: workspaceDetails } = workspaceStore; + const { + workspace: { currentWorkspace }, + appConfig: { envConfig }, + } = useMobxStore(); const { data: unsplashImages, error: unsplashError } = useSWR( `UNSPLASH_IMAGES_${searchParams}`, @@ -86,7 +90,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { accept: { "image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"], }, - maxSize: 5 * 1024 * 1024, + maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE, }); const handleSubmit = async () => { @@ -112,7 +116,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { if (isUnsplashImage) return; - if (oldValue && workspaceDetails) fileService.deleteFile(workspaceDetails.id, oldValue); + if (oldValue && currentWorkspace) fileService.deleteFile(currentWorkspace.id, oldValue); }) .catch((err) => { console.log(err); diff --git a/web/components/core/modals/index.ts b/web/components/core/modals/index.ts index 5f55020e4..aa2c163a6 100644 --- a/web/components/core/modals/index.ts +++ b/web/components/core/modals/index.ts @@ -1,5 +1,6 @@ export * from "./bulk-delete-issues-modal"; export * from "./existing-issues-list-modal"; export * from "./gpt-assistant-modal"; -export * from "./image-upload-modal"; export * from "./link-modal"; +export * from "./user-image-upload-modal"; +export * from "./workspace-image-upload-modal"; diff --git a/web/components/core/modals/user-image-upload-modal.tsx b/web/components/core/modals/user-image-upload-modal.tsx new file mode 100644 index 000000000..6358a4aee --- /dev/null +++ b/web/components/core/modals/user-image-upload-modal.tsx @@ -0,0 +1,199 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { useDropzone } from "react-dropzone"; +import { Transition, Dialog } from "@headlessui/react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { FileService } from "services/file.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Button } from "@plane/ui"; +// icons +import { UserCircle2 } from "lucide-react"; +// constants +import { MAX_FILE_SIZE } from "constants/common"; + +type Props = { + handleDelete?: () => void; + isOpen: boolean; + isRemoving: boolean; + onClose: () => void; + onSuccess: (url: string) => void; + value: string | null; +}; + +// services +const fileService = new FileService(); + +export const UserImageUploadModal: React.FC = observer((props) => { + const { value, onSuccess, isOpen, onClose, isRemoving, handleDelete } = props; + // states + const [image, setImage] = useState(null); + const [isImageUploading, setIsImageUploading] = useState(false); + + const { setToastAlert } = useToast(); + + const { + appConfig: { envConfig }, + } = useMobxStore(); + + const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]); + + const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ + onDrop, + accept: { + "image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"], + }, + maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE, + multiple: false, + }); + + const handleClose = () => { + setImage(null); + setIsImageUploading(false); + onClose(); + }; + + const handleSubmit = async () => { + console.log("Submit triggered"); + + if (!image) return; + + console.log("Inside submit"); + + setIsImageUploading(true); + + const formData = new FormData(); + formData.append("asset", image); + formData.append("attributes", JSON.stringify({})); + + fileService + .uploadUserFile(formData) + .then((res) => { + const imageUrl = res.asset; + + onSuccess(imageUrl); + setImage(null); + + if (value) fileService.deleteUserFile(value); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", + }) + ) + .finally(() => setIsImageUploading(false)); + }; + + return ( + + + +
+ + +
+
+ + +
+ + Upload Image + +
+
+
+ {image !== null || (value && value !== "") ? ( + <> + + image + + ) : ( +
+ + + {isDragActive ? "Drop image here to upload" : "Drag & drop image here"} + +
+ )} + + +
+
+ {fileRejections.length > 0 && ( +

+ {fileRejections[0].errors[0].code === "file-too-large" + ? "The image size cannot exceed 5 MB." + : "Please upload a file in a valid format."} +

+ )} +
+
+

+ File formats supported- .jpeg, .jpg, .png, .webp, .svg +

+
+ {handleDelete && ( + + )} +
+ + +
+
+
+
+
+
+
+
+ ); +}); diff --git a/web/components/core/modals/image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx similarity index 81% rename from web/components/core/modals/image-upload-modal.tsx rename to web/components/core/modals/workspace-image-upload-modal.tsx index a879b4705..a99c1445b 100644 --- a/web/components/core/modals/image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; @@ -7,89 +7,89 @@ import { Transition, Dialog } from "@headlessui/react"; import { useMobxStore } from "lib/mobx/store-provider"; // services import { FileService } from "services/file.service"; +// hooks +import useToast from "hooks/use-toast"; // ui import { Button } from "@plane/ui"; // icons import { UserCircle2 } from "lucide-react"; +// constants +import { MAX_FILE_SIZE } from "constants/common"; type Props = { - value?: string | null; - onClose: () => void; + handleRemove?: () => void; isOpen: boolean; - onSuccess: (url: string) => void; isRemoving: boolean; - handleDelete: () => void; - userImage?: boolean; + onClose: () => void; + onSuccess: (url: string) => void; + value: string | null; }; // services const fileService = new FileService(); -export const ImageUploadModal: React.FC = observer((props) => { - const { value, onSuccess, isOpen, onClose, isRemoving, handleDelete, userImage } = props; - +export const WorkspaceImageUploadModal: React.FC = observer((props) => { + const { value, onSuccess, isOpen, onClose, isRemoving, handleRemove } = props; + // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - const { workspace: workspaceStore } = useMobxStore(); - const { currentWorkspace: workspaceDetails } = workspaceStore; + const { setToastAlert } = useToast(); - const onDrop = useCallback((acceptedFiles: File[]) => { - setImage(acceptedFiles[0]); - }, []); + const { + workspace: { currentWorkspace }, + appConfig: { envConfig }, + } = useMobxStore(); + + const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]); const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ onDrop, accept: { "image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"], }, - maxSize: 5 * 1024 * 1024, + maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE, + multiple: false, }); + const handleClose = () => { + setImage(null); + setIsImageUploading(false); + onClose(); + }; + const handleSubmit = async () => { - if (!image || (!workspaceSlug && router.pathname != "/onboarding")) return; + if (!image || (!workspaceSlug && router.pathname !== "/onboarding")) return; + setIsImageUploading(true); + const formData = new FormData(); formData.append("asset", image); formData.append("attributes", JSON.stringify({})); - if (userImage) { - fileService - .uploadUserFile(formData) - .then((res) => { - const imageUrl = res.asset; + if (!workspaceSlug) return; - onSuccess(imageUrl); - setIsImageUploading(false); - setImage(null); + fileService + .uploadFile(workspaceSlug.toString(), formData) + .then((res) => { + const imageUrl = res.asset; - if (value) fileService.deleteUserFile(value); + onSuccess(imageUrl); + setImage(null); + + if (value && currentWorkspace) fileService.deleteFile(currentWorkspace.id, value); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error ?? "Something went wrong. Please try again.", }) - .catch((err) => { - console.error(err); - }); - } else - fileService - .uploadFile(workspaceSlug as string, formData) - .then((res) => { - const imageUrl = res.asset; - onSuccess(imageUrl); - setIsImageUploading(false); - setImage(null); - - if (value && workspaceDetails) fileService.deleteFile(workspaceDetails.id, value); - }) - .catch((err) => { - console.error(err); - }); - }; - - const handleClose = () => { - setImage(null); - onClose(); + ) + .finally(() => setIsImageUploading(false)); }; return ( @@ -172,11 +172,11 @@ export const ImageUploadModal: React.FC = observer((props) => { File formats supported- .jpeg, .jpg, .png, .webp, .svg

-
- -
+ )}