From 38f8aa90c1f77c6c480af38d7b4f3b2aae0a61a0 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Fri, 5 Jul 2024 16:09:33 +0530 Subject: [PATCH] [WEB-1519] chore: update component structure in project state settings and implement DND (#5043) * chore: updated project settings state * chore: updated sorting on project state * chore: updated grab handler in state item * chore: Updated UI and added garb handler icon * chore: handled top and bottom sequence in middle element swap * chore: handled input state element char limit to 100 * chore: typos and code cleanup in create state * chore: handled typos and comments wherever is required * chore: handled sorting logic --- .../[projectId]/settings/states/page.tsx | 9 +- .../project-states/create-update/create.tsx | 93 ++++++ .../project-states/create-update/form.tsx | 112 +++++++ .../project-states/create-update/index.ts | 3 + .../project-states/create-update/update.tsx | 92 ++++++ .../components/project-states/group-item.tsx | 55 ++++ .../components/project-states/group-list.tsx | 36 ++ web/core/components/project-states/index.ts | 12 + web/core/components/project-states/loader.tsx | 12 + .../project-states/options/delete.tsx | 119 +++++++ .../project-states/options/index.ts | 2 + .../options/mark-as-default.tsx | 44 +++ web/core/components/project-states/root.tsx | 34 ++ .../state-delete-modal.tsx} | 4 +- .../components/project-states/state-item.tsx | 179 ++++++++++ .../components/project-states/state-list.tsx | 35 ++ .../components/states/create-state-modal.tsx | 259 --------------- .../states/create-update-state-inline.tsx | 310 ------------------ web/core/components/states/index.ts | 5 - .../project-setting-state-list-item.tsx | 128 -------- .../states/project-setting-state-list.tsx | 183 ----------- web/core/constants/state.ts | 5 + web/core/store/state.store.ts | 32 +- web/helpers/state.helper.ts | 32 +- 24 files changed, 880 insertions(+), 915 deletions(-) create mode 100644 web/core/components/project-states/create-update/create.tsx create mode 100644 web/core/components/project-states/create-update/form.tsx create mode 100644 web/core/components/project-states/create-update/index.ts create mode 100644 web/core/components/project-states/create-update/update.tsx create mode 100644 web/core/components/project-states/group-item.tsx create mode 100644 web/core/components/project-states/group-list.tsx create mode 100644 web/core/components/project-states/index.ts create mode 100644 web/core/components/project-states/loader.tsx create mode 100644 web/core/components/project-states/options/delete.tsx create mode 100644 web/core/components/project-states/options/index.ts create mode 100644 web/core/components/project-states/options/mark-as-default.tsx create mode 100644 web/core/components/project-states/root.tsx rename web/core/components/{states/delete-state-modal.tsx => project-states/state-delete-modal.tsx} (95%) create mode 100644 web/core/components/project-states/state-item.tsx create mode 100644 web/core/components/project-states/state-list.tsx delete mode 100644 web/core/components/states/create-state-modal.tsx delete mode 100644 web/core/components/states/create-update-state-inline.tsx delete mode 100644 web/core/components/states/index.ts delete mode 100644 web/core/components/states/project-setting-state-list-item.tsx delete mode 100644 web/core/components/states/project-setting-state-list.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx index 6c030a1fd..50b0c0abc 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx @@ -1,17 +1,20 @@ "use client"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; // components import { PageHead } from "@/components/core"; -import { ProjectSettingStateList } from "@/components/states"; +import { ProjectStateRoot } from "@/components/project-states"; // hook import { useProject } from "@/hooks/store"; const StatesSettingsPage = observer(() => { + const { workspaceSlug, projectId } = useParams(); // store const { currentProjectDetails } = useProject(); // derived values const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; + return ( <> @@ -19,7 +22,9 @@ const StatesSettingsPage = observer(() => {

States

- + {workspaceSlug && projectId && ( + + )} ); diff --git a/web/core/components/project-states/create-update/create.tsx b/web/core/components/project-states/create-update/create.tsx new file mode 100644 index 000000000..b581eacec --- /dev/null +++ b/web/core/components/project-states/create-update/create.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { IState, TStateGroups } from "@plane/types"; +import { TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { StateForm } from "@/components/project-states"; +// constants +import { STATE_CREATED } from "@/constants/event-tracker"; +import { STATE_GROUPS } from "@/constants/state"; +// hooks +import { useEventTracker, useProjectState } from "@/hooks/store"; + +type TStateCreate = { + workspaceSlug: string; + projectId: string; + groupKey: TStateGroups; + handleClose: () => void; +}; + +export const StateCreate: FC = observer((props) => { + const { workspaceSlug, projectId, groupKey, handleClose } = props; + // hooks + const { captureProjectStateEvent, setTrackElement } = useEventTracker(); + const { createState } = useProjectState(); + // states + const [loader, setLoader] = useState(false); + + const onCancel = () => { + setLoader(false); + handleClose(); + }; + + const onSubmit = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !groupKey) return { status: "error" }; + + setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + try { + const stateResponse = await createState(workspaceSlug, projectId, { ...formData, group: groupKey }); + captureProjectStateEvent({ + eventName: STATE_CREATED, + payload: { + ...stateResponse, + state: "SUCCESS", + element: "Project settings states page", + }, + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "State created successfully.", + }); + handleClose(); + return { status: "success" }; + } catch (error) { + const errorStatus = error as unknown as { status: number; data: { error: string } }; + captureProjectStateEvent({ + eventName: STATE_CREATED, + payload: { + ...formData, + state: "FAILED", + element: "Project settings states page", + }, + }); + if (errorStatus?.status === 400) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "State with that name already exists. Please try again with another name.", + }); + return { status: "already_exists" }; + } else { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: errorStatus.data.error ?? "State could not be created. Please try again.", + }); + return { status: "error" }; + } + } + }; + + return ( + + ); +}); diff --git a/web/core/components/project-states/create-update/form.tsx b/web/core/components/project-states/create-update/form.tsx new file mode 100644 index 000000000..6a26d7cdd --- /dev/null +++ b/web/core/components/project-states/create-update/form.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { FormEvent, FC, useEffect, useState, Fragment } from "react"; +import { TwitterPicker } from "react-color"; +import { Popover, Transition } from "@headlessui/react"; +import { IState } from "@plane/types"; +import { Button, Input } from "@plane/ui"; + +type TStateForm = { + data: Partial; + onSubmit: (formData: Partial) => Promise<{ status: string }>; + onCancel: () => void; + buttonDisabled: boolean; + buttonTitle: string; +}; + +export const StateForm: FC = (props) => { + const { data, onSubmit, onCancel, buttonDisabled, buttonTitle } = props; + // states + const [formData, setFromData] = useState | undefined>(undefined); + const [errors, setErrors] = useState> | undefined>(undefined); + + useEffect(() => { + if (data && !formData) setFromData(data); + }, [data, formData]); + + const handleFormData = (key: T, value: IState[T]) => { + setFromData((prev) => ({ ...prev, [key]: value })); + setErrors((prev) => ({ ...prev, [key]: "" })); + }; + + const formSubmit = async (event: FormEvent) => { + event.preventDefault(); + + const name = formData?.name || undefined; + if (!formData || !name) { + let currentErrors: Partial> = {}; + if (!name) currentErrors = { ...currentErrors, name: "Name is required" }; + setErrors(currentErrors); + return; + } + + try { + await onSubmit(formData); + } catch (error) { + console.log("error", error); + } + }; + + return ( +
+ {/* color */} +
+ + + + + handleFormData("color", value.hex)} /> + + + +
+ + {/* title */} + handleFormData("name", e.target.value)} + hasError={(errors && Boolean(errors.name)) || false} + className="w-full" + maxLength={100} + autoFocus + /> + + {/* description */} + handleFormData("description", e.target.value)} + hasError={(errors && Boolean(errors.description)) || false} + className="w-full" + /> + + + + +
+ ); +}; diff --git a/web/core/components/project-states/create-update/index.ts b/web/core/components/project-states/create-update/index.ts new file mode 100644 index 000000000..a295e4a83 --- /dev/null +++ b/web/core/components/project-states/create-update/index.ts @@ -0,0 +1,3 @@ +export * from "./create"; +export * from "./update"; +export * from "./form"; diff --git a/web/core/components/project-states/create-update/update.tsx b/web/core/components/project-states/create-update/update.tsx new file mode 100644 index 000000000..669a86516 --- /dev/null +++ b/web/core/components/project-states/create-update/update.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { IState } from "@plane/types"; +import { TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { StateForm } from "@/components/project-states"; +// constants +import { STATE_UPDATED } from "@/constants/event-tracker"; +// hooks +import { useEventTracker, useProjectState } from "@/hooks/store"; + +type TStateUpdate = { + workspaceSlug: string; + projectId: string; + state: IState; + handleClose: () => void; +}; + +export const StateUpdate: FC = observer((props) => { + const { workspaceSlug, projectId, state, handleClose } = props; + // hooks + const { captureProjectStateEvent, setTrackElement } = useEventTracker(); + const { updateState } = useProjectState(); + // states + const [loader, setLoader] = useState(false); + + const onCancel = () => { + setLoader(false); + handleClose(); + }; + + const onSubmit = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !state.id) return { status: "error" }; + + setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + try { + const stateResponse = await updateState(workspaceSlug, projectId, state.id, formData); + captureProjectStateEvent({ + eventName: STATE_UPDATED, + payload: { + ...stateResponse, + state: "SUCCESS", + element: "Project settings states page", + }, + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "State updated successfully.", + }); + handleClose(); + return { status: "success" }; + } catch (error) { + const errorStatus = error as unknown as { status: number }; + if (errorStatus?.status === 400) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Another state exists with the same name. Please try again with another name.", + }); + return { status: "already_exists" }; + } else { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "State could not be updated. Please try again.", + }); + captureProjectStateEvent({ + eventName: STATE_UPDATED, + payload: { + ...formData, + state: "FAILED", + element: "Project settings states page", + }, + }); + return { status: "error" }; + } + } + }; + + return ( + + ); +}); diff --git a/web/core/components/project-states/group-item.tsx b/web/core/components/project-states/group-item.tsx new file mode 100644 index 000000000..5316871a1 --- /dev/null +++ b/web/core/components/project-states/group-item.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { Plus } from "lucide-react"; +import { IState, TStateGroups } from "@plane/types"; +// components +import { StateList, StateCreate } from "@/components/project-states"; + +type TGroupItem = { + workspaceSlug: string; + projectId: string; + groupKey: TStateGroups; + groupedStates: Record; + states: IState[]; +}; + +export const GroupItem: FC = observer((props) => { + const { workspaceSlug, projectId, groupKey, groupedStates, states } = props; + // state + const [createState, setCreateState] = useState(false); + + return ( +
+
+
{groupKey}
+
!createState && setCreateState(true)} + > + +
+
+ + {createState && ( + setCreateState(false)} + /> + )} + +
+ +
+
+ ); +}); diff --git a/web/core/components/project-states/group-list.tsx b/web/core/components/project-states/group-list.tsx new file mode 100644 index 000000000..59b5e66be --- /dev/null +++ b/web/core/components/project-states/group-list.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { IState, TStateGroups } from "@plane/types"; +// components +import { GroupItem } from "@/components/project-states"; + +type TGroupList = { + workspaceSlug: string; + projectId: string; + groupedStates: Record; +}; + +export const GroupList: FC = observer((props) => { + const { workspaceSlug, projectId, groupedStates } = props; + + return ( +
+ {Object.entries(groupedStates).map(([key, value]) => { + const groupKey = key as TStateGroups; + const groupStates = value; + return ( + + ); + })} +
+ ); +}); diff --git a/web/core/components/project-states/index.ts b/web/core/components/project-states/index.ts new file mode 100644 index 000000000..8c2901392 --- /dev/null +++ b/web/core/components/project-states/index.ts @@ -0,0 +1,12 @@ +export * from "./root"; + +export * from "./group-list"; +export * from "./group-item"; + +export * from "./state-list"; +export * from "./state-item"; +export * from "./options"; + +export * from "./loader"; +export * from "./create-update"; +export * from "./state-delete-modal"; diff --git a/web/core/components/project-states/loader.tsx b/web/core/components/project-states/loader.tsx new file mode 100644 index 000000000..958d0ea9e --- /dev/null +++ b/web/core/components/project-states/loader.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { Loader } from "@plane/ui"; + +export const ProjectStateLoader = () => ( + + + + + + +); diff --git a/web/core/components/project-states/options/delete.tsx b/web/core/components/project-states/options/delete.tsx new file mode 100644 index 000000000..00f88f034 --- /dev/null +++ b/web/core/components/project-states/options/delete.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { Loader, X } from "lucide-react"; +import { IState } from "@plane/types"; +import { AlertModalCore, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +// constants +import { STATE_DELETED } from "@/constants/event-tracker"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useEventTracker, useProjectState } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; + +type TStateDelete = { + workspaceSlug: string; + projectId: string; + totalStates: number; + state: IState; +}; + +export const StateDelete: FC = observer((props) => { + const { workspaceSlug, projectId, totalStates, state } = props; + // hooks + const { isMobile } = usePlatformOS(); + const { captureProjectStateEvent, setTrackElement } = useEventTracker(); + const { deleteState } = useProjectState(); + // states + const [isDeleteModal, setIsDeleteModal] = useState(false); + const [isDelete, setIsDelete] = useState(false); + + // derived values + const isDeleteDisabled = state.default ? true : totalStates === 1 ? true : false; + + const handleDeleteState = async () => { + if (!workspaceSlug || !projectId || isDeleteDisabled) return; + + setTrackElement("PROJECT_SETTINGS_STATE_PAGE"); + setIsDelete(true); + + try { + await deleteState(workspaceSlug, projectId, state.id); + captureProjectStateEvent({ + eventName: STATE_DELETED, + payload: { + ...state, + state: "SUCCESS", + }, + }); + setIsDelete(false); + } catch (error) { + const errorStatus = error as unknown as { status: number; data: { error: string } }; + captureProjectStateEvent({ + eventName: STATE_DELETED, + payload: { + ...state, + state: "FAILED", + }, + }); + if (errorStatus.status === 400) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: + "This state contains some issues within it, please move them to some other state to delete this state.", + }); + } else { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "State could not be deleted. Please try again.", + }); + } + setIsDelete(false); + } + }; + + return ( + <> + setIsDeleteModal(false)} + handleSubmit={handleDeleteState} + isSubmitting={isDelete} + isOpen={isDeleteModal} + title="Delete State" + content={ + <> + Are you sure you want to delete state-{" "} + {state?.name}? All of the data related to the + state will be permanently removed. This action cannot be undone. + + } + /> + + + + ); +}); diff --git a/web/core/components/project-states/options/index.ts b/web/core/components/project-states/options/index.ts new file mode 100644 index 000000000..6aad9566c --- /dev/null +++ b/web/core/components/project-states/options/index.ts @@ -0,0 +1,2 @@ +export * from "./mark-as-default"; +export * from "./delete"; diff --git a/web/core/components/project-states/options/mark-as-default.tsx b/web/core/components/project-states/options/mark-as-default.tsx new file mode 100644 index 000000000..667b2063b --- /dev/null +++ b/web/core/components/project-states/options/mark-as-default.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useProjectState } from "@/hooks/store"; + +type TStateMarksAsDefault = { workspaceSlug: string; projectId: string; stateId: string; isDefault: boolean }; + +export const StateMarksAsDefault: FC = observer((props) => { + const { workspaceSlug, projectId, stateId, isDefault } = props; + // hooks + const { markStateAsDefault } = useProjectState(); + // states + const [isLoading, setIsLoading] = useState(false); + + const handleMarkAsDefault = async () => { + if (!workspaceSlug || !projectId || !stateId || isDefault) return; + setIsLoading(true); + + try { + setIsLoading(false); + await markStateAsDefault(workspaceSlug, projectId, stateId); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + } + }; + + return ( + + ); +}); diff --git a/web/core/components/project-states/root.tsx b/web/core/components/project-states/root.tsx new file mode 100644 index 000000000..aca642ef0 --- /dev/null +++ b/web/core/components/project-states/root.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +// components +import { ProjectStateLoader, GroupList } from "@/components/project-states"; +// hooks +import { useProjectState } from "@/hooks/store"; + +type TProjectState = { + workspaceSlug: string; + projectId: string; +}; + +export const ProjectStateRoot: FC = observer((props) => { + const { workspaceSlug, projectId } = props; + // hooks + const { groupedProjectStates, fetchProjectStates } = useProjectState(); + + useSWR( + workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null + ); + + // Loader + if (!groupedProjectStates) return ; + + return ( +
+ +
+ ); +}); diff --git a/web/core/components/states/delete-state-modal.tsx b/web/core/components/project-states/state-delete-modal.tsx similarity index 95% rename from web/core/components/states/delete-state-modal.tsx rename to web/core/components/project-states/state-delete-modal.tsx index de66c3b49..f36c4ca12 100644 --- a/web/core/components/states/delete-state-modal.tsx +++ b/web/core/components/project-states/state-delete-modal.tsx @@ -12,13 +12,13 @@ import { STATE_DELETED } from "@/constants/event-tracker"; // hooks import { useEventTracker, useProjectState } from "@/hooks/store"; -type Props = { +type TStateDeleteModal = { isOpen: boolean; onClose: () => void; data: IState | null; }; -export const DeleteStateModal: React.FC = observer((props) => { +export const StateDeleteModal: React.FC = observer((props) => { const { isOpen, onClose, data } = props; // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); diff --git a/web/core/components/project-states/state-item.tsx b/web/core/components/project-states/state-item.tsx new file mode 100644 index 000000000..e15a708d7 --- /dev/null +++ b/web/core/components/project-states/state-item.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { FC, Fragment, useCallback, useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; +import { observer } from "mobx-react"; +import { GripVertical, Pencil } from "lucide-react"; +import { IState, TStateGroups } from "@plane/types"; +import { DropIndicator, StateGroupIcon } from "@plane/ui"; +// components +import { StateUpdate, StateDelete, StateMarksAsDefault } from "@/components/project-states"; +// helpers +import { TDraggableData } from "@/constants/state"; +import { cn } from "@/helpers/common.helper"; +import { getCurrentStateSequence } from "@/helpers/state.helper"; +// hooks +import { useProjectState } from "@/hooks/store"; + +type TStateItem = { + workspaceSlug: string; + projectId: string; + groupKey: TStateGroups; + groupedStates: Record; + totalStates: number; + state: IState; +}; + +export const StateItem: FC = observer((props) => { + const { workspaceSlug, projectId, groupKey, groupedStates, totalStates, state } = props; + // hooks + const { moveStatePosition } = useProjectState(); + // states + const [updateStateModal, setUpdateStateModal] = useState(false); + + const handleStateSequence = useCallback( + async (payload: Partial) => { + try { + if (!workspaceSlug || !projectId || !payload.id) return; + await moveStatePosition(workspaceSlug, projectId, payload.id, payload); + } catch (error) { + console.error("error", error); + } + }, + [workspaceSlug, projectId, moveStatePosition] + ); + + // derived values + const isDraggable = totalStates === 1 ? false : true; + + // DND starts + // ref + const draggableElementRef = useRef(null); + // states + const [isDragging, setIsDragging] = useState(false); + const [isDraggedOver, setIsDraggedOver] = useState(false); + const [closestEdge, setClosestEdge] = useState(null); + useEffect(() => { + const elementRef = draggableElementRef.current; + const initialData: TDraggableData = { groupKey: groupKey, id: state.id }; + + if (elementRef && state) { + combine( + draggable({ + element: elementRef, + getInitialData: () => initialData, + onDragStart: () => setIsDragging(true), + onDrop: () => setIsDragging(false), + canDrag: () => isDraggable, + }), + dropTargetForElements({ + element: elementRef, + getData: ({ input, element }) => + attachClosestEdge(initialData, { + input, + element, + allowedEdges: ["top", "bottom"], + }), + onDragEnter: (args) => { + setIsDraggedOver(true); + setClosestEdge(extractClosestEdge(args.self.data)); + }, + onDragLeave: () => { + setIsDraggedOver(false); + setClosestEdge(null); + }, + onDrop: (data) => { + setIsDraggedOver(false); + const { self, source } = data; + const sourceData = source.data as TDraggableData; + const destinationData = self.data as TDraggableData; + + if (sourceData && destinationData && sourceData.id) { + const destinationGroupKey = destinationData.groupKey as TStateGroups; + const edge = extractClosestEdge(destinationData) || undefined; + const payload: Partial = { + id: sourceData.id as string, + group: destinationGroupKey, + sequence: getCurrentStateSequence(groupedStates[destinationGroupKey], destinationData, edge), + }; + handleStateSequence(payload); + } + }, + }) + ); + } + }, [draggableElementRef, state, groupKey, isDraggable, groupedStates, handleStateSequence]); + // DND ends + + if (updateStateModal) + return ( + setUpdateStateModal(false)} + /> + ); + + return ( + + {/* draggable drop top indicator */} + + +
+ {/* draggable indicator */} + {totalStates != 1 && ( +
+ +
+ )} + + {/* state icon */} +
+ +
+ + {/* state title and description */} +
+
{state.name}
+

