From b0941974e5a9b29c9aedeff77dc6e88ccb4ace32 Mon Sep 17 00:00:00 2001 From: gakshita Date: Wed, 7 Aug 2024 20:21:14 +0530 Subject: [PATCH 1/8] chore: seperated project components for CE --- .../(projects)/projects/(list)/layout.tsx | 5 +- .../(projects)/projects/(list)/page.tsx | 84 +------------------ .../components/projects}/header.tsx | 0 .../components/projects}/mobile-header.tsx | 0 web/ce/components/projects/page.tsx | 84 +++++++++++++++++++ web/ee/components/projects/header.tsx | 1 + web/ee/components/projects/mobile-header.tsx | 1 + web/ee/components/projects/page.tsx | 1 + 8 files changed, 90 insertions(+), 86 deletions(-) rename web/{app/[workspaceSlug]/(projects)/projects/(list) => ce/components/projects}/header.tsx (100%) rename web/{app/[workspaceSlug]/(projects)/projects/(list) => ce/components/projects}/mobile-header.tsx (100%) create mode 100644 web/ce/components/projects/page.tsx create mode 100644 web/ee/components/projects/header.tsx create mode 100644 web/ee/components/projects/mobile-header.tsx create mode 100644 web/ee/components/projects/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx index 259c412dcb7..a308e197897 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx @@ -4,9 +4,8 @@ import { ReactNode } from "react"; // components import { AppHeader, ContentWrapper } from "@/components/core"; // local components -import { ProjectsListHeader } from "./header"; -import { ProjectsListMobileHeader } from "./mobile-header"; - +import { ProjectsListHeader } from "@/plane-web/components/projects/header"; +import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header"; export default function ProjectListLayout({ children }: { children: ReactNode }) { return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx index 40e7f30a2a1..020d7085a71 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx @@ -1,84 +1,2 @@ -"use client"; - -import { useCallback } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// types -import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types"; -// components -import { PageHead } from "@/components/core"; -import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project"; -// helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; -// hooks -import { useProject, useProjectFilter, useWorkspace } from "@/hooks/store"; - -const ProjectsPage = observer(() => { - // store - const { workspaceSlug } = useParams(); - const { currentWorkspace } = useWorkspace(); - const { totalProjectIds, filteredProjectIds } = useProject(); - const { - currentWorkspaceFilters, - currentWorkspaceAppliedDisplayFilters, - clearAllFilters, - clearAllAppliedDisplayFilters, - updateFilters, - updateDisplayFilters, - } = useProjectFilter(); - // derived values - const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined; - - const handleRemoveFilter = useCallback( - (key: keyof TProjectFilters, value: string | null) => { - if (!workspaceSlug) return; - let newValues = currentWorkspaceFilters?.[key] ?? []; - - if (!value) newValues = []; - else newValues = newValues.filter((val) => val !== value); - - updateFilters(workspaceSlug.toString(), { [key]: newValues }); - }, - [currentWorkspaceFilters, updateFilters, workspaceSlug] - ); - - const handleRemoveDisplayFilter = useCallback( - (key: TProjectAppliedDisplayFilterKeys) => { - if (!workspaceSlug) return; - updateDisplayFilters(workspaceSlug.toString(), { [key]: false }); - }, - [updateDisplayFilters, workspaceSlug] - ); - - const handleClearAllFilters = useCallback(() => { - if (!workspaceSlug) return; - clearAllFilters(workspaceSlug.toString()); - clearAllAppliedDisplayFilters(workspaceSlug.toString()); - }, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]); - - return ( - <> - -
- {(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 || - currentWorkspaceAppliedDisplayFilters?.length !== 0) && ( -
- -
- )} - -
- - ); -}); - +import ProjectsPage from "@/plane-web/components/projects/page"; export default ProjectsPage; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/header.tsx b/web/ce/components/projects/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(list)/header.tsx rename to web/ce/components/projects/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/mobile-header.tsx b/web/ce/components/projects/mobile-header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(list)/mobile-header.tsx rename to web/ce/components/projects/mobile-header.tsx diff --git a/web/ce/components/projects/page.tsx b/web/ce/components/projects/page.tsx new file mode 100644 index 00000000000..40e7f30a2a1 --- /dev/null +++ b/web/ce/components/projects/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useCallback } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// types +import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types"; +// components +import { PageHead } from "@/components/core"; +import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project"; +// helpers +import { calculateTotalFilters } from "@/helpers/filter.helper"; +// hooks +import { useProject, useProjectFilter, useWorkspace } from "@/hooks/store"; + +const ProjectsPage = observer(() => { + // store + const { workspaceSlug } = useParams(); + const { currentWorkspace } = useWorkspace(); + const { totalProjectIds, filteredProjectIds } = useProject(); + const { + currentWorkspaceFilters, + currentWorkspaceAppliedDisplayFilters, + clearAllFilters, + clearAllAppliedDisplayFilters, + updateFilters, + updateDisplayFilters, + } = useProjectFilter(); + // derived values + const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined; + + const handleRemoveFilter = useCallback( + (key: keyof TProjectFilters, value: string | null) => { + if (!workspaceSlug) return; + let newValues = currentWorkspaceFilters?.[key] ?? []; + + if (!value) newValues = []; + else newValues = newValues.filter((val) => val !== value); + + updateFilters(workspaceSlug.toString(), { [key]: newValues }); + }, + [currentWorkspaceFilters, updateFilters, workspaceSlug] + ); + + const handleRemoveDisplayFilter = useCallback( + (key: TProjectAppliedDisplayFilterKeys) => { + if (!workspaceSlug) return; + updateDisplayFilters(workspaceSlug.toString(), { [key]: false }); + }, + [updateDisplayFilters, workspaceSlug] + ); + + const handleClearAllFilters = useCallback(() => { + if (!workspaceSlug) return; + clearAllFilters(workspaceSlug.toString()); + clearAllAppliedDisplayFilters(workspaceSlug.toString()); + }, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]); + + return ( + <> + +
+ {(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 || + currentWorkspaceAppliedDisplayFilters?.length !== 0) && ( +
+ +
+ )} + +
+ + ); +}); + +export default ProjectsPage; diff --git a/web/ee/components/projects/header.tsx b/web/ee/components/projects/header.tsx new file mode 100644 index 00000000000..983c3d046bc --- /dev/null +++ b/web/ee/components/projects/header.tsx @@ -0,0 +1 @@ +export * from "ce/components/projects/header"; diff --git a/web/ee/components/projects/mobile-header.tsx b/web/ee/components/projects/mobile-header.tsx new file mode 100644 index 00000000000..c845999a1f8 --- /dev/null +++ b/web/ee/components/projects/mobile-header.tsx @@ -0,0 +1 @@ +export * from "ce/components/projects/mobile-header"; diff --git a/web/ee/components/projects/page.tsx b/web/ee/components/projects/page.tsx new file mode 100644 index 00000000000..569615c49e2 --- /dev/null +++ b/web/ee/components/projects/page.tsx @@ -0,0 +1 @@ +export * from "ce/components/projects/page"; From 836c3be58136412832cbeb08898d71054a4031ca Mon Sep 17 00:00:00 2001 From: gakshita Date: Wed, 7 Aug 2024 23:20:51 +0530 Subject: [PATCH 2/8] chore: splitted the code for project creation form --- .../components/projects/create/attributes.tsx | 80 ++++ web/ce/components/projects/create/root.tsx | 138 ++++++ web/ce/types/projects/projects.ts | 3 + .../project/create-project-form.tsx | 405 ------------------ .../project/create-project-modal.tsx | 2 +- .../project/create/common-attributes.tsx | 135 ++++++ web/core/components/project/create/header.tsx | 85 ++++ .../project/create/project-create-buttons.tsx | 26 ++ web/core/components/project/index.ts | 2 +- web/core/store/project/project.store.ts | 31 +- .../components/projects/create/attributes.tsx | 1 + web/ee/components/projects/create/root.tsx | 1 + web/ee/types/projects/projects.ts | 1 + 13 files changed, 487 insertions(+), 423 deletions(-) create mode 100644 web/ce/components/projects/create/attributes.tsx create mode 100644 web/ce/components/projects/create/root.tsx create mode 100644 web/ce/types/projects/projects.ts delete mode 100644 web/core/components/project/create-project-form.tsx create mode 100644 web/core/components/project/create/common-attributes.tsx create mode 100644 web/core/components/project/create/header.tsx create mode 100644 web/core/components/project/create/project-create-buttons.tsx create mode 100644 web/ee/components/projects/create/attributes.tsx create mode 100644 web/ee/components/projects/create/root.tsx create mode 100644 web/ee/types/projects/projects.ts diff --git a/web/ce/components/projects/create/attributes.tsx b/web/ce/components/projects/create/attributes.tsx new file mode 100644 index 00000000000..ead92208f44 --- /dev/null +++ b/web/ce/components/projects/create/attributes.tsx @@ -0,0 +1,80 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { IProject } from "@plane/types"; +import { CustomSelect } from "@plane/ui"; +import { MemberDropdown } from "@/components/dropdowns"; +import { NETWORK_CHOICES } from "@/constants/project"; + +const ProjectAttributes = () => { + const { control } = useFormContext(); + return ( +
+ { + const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value); + + return ( +
+ + {currentNetwork ? ( + <> + + {currentNetwork.label} + + ) : ( + Select network + )} +
+ } + placement="bottom-start" + className="h-full" + buttonClassName="h-full" + noChevron + tabIndex={4} + > + {NETWORK_CHOICES.map((network) => ( + +
+ +
+

{network.label}

+

{network.description}

+
+
+
+ ))} + +
+ ); + }} + /> + { + if (value === undefined || value === null || typeof value === "string") + return ( +
+ onChange(lead === value ? null : lead)} + placeholder="Lead" + multiple={false} + buttonVariant="border-with-text" + tabIndex={5} + /> +
+ ); + else return <>; + }} + /> + + ); +}; + +export default ProjectAttributes; diff --git a/web/ce/components/projects/create/root.tsx b/web/ce/components/projects/create/root.tsx new file mode 100644 index 00000000000..00220f61854 --- /dev/null +++ b/web/ce/components/projects/create/root.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useState, FC } from "react"; +import { observer } from "mobx-react"; +import { FormProvider, useForm } from "react-hook-form"; +import { IProject } from "@plane/types"; +// ui +import { setToast, TOAST_TYPE } from "@plane/ui"; +// constants +import ProjectCommonAttributes from "@/components/project/create/common-attributes"; +import ProjectCreateHeader from "@/components/project/create/header"; +import ProjectCreateButtons from "@/components/project/create/project-create-buttons"; +import { PROJECT_CREATED } from "@/constants/event-tracker"; +import { PROJECT_UNSPLASH_COVERS } from "@/constants/project"; +// helpers +import { getRandomEmoji } from "@/helpers/emoji.helper"; +// hooks +import { useEventTracker, useProject } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +import ProjectAttributes from "./attributes"; + +type Props = { + setToFavorite?: boolean; + workspaceSlug: string; + onClose: () => void; + handleNextStep: (projectId: string) => void; +}; + +const defaultValues: Partial = { + cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: getRandomEmoji(), + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; + +export const CreateProjectForm: FC = observer((props) => { + const { setToFavorite, workspaceSlug, onClose, handleNextStep } = props; + // store + const { captureProjectEvent } = useEventTracker(); + const { addProjectToFavorites, createProject } = useProject(); + // states + const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); + // form info + const methods = useForm({ + defaultValues, + reValidateMode: "onChange", + }); + const { handleSubmit, reset, setValue } = methods; + const { isMobile } = usePlatformOS(); + const handleAddToFavorites = (projectId: string) => { + if (!workspaceSlug) return; + + addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }); + }); + }; + + const onSubmit = async (formData: Partial) => { + // Upper case identifier + formData.identifier = formData.identifier?.toUpperCase(); + + return createProject(workspaceSlug.toString(), formData) + .then((res) => { + const newPayload = { + ...res, + state: "SUCCESS", + }; + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: newPayload, + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Project created successfully.", + }); + if (setToFavorite) { + handleAddToFavorites(res.id); + } + handleNextStep(res.id); + }) + .catch((err) => { + Object.keys(err.data).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err.data[key], + }); + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: { + ...formData, + state: "FAILED", + }, + }); + }); + }); + }; + + const handleClose = () => { + onClose(); + setIsChangeInIdentifierRequired(true); + setTimeout(() => { + reset(); + }, 300); + }; + + return ( + + + +
+
+ + +
+ + +
+ ); +}); diff --git a/web/ce/types/projects/projects.ts b/web/ce/types/projects/projects.ts new file mode 100644 index 00000000000..567c9488db7 --- /dev/null +++ b/web/ce/types/projects/projects.ts @@ -0,0 +1,3 @@ +import { IProject } from "@plane/types"; + +export type TProject = IProject; diff --git a/web/core/components/project/create-project-form.tsx b/web/core/components/project/create-project-form.tsx deleted file mode 100644 index 65c620635c6..00000000000 --- a/web/core/components/project/create-project-form.tsx +++ /dev/null @@ -1,405 +0,0 @@ -"use client"; - -import { useState, FC, ChangeEvent } from "react"; -import { observer } from "mobx-react"; -import { useForm, Controller } from "react-hook-form"; -import { Info, X } from "lucide-react"; -import { IProject } from "@plane/types"; -// ui -import { - Button, - CustomEmojiIconPicker, - CustomSelect, - EmojiIconPickerTypes, - Input, - setToast, - TextArea, - TOAST_TYPE, - Tooltip, -} from "@plane/ui"; -// components -import { Logo } from "@/components/common"; -import { ImagePickerPopover } from "@/components/core"; -import { MemberDropdown } from "@/components/dropdowns"; -// constants -import { PROJECT_CREATED } from "@/constants/event-tracker"; -import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "@/constants/project"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { convertHexEmojiToDecimal, getRandomEmoji } from "@/helpers/emoji.helper"; -import { projectIdentifierSanitizer } from "@/helpers/project.helper"; -// hooks -import { useEventTracker, useProject } from "@/hooks/store"; -import { usePlatformOS } from "@/hooks/use-platform-os"; - -type Props = { - setToFavorite?: boolean; - workspaceSlug: string; - onClose: () => void; - handleNextStep: (projectId: string) => void; -}; - -const defaultValues: Partial = { - cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], - description: "", - logo_props: { - in_use: "emoji", - emoji: { - value: getRandomEmoji(), - }, - }, - identifier: "", - name: "", - network: 2, - project_lead: null, -}; - -export const CreateProjectForm: FC = observer((props) => { - const { setToFavorite, workspaceSlug, onClose, handleNextStep } = props; - // store - const { captureProjectEvent } = useEventTracker(); - const { addProjectToFavorites, createProject } = useProject(); - // states - const [isOpen, setIsOpen] = useState(false); - const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); - // form info - const { - formState: { errors, isSubmitting }, - handleSubmit, - reset, - control, - watch, - setValue, - } = useForm({ - defaultValues, - reValidateMode: "onChange", - }); - const { isMobile } = usePlatformOS(); - const handleAddToFavorites = (projectId: string) => { - if (!workspaceSlug) return; - - addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); - }); - }; - - const onSubmit = async (formData: Partial) => { - // Upper case identifier - formData.identifier = formData.identifier?.toUpperCase(); - - return createProject(workspaceSlug.toString(), formData) - .then((res) => { - const newPayload = { - ...res, - state: "SUCCESS", - }; - captureProjectEvent({ - eventName: PROJECT_CREATED, - payload: newPayload, - }); - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Project created successfully.", - }); - if (setToFavorite) { - handleAddToFavorites(res.id); - } - handleNextStep(res.id); - }) - .catch((err) => { - Object.keys(err.data).map((key) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: err.data[key], - }); - captureProjectEvent({ - eventName: PROJECT_CREATED, - payload: { - ...formData, - state: "FAILED", - }, - }); - }); - }); - }; - - const handleNameChange = (onChange: any) => (e: ChangeEvent) => { - if (!isChangeInIdentifierRequired) { - onChange(e); - return; - } - if (e.target.value === "") setValue("identifier", ""); - else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5)); - onChange(e); - }; - - const handleIdentifierChange = (onChange: any) => (e: ChangeEvent) => { - const { value } = e.target; - const alphanumericValue = projectIdentifierSanitizer(value); - setIsChangeInIdentifierRequired(false); - onChange(alphanumericValue); - }; - - const handleClose = () => { - onClose(); - setIsChangeInIdentifierRequired(true); - setTimeout(() => { - reset(); - }, 300); - }; - - return ( - <> -
- {watch("cover_image") && ( - Cover image - )} - -
- -
-
- ( - - )} - /> -
-
- ( - setIsOpen(val)} - className="flex items-center justify-center" - buttonClassName="flex items-center justify-center" - label={ - - - - } - onChange={(val: any) => { - let logoValue = {}; - - if (val?.type === "emoji") - logoValue = { - value: convertHexEmojiToDecimal(val.value.unified), - url: val.value.imageUrl, - }; - else if (val?.type === "icon") logoValue = val.value; - - onChange({ - in_use: val?.type, - [val?.type]: logoValue, - }); - setIsOpen(false); - }} - defaultIconColor={value.in_use && value.in_use === "icon" ? value.icon?.color : undefined} - defaultOpen={ - value.in_use && value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON - } - /> - )} - /> -
-
-
-
-
-
- ( - - )} - /> - - <>{errors?.name?.message} - -
-
- - /^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || - "Only Alphanumeric & Non-latin characters are allowed.", - minLength: { - value: 1, - message: "Project ID must at least be of 1 character", - }, - maxLength: { - value: 5, - message: "Project ID must at most be of 5 characters", - }, - }} - render={({ field: { value, onChange } }) => ( - - )} - /> - - - - {errors?.identifier?.message} -
-
- ( -