From 02dbfc9abbd641cd72720e49b1d954b3e977392c Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Thu, 5 Dec 2024 01:22:41 +0530 Subject: [PATCH] refactor project states to ake way for new features --- packages/ui/src/tooltip/tooltip.tsx | 32 ++++---- .../workflow/add-state-transition.tsx | 20 +++++ web/ce/components/workflow/index.tsx | 6 ++ .../components/workflow/state-item-child.tsx | 35 +++++++++ web/ce/components/workflow/state-option.tsx | 36 +++++++++ .../workflow/state-transition-count.tsx | 7 ++ .../workflow/use-workflow-drag-n-drop.ts | 15 ++++ .../workflow/workflow-disabled-message.tsx | 6 ++ .../workflow/workflow-group-tree.tsx | 8 ++ web/ce/services/index.ts | 2 +- .../services/project/project-state.service.ts | 1 + web/ce/store/state.store.ts | 1 + web/ce/types/index.ts | 1 + web/ce/types/state.d.ts | 8 ++ web/core/components/dropdowns/state.tsx | 42 +++++------ .../issue-layouts/group-drag-overlay.tsx | 65 ++++++++++------- .../kanban/headers/group-by-card.tsx | 6 +- .../kanban/headers/sub-group-by-card.tsx | 10 ++- .../issue-layouts/kanban/kanban-group.tsx | 30 ++++++-- .../issues/issue-layouts/kanban/swimlanes.tsx | 1 + .../list/headers/group-by-card.tsx | 9 ++- .../issues/issue-layouts/list/list-group.tsx | 32 ++++++-- web/core/components/project-states/index.ts | 1 + web/core/components/project-states/root.tsx | 10 ++- .../project-states/state-item-title.tsx | 73 +++++++++++++++++++ .../components/project-states/state-item.tsx | 65 ++++------------- web/core/hooks/store/use-project-state.ts | 4 +- .../layouts/auth-layout/project-wrapper.tsx | 11 ++- .../store/issue/issue-details/issue.store.ts | 4 +- .../issue/issue-details/sub_issues.store.ts | 8 +- web/core/store/issue/root.store.ts | 6 -- web/core/store/root.store.ts | 12 +-- web/core/store/state.store.ts | 37 ++++++++-- web/ee/components/workflow/index.ts | 1 + .../services/project/project-state.service.ts | 1 + web/ee/store/state.store.ts | 1 + web/ee/types/index.ts | 1 + web/helpers/array.helper.ts | 15 ++++ 38 files changed, 462 insertions(+), 161 deletions(-) create mode 100644 web/ce/components/workflow/add-state-transition.tsx create mode 100644 web/ce/components/workflow/index.tsx create mode 100644 web/ce/components/workflow/state-item-child.tsx create mode 100644 web/ce/components/workflow/state-option.tsx create mode 100644 web/ce/components/workflow/state-transition-count.tsx create mode 100644 web/ce/components/workflow/use-workflow-drag-n-drop.ts create mode 100644 web/ce/components/workflow/workflow-disabled-message.tsx create mode 100644 web/ce/components/workflow/workflow-group-tree.tsx create mode 100644 web/ce/services/project/project-state.service.ts create mode 100644 web/ce/store/state.store.ts create mode 100644 web/ce/types/state.d.ts create mode 100644 web/core/components/project-states/state-item-title.tsx create mode 100644 web/ee/components/workflow/index.ts create mode 100644 web/ee/services/project/project-state.service.ts create mode 100644 web/ee/store/state.store.ts diff --git a/packages/ui/src/tooltip/tooltip.tsx b/packages/ui/src/tooltip/tooltip.tsx index ca4f5c88a54..e485166ebc2 100644 --- a/packages/ui/src/tooltip/tooltip.tsx +++ b/packages/ui/src/tooltip/tooltip.tsx @@ -23,6 +23,7 @@ export type TPosition = interface ITooltipProps { tooltipHeading?: string; tooltipContent: string | React.ReactNode; + jsxContent?: string | React.ReactNode; position?: TPosition; children: JSX.Element; disabled?: boolean; @@ -38,13 +39,14 @@ export const Tooltip: React.FC = ({ tooltipContent, position = "top", children, + jsxContent, disabled = false, className = "", openDelay = 200, closeDelay, isMobile = false, renderByDefault = true, //FIXME: tooltip should always render on hover and not by default, this is a temporary fix -}) => { +}: ITooltipProps) => { const toolTipRef = useRef(null); const [shouldRender, setShouldRender] = useState(renderByDefault); @@ -79,18 +81,22 @@ export const Tooltip: React.FC = ({ hoverOpenDelay={openDelay} hoverCloseDelay={closeDelay} content={ -
- {tooltipHeading &&
{tooltipHeading}
} - {tooltipContent} -
+ jsxContent ? ( + <>{jsxContent} + ) : ( +
+ {tooltipHeading &&
{tooltipHeading}
} + {tooltipContent} +
+ ) } position={position} renderTarget={({ diff --git a/web/ce/components/workflow/add-state-transition.tsx b/web/ce/components/workflow/add-state-transition.tsx new file mode 100644 index 00000000000..48ba6c02ab9 --- /dev/null +++ b/web/ce/components/workflow/add-state-transition.tsx @@ -0,0 +1,20 @@ +import { Plus } from "lucide-react"; +// Plane +import { cn } from "@plane/editor"; + +type Props = { + workspaceSlug: string; + projectId: string; + parentStateId: string; + onTransitionAdd?: () => void; +}; + +export const AddStateTransition = (props: Props) => ( +
+ <> + + Add Transition +
Pro
+ +
+); diff --git a/web/ce/components/workflow/index.tsx b/web/ce/components/workflow/index.tsx new file mode 100644 index 00000000000..3cf9d8d3f46 --- /dev/null +++ b/web/ce/components/workflow/index.tsx @@ -0,0 +1,6 @@ +export * from "./state-option"; +export * from "./state-item-child"; +export * from "./state-transition-count"; +export * from "./use-workflow-drag-n-drop"; +export * from "./workflow-disabled-message"; +export * from "./workflow-group-tree"; diff --git a/web/ce/components/workflow/state-item-child.tsx b/web/ce/components/workflow/state-item-child.tsx new file mode 100644 index 00000000000..aa94b52815c --- /dev/null +++ b/web/ce/components/workflow/state-item-child.tsx @@ -0,0 +1,35 @@ +import { SetStateAction } from "react"; +import { observer } from "mobx-react"; +// Plane +import { IState } from "@plane/types"; +// components +import { StateItemTitle } from "@/components/project-states/state-item-title"; +// +import { AddStateTransition } from "./add-state-transition"; + +export type StateItemChildProps = { + workspaceSlug: string; + projectId: string; + stateCount: number; + disabled: boolean; + state: IState; + setUpdateStateModal: (value: SetStateAction) => void; +}; + +export const StateItemChild = observer((props: StateItemChildProps) => { + const { workspaceSlug, projectId, stateCount, setUpdateStateModal, disabled, state } = props; + + return ( +
+ + +
+ ); +}); diff --git a/web/ce/components/workflow/state-option.tsx b/web/ce/components/workflow/state-option.tsx new file mode 100644 index 00000000000..aa9665d90b9 --- /dev/null +++ b/web/ce/components/workflow/state-option.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +import { Check } from "lucide-react"; +import { Combobox } from "@headlessui/react"; + +type Props = { + projectId: string | null | undefined; + option: { + value: string | undefined; + query: string; + content: JSX.Element; + }; + filterAvailableStateIds: boolean; + selectedValue: string | null | undefined; + className?: string; +}; + +export const StateOption = observer((props: Props) => { + const { option, className = "" } = props; + + return ( + + `${className} ${active ? "bg-custom-background-80" : ""} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + ); +}); diff --git a/web/ce/components/workflow/state-transition-count.tsx b/web/ce/components/workflow/state-transition-count.tsx new file mode 100644 index 00000000000..b9e4a22f9db --- /dev/null +++ b/web/ce/components/workflow/state-transition-count.tsx @@ -0,0 +1,7 @@ +import { IStateWorkFlow } from "@/plane-web/types"; + +type Props = { + currentTransitionMap?: IStateWorkFlow; +}; + +export const StateTransitionCount = (props: Props) => <>; diff --git a/web/ce/components/workflow/use-workflow-drag-n-drop.ts b/web/ce/components/workflow/use-workflow-drag-n-drop.ts new file mode 100644 index 00000000000..e4044d843b8 --- /dev/null +++ b/web/ce/components/workflow/use-workflow-drag-n-drop.ts @@ -0,0 +1,15 @@ +import { TIssueGroupByOptions } from "@plane/types"; + +export const useWorkFlowFDragNDrop = ( + groupBy: TIssueGroupByOptions | undefined, + subGroupBy?: TIssueGroupByOptions +) => ({ + workflowDisabledSource: undefined, + isWorkflowDropDisabled: false, + handleWorkFlowState: ( + sourceGroupId: string, + destinationGroupId: string, + sourceSubGroupId?: string, + destinationSubGroupId?: string + ) => {}, +}); diff --git a/web/ce/components/workflow/workflow-disabled-message.tsx b/web/ce/components/workflow/workflow-disabled-message.tsx new file mode 100644 index 00000000000..bc2c2ee535f --- /dev/null +++ b/web/ce/components/workflow/workflow-disabled-message.tsx @@ -0,0 +1,6 @@ +type Props = { + parentStateId: string; + className?: string; +}; + +export const WorkFlowDisabledMessage = (props: Props) => <>; diff --git a/web/ce/components/workflow/workflow-group-tree.tsx b/web/ce/components/workflow/workflow-group-tree.tsx new file mode 100644 index 00000000000..934db70f3ca --- /dev/null +++ b/web/ce/components/workflow/workflow-group-tree.tsx @@ -0,0 +1,8 @@ +import { TIssueGroupByOptions } from "@plane/types"; + +type Props = { + groupBy?: TIssueGroupByOptions; + groupId: string | undefined; +}; + +export const WorkFlowGroupTree = (props: Props) => <>; diff --git a/web/ce/services/index.ts b/web/ce/services/index.ts index 3a7bd700542..d0c05946189 100644 --- a/web/ce/services/index.ts +++ b/web/ce/services/index.ts @@ -1,2 +1,2 @@ export * from "./project"; -export * from "./workspace.service"; \ No newline at end of file +export * from "./workspace.service"; diff --git a/web/ce/services/project/project-state.service.ts b/web/ce/services/project/project-state.service.ts new file mode 100644 index 00000000000..f4a48ae7177 --- /dev/null +++ b/web/ce/services/project/project-state.service.ts @@ -0,0 +1 @@ +export * from "@/services/project/project-state.service"; diff --git a/web/ce/store/state.store.ts b/web/ce/store/state.store.ts new file mode 100644 index 00000000000..a25412ca8ab --- /dev/null +++ b/web/ce/store/state.store.ts @@ -0,0 +1 @@ +export * from "@/store/state.store"; diff --git a/web/ce/types/index.ts b/web/ce/types/index.ts index 105b7e96a46..d18d0137a59 100644 --- a/web/ce/types/index.ts +++ b/web/ce/types/index.ts @@ -1,3 +1,4 @@ export * from "./projects"; export * from "./issue-types"; export * from "./gantt-chart"; +export * from "./state.d"; diff --git a/web/ce/types/state.d.ts b/web/ce/types/state.d.ts new file mode 100644 index 00000000000..22309db819b --- /dev/null +++ b/web/ce/types/state.d.ts @@ -0,0 +1,8 @@ +export interface IStateTransition { + transition_state_id: string; + actors: string[]; +} + +export interface IStateWorkFlow { + [transitionId: string]: IStateTransition; +} diff --git a/web/core/components/dropdowns/state.tsx b/web/core/components/dropdowns/state.tsx index c139d23c904..8cd4cdf4826 100644 --- a/web/core/components/dropdowns/state.tsx +++ b/web/core/components/dropdowns/state.tsx @@ -1,10 +1,10 @@ "use client"; -import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; +import { ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search } from "lucide-react"; +import { ChevronDown, Search } from "lucide-react"; import { Combobox } from "@headlessui/react"; // ui import { ComboDropDown, Spinner, StateGroupIcon } from "@plane/ui"; @@ -13,6 +13,8 @@ import { cn } from "@/helpers/common.helper"; // hooks import { useProjectState } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; +// Plane-web +import { StateOption } from "@/plane-web/components/workflow"; // components import { DropdownButton } from "./buttons"; // constants @@ -30,6 +32,8 @@ type Props = TDropdownProps & { showDefaultState?: boolean; value: string | undefined | null; renderByDefault?: boolean; + stateIds?: string[]; + filterAvailableStateIds?: boolean; }; export const StateDropdown: React.FC = observer((props) => { @@ -52,6 +56,8 @@ export const StateDropdown: React.FC = observer((props) => { tabIndex, value, renderByDefault = true, + stateIds, + filterAvailableStateIds = true, } = props; // states const [query, setQuery] = useState(""); @@ -78,16 +84,18 @@ export const StateDropdown: React.FC = observer((props) => { // store hooks const { workspaceSlug } = useParams(); const { fetchProjectStates, getProjectStates, getStateById } = useProjectState(); - const statesList = getProjectStates(projectId); - const defaultState = statesList?.find((state) => state.default); + const statesList = stateIds + ? stateIds.map((stateId) => getStateById(stateId)).filter((state) => !!state) + : getProjectStates(projectId); + const defaultState = statesList?.find((state) => state?.default); const stateValue = !!value ? value : showDefaultState ? defaultState?.id : undefined; const options = statesList?.map((state) => ({ - value: state.id, + value: state?.id, query: `${state?.name}`, content: (
- + {state?.name}
), @@ -226,22 +234,14 @@ export const StateDropdown: React.FC = observer((props) => { {filteredOptions ? ( filteredOptions.length > 0 ? ( filteredOptions.map((option) => ( - - `flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - + option={option} + projectId={projectId} + filterAvailableStateIds={filterAvailableStateIds} + selectedValue={value} + className="flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5" + /> )) ) : (

No matches found

diff --git a/web/core/components/issues/issue-layouts/group-drag-overlay.tsx b/web/core/components/issues/issue-layouts/group-drag-overlay.tsx index ab84eba6dcb..822b2e0df11 100644 --- a/web/core/components/issues/issue-layouts/group-drag-overlay.tsx +++ b/web/core/components/issues/issue-layouts/group-drag-overlay.tsx @@ -1,10 +1,16 @@ import { AlertCircle } from "lucide-react"; +// Plane import { TIssueOrderByOptions } from "@plane/types"; +// constants import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue"; +// helpers import { cn } from "@/helpers/common.helper"; +// Plane-web +import { WorkFlowDisabledMessage } from "@/plane-web/components/workflow"; type Props = { dragColumnOrientation: "justify-start" | "justify-center" | "justify-end"; + workflowDisabledSource?: string; canOverlayBeVisible: boolean; isDropDisabled: boolean; dropErrorMessage?: string; @@ -16,6 +22,7 @@ export const GroupDragOverlay = (props: Props) => { const { dragColumnOrientation, canOverlayBeVisible, + workflowDisabledSource, isDropDisabled, dropErrorMessage, orderBy, @@ -35,33 +42,37 @@ export const GroupDragOverlay = (props: Props) => { { hidden: !shouldOverlayBeVisible } )} > -
- {dropErrorMessage ? ( -
-   - {dropErrorMessage} -
- ) : ( - <> - {readableOrderBy && ( - - The layout is ordered by {readableOrderBy}. - - )} - Drop here to move the issue. - - )} -
+ {workflowDisabledSource ? ( + + ) : ( +
+ {dropErrorMessage ? ( +
+   + {dropErrorMessage} +
+ ) : ( + <> + {readableOrderBy && ( + + The layout is ordered by {readableOrderBy}. + + )} + Drop here to move the issue. + + )} +
+ )} ); }; diff --git a/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index b09b881e4e9..4303946dc1d 100644 --- a/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -15,7 +15,8 @@ import { CreateUpdateIssueModal } from "@/components/issues"; // hooks import { useEventTracker } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; -// types +// Plane-web +import { WorkFlowGroupTree } from "@/plane-web/components/workflow"; interface IHeaderGroupByCard { sub_group_by: TIssueGroupByOptions | undefined; @@ -33,6 +34,7 @@ interface IHeaderGroupByCard { export const HeaderGroupByCard: FC = observer((props) => { const { + group_by, sub_group_by, column_id, icon, @@ -130,6 +132,8 @@ export const HeaderGroupByCard: FC = observer((props) => { + + {sub_group_by === null && (
void; } export const HeaderSubGroupByCard: FC = observer((props) => { - const { icon, title, count, column_id, collapsedGroups, handleCollapsedGroups } = props; + const { icon, title, count, column_id, collapsedGroups, sub_group_by, handleCollapsedGroups } = props; return (
= observer((props)
{title}
{count || 0}
+ +
); }); diff --git a/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx index b27c9b1f754..15720b54368 100644 --- a/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -26,6 +26,9 @@ import { cn } from "@/helpers/common.helper"; import { useProjectState } from "@/hooks/store"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import { useIssuesStore } from "@/hooks/use-issue-layout-store"; +// Plane-web +import { useWorkFlowFDragNDrop } from "@/plane-web/components/workflow"; +// import { GroupDragOverlay } from "../group-drag-overlay"; import { TRenderQuickActions } from "../list/list-view-types"; import { GroupDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload, getIssueBlockId } from "../utils"; @@ -103,6 +106,11 @@ export const KanbanGroup = observer((props: IKanbanGroup) => { ); const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); + const { workflowDisabledSource, isWorkflowDropDisabled, handleWorkFlowState } = useWorkFlowFDragNDrop( + group_by, + sub_group_by + ); + // Enable Kanban Columns as Drop Targets useEffect(() => { const element = columnRef.current; @@ -113,14 +121,24 @@ export const KanbanGroup = observer((props: IKanbanGroup) => { dropTargetForElements({ element, getData: () => ({ groupId, subGroupId: sub_group_id, columnId: `${groupId}__${sub_group_id}`, type: "COLUMN" }), - onDragEnter: () => { + onDragEnter: (payload) => { + const source = getSourceFromDropPayload(payload); setIsDraggingOverColumn(true); + // handle if dragging a workflowState + if (source) { + handleWorkFlowState(source?.groupId, groupId, source?.subGroupId, sub_group_id); + } }, onDragLeave: () => { setIsDraggingOverColumn(false); }, - onDragStart: () => { + onDragStart: (payload) => { + const source = getSourceFromDropPayload(payload); setIsDraggingOverColumn(true); + // handle if dragging a workflowState + if (source) { + handleWorkFlowState(source?.groupId, groupId, source?.subGroupId, sub_group_id); + } }, onDrop: (payload) => { setIsDraggingOverColumn(false); @@ -129,7 +147,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => { if (!source || !destination) return; - if (isDropDisabled) { + if (isWorkflowDropDisabled || isDropDisabled) { dropErrorMessage && setToast({ type: TOAST_TYPE.WARNING, @@ -158,6 +176,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => { setIsDraggingOverColumn, orderBy, isDropDisabled, + isWorkflowDropDisabled, dropErrorMessage, handleOnDrop, ]); @@ -237,7 +256,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => { ); const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults; - const canOverlayBeVisible = orderBy !== "sort_order" || isDropDisabled; + const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || isDropDisabled; const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible; return ( @@ -253,7 +272,8 @@ export const KanbanGroup = observer((props: IKanbanGroup) => { = observer((props) => { count={issueCount} collapsedGroups={collapsedGroups} handleCollapsedGroups={handleCollapsedGroups} + sub_group_by={sub_group_by} /> diff --git a/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx index 83e428fbedb..6d2f57a9904 100644 --- a/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; import { CircleDashed, Plus } from "lucide-react"; // types -import { TIssue, ISearchIssueResponse } from "@plane/types"; +import { TIssue, ISearchIssueResponse, TIssueGroupByOptions } from "@plane/types"; // ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components @@ -18,9 +18,12 @@ import { cn } from "@/helpers/common.helper"; import { useEventTracker } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; +// Plane-web +import { WorkFlowGroupTree } from "@/plane-web/components/workflow"; interface IHeaderGroupByCard { groupID: string; + groupBy: TIssueGroupByOptions; icon?: React.ReactNode; title: string; count: number; @@ -35,6 +38,7 @@ interface IHeaderGroupByCard { export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => { const { groupID, + groupBy, icon, title, count, @@ -43,7 +47,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => { disableIssueCreation, addIssuesToView, selectionHelpers, - handleCollapsedGroups + handleCollapsedGroups, } = props; // states const [isOpen, setIsOpen] = useState(false); @@ -112,6 +116,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => { >
{title}
{count || 0}
+ {!disableIssueCreation && diff --git a/web/core/components/issues/issue-layouts/list/list-group.tsx b/web/core/components/issues/issue-layouts/list/list-group.tsx index 7bf0dc553c7..aae8d97137c 100644 --- a/web/core/components/issues/issue-layouts/list/list-group.tsx +++ b/web/core/components/issues/issue-layouts/list/list-group.tsx @@ -26,7 +26,9 @@ import { useProjectState } from "@/hooks/store"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; -// components +// Plane-web +import { useWorkFlowFDragNDrop } from "@/plane-web/components/workflow"; +// import { GroupDragOverlay } from "../group-drag-overlay"; import { ListQuickAddIssueButton, QuickAddIssueRoot } from "../quick-add"; import { @@ -93,7 +95,7 @@ export const ListGroup = observer((props: Props) => { const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const [dragColumnOrientation, setDragColumnOrientation] = useState<"justify-start" | "justify-end">("justify-start"); - const isExpanded = !(collapsedGroups?.group_by.includes(group.id)) + const isExpanded = !collapsedGroups?.group_by.includes(group.id); const groupRef = useRef(null); const { projectId } = useParams(); @@ -105,6 +107,8 @@ export const ListGroup = observer((props: Props) => { const [intersectionElement, setIntersectionElement] = useState(null); + const { workflowDisabledSource, isWorkflowDropDisabled, handleWorkFlowState } = useWorkFlowFDragNDrop(group_by); + const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0; const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults; const isPaginating = !!getIssueLoader(group.id); @@ -185,6 +189,8 @@ export const ListGroup = observer((props: Props) => { const sourceGroupId = source?.data?.groupId as string | undefined; const currentGroupId = group.id; + sourceGroupId && handleWorkFlowState(sourceGroupId, currentGroupId); + const sourceIndex = getGroupIndex(sourceGroupId); const currentIndex = getGroupIndex(currentGroupId); @@ -201,7 +207,7 @@ export const ListGroup = observer((props: Props) => { if (!source || !destination) return; - if (group.isDropDisabled) { + if (isWorkflowDropDisabled || group.isDropDisabled) { group.dropErrorMessage && setToast({ type: TOAST_TYPE.WARNING, @@ -215,17 +221,25 @@ export const ListGroup = observer((props: Props) => { highlightIssueOnDrop(getIssueBlockId(source.id, destination?.groupId), orderBy !== "sort_order"); - if(!isExpanded){ - handleCollapsedGroups(group.id) + if (!isExpanded) { + handleCollapsedGroups(group.id); } }, }) ); - }, [groupRef?.current, group, orderBy, getGroupIndex, setDragColumnOrientation, setIsDraggingOverColumn]); + }, [ + groupRef?.current, + group, + orderBy, + getGroupIndex, + setDragColumnOrientation, + setIsDraggingOverColumn, + isWorkflowDropDisabled, + ]); const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by) && canEditProperties(projectId?.toString()); - const canOverlayBeVisible = orderBy !== "sort_order" || !!group.isDropDisabled; + const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled; const isGroupByCreatedBy = group_by === "created_by"; const shouldExpand = (!!groupIssueCount && isExpanded) || !group_by; @@ -245,6 +259,7 @@ export const ListGroup = observer((props: Props) => { > { = observer((props) => { const { workspaceSlug, projectId } = props; // hooks - const { groupedProjectStates, fetchProjectStates } = useProjectState(); + const { groupedProjectStates, fetchProjectStates, fetchProjectStateTransitions } = useProjectState(); useSWR( workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId + ? () => { + fetchProjectStates(workspaceSlug.toString(), projectId.toString()); + fetchProjectStateTransitions(workspaceSlug.toString(), projectId.toString()); + } + : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // Loader diff --git a/web/core/components/project-states/state-item-title.tsx b/web/core/components/project-states/state-item-title.tsx new file mode 100644 index 00000000000..6191ccd6bd2 --- /dev/null +++ b/web/core/components/project-states/state-item-title.tsx @@ -0,0 +1,73 @@ +import { SetStateAction } from "react"; +import { observer } from "mobx-react"; +import { GripVertical, Pencil } from "lucide-react"; +// Plane +import { IState } from "@plane/types"; +import { StateGroupIcon } from "@plane/ui"; +// Plane-web +import { StateTransitionCount } from "@/plane-web/components/workflow"; +import { IStateWorkFlow } from "@/plane-web/types"; +// +import { StateDelete, StateMarksAsDefault } from "./options"; + +export type StateItemTitleProps = { + workspaceSlug: string; + projectId: string; + setUpdateStateModal: (value: SetStateAction) => void; + stateCount: number; + disabled: boolean; + state: IState; + currentTransitionMap?: IStateWorkFlow; +}; + +export const StateItemTitle = observer((props: StateItemTitleProps) => { + const { workspaceSlug, projectId, stateCount, setUpdateStateModal, disabled, state, currentTransitionMap } = props; + return ( +
+
+ {/* draggable indicator */} + {!disabled && stateCount != 1 && ( +
+ +
+ )} + {/* state icon */} +
+ +
+ {/* state title and description */} +
+
{state.name}
+

{state.description}

+
+ {/* Transition count */} + +
+ + {!disabled && ( +
+ {/* state mark as default option */} +
+ +
+ + {/* state edit options */} +
+ + +
+
+ )} +
+ ); +}); diff --git a/web/core/components/project-states/state-item.tsx b/web/core/components/project-states/state-item.tsx index a29d5efbceb..4e665969b1f 100644 --- a/web/core/components/project-states/state-item.tsx +++ b/web/core/components/project-states/state-item.tsx @@ -5,17 +5,19 @@ 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"; +// Plane import { IState, TStateGroups } from "@plane/types"; -import { DropIndicator, StateGroupIcon } from "@plane/ui"; +import { DropIndicator } from "@plane/ui"; // components -import { StateUpdate, StateDelete, StateMarksAsDefault } from "@/components/project-states"; +import { StateUpdate } 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"; +// Plane-web +import { StateItemChild } from "@/plane-web/components/workflow"; type TStateItem = { workspaceSlug: string; @@ -126,58 +128,19 @@ export const StateItem: FC = observer((props) => {
- {/* draggable indicator */} - {!disabled && totalStates != 1 && ( -
- -
- )} - - {/* state icon */} -
- -
- - {/* state title and description */} -
-
{state.name}
-