{state.description}

+
+ +
+ {/* state mark as default option */} +
+ +
+ + {/* state edit options */} +
+ + +
+
+
+ + {/* draggable drop bottom indicator */} + +
+ ); +}); diff --git a/web/core/components/project-states/state-list.tsx b/web/core/components/project-states/state-list.tsx new file mode 100644 index 000000000..42ddb6955 --- /dev/null +++ b/web/core/components/project-states/state-list.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { IState, TStateGroups } from "@plane/types"; +// components +import { StateItem } from "@/components/project-states"; + +type TStateList = { + workspaceSlug: string; + projectId: string; + groupKey: TStateGroups; + groupedStates: Record; + states: IState[]; +}; + +export const StateList: FC = observer((props) => { + const { workspaceSlug, projectId, groupKey, groupedStates, states } = props; + + return ( + <> + {states.map((state: IState) => ( + + ))} + + ); +}); diff --git a/web/core/components/states/create-state-modal.tsx b/web/core/components/states/create-state-modal.tsx deleted file mode 100644 index ccbcf8895..000000000 --- a/web/core/components/states/create-state-modal.tsx +++ /dev/null @@ -1,259 +0,0 @@ -"use client"; - -import React from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { TwitterPicker } from "react-color"; -import { Controller, useForm } from "react-hook-form"; -import { ChevronDown } from "lucide-react"; -import { Dialog, Popover, Transition } from "@headlessui/react"; -// icons -import type { IState } from "@plane/types"; -// ui -import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; -// constants -import { GROUP_CHOICES } from "@/constants/project"; -// hooks -import { useProjectState } from "@/hooks/store"; -// types - -// types -type Props = { - isOpen: boolean; - projectId: string; - handleClose: () => void; -}; - -const defaultValues: Partial = { - name: "", - description: "", - color: "rgb(var(--color-text-200))", - group: "backlog", -}; - -export const CreateStateModal: React.FC = observer((props) => { - const { isOpen, projectId, handleClose } = props; - // router - const { workspaceSlug } = useParams(); - // store hooks - const { createState } = useProjectState(); - // form info - const { - formState: { errors, isSubmitting }, - handleSubmit, - watch, - control, - reset, - } = useForm({ - defaultValues, - }); - - const onClose = () => { - handleClose(); - reset(defaultValues); - }; - - const onSubmit = async (formData: IState) => { - if (!workspaceSlug) return; - - const payload: IState = { - ...formData, - }; - - await createState(workspaceSlug.toString(), projectId.toString(), payload) - .then(() => { - onClose(); - }) - .catch((err) => { - const error = err.response; - - if (typeof error === "object") { - Object.keys(error).forEach((key) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: Array.isArray(error[key]) ? error[key].join(", ") : error[key], - }); - }); - } else { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: - error ?? err.status === 400 - ? "Another state exists with the same name. Please try again with another name." - : "State could not be created. Please try again.", - }); - } - }); - }; - - return ( - - - -
- - -
-
- - -
-
- - Create State - -
-
- ( - <> - - - - )} - /> -
-
- ( - - {Object.keys(GROUP_CHOICES).map((key) => ( - - {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} - - ))} - - )} - /> -
-
- - {({ open }) => ( - <> - - Color - {watch("color") && watch("color") !== "" && ( - - )} - - - - - ( - onChange(value.hex)} /> - )} - /> - - - - )} - -
-
- - ( -