diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx new file mode 100644 index 00000000000..a308e197897 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { ReactNode } from "react"; +// components +import { AppHeader, ContentWrapper } from "@/components/core"; +// local components +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 ( + <> + } mobileHeader={} /> + {children} + + ); +} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx new file mode 100644 index 00000000000..b9b78bd5f46 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx @@ -0,0 +1,4 @@ +import ProjectPageRoot from "@/plane-web/components/projects/page"; + +const ProjectsPage = () => ; +export default ProjectsPage; 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..b9b78bd5f46 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx @@ -1,84 +1,4 @@ -"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 ProjectPageRoot from "@/plane-web/components/projects/page"; +const ProjectsPage = () => ; export default ProjectsPage; 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..76fea48f798 --- /dev/null +++ b/web/ce/components/projects/create/root.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useState, FC } from "react"; +import { observer } from "mobx-react"; +import { FormProvider, useForm } from "react-hook-form"; +// 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 { TProject } from "@/plane-web/types/projects"; +import ProjectAttributes from "./attributes"; + +type Props = { + setToFavorite?: boolean; + workspaceSlug: string; + onClose: () => void; + handleNextStep: (projectId: string) => void; + data?: Partial; +}; + +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/components/projects/header.tsx b/web/ce/components/projects/header.tsx new file mode 100644 index 00000000000..08871ec9b6c --- /dev/null +++ b/web/ce/components/projects/header.tsx @@ -0,0 +1,5 @@ +"use client"; + +import { ProjectsBaseHeader } from "@/components/project/header"; + +export const ProjectsListHeader = () => ; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/mobile-header.tsx b/web/ce/components/projects/mobile-header.tsx similarity index 99% rename from web/app/[workspaceSlug]/(projects)/projects/(list)/mobile-header.tsx rename to web/ce/components/projects/mobile-header.tsx index cd8eb9dfe08..3804721600e 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(list)/mobile-header.tsx +++ b/web/ce/components/projects/mobile-header.tsx @@ -1,3 +1,4 @@ +"use client"; import { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; @@ -23,7 +24,6 @@ export const ProjectsListMobileHeader = observer(() => { updateFilters, } = useProjectFilter(); - const { workspace: { workspaceMemberIds }, } = useMember(); diff --git a/web/ce/components/projects/page.tsx b/web/ce/components/projects/page.tsx new file mode 100644 index 00000000000..960c5973000 --- /dev/null +++ b/web/ce/components/projects/page.tsx @@ -0,0 +1,5 @@ +import Root from "@/components/project/root"; + +const ProjectPageRoot = () => ; + +export default ProjectPageRoot; diff --git a/web/ce/types/projects/index.ts b/web/ce/types/projects/index.ts new file mode 100644 index 00000000000..244d8c4df33 --- /dev/null +++ b/web/ce/types/projects/index.ts @@ -0,0 +1 @@ +export * from "./projects"; 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/gantt-chart/blocks/blocks-list.tsx b/web/core/components/gantt-chart/blocks/blocks-list.tsx index 8c94b07d036..c4ffae1387e 100644 --- a/web/core/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/core/components/gantt-chart/blocks/blocks-list.tsx @@ -14,10 +14,10 @@ export type GanttChartBlocksProps = { getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; blockToRender: (data: any) => React.ReactNode; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - enableBlockLeftResize: boolean; - enableBlockRightResize: boolean; - enableBlockMove: boolean; - enableAddBlock: boolean; + enableBlockLeftResize: boolean | ((blockId: string) => boolean); + enableBlockRightResize: boolean | ((blockId: string) => boolean); + enableBlockMove: boolean | ((blockId: string) => boolean); + enableAddBlock: boolean | ((blockId: string) => boolean); ganttContainerRef: React.RefObject; showAllBlocks: boolean; selectionHelpers: TSelectionHelper; @@ -55,10 +55,14 @@ export const GanttChartBlocksList: FC = (props) => { showAllBlocks={showAllBlocks} blockToRender={blockToRender} blockUpdateHandler={blockUpdateHandler} - enableBlockLeftResize={enableBlockLeftResize} - enableBlockRightResize={enableBlockRightResize} - enableBlockMove={enableBlockMove} - enableAddBlock={enableAddBlock} + enableBlockLeftResize={ + typeof enableBlockLeftResize === "function" ? enableBlockLeftResize(blockId) : enableBlockLeftResize + } + enableBlockRightResize={ + typeof enableBlockRightResize === "function" ? enableBlockRightResize(blockId) : enableBlockRightResize + } + enableBlockMove={typeof enableBlockMove === "function" ? enableBlockMove(blockId) : enableBlockMove} + enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock} ganttContainerRef={ganttContainerRef} selectionHelpers={selectionHelpers} /> diff --git a/web/core/components/gantt-chart/chart/header.tsx b/web/core/components/gantt-chart/chart/header.tsx index 8756e200fd8..4e16436df40 100644 --- a/web/core/components/gantt-chart/chart/header.tsx +++ b/web/core/components/gantt-chart/chart/header.tsx @@ -16,10 +16,12 @@ type Props = { handleToday: () => void; loaderTitle: string; toggleFullScreenMode: () => void; + showToday: boolean; }; export const GanttChartHeader: React.FC = observer((props) => { - const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode } = props; + const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, toggleFullScreenMode, showToday } = + props; // chart hook const { currentView } = useGanttChart(); @@ -46,9 +48,15 @@ export const GanttChartHeader: React.FC = observer((props) => { ))} - + {showToday && ( + + )} - -
- ( - - )} - /> -
-
- ( - 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} -
-
- ( -