{state.description}

-
- - {!disabled && ( -
- {/* state mark as default option */} -
- -
- - {/* state edit options */} -
- - -
-
- )} +
{/* draggable drop bottom indicator */} diff --git a/web/core/hooks/store/use-project-state.ts b/web/core/hooks/store/use-project-state.ts index caaf76c287c..e7f73546583 100644 --- a/web/core/hooks/store/use-project-state.ts +++ b/web/core/hooks/store/use-project-state.ts @@ -1,8 +1,8 @@ import { useContext } from "react"; // mobx store import { StoreContext } from "@/lib/store-context"; -// types -import { IStateStore } from "@/store/state.store"; +// Plane-web +import { IStateStore } from "@/plane-web/store/state.store"; export const useProjectState = (): IStateStore => { const context = useContext(StoreContext); diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index 70e427043ed..4cca052573a 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -51,7 +51,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { project: { fetchProjectMembers }, } = useMember(); - const { fetchProjectStates } = useProjectState(); + const { fetchProjectStates, fetchProjectStateTransitions } = useProjectState(); const { fetchProjectLabels } = useLabel(); const { getProjectEstimates } = useProjectEstimates(); // router @@ -105,7 +105,12 @@ export const ProjectAuthWrapper: FC = observer((props) => { // fetching project states useSWR( workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null, + workspaceSlug && projectId + ? () => { + fetchProjectStates(workspaceSlug.toString(), projectId.toString()); + fetchProjectStateTransitions(workspaceSlug.toString(), projectId.toString()); + } + : null, { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project estimates @@ -169,7 +174,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { layout="screen-detailed" primaryButtonOnClick={() => { setTrackElement("Projects page empty state"); - toggleCreateProjectModal(true) + toggleCreateProjectModal(true); }} /> diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts index db0ccc39af2..a7c439beedd 100644 --- a/web/core/store/issue/issue-details/issue.store.ts +++ b/web/core/store/issue/issue-details/issue.store.ts @@ -15,7 +15,7 @@ export interface IIssueStoreActions { workspaceSlug: string, projectId: string, issueId: string, - issueStatus?: "DEFAULT" | "DRAFT", + issueStatus?: "DEFAULT" | "DRAFT" ) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -146,7 +146,7 @@ export class IssueStore implements IIssueStore { // fetching states // TODO: check if this function is required - this.rootIssueDetailStore.rootIssueStore.state.fetchProjectStates(workspaceSlug, projectId); + this.rootIssueDetailStore.rootIssueStore.rootStore.state.fetchProjectStates(workspaceSlug, projectId); return issue; }; diff --git a/web/core/store/issue/issue-details/sub_issues.store.ts b/web/core/store/issue/issue-details/sub_issues.store.ts index bdec29db9ec..9d5fd25c4f9 100644 --- a/web/core/store/issue/issue-details/sub_issues.store.ts +++ b/web/core/store/issue/issue-details/sub_issues.store.ts @@ -218,12 +218,12 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { let issueStateGroup: string | undefined = undefined; if (oldIssue.state_id) { - const state = this.rootIssueDetailStore.rootIssueStore.state.getStateById(oldIssue.state_id); + const state = this.rootIssueDetailStore.rootIssueStore.rootStore.state.getStateById(oldIssue.state_id); if (state?.group) oldIssueStateGroup = state.group; } if (issueData.state_id) { - const state = this.rootIssueDetailStore.rootIssueStore.state.getStateById(issueData.state_id); + const state = this.rootIssueDetailStore.rootIssueStore.rootStore.state.getStateById(issueData.state_id); if (state?.group) issueStateGroup = state.group; } @@ -255,7 +255,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { const issue = this.rootIssueDetailStore.issue.getIssueById(issueId); if (issue && issue.state_id) { let issueStateGroup: string | undefined = undefined; - const state = this.rootIssueDetailStore.rootIssueStore.state.getStateById(issue.state_id); + const state = this.rootIssueDetailStore.rootIssueStore.rootStore.state.getStateById(issue.state_id); if (state?.group) issueStateGroup = state.group; if (issueStateGroup) { @@ -290,7 +290,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { const issue = this.rootIssueDetailStore.issue.getIssueById(issueId); if (issue && issue.state_id) { let issueStateGroup: string | undefined = undefined; - const state = this.rootIssueDetailStore.rootIssueStore.state.getStateById(issue.state_id); + const state = this.rootIssueDetailStore.rootIssueStore.rootStore.state.getStateById(issue.state_id); if (state?.group) issueStateGroup = state.group; if (issueStateGroup) { diff --git a/web/core/store/issue/root.store.ts b/web/core/store/issue/root.store.ts index f4d794e4d06..ae67bbe9aea 100644 --- a/web/core/store/issue/root.store.ts +++ b/web/core/store/issue/root.store.ts @@ -53,8 +53,6 @@ export interface IIssueRootStore { issues: IIssueStore; - state: IStateStore; - issueDetail: IIssueDetail; workspaceIssuesFilter: IWorkspaceIssuesFilter; @@ -111,8 +109,6 @@ export class IssueRootStore implements IIssueRootStore { issues: IIssueStore; - state: IStateStore; - issueDetail: IIssueDetail; workspaceIssuesFilter: IWorkspaceIssuesFilter; @@ -191,8 +187,6 @@ export class IssueRootStore implements IIssueRootStore { this.issues = new IssueStore(); - this.state = new StateStore(rootStore); - this.issueDetail = new IssueDetail(this); this.workspaceIssuesFilter = new WorkspaceIssuesFilter(this); diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index af38f51b28c..33e4e7e92ae 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -1,4 +1,7 @@ import { enableStaticRendering } from "mobx-react"; +// plane web store +import { RootStore } from "@/plane-web/store/root.store"; +import { IStateStore, StateStore } from "@/plane-web/store/state.store"; // stores import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store"; import { CycleStore, ICycleStore } from "./cycle.store"; @@ -21,7 +24,6 @@ import { IProjectPageStore, ProjectPageStore } from "./pages/project-page.store" import { IProjectRootStore, ProjectRootStore } from "./project"; import { IProjectViewStore, ProjectViewStore } from "./project-view.store"; import { RouterStore, IRouterStore } from "./router.store"; -import { IStateStore, StateStore } from "./state.store"; import { ThemeStore, IThemeStore } from "./theme.store"; import { ITransientStore, TransientStore } from "./transient.store"; import { IUserStore, UserStore } from "./user"; @@ -72,8 +74,8 @@ export class CoreRootStore { this.moduleFilter = new ModuleFilterStore(this); this.projectView = new ProjectViewStore(this); this.globalView = new GlobalViewStore(this); - this.issue = new IssueRootStore(this); - this.state = new StateStore(this); + this.issue = new IssueRootStore(this as unknown as RootStore); + this.state = new StateStore(this as unknown as RootStore); this.label = new LabelStore(this); this.dashboard = new DashboardStore(this); this.eventTracker = new EventTrackerStore(this); @@ -103,8 +105,8 @@ export class CoreRootStore { this.moduleFilter = new ModuleFilterStore(this); this.projectView = new ProjectViewStore(this); this.globalView = new GlobalViewStore(this); - this.issue = new IssueRootStore(this); - this.state = new StateStore(this); + this.issue = new IssueRootStore(this as unknown as RootStore); + this.state = new StateStore(this as unknown as RootStore); this.label = new LabelStore(this); this.dashboard = new DashboardStore(this); this.eventTracker = new EventTrackerStore(this); diff --git a/web/core/store/state.store.ts b/web/core/store/state.store.ts index 3b7abc1b6cd..b76089126e3 100644 --- a/web/core/store/state.store.ts +++ b/web/core/store/state.store.ts @@ -5,11 +5,11 @@ import { computedFn } from "mobx-utils"; // types import { IState } from "@plane/types"; // helpers +import { convertStringArrayToBooleanObject } from "@/helpers/array.helper"; import { sortStates } from "@/helpers/state.helper"; -// services -import { ProjectStateService } from "@/services/project"; -// plane web store -import { CoreRootStore } from "./root.store"; +// plane web +import { ProjectStateService } from "@/plane-web/services/project/project-state.service"; +import { RootStore } from "@/plane-web/store/root.store"; export interface IStateStore { //Loaders @@ -23,6 +23,10 @@ export interface IStateStore { // computed actions getStateById: (stateId: string | null | undefined) => IState | undefined; getProjectStates: (projectId: string | null | undefined) => IState[] | undefined; + getAvailableProjectStateIdMap: ( + projectId: string | null | undefined, + currStateId: string | null | undefined + ) => { [key: string]: boolean }; // fetch actions fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise; fetchWorkspaceStates: (workspaceSlug: string) => Promise; @@ -42,16 +46,19 @@ export interface IStateStore { stateId: string, payload: Partial ) => Promise; + //Dummy method + fetchProjectStateTransitions: (workspaceSlug: string, projectId: string) => void; } export class StateStore implements IStateStore { stateMap: Record = {}; //loaders fetchedMap: Record = {}; + rootStore: RootStore; router; - stateService; + stateService: ProjectStateService; - constructor(_rootStore: CoreRootStore) { + constructor(_rootStore: RootStore) { makeObservable(this, { // observables stateMap: observable, @@ -71,6 +78,7 @@ export class StateStore implements IStateStore { }); this.stateService = new ProjectStateService(); this.router = _rootStore.router; + this.rootStore = _rootStore; } /** @@ -120,6 +128,20 @@ export class StateStore implements IStateStore { return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); }); + /** + * Returns an object linking state permissions as boolean values + * @param projectId + */ + getAvailableProjectStateIdMap = computedFn( + (projectId: string | null | undefined, currStateId: string | null | undefined) => { + const projectStates = this.getProjectStates(projectId); + + if (!projectStates) return {}; + + return convertStringArrayToBooleanObject(projectStates.map((projectState) => projectState.id)); + } + ); + /** * fetches the stateMap of a project * @param workspaceSlug @@ -261,4 +283,7 @@ export class StateStore implements IStateStore { }); } }; + + // Dummy method + fetchProjectStateTransitions = (workspaceSlug: string, projectId: string) => {}; } diff --git a/web/ee/components/workflow/index.ts b/web/ee/components/workflow/index.ts new file mode 100644 index 00000000000..645183ae137 --- /dev/null +++ b/web/ee/components/workflow/index.ts @@ -0,0 +1 @@ +export * from "ce/components/workflow"; diff --git a/web/ee/services/project/project-state.service.ts b/web/ee/services/project/project-state.service.ts new file mode 100644 index 00000000000..f4a48ae7177 --- /dev/null +++ b/web/ee/services/project/project-state.service.ts @@ -0,0 +1 @@ +export * from "@/services/project/project-state.service"; diff --git a/web/ee/store/state.store.ts b/web/ee/store/state.store.ts new file mode 100644 index 00000000000..a25412ca8ab --- /dev/null +++ b/web/ee/store/state.store.ts @@ -0,0 +1 @@ +export * from "@/store/state.store"; diff --git a/web/ee/types/index.ts b/web/ee/types/index.ts index 0d4b66523e9..4e4c63feb0b 100644 --- a/web/ee/types/index.ts +++ b/web/ee/types/index.ts @@ -1,2 +1,3 @@ export * from "./projects"; export * from "./issue-types"; +export * from "ce/types/state.d"; diff --git a/web/helpers/array.helper.ts b/web/helpers/array.helper.ts index 4efeb352371..028d6c0ea74 100644 --- a/web/helpers/array.helper.ts +++ b/web/helpers/array.helper.ts @@ -102,3 +102,18 @@ export const getValidKeysFromObject = (obj: any) => { return Object.keys(obj).filter((key) => !!obj[key]); }; + +/** + * Convert an array into an object of keys and boolean strue + * @param arrayStrings + * @returns + */ +export const convertStringArrayToBooleanObject = (arrayStrings: string[]) => { + const obj: { [key: string]: boolean } = {}; + + for (const arrayString of arrayStrings) { + obj[arrayString] = true; + } + + return obj; +};