forked from github/plane
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 <aaryankhandu123@gmail.com>
This commit is contained in:
parent
069b8b3ed9
commit
c7e6118804
@ -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)
|
||||
|
@ -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<Props> = 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<Props> = 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<Props> = 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);
|
||||
|
@ -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";
|
||||
|
199
web/components/core/modals/user-image-upload-modal.tsx
Normal file
199
web/components/core/modals/user-image-upload-modal.tsx
Normal file
@ -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<Props> = observer((props) => {
|
||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleDelete } = props;
|
||||
// states
|
||||
const [image, setImage] = useState<File | null>(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 (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Upload Image
|
||||
</Dialog.Title>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`relative grid h-80 w-80 cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-custom-primary focus:ring-offset-2 ${
|
||||
(image === null && isDragActive) || !value
|
||||
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{image !== null || (value && value !== "") ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-custom-background-90 px-2 py-0.5 text-xs font-medium text-custom-text-200"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<img
|
||||
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
||||
alt="image"
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<UserCircle2 className="mx-auto h-16 w-16 text-custom-text-200" />
|
||||
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
||||
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input {...getInputProps()} type="text" />
|
||||
</div>
|
||||
</div>
|
||||
{fileRejections.length > 0 && (
|
||||
<p className="text-sm text-red-500">
|
||||
{fileRejections[0].errors[0].code === "file-too-large"
|
||||
? "The image size cannot exceed 5 MB."
|
||||
: "Please upload a file in a valid format."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="my-4 text-custom-text-200 text-sm">
|
||||
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
{handleDelete && (
|
||||
<Button variant="danger" size="sm" onClick={handleDelete} disabled={!value}>
|
||||
{isRemoving ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
disabled={!image}
|
||||
loading={isImageUploading}
|
||||
>
|
||||
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
@ -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<Props> = observer((props) => {
|
||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleDelete, userImage } = props;
|
||||
|
||||
export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleRemove } = props;
|
||||
// states
|
||||
const [image, setImage] = useState<File | null>(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<Props> = observer((props) => {
|
||||
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button variant="danger" size="sm" onClick={handleDelete} disabled={!value}>
|
||||
{handleRemove && (
|
||||
<Button variant="danger" size="sm" onClick={handleRemove} disabled={!value}>
|
||||
{isRemoving ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
@ -1,7 +1,10 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { IssueAttachmentService } from "services/issue";
|
||||
// hooks
|
||||
@ -10,8 +13,8 @@ import useToast from "hooks/use-toast";
|
||||
import { IIssueAttachment } from "types";
|
||||
// fetch-keys
|
||||
import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
const maxFileSize = 5 * 1024 * 1024; // 5 MB
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "constants/common";
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
@ -19,14 +22,20 @@ type Props = {
|
||||
|
||||
const issueAttachmentService = new IssueAttachmentService();
|
||||
|
||||
export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) => {
|
||||
export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
||||
const { disabled = false } = props;
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (!acceptedFiles[0] || !workspaceSlug) return;
|
||||
|
||||
@ -70,13 +79,15 @@ export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) =>
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
||||
onDrop,
|
||||
maxSize: maxFileSize,
|
||||
maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
multiple: false,
|
||||
disabled: isLoading || disabled,
|
||||
});
|
||||
|
||||
const fileError =
|
||||
fileRejections.length > 0 ? `Invalid file type or size (max ${maxFileSize / 1024 / 1024} MB)` : null;
|
||||
fileRejections.length > 0
|
||||
? `Invalid file type or size (max ${envConfig?.file_size_limit ?? MAX_FILE_SIZE / 1024 / 1024} MB)`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -99,4 +110,4 @@ export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) =>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -9,15 +9,13 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { Button, Input } from "@plane/ui";
|
||||
import DummySidebar from "components/account/sidebar";
|
||||
import OnboardingStepIndicator from "components/account/step-indicator";
|
||||
import { UserImageUploadModal } from "components/core";
|
||||
// types
|
||||
import { IUser } from "types";
|
||||
// constants
|
||||
import { TIME_ZONES } from "constants/timezones";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// assets
|
||||
import IssuesSvg from "public/onboarding/onboarding-issues.svg";
|
||||
import { ImageUploadModal } from "components/core";
|
||||
|
||||
const defaultValues: Partial<IUser> = {
|
||||
first_name: "",
|
||||
@ -29,13 +27,7 @@ type Props = {
|
||||
user?: IUser;
|
||||
};
|
||||
|
||||
// const timeZoneOptions = TIME_ZONES.map((timeZone) => ({
|
||||
// value: timeZone.value,
|
||||
// query: timeZone.label + " " + timeZone.value,
|
||||
// content: timeZone.label,
|
||||
// }));
|
||||
|
||||
const useCases = [
|
||||
const USE_CASES = [
|
||||
"Build Products",
|
||||
"Manage Feedbacks",
|
||||
"Service delivery",
|
||||
@ -43,15 +35,15 @@ const useCases = [
|
||||
"Code Repository Integration",
|
||||
"Bug Tracking",
|
||||
"Test Case Management",
|
||||
"Rescource allocation",
|
||||
"Resource allocation",
|
||||
];
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
export const UserDetails: React.FC<Props> = observer((props) => {
|
||||
const { user } = props;
|
||||
// states
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
// const [selectedUsecase, setSelectedUsecase] = useState<number | null>();
|
||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||
const {
|
||||
user: userStore,
|
||||
@ -100,19 +92,22 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
||||
<div className="h-full fixed hidden lg:block w-1/5 max-w-[320px]">
|
||||
<DummySidebar showProject workspaceName={workspaceName} />
|
||||
</div>
|
||||
<ImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isRemoving}
|
||||
handleDelete={() => {
|
||||
handleDelete(getValues("avatar"));
|
||||
}}
|
||||
onSuccess={(url) => {
|
||||
setValue("avatar", url);
|
||||
setIsImageUploadModalOpen(false);
|
||||
}}
|
||||
value={watch("avatar") !== "" ? watch("avatar") : undefined}
|
||||
userImage
|
||||
<Controller
|
||||
control={control}
|
||||
name="avatar"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<UserImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isRemoving}
|
||||
handleDelete={() => handleDelete(getValues("avatar"))}
|
||||
onSuccess={(url) => {
|
||||
onChange(url);
|
||||
setIsImageUploadModalOpen(false);
|
||||
}}
|
||||
value={value && value.trim() !== "" ? value : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="lg:w-2/3 w-full flex flex-col justify-between ml-auto ">
|
||||
<div className="flex lg:w-4/5 md:px-0 px-7 pt-3 mx-auto flex-col">
|
||||
@ -189,7 +184,7 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
||||
name="use_case"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="flex flex-wrap break-all overflow-auto">
|
||||
{useCases.map((useCase) => (
|
||||
{USE_CASES.map((useCase) => (
|
||||
<div
|
||||
className={`border mb-3 hover:cursor-pointer hover:bg-onboarding-background-300/30 flex-shrink-0 ${
|
||||
value === useCase ? "border-custom-primary-100" : "border-onboarding-border-100"
|
||||
|
@ -11,7 +11,7 @@ import { FileService } from "services/file.service";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { DeleteWorkspaceModal } from "components/workspace";
|
||||
import { ImageUploadModal } from "components/core";
|
||||
import { WorkspaceImageUploadModal } from "components/core";
|
||||
// ui
|
||||
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
|
||||
// types
|
||||
@ -19,6 +19,7 @@ import { IWorkspace } from "types";
|
||||
// constants
|
||||
import { ORGANIZATION_SIZE } from "constants/workspace";
|
||||
import { trackEvent } from "helpers/event-tracker.helper";
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
|
||||
const defaultValues: Partial<IWorkspace> = {
|
||||
name: "",
|
||||
@ -33,8 +34,6 @@ const fileService = new FileService();
|
||||
export const WorkspaceDetails: FC = observer(() => {
|
||||
// states
|
||||
const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
const [isImageRemoving, setIsImageRemoving] = useState(false);
|
||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||
// store
|
||||
@ -51,7 +50,6 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
control,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IWorkspace>({
|
||||
defaultValues: { ...defaultValues, ...currentWorkspace },
|
||||
@ -78,8 +76,12 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
const handleDelete = (url: string | null | undefined) => {
|
||||
if (!currentWorkspace || !url) return;
|
||||
const handleRemoveLogo = () => {
|
||||
if (!currentWorkspace) return;
|
||||
|
||||
const url = currentWorkspace.logo;
|
||||
|
||||
if (!url) return;
|
||||
|
||||
setIsImageRemoving(true);
|
||||
|
||||
@ -104,6 +106,17 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyUrl = () => {
|
||||
if (!currentWorkspace) return;
|
||||
|
||||
copyUrlToClipboard(`${currentWorkspace.slug}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Workspace URL copied to the clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) reset({ ...currentWorkspace });
|
||||
}, [currentWorkspace, reset]);
|
||||
@ -118,22 +131,27 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<DeleteWorkspaceModal
|
||||
data={currentWorkspace}
|
||||
isOpen={deleteWorkspaceModal}
|
||||
onClose={() => setDeleteWorkspaceModal(false)}
|
||||
data={currentWorkspace}
|
||||
/>
|
||||
<ImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isImageRemoving}
|
||||
handleDelete={() => handleDelete(currentWorkspace?.logo)}
|
||||
onSuccess={(imageUrl) => {
|
||||
setIsImageUploading(true);
|
||||
setValue("logo", imageUrl);
|
||||
setIsImageUploadModalOpen(false);
|
||||
handleSubmit(onSubmit)().then(() => setIsImageUploading(false));
|
||||
}}
|
||||
value={watch("logo")}
|
||||
<Controller
|
||||
control={control}
|
||||
name="logo"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<WorkspaceImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isImageRemoving}
|
||||
handleRemove={handleRemoveLogo}
|
||||
onSuccess={(imageUrl) => {
|
||||
onChange(imageUrl);
|
||||
setIsImageUploadModalOpen(false);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
||||
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-100">
|
||||
@ -156,9 +174,9 @@ export const WorkspaceDetails: FC = observer(() => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
|
||||
<span className="text-sm tracking-tight">{`${
|
||||
<button type="button" onClick={handleCopyUrl} className="text-sm tracking-tight">{`${
|
||||
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
||||
}/${currentWorkspace.slug}`}</span>
|
||||
}/${currentWorkspace.slug}`}</button>
|
||||
<div className="flex item-center gap-2.5">
|
||||
<button
|
||||
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
||||
|
@ -148,10 +148,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md
|
||||
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 shadow-lg outline-none"
|
||||
>
|
||||
<Menu.Items className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 shadow-lg outline-none">
|
||||
<div className="flex flex-col items-start justify-start gap-3 p-3">
|
||||
<span className="text-sm font-medium text-custom-sidebar-text-200">Workspace</span>
|
||||
{workspaces ? (
|
||||
|
1
web/constants/common.ts
Normal file
1
web/constants/common.ts
Normal file
@ -0,0 +1 @@
|
||||
export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
@ -1,10 +1,18 @@
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { MoveLeft, Plus, UserPlus } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { LogIn, LogOut, MoveLeft, Plus, User, UserPlus } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { Avatar, Tooltip } from "@plane/ui";
|
||||
import { Fragment } from "react";
|
||||
import { mutate } from "swr";
|
||||
import { useRouter } from "next/router";
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
const SIDEBAR_LINKS = [
|
||||
{
|
||||
@ -21,12 +29,44 @@ const SIDEBAR_LINKS = [
|
||||
},
|
||||
];
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
export const ProfileLayoutSidebar = observer(() => {
|
||||
const router = useRouter();
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
theme: { sidebarCollapsed, toggleSidebar },
|
||||
workspace: { workspaces },
|
||||
user: { currentUser, currentUserSettings },
|
||||
} = useMobxStore();
|
||||
|
||||
// redirect url for normal mode
|
||||
const redirectWorkspaceSlug =
|
||||
currentUserSettings?.workspace?.last_workspace_slug ||
|
||||
currentUserSettings?.workspace?.fallback_workspace_slug ||
|
||||
"";
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await authService
|
||||
.signOut()
|
||||
.then(() => {
|
||||
mutate("CURRENT_USER_DETAILS", null);
|
||||
setTheme("system");
|
||||
router.push("/");
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Failed to sign out. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
||||
@ -34,6 +74,73 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
} ${sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
||||
>
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="flex items-center gap-x-3 gap-y-2 px-4 pt-4">
|
||||
<div className="w-full h-full truncate">
|
||||
<div
|
||||
className={`flex flex-grow items-center gap-x-2 rounded p-1 truncate ${
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}
|
||||
>
|
||||
<User className="h-5 w-5 text-custom-text-200" />
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && <h4 className="text-custom-text-200 font-medium text-base truncate">My Profile</h4>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<Tooltip position="bottom-left" tooltipContent="Go back to your workspace">
|
||||
<div className="flex-shrink-0">
|
||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||
<a>
|
||||
<LogIn className="h-5 w-5 text-custom-text-200 rotate-180" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<Menu as="div" className="relative flex-shrink-0 ">
|
||||
<Menu.Button className="flex gap-4 place-items-center outline-none">
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={currentUser?.avatar}
|
||||
size={24}
|
||||
shape="square"
|
||||
className="!text-base"
|
||||
/>
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute left-0 z-20 mt-1 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 shadow-custom-shadow-rg text-xs space-y-2 outline-none">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full cursor-pointer space-y-1 p-4 flex-shrink-0">
|
||||
{SIDEBAR_LINKS.map((link) => (
|
||||
<Link key={link.key} href={link.href}>
|
||||
|
@ -10,7 +10,7 @@ import useToast from "hooks/use-toast";
|
||||
// layouts
|
||||
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { ImagePickerPopover, ImageUploadModal } from "components/core";
|
||||
import { ImagePickerPopover, UserImageUploadModal } from "components/core";
|
||||
import { ProfileSettingsHeader } from "components/headers";
|
||||
import { DeactivateAccountModal } from "components/account";
|
||||
// ui
|
||||
@ -48,7 +48,6 @@ const ProfileSettingsPage: NextPageWithLayout = () => {
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IUser>({ defaultValues });
|
||||
@ -151,18 +150,23 @@ const ProfileSettingsPage: NextPageWithLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isRemoving}
|
||||
handleDelete={() => handleDelete(myProfile?.avatar, true)}
|
||||
onSuccess={(url) => {
|
||||
setValue("avatar", url);
|
||||
handleSubmit(onSubmit)();
|
||||
setIsImageUploadModalOpen(false);
|
||||
}}
|
||||
value={watch("avatar") !== "" ? watch("avatar") : undefined}
|
||||
userImage
|
||||
<Controller
|
||||
control={control}
|
||||
name="avatar"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<UserImageUploadModal
|
||||
isOpen={isImageUploadModalOpen}
|
||||
onClose={() => setIsImageUploadModalOpen(false)}
|
||||
isRemoving={isRemoving}
|
||||
handleDelete={() => handleDelete(myProfile?.avatar, true)}
|
||||
onSuccess={(url) => {
|
||||
onChange(url);
|
||||
handleSubmit(onSubmit)();
|
||||
setIsImageUploadModalOpen(false);
|
||||
}}
|
||||
value={value && value.trim() !== "" ? value : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
||||
<div className="h-full w-full flex flex-col py-9 pr-9 space-y-10 overflow-y-auto">
|
||||
|
1
web/types/app.d.ts
vendored
1
web/types/app.d.ts
vendored
@ -4,6 +4,7 @@ export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
|
||||
export interface IAppConfig {
|
||||
email_password_login: boolean;
|
||||
file_size_limit: number;
|
||||
google_client_id: string | null;
|
||||
github_app_name: string | null;
|
||||
github_client_id: string | null;
|
||||
|
Loading…
Reference in New Issue
Block a user