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)
|
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";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { Button, Input, Loader } from "@plane/ui";
|
import { Button, Input, Loader } from "@plane/ui";
|
||||||
|
// constants
|
||||||
|
import { MAX_FILE_SIZE } from "constants/common";
|
||||||
|
|
||||||
const tabOptions = [
|
const tabOptions = [
|
||||||
{
|
{
|
||||||
@ -58,8 +60,10 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { workspace: workspaceStore } = useMobxStore();
|
const {
|
||||||
const { currentWorkspace: workspaceDetails } = workspaceStore;
|
workspace: { currentWorkspace },
|
||||||
|
appConfig: { envConfig },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const { data: unsplashImages, error: unsplashError } = useSWR(
|
const { data: unsplashImages, error: unsplashError } = useSWR(
|
||||||
`UNSPLASH_IMAGES_${searchParams}`,
|
`UNSPLASH_IMAGES_${searchParams}`,
|
||||||
@ -86,7 +90,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
|||||||
accept: {
|
accept: {
|
||||||
"image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"],
|
"image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"],
|
||||||
},
|
},
|
||||||
maxSize: 5 * 1024 * 1024,
|
maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
@ -112,7 +116,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
if (isUnsplashImage) return;
|
if (isUnsplashImage) return;
|
||||||
|
|
||||||
if (oldValue && workspaceDetails) fileService.deleteFile(workspaceDetails.id, oldValue);
|
if (oldValue && currentWorkspace) fileService.deleteFile(currentWorkspace.id, oldValue);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./bulk-delete-issues-modal";
|
export * from "./bulk-delete-issues-modal";
|
||||||
export * from "./existing-issues-list-modal";
|
export * from "./existing-issues-list-modal";
|
||||||
export * from "./gpt-assistant-modal";
|
export * from "./gpt-assistant-modal";
|
||||||
export * from "./image-upload-modal";
|
|
||||||
export * from "./link-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 { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
@ -7,89 +7,89 @@ import { Transition, Dialog } from "@headlessui/react";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { UserCircle2 } from "lucide-react";
|
import { UserCircle2 } from "lucide-react";
|
||||||
|
// constants
|
||||||
|
import { MAX_FILE_SIZE } from "constants/common";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value?: string | null;
|
handleRemove?: () => void;
|
||||||
onClose: () => void;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onSuccess: (url: string) => void;
|
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
handleDelete: () => void;
|
onClose: () => void;
|
||||||
userImage?: boolean;
|
onSuccess: (url: string) => void;
|
||||||
|
value: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
export const ImageUploadModal: React.FC<Props> = observer((props) => {
|
export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
||||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleDelete, userImage } = props;
|
const { value, onSuccess, isOpen, onClose, isRemoving, handleRemove } = props;
|
||||||
|
// states
|
||||||
const [image, setImage] = useState<File | null>(null);
|
const [image, setImage] = useState<File | null>(null);
|
||||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { workspace: workspaceStore } = useMobxStore();
|
const { setToastAlert } = useToast();
|
||||||
const { currentWorkspace: workspaceDetails } = workspaceStore;
|
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const {
|
||||||
setImage(acceptedFiles[0]);
|
workspace: { currentWorkspace },
|
||||||
}, []);
|
appConfig: { envConfig },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
accept: {
|
accept: {
|
||||||
"image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"],
|
"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 () => {
|
const handleSubmit = async () => {
|
||||||
if (!image || (!workspaceSlug && router.pathname != "/onboarding")) return;
|
if (!image || (!workspaceSlug && router.pathname !== "/onboarding")) return;
|
||||||
|
|
||||||
setIsImageUploading(true);
|
setIsImageUploading(true);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("asset", image);
|
formData.append("asset", image);
|
||||||
formData.append("attributes", JSON.stringify({}));
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
|
||||||
if (userImage) {
|
if (!workspaceSlug) return;
|
||||||
fileService
|
|
||||||
.uploadUserFile(formData)
|
|
||||||
.then((res) => {
|
|
||||||
const imageUrl = res.asset;
|
|
||||||
|
|
||||||
onSuccess(imageUrl);
|
fileService
|
||||||
setIsImageUploading(false);
|
.uploadFile(workspaceSlug.toString(), formData)
|
||||||
setImage(null);
|
.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);
|
.finally(() => setIsImageUploading(false));
|
||||||
});
|
|
||||||
} 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();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -172,11 +172,11 @@ export const ImageUploadModal: React.FC<Props> = observer((props) => {
|
|||||||
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
{handleRemove && (
|
||||||
<Button variant="danger" size="sm" onClick={handleDelete} disabled={!value}>
|
<Button variant="danger" size="sm" onClick={handleRemove} disabled={!value}>
|
||||||
{isRemoving ? "Removing..." : "Remove"}
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
@ -1,7 +1,10 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { IssueAttachmentService } from "services/issue";
|
import { IssueAttachmentService } from "services/issue";
|
||||||
// hooks
|
// hooks
|
||||||
@ -10,8 +13,8 @@ import useToast from "hooks/use-toast";
|
|||||||
import { IIssueAttachment } from "types";
|
import { IIssueAttachment } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
const maxFileSize = 5 * 1024 * 1024; // 5 MB
|
import { MAX_FILE_SIZE } from "constants/common";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -19,14 +22,20 @@ type Props = {
|
|||||||
|
|
||||||
const issueAttachmentService = new IssueAttachmentService();
|
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);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const {
|
||||||
|
appConfig: { envConfig },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
if (!acceptedFiles[0] || !workspaceSlug) return;
|
if (!acceptedFiles[0] || !workspaceSlug) return;
|
||||||
|
|
||||||
@ -70,13 +79,15 @@ export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) =>
|
|||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
maxSize: maxFileSize,
|
maxSize: envConfig?.file_size_limit ?? MAX_FILE_SIZE,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
disabled: isLoading || disabled,
|
disabled: isLoading || disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileError =
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -99,4 +110,4 @@ export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) =>
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -9,15 +9,13 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
import DummySidebar from "components/account/sidebar";
|
import DummySidebar from "components/account/sidebar";
|
||||||
import OnboardingStepIndicator from "components/account/step-indicator";
|
import OnboardingStepIndicator from "components/account/step-indicator";
|
||||||
|
import { UserImageUploadModal } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { IUser } from "types";
|
import { IUser } from "types";
|
||||||
// constants
|
|
||||||
import { TIME_ZONES } from "constants/timezones";
|
|
||||||
// services
|
// services
|
||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
// assets
|
// assets
|
||||||
import IssuesSvg from "public/onboarding/onboarding-issues.svg";
|
import IssuesSvg from "public/onboarding/onboarding-issues.svg";
|
||||||
import { ImageUploadModal } from "components/core";
|
|
||||||
|
|
||||||
const defaultValues: Partial<IUser> = {
|
const defaultValues: Partial<IUser> = {
|
||||||
first_name: "",
|
first_name: "",
|
||||||
@ -29,13 +27,7 @@ type Props = {
|
|||||||
user?: IUser;
|
user?: IUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
// const timeZoneOptions = TIME_ZONES.map((timeZone) => ({
|
const USE_CASES = [
|
||||||
// value: timeZone.value,
|
|
||||||
// query: timeZone.label + " " + timeZone.value,
|
|
||||||
// content: timeZone.label,
|
|
||||||
// }));
|
|
||||||
|
|
||||||
const useCases = [
|
|
||||||
"Build Products",
|
"Build Products",
|
||||||
"Manage Feedbacks",
|
"Manage Feedbacks",
|
||||||
"Service delivery",
|
"Service delivery",
|
||||||
@ -43,15 +35,15 @@ const useCases = [
|
|||||||
"Code Repository Integration",
|
"Code Repository Integration",
|
||||||
"Bug Tracking",
|
"Bug Tracking",
|
||||||
"Test Case Management",
|
"Test Case Management",
|
||||||
"Rescource allocation",
|
"Resource allocation",
|
||||||
];
|
];
|
||||||
|
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
export const UserDetails: React.FC<Props> = observer((props) => {
|
export const UserDetails: React.FC<Props> = observer((props) => {
|
||||||
const { user } = props;
|
const { user } = props;
|
||||||
|
// states
|
||||||
const [isRemoving, setIsRemoving] = useState(false);
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
// const [selectedUsecase, setSelectedUsecase] = useState<number | null>();
|
|
||||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||||
const {
|
const {
|
||||||
user: userStore,
|
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]">
|
<div className="h-full fixed hidden lg:block w-1/5 max-w-[320px]">
|
||||||
<DummySidebar showProject workspaceName={workspaceName} />
|
<DummySidebar showProject workspaceName={workspaceName} />
|
||||||
</div>
|
</div>
|
||||||
<ImageUploadModal
|
<Controller
|
||||||
isOpen={isImageUploadModalOpen}
|
control={control}
|
||||||
onClose={() => setIsImageUploadModalOpen(false)}
|
name="avatar"
|
||||||
isRemoving={isRemoving}
|
render={({ field: { onChange, value } }) => (
|
||||||
handleDelete={() => {
|
<UserImageUploadModal
|
||||||
handleDelete(getValues("avatar"));
|
isOpen={isImageUploadModalOpen}
|
||||||
}}
|
onClose={() => setIsImageUploadModalOpen(false)}
|
||||||
onSuccess={(url) => {
|
isRemoving={isRemoving}
|
||||||
setValue("avatar", url);
|
handleDelete={() => handleDelete(getValues("avatar"))}
|
||||||
setIsImageUploadModalOpen(false);
|
onSuccess={(url) => {
|
||||||
}}
|
onChange(url);
|
||||||
value={watch("avatar") !== "" ? watch("avatar") : undefined}
|
setIsImageUploadModalOpen(false);
|
||||||
userImage
|
}}
|
||||||
|
value={value && value.trim() !== "" ? value : null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="lg:w-2/3 w-full flex flex-col justify-between ml-auto ">
|
<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">
|
<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"
|
name="use_case"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex flex-wrap break-all overflow-auto">
|
<div className="flex flex-wrap break-all overflow-auto">
|
||||||
{useCases.map((useCase) => (
|
{USE_CASES.map((useCase) => (
|
||||||
<div
|
<div
|
||||||
className={`border mb-3 hover:cursor-pointer hover:bg-onboarding-background-300/30 flex-shrink-0 ${
|
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"
|
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";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { DeleteWorkspaceModal } from "components/workspace";
|
import { DeleteWorkspaceModal } from "components/workspace";
|
||||||
import { ImageUploadModal } from "components/core";
|
import { WorkspaceImageUploadModal } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
|
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -19,6 +19,7 @@ import { IWorkspace } from "types";
|
|||||||
// constants
|
// constants
|
||||||
import { ORGANIZATION_SIZE } from "constants/workspace";
|
import { ORGANIZATION_SIZE } from "constants/workspace";
|
||||||
import { trackEvent } from "helpers/event-tracker.helper";
|
import { trackEvent } from "helpers/event-tracker.helper";
|
||||||
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
|
|
||||||
const defaultValues: Partial<IWorkspace> = {
|
const defaultValues: Partial<IWorkspace> = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -33,8 +34,6 @@ const fileService = new FileService();
|
|||||||
export const WorkspaceDetails: FC = observer(() => {
|
export const WorkspaceDetails: FC = observer(() => {
|
||||||
// states
|
// states
|
||||||
const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false);
|
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 [isImageRemoving, setIsImageRemoving] = useState(false);
|
||||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||||
// store
|
// store
|
||||||
@ -51,7 +50,6 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<IWorkspace>({
|
} = useForm<IWorkspace>({
|
||||||
defaultValues: { ...defaultValues, ...currentWorkspace },
|
defaultValues: { ...defaultValues, ...currentWorkspace },
|
||||||
@ -78,8 +76,12 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (url: string | null | undefined) => {
|
const handleRemoveLogo = () => {
|
||||||
if (!currentWorkspace || !url) return;
|
if (!currentWorkspace) return;
|
||||||
|
|
||||||
|
const url = currentWorkspace.logo;
|
||||||
|
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
setIsImageRemoving(true);
|
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(() => {
|
useEffect(() => {
|
||||||
if (currentWorkspace) reset({ ...currentWorkspace });
|
if (currentWorkspace) reset({ ...currentWorkspace });
|
||||||
}, [currentWorkspace, reset]);
|
}, [currentWorkspace, reset]);
|
||||||
@ -118,22 +131,27 @@ export const WorkspaceDetails: FC = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteWorkspaceModal
|
<DeleteWorkspaceModal
|
||||||
|
data={currentWorkspace}
|
||||||
isOpen={deleteWorkspaceModal}
|
isOpen={deleteWorkspaceModal}
|
||||||
onClose={() => setDeleteWorkspaceModal(false)}
|
onClose={() => setDeleteWorkspaceModal(false)}
|
||||||
data={currentWorkspace}
|
|
||||||
/>
|
/>
|
||||||
<ImageUploadModal
|
<Controller
|
||||||
isOpen={isImageUploadModalOpen}
|
control={control}
|
||||||
onClose={() => setIsImageUploadModalOpen(false)}
|
name="logo"
|
||||||
isRemoving={isImageRemoving}
|
render={({ field: { onChange, value } }) => (
|
||||||
handleDelete={() => handleDelete(currentWorkspace?.logo)}
|
<WorkspaceImageUploadModal
|
||||||
onSuccess={(imageUrl) => {
|
isOpen={isImageUploadModalOpen}
|
||||||
setIsImageUploading(true);
|
onClose={() => setIsImageUploadModalOpen(false)}
|
||||||
setValue("logo", imageUrl);
|
isRemoving={isImageRemoving}
|
||||||
setIsImageUploadModalOpen(false);
|
handleRemove={handleRemoveLogo}
|
||||||
handleSubmit(onSubmit)().then(() => setIsImageUploading(false));
|
onSuccess={(imageUrl) => {
|
||||||
}}
|
onChange(imageUrl);
|
||||||
value={watch("logo")}
|
setIsImageUploadModalOpen(false);
|
||||||
|
handleSubmit(onSubmit)();
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
<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">
|
<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>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
|
<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://", "")
|
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
|
||||||
}/${currentWorkspace.slug}`}</span>
|
}/${currentWorkspace.slug}`}</button>
|
||||||
<div className="flex item-center gap-2.5">
|
<div className="flex item-center gap-2.5">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
|
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"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<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">
|
||||||
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">
|
<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>
|
<span className="text-sm font-medium text-custom-sidebar-text-200">Workspace</span>
|
||||||
{workspaces ? (
|
{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 Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
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
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// services
|
||||||
|
import { AuthService } from "services/auth.service";
|
||||||
// ui
|
// 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 = [
|
const SIDEBAR_LINKS = [
|
||||||
{
|
{
|
||||||
@ -21,12 +29,44 @@ const SIDEBAR_LINKS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
export const ProfileLayoutSidebar = observer(() => {
|
export const ProfileLayoutSidebar = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
theme: { sidebarCollapsed, toggleSidebar },
|
theme: { sidebarCollapsed, toggleSidebar },
|
||||||
workspace: { workspaces },
|
workspace: { workspaces },
|
||||||
|
user: { currentUser, currentUserSettings },
|
||||||
} = useMobxStore();
|
} = 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 (
|
return (
|
||||||
<div
|
<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 ${
|
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"}`}
|
} ${sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
||||||
>
|
>
|
||||||
<div className="h-full w-full flex flex-col">
|
<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">
|
<div className="w-full cursor-pointer space-y-1 p-4 flex-shrink-0">
|
||||||
{SIDEBAR_LINKS.map((link) => (
|
{SIDEBAR_LINKS.map((link) => (
|
||||||
<Link key={link.key} href={link.href}>
|
<Link key={link.key} href={link.href}>
|
||||||
|
@ -10,7 +10,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// layouts
|
// layouts
|
||||||
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||||
// components
|
// components
|
||||||
import { ImagePickerPopover, ImageUploadModal } from "components/core";
|
import { ImagePickerPopover, UserImageUploadModal } from "components/core";
|
||||||
import { ProfileSettingsHeader } from "components/headers";
|
import { ProfileSettingsHeader } from "components/headers";
|
||||||
import { DeactivateAccountModal } from "components/account";
|
import { DeactivateAccountModal } from "components/account";
|
||||||
// ui
|
// ui
|
||||||
@ -48,7 +48,6 @@ const ProfileSettingsPage: NextPageWithLayout = () => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
|
||||||
control,
|
control,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<IUser>({ defaultValues });
|
} = useForm<IUser>({ defaultValues });
|
||||||
@ -151,18 +150,23 @@ const ProfileSettingsPage: NextPageWithLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ImageUploadModal
|
<Controller
|
||||||
isOpen={isImageUploadModalOpen}
|
control={control}
|
||||||
onClose={() => setIsImageUploadModalOpen(false)}
|
name="avatar"
|
||||||
isRemoving={isRemoving}
|
render={({ field: { onChange, value } }) => (
|
||||||
handleDelete={() => handleDelete(myProfile?.avatar, true)}
|
<UserImageUploadModal
|
||||||
onSuccess={(url) => {
|
isOpen={isImageUploadModalOpen}
|
||||||
setValue("avatar", url);
|
onClose={() => setIsImageUploadModalOpen(false)}
|
||||||
handleSubmit(onSubmit)();
|
isRemoving={isRemoving}
|
||||||
setIsImageUploadModalOpen(false);
|
handleDelete={() => handleDelete(myProfile?.avatar, true)}
|
||||||
}}
|
onSuccess={(url) => {
|
||||||
value={watch("avatar") !== "" ? watch("avatar") : undefined}
|
onChange(url);
|
||||||
userImage
|
handleSubmit(onSubmit)();
|
||||||
|
setIsImageUploadModalOpen(false);
|
||||||
|
}}
|
||||||
|
value={value && value.trim() !== "" ? value : null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
<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">
|
<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 {
|
export interface IAppConfig {
|
||||||
email_password_login: boolean;
|
email_password_login: boolean;
|
||||||
|
file_size_limit: number;
|
||||||
google_client_id: string | null;
|
google_client_id: string | null;
|
||||||
github_app_name: string | null;
|
github_app_name: string | null;
|
||||||
github_client_id: string | null;
|
github_client_id: string | null;
|
||||||
|
Loading…
Reference in New Issue
Block a user