From c342ab302e0f3af518e9092b832f06aa436f5a99 Mon Sep 17 00:00:00 2001 From: sriramveeraghanta Date: Wed, 27 Sep 2023 15:59:37 +0530 Subject: [PATCH] fix: ui package setup and project update form refactor --- .../project/create-project-modal.tsx | 75 +-- web/components/project/form-loader.tsx | 62 +++ web/components/project/form.tsx | 262 ++++++++++ web/components/project/index.ts | 2 + web/package.json | 1 + .../projects/[projectId]/settings/index.tsx | 478 ++++-------------- web/pages/api/unsplash.ts | 31 +- web/services/project.service.ts | 13 +- web/store/project.ts | 86 +++- 9 files changed, 529 insertions(+), 481 deletions(-) create mode 100644 web/components/project/form-loader.tsx create mode 100644 web/components/project/form.tsx diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index b6a2be8bb..d2ea87d6b 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -1,13 +1,10 @@ import React, { useState, useEffect } from "react"; - import { useRouter } from "next/router"; - import { mutate } from "swr"; - -// react-hook-form import { useForm, Controller } from "react-hook-form"; -// headless ui import { Dialog, Transition } from "@headlessui/react"; +// icons +import { XMarkIcon } from "@heroicons/react/24/outline"; // services import projectServices from "services/project.service"; // hooks @@ -25,8 +22,6 @@ import { Avatar, CustomSearchSelect, } from "components/ui"; -// icons -import { XMarkIcon } from "@heroicons/react/24/outline"; // components import { ImagePickerPopover } from "components/core"; import EmojiIconPicker from "components/emoji-icon-picker"; @@ -38,6 +33,7 @@ import { ICurrentUserResponse, IProject } from "types"; import { PROJECTS_LIST } from "constants/fetch-keys"; // constants import { NETWORK_CHOICES } from "constants/project"; +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { isOpen: boolean; @@ -75,12 +71,11 @@ const IsGuestCondition: React.FC<{ return null; }; -export const CreateProjectModal: React.FC = ({ - isOpen, - setIsOpen, - setToFavorite = false, - user, -}) => { +export const CreateProjectModal: React.FC = (props) => { + const { isOpen, setIsOpen, setToFavorite = false, user } = props; + // store + const { project: projectStore } = useMobxStore(); + // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); const { setToastAlert } = useToast(); @@ -113,24 +108,13 @@ export const CreateProjectModal: React.FC = ({ const handleAddToFavorites = (projectId: string) => { if (!workspaceSlug) return; - mutate( - PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), - (prevData) => - (prevData ?? []).map((p) => (p.id === projectId ? { ...p, is_favorite: true } : p)), - false - ); - - projectServices - .addProjectToFavorites(workspaceSlug as string, { - project: projectId, - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }) - ); + projectStore.addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }); + }); }; const onSubmit = async (formData: IProject) => { @@ -141,14 +125,9 @@ export const CreateProjectModal: React.FC = ({ if (typeof formData.emoji_and_icon === "object") payload.icon_prop = formData.emoji_and_icon; else payload.emoji = formData.emoji_and_icon; - await projectServices - .createProject(workspaceSlug.toString(), payload, user) + await projectStore + .createProject(workspaceSlug.toString(), payload) .then((res) => { - mutate( - PROJECTS_LIST(workspaceSlug.toString(), { is_favorite: "all" }), - (prevData) => [res, ...(prevData ?? [])], - false - ); setToastAlert({ type: "success", title: "Success!", @@ -206,8 +185,7 @@ export const CreateProjectModal: React.FC = ({ const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); - if (memberDetails && isOpen) - if (memberDetails.role <= 10) return ; + if (memberDetails && isOpen) if (memberDetails.role <= 10) return ; return ( @@ -277,10 +255,7 @@ export const CreateProjectModal: React.FC = ({ /> -
+
@@ -316,8 +291,7 @@ export const CreateProjectModal: React.FC = ({ validations={{ required: "Identifier is required", validate: (value) => - /^[A-Z0-9]+$/.test(value.toUpperCase()) || - "Identifier must be in uppercase.", + /^[A-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.", minLength: { value: 1, message: "Identifier must at least be of 1 character", @@ -384,9 +358,7 @@ export const CreateProjectModal: React.FC = ({ name="project_lead" control={control} render={({ field: { value, onChange } }) => { - const selectedMember = workspaceMembers?.find( - (m) => m.member.id === value - ); + const selectedMember = workspaceMembers?.find((m) => m.member.id === value); return ( = ({ ) : ( <> - + Lead )} diff --git a/web/components/project/form-loader.tsx b/web/components/project/form-loader.tsx new file mode 100644 index 000000000..325807b83 --- /dev/null +++ b/web/components/project/form-loader.tsx @@ -0,0 +1,62 @@ +import { FC } from "react"; +// components +import { Loader } from "components/ui"; + +export interface IProjectDetailsFormLoader {} + +export const ProjectDetailsFormLoader: FC = () => ( + <> +
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+
+
+

Project Name

+ + + +
+
+

Description

+ + + +
+
+
+

Identifier

+ + + +
+
+

Network

+ + + +
+
+
+ + + +
+
+ +); diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx new file mode 100644 index 000000000..ae154a59c --- /dev/null +++ b/web/components/project/form.tsx @@ -0,0 +1,262 @@ +import { FC } from "react"; +import { Controller, useForm } from "react-hook-form"; +// components +import EmojiIconPicker from "components/emoji-icon-picker"; +import { ImagePickerPopover } from "components/core"; +import { Input, TextArea, CustomSelect, PrimaryButton } from "components/ui"; +import { Input as InputElement } from "@plane/ui"; +// types +import { IProject, IWorkspace } from "types"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; +import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; +// constants +import { NETWORK_CHOICES } from "constants/project"; +// services +import projectService from "services/project.service"; +// hooks +import useToast from "hooks/use-toast"; +import { useMobxStore } from "lib/mobx/store-provider"; + +export interface IProjectDetailsForm { + project: IProject; + workspaceSlug: string; + isAdmin: boolean; +} + +export const ProjectDetailsForm: FC = (props) => { + const { project, workspaceSlug, isAdmin } = props; + // store + const { project: projectStore } = useMobxStore(); + // toast + const { setToastAlert } = useToast(); + // form data + const { + register, + handleSubmit, + watch, + control, + setValue, + setError, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + ...project, + emoji_and_icon: project.emoji ?? project.icon_prop, + workspace: (project.workspace as IWorkspace).id, + }, + }); + + const handleIdentifierChange = (event: React.ChangeEvent) => { + const { value } = event.target; + + const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, ""); + const formattedValue = alphanumericValue.toUpperCase(); + + setValue("identifier", formattedValue); + }; + + const updateProject = async (payload: Partial) => { + if (!workspaceSlug || !project) return; + + return projectStore + .updateProject(workspaceSlug.toString(), project.id, payload) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Project updated successfully", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Project could not be updated. Please try again.", + }); + }); + }; + + const onSubmit = async (formData: IProject) => { + if (!workspaceSlug) return; + + const payload: Partial = { + name: formData.name, + network: formData.network, + identifier: formData.identifier, + description: formData.description, + cover_image: formData.cover_image, + }; + + if (typeof formData.emoji_and_icon === "object") { + payload.emoji = null; + payload.icon_prop = formData.emoji_and_icon; + } else { + payload.emoji = formData.emoji_and_icon; + payload.icon_prop = null; + } + + if (project.identifier !== formData.identifier) + await projectService + .checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "") + .then(async (res) => { + if (res.exists) setError("identifier", { message: "Identifier already exists" }); + else await updateProject(payload); + }); + else await updateProject(payload); + }; + + const currentNetwork = NETWORK_CHOICES.find((n) => n.key === project?.network); + const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); + + return ( + +
+ {watch("cover_image")!} +
+
+
+
+ ( + + )} + /> +
+
+
+ {watch("name")} + + + {watch("identifier")} . {currentNetwork?.label} + + +
+
+ +
+
+ ( + + )} + /> +
+
+
+
+
+
+

Project Name

+ ( + + )} + /> +
+ +
+

Description

+