refactor project states to ake way for new features (#6156)

This commit is contained in:
rahulramesha 2024-12-05 12:46:51 +05:30 committed by GitHub
parent 3bccda0c86
commit 66652a5d71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 462 additions and 161 deletions

View file

@ -23,6 +23,7 @@ export type TPosition =
interface ITooltipProps { interface ITooltipProps {
tooltipHeading?: string; tooltipHeading?: string;
tooltipContent: string | React.ReactNode; tooltipContent: string | React.ReactNode;
jsxContent?: string | React.ReactNode;
position?: TPosition; position?: TPosition;
children: JSX.Element; children: JSX.Element;
disabled?: boolean; disabled?: boolean;
@ -38,13 +39,14 @@ export const Tooltip: React.FC<ITooltipProps> = ({
tooltipContent, tooltipContent,
position = "top", position = "top",
children, children,
jsxContent,
disabled = false, disabled = false,
className = "", className = "",
openDelay = 200, openDelay = 200,
closeDelay, closeDelay,
isMobile = false, isMobile = false,
renderByDefault = true, //FIXME: tooltip should always render on hover and not by default, this is a temporary fix renderByDefault = true, //FIXME: tooltip should always render on hover and not by default, this is a temporary fix
}) => { }: ITooltipProps) => {
const toolTipRef = useRef<HTMLDivElement | null>(null); const toolTipRef = useRef<HTMLDivElement | null>(null);
const [shouldRender, setShouldRender] = useState(renderByDefault); const [shouldRender, setShouldRender] = useState(renderByDefault);
@ -79,18 +81,22 @@ export const Tooltip: React.FC<ITooltipProps> = ({
hoverOpenDelay={openDelay} hoverOpenDelay={openDelay}
hoverCloseDelay={closeDelay} hoverCloseDelay={closeDelay}
content={ content={
<div jsxContent ? (
className={cn( <>{jsxContent}</>
"relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md", ) : (
{ <div
hidden: isMobile, className={cn(
}, "relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md",
className {
)} hidden: isMobile,
> },
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>} className
{tooltipContent} )}
</div> >
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent}
</div>
)
} }
position={position} position={position}
renderTarget={({ renderTarget={({

View file

@ -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) => (
<div className={cn("flex w-full px-3 h-6 items-center justify-start gap-2 text-sm bg-custom-background-90")}>
<>
<Plus className="h-4 w-4" color="#8591AD" />
<span className="text-custom-text-400 font-medium"> Add Transition</span>
<div className="text-white bg-custom-background-80 font-semibold px-2 rounded-lg">Pro</div>
</>
</div>
);

View file

@ -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";

View file

@ -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<boolean>) => void;
};
export const StateItemChild = observer((props: StateItemChildProps) => {
const { workspaceSlug, projectId, stateCount, setUpdateStateModal, disabled, state } = props;
return (
<div className="flex flex-col w-full items-center justify-between">
<StateItemTitle
workspaceSlug={workspaceSlug}
projectId={projectId}
setUpdateStateModal={setUpdateStateModal}
stateCount={stateCount}
disabled={disabled}
state={state}
/>
<AddStateTransition workspaceSlug={workspaceSlug} projectId={projectId} parentStateId={state.id} />
</div>
);
});

View file

@ -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 (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`${className} ${active ? "bg-custom-background-80" : ""} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
);
});

View file

@ -0,0 +1,7 @@
import { IStateWorkFlow } from "@/plane-web/types";
type Props = {
currentTransitionMap?: IStateWorkFlow;
};
export const StateTransitionCount = (props: Props) => <></>;

View file

@ -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
) => {},
});

View file

@ -0,0 +1,6 @@
type Props = {
parentStateId: string;
className?: string;
};
export const WorkFlowDisabledMessage = (props: Props) => <></>;

View file

@ -0,0 +1,8 @@
import { TIssueGroupByOptions } from "@plane/types";
type Props = {
groupBy?: TIssueGroupByOptions;
groupId: string | undefined;
};
export const WorkFlowGroupTree = (props: Props) => <></>;

View file

@ -1,2 +1,2 @@
export * from "./project"; export * from "./project";
export * from "./workspace.service"; export * from "./workspace.service";

View file

@ -0,0 +1 @@
export * from "@/services/project/project-state.service";

View file

@ -0,0 +1 @@
export * from "@/store/state.store";

View file

@ -1,3 +1,4 @@
export * from "./projects"; export * from "./projects";
export * from "./issue-types"; export * from "./issue-types";
export * from "./gantt-chart"; export * from "./gantt-chart";
export * from "./state.d";

8
web/ce/types/state.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
export interface IStateTransition {
transition_state_id: string;
actors: string[];
}
export interface IStateWorkFlow {
[transitionId: string]: IStateTransition;
}

View file

@ -1,10 +1,10 @@
"use client"; "use client";
import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react"; import { ChevronDown, Search } from "lucide-react";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
// ui // ui
import { ComboDropDown, Spinner, StateGroupIcon } from "@plane/ui"; import { ComboDropDown, Spinner, StateGroupIcon } from "@plane/ui";
@ -13,6 +13,8 @@ import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
import { useDropdown } from "@/hooks/use-dropdown"; import { useDropdown } from "@/hooks/use-dropdown";
// Plane-web
import { StateOption } from "@/plane-web/components/workflow";
// components // components
import { DropdownButton } from "./buttons"; import { DropdownButton } from "./buttons";
// constants // constants
@ -30,6 +32,8 @@ type Props = TDropdownProps & {
showDefaultState?: boolean; showDefaultState?: boolean;
value: string | undefined | null; value: string | undefined | null;
renderByDefault?: boolean; renderByDefault?: boolean;
stateIds?: string[];
filterAvailableStateIds?: boolean;
}; };
export const StateDropdown: React.FC<Props> = observer((props) => { export const StateDropdown: React.FC<Props> = observer((props) => {
@ -52,6 +56,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
tabIndex, tabIndex,
value, value,
renderByDefault = true, renderByDefault = true,
stateIds,
filterAvailableStateIds = true,
} = props; } = props;
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -78,16 +84,18 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
// store hooks // store hooks
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
const { fetchProjectStates, getProjectStates, getStateById } = useProjectState(); const { fetchProjectStates, getProjectStates, getStateById } = useProjectState();
const statesList = getProjectStates(projectId); const statesList = stateIds
const defaultState = statesList?.find((state) => state.default); ? 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 stateValue = !!value ? value : showDefaultState ? defaultState?.id : undefined;
const options = statesList?.map((state) => ({ const options = statesList?.map((state) => ({
value: state.id, value: state?.id,
query: `${state?.name}`, query: `${state?.name}`,
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StateGroupIcon stateGroup={state.group} color={state.color} className="h-3 w-3 flex-shrink-0" /> <StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{state?.name}</span> <span className="flex-grow truncate">{state?.name}</span>
</div> </div>
), ),
@ -226,22 +234,14 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
{filteredOptions ? ( {filteredOptions ? (
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((option) => ( filteredOptions.map((option) => (
<Combobox.Option <StateOption
key={option.value} key={option.value}
value={option.value} option={option}
className={({ active, selected }) => projectId={projectId}
`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ filterAvailableStateIds={filterAvailableStateIds}
active ? "bg-custom-background-80" : "" selectedValue={value}
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` className="flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5"
} />
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
)) ))
) : ( ) : (
<p className="px-1.5 py-1 italic text-custom-text-400">No matches found</p> <p className="px-1.5 py-1 italic text-custom-text-400">No matches found</p>

View file

@ -1,10 +1,16 @@
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
// Plane
import { TIssueOrderByOptions } from "@plane/types"; import { TIssueOrderByOptions } from "@plane/types";
// constants
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue"; import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
// helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// Plane-web
import { WorkFlowDisabledMessage } from "@/plane-web/components/workflow";
type Props = { type Props = {
dragColumnOrientation: "justify-start" | "justify-center" | "justify-end"; dragColumnOrientation: "justify-start" | "justify-center" | "justify-end";
workflowDisabledSource?: string;
canOverlayBeVisible: boolean; canOverlayBeVisible: boolean;
isDropDisabled: boolean; isDropDisabled: boolean;
dropErrorMessage?: string; dropErrorMessage?: string;
@ -16,6 +22,7 @@ export const GroupDragOverlay = (props: Props) => {
const { const {
dragColumnOrientation, dragColumnOrientation,
canOverlayBeVisible, canOverlayBeVisible,
workflowDisabledSource,
isDropDisabled, isDropDisabled,
dropErrorMessage, dropErrorMessage,
orderBy, orderBy,
@ -35,33 +42,37 @@ export const GroupDragOverlay = (props: Props) => {
{ hidden: !shouldOverlayBeVisible } { hidden: !shouldOverlayBeVisible }
)} )}
> >
<div {workflowDisabledSource ? (
className={cn( <WorkFlowDisabledMessage parentStateId={workflowDisabledSource} className="my-2" />
"p-3 my-8 flex flex-col rounded items-center", ) : (
{ <div
"text-custom-text-200": shouldOverlayBeVisible, className={cn(
}, "p-3 my-8 flex flex-col rounded items-center",
{ {
"text-custom-text-error": isDropDisabled, "text-custom-text-200": shouldOverlayBeVisible,
} },
)} {
> "text-custom-text-error": isDropDisabled,
{dropErrorMessage ? ( }
<div className="flex items-center"> )}
<AlertCircle width={13} height={13} /> &nbsp; >
<span>{dropErrorMessage}</span> {dropErrorMessage ? (
</div> <div className="flex items-center">
) : ( <AlertCircle width={13} height={13} /> &nbsp;
<> <span>{dropErrorMessage}</span>
{readableOrderBy && ( </div>
<span> ) : (
The layout is ordered by <span className="font-semibold">{readableOrderBy}</span>. <>
</span> {readableOrderBy && (
)} <span>
<span>Drop here to move the issue.</span> The layout is ordered by <span className="font-semibold">{readableOrderBy}</span>.
</> </span>
)} )}
</div> <span>Drop here to move the issue.</span>
</>
)}
</div>
)}
</div> </div>
); );
}; };

View file

@ -15,7 +15,8 @@ import { CreateUpdateIssueModal } from "@/components/issues";
// hooks // hooks
import { useEventTracker } from "@/hooks/store"; import { useEventTracker } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
// types // Plane-web
import { WorkFlowGroupTree } from "@/plane-web/components/workflow";
interface IHeaderGroupByCard { interface IHeaderGroupByCard {
sub_group_by: TIssueGroupByOptions | undefined; sub_group_by: TIssueGroupByOptions | undefined;
@ -33,6 +34,7 @@ interface IHeaderGroupByCard {
export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => { export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
const { const {
group_by,
sub_group_by, sub_group_by,
column_id, column_id,
icon, icon,
@ -130,6 +132,8 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
</div> </div>
</div> </div>
<WorkFlowGroupTree groupBy={group_by} groupId={column_id} />
{sub_group_by === null && ( {sub_group_by === null && (
<div <div
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80" className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"

View file

@ -1,7 +1,10 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Circle, ChevronDown, ChevronUp } from "lucide-react"; import { Circle, ChevronDown, ChevronUp } from "lucide-react";
import { TIssueKanbanFilters } from "@plane/types"; // Plane
import { TIssueGroupByOptions, TIssueKanbanFilters } from "@plane/types";
// Plane-web
import { WorkFlowGroupTree } from "@/plane-web/components/workflow";
// mobx // mobx
interface IHeaderSubGroupByCard { interface IHeaderSubGroupByCard {
@ -10,11 +13,12 @@ interface IHeaderSubGroupByCard {
count: number; count: number;
column_id: string; column_id: string;
collapsedGroups: TIssueKanbanFilters; collapsedGroups: TIssueKanbanFilters;
sub_group_by: TIssueGroupByOptions | undefined;
handleCollapsedGroups: (toggle: "group_by" | "sub_group_by", value: string) => void; handleCollapsedGroups: (toggle: "group_by" | "sub_group_by", value: string) => void;
} }
export const HeaderSubGroupByCard: FC<IHeaderSubGroupByCard> = observer((props) => { export const HeaderSubGroupByCard: FC<IHeaderSubGroupByCard> = observer((props) => {
const { icon, title, count, column_id, collapsedGroups, handleCollapsedGroups } = props; const { icon, title, count, column_id, collapsedGroups, sub_group_by, handleCollapsedGroups } = props;
return ( return (
<div <div
className={`relative flex w-full flex-shrink-0 flex-row items-center gap-1 rounded-sm py-1.5 cursor-pointer`} className={`relative flex w-full flex-shrink-0 flex-row items-center gap-1 rounded-sm py-1.5 cursor-pointer`}
@ -36,6 +40,8 @@ export const HeaderSubGroupByCard: FC<IHeaderSubGroupByCard> = observer((props)
<div className="line-clamp-1 text-custom-text-100">{title}</div> <div className="line-clamp-1 text-custom-text-100">{title}</div>
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div> <div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
</div> </div>
<WorkFlowGroupTree groupBy={sub_group_by} groupId={column_id} />
</div> </div>
); );
}); });

View file

@ -26,6 +26,9 @@ import { cn } from "@/helpers/common.helper";
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { useIssuesStore } from "@/hooks/use-issue-layout-store";
// Plane-web
import { useWorkFlowFDragNDrop } from "@/plane-web/components/workflow";
//
import { GroupDragOverlay } from "../group-drag-overlay"; import { GroupDragOverlay } from "../group-drag-overlay";
import { TRenderQuickActions } from "../list/list-view-types"; import { TRenderQuickActions } from "../list/list-view-types";
import { GroupDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload, getIssueBlockId } from "../utils"; import { GroupDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload, getIssueBlockId } from "../utils";
@ -103,6 +106,11 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
); );
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
const { workflowDisabledSource, isWorkflowDropDisabled, handleWorkFlowState } = useWorkFlowFDragNDrop(
group_by,
sub_group_by
);
// Enable Kanban Columns as Drop Targets // Enable Kanban Columns as Drop Targets
useEffect(() => { useEffect(() => {
const element = columnRef.current; const element = columnRef.current;
@ -113,14 +121,24 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
dropTargetForElements({ dropTargetForElements({
element, element,
getData: () => ({ groupId, subGroupId: sub_group_id, columnId: `${groupId}__${sub_group_id}`, type: "COLUMN" }), getData: () => ({ groupId, subGroupId: sub_group_id, columnId: `${groupId}__${sub_group_id}`, type: "COLUMN" }),
onDragEnter: () => { onDragEnter: (payload) => {
const source = getSourceFromDropPayload(payload);
setIsDraggingOverColumn(true); setIsDraggingOverColumn(true);
// handle if dragging a workflowState
if (source) {
handleWorkFlowState(source?.groupId, groupId, source?.subGroupId, sub_group_id);
}
}, },
onDragLeave: () => { onDragLeave: () => {
setIsDraggingOverColumn(false); setIsDraggingOverColumn(false);
}, },
onDragStart: () => { onDragStart: (payload) => {
const source = getSourceFromDropPayload(payload);
setIsDraggingOverColumn(true); setIsDraggingOverColumn(true);
// handle if dragging a workflowState
if (source) {
handleWorkFlowState(source?.groupId, groupId, source?.subGroupId, sub_group_id);
}
}, },
onDrop: (payload) => { onDrop: (payload) => {
setIsDraggingOverColumn(false); setIsDraggingOverColumn(false);
@ -129,7 +147,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
if (!source || !destination) return; if (!source || !destination) return;
if (isDropDisabled) { if (isWorkflowDropDisabled || isDropDisabled) {
dropErrorMessage && dropErrorMessage &&
setToast({ setToast({
type: TOAST_TYPE.WARNING, type: TOAST_TYPE.WARNING,
@ -158,6 +176,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
setIsDraggingOverColumn, setIsDraggingOverColumn,
orderBy, orderBy,
isDropDisabled, isDropDisabled,
isWorkflowDropDisabled,
dropErrorMessage, dropErrorMessage,
handleOnDrop, handleOnDrop,
]); ]);
@ -237,7 +256,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
); );
const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults; 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; const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
return ( return (
@ -253,7 +272,8 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
<GroupDragOverlay <GroupDragOverlay
dragColumnOrientation={sub_group_by ? "justify-start" : "justify-center"} dragColumnOrientation={sub_group_by ? "justify-start" : "justify-center"}
canOverlayBeVisible={canOverlayBeVisible} canOverlayBeVisible={canOverlayBeVisible}
isDropDisabled={isDropDisabled} isDropDisabled={isWorkflowDropDisabled || isDropDisabled}
workflowDisabledSource={workflowDisabledSource}
dropErrorMessage={dropErrorMessage} dropErrorMessage={dropErrorMessage}
orderBy={orderBy} orderBy={orderBy}
isDraggingOverColumn={isDraggingOverColumn} isDraggingOverColumn={isDraggingOverColumn}

View file

@ -170,6 +170,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
count={issueCount} count={issueCount}
collapsedGroups={collapsedGroups} collapsedGroups={collapsedGroups}
handleCollapsedGroups={handleCollapsedGroups} handleCollapsedGroups={handleCollapsedGroups}
sub_group_by={sub_group_by}
/> />
</Row> </Row>
</div> </div>

View file

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
import { CircleDashed, Plus } from "lucide-react"; import { CircleDashed, Plus } from "lucide-react";
// types // types
import { TIssue, ISearchIssueResponse } from "@plane/types"; import { TIssue, ISearchIssueResponse, TIssueGroupByOptions } from "@plane/types";
// ui // ui
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
@ -18,9 +18,12 @@ import { cn } from "@/helpers/common.helper";
import { useEventTracker } from "@/hooks/store"; import { useEventTracker } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { TSelectionHelper } from "@/hooks/use-multiple-select";
// Plane-web
import { WorkFlowGroupTree } from "@/plane-web/components/workflow";
interface IHeaderGroupByCard { interface IHeaderGroupByCard {
groupID: string; groupID: string;
groupBy: TIssueGroupByOptions;
icon?: React.ReactNode; icon?: React.ReactNode;
title: string; title: string;
count: number; count: number;
@ -35,6 +38,7 @@ interface IHeaderGroupByCard {
export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => { export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
const { const {
groupID, groupID,
groupBy,
icon, icon,
title, title,
count, count,
@ -43,7 +47,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
disableIssueCreation, disableIssueCreation,
addIssuesToView, addIssuesToView,
selectionHelpers, selectionHelpers,
handleCollapsedGroups handleCollapsedGroups,
} = props; } = props;
// states // states
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -112,6 +116,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
> >
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div> <div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div>
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div> <div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
<WorkFlowGroupTree groupBy={groupBy} groupId={groupID} />
</div> </div>
{!disableIssueCreation && {!disableIssueCreation &&

View file

@ -26,7 +26,9 @@ import { useProjectState } from "@/hooks/store";
import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { useIssuesStore } from "@/hooks/use-issue-layout-store";
import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { TSelectionHelper } from "@/hooks/use-multiple-select";
// components // Plane-web
import { useWorkFlowFDragNDrop } from "@/plane-web/components/workflow";
//
import { GroupDragOverlay } from "../group-drag-overlay"; import { GroupDragOverlay } from "../group-drag-overlay";
import { ListQuickAddIssueButton, QuickAddIssueRoot } from "../quick-add"; import { ListQuickAddIssueButton, QuickAddIssueRoot } from "../quick-add";
import { import {
@ -93,7 +95,7 @@ export const ListGroup = observer((props: Props) => {
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
const [dragColumnOrientation, setDragColumnOrientation] = useState<"justify-start" | "justify-end">("justify-start"); 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<HTMLDivElement | null>(null); const groupRef = useRef<HTMLDivElement | null>(null);
const { projectId } = useParams(); const { projectId } = useParams();
@ -105,6 +107,8 @@ export const ListGroup = observer((props: Props) => {
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null); const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
const { workflowDisabledSource, isWorkflowDropDisabled, handleWorkFlowState } = useWorkFlowFDragNDrop(group_by);
const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0; const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0;
const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults; const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults;
const isPaginating = !!getIssueLoader(group.id); const isPaginating = !!getIssueLoader(group.id);
@ -185,6 +189,8 @@ export const ListGroup = observer((props: Props) => {
const sourceGroupId = source?.data?.groupId as string | undefined; const sourceGroupId = source?.data?.groupId as string | undefined;
const currentGroupId = group.id; const currentGroupId = group.id;
sourceGroupId && handleWorkFlowState(sourceGroupId, currentGroupId);
const sourceIndex = getGroupIndex(sourceGroupId); const sourceIndex = getGroupIndex(sourceGroupId);
const currentIndex = getGroupIndex(currentGroupId); const currentIndex = getGroupIndex(currentGroupId);
@ -201,7 +207,7 @@ export const ListGroup = observer((props: Props) => {
if (!source || !destination) return; if (!source || !destination) return;
if (group.isDropDisabled) { if (isWorkflowDropDisabled || group.isDropDisabled) {
group.dropErrorMessage && group.dropErrorMessage &&
setToast({ setToast({
type: TOAST_TYPE.WARNING, type: TOAST_TYPE.WARNING,
@ -215,17 +221,25 @@ export const ListGroup = observer((props: Props) => {
highlightIssueOnDrop(getIssueBlockId(source.id, destination?.groupId), orderBy !== "sort_order"); highlightIssueOnDrop(getIssueBlockId(source.id, destination?.groupId), orderBy !== "sort_order");
if(!isExpanded){ if (!isExpanded) {
handleCollapsedGroups(group.id) handleCollapsedGroups(group.id);
} }
}, },
}) })
); );
}, [groupRef?.current, group, orderBy, getGroupIndex, setDragColumnOrientation, setIsDraggingOverColumn]); }, [
groupRef?.current,
group,
orderBy,
getGroupIndex,
setDragColumnOrientation,
setIsDraggingOverColumn,
isWorkflowDropDisabled,
]);
const isDragAllowed = const isDragAllowed =
!!group_by && DRAG_ALLOWED_GROUPS.includes(group_by) && canEditProperties(projectId?.toString()); !!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 isGroupByCreatedBy = group_by === "created_by";
const shouldExpand = (!!groupIssueCount && isExpanded) || !group_by; const shouldExpand = (!!groupIssueCount && isExpanded) || !group_by;
@ -245,6 +259,7 @@ export const ListGroup = observer((props: Props) => {
> >
<HeaderGroupByCard <HeaderGroupByCard
groupID={group.id} groupID={group.id}
groupBy={group_by}
icon={group.icon} icon={group.icon}
title={group.name || ""} title={group.name || ""}
count={groupIssueCount} count={groupIssueCount}
@ -261,7 +276,8 @@ export const ListGroup = observer((props: Props) => {
<GroupDragOverlay <GroupDragOverlay
dragColumnOrientation={dragColumnOrientation} dragColumnOrientation={dragColumnOrientation}
canOverlayBeVisible={canOverlayBeVisible} canOverlayBeVisible={canOverlayBeVisible}
isDropDisabled={!!group.isDropDisabled} isDropDisabled={isWorkflowDropDisabled || !!group.isDropDisabled}
workflowDisabledSource={workflowDisabledSource}
dropErrorMessage={group.dropErrorMessage} dropErrorMessage={group.dropErrorMessage}
orderBy={orderBy} orderBy={orderBy}
isDraggingOverColumn={isDraggingOverColumn} isDraggingOverColumn={isDraggingOverColumn}

View file

@ -10,3 +10,4 @@ export * from "./options";
export * from "./loader"; export * from "./loader";
export * from "./create-update"; export * from "./create-update";
export * from "./state-delete-modal"; export * from "./state-delete-modal";
export * from "./state-item-title";

View file

@ -16,11 +16,17 @@ type TProjectState = {
export const ProjectStateRoot: FC<TProjectState> = observer((props) => { export const ProjectStateRoot: FC<TProjectState> = observer((props) => {
const { workspaceSlug, projectId } = props; const { workspaceSlug, projectId } = props;
// hooks // hooks
const { groupedProjectStates, fetchProjectStates } = useProjectState(); const { groupedProjectStates, fetchProjectStates, fetchProjectStateTransitions } = useProjectState();
useSWR( useSWR(
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, 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 // Loader

View file

@ -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<boolean>) => 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 (
<div className="py-4 px-2 flex items-center gap-2 w-full justify-between">
<div className="flex items-center gap-2">
{/* draggable indicator */}
{!disabled && stateCount != 1 && (
<div className="flex-shrink-0 w-3 h-3 rounded-sm absolute left-0 hidden group-hover:flex justify-center items-center transition-colors bg-custom-background-90 cursor-pointer text-custom-text-200 hover:text-custom-text-100">
<GripVertical className="w-3 h-3" />
</div>
)}
{/* state icon */}
<div className="flex-shrink-0">
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
</div>
{/* state title and description */}
<div className="text-sm px-2 min-h-5">
<h6 className="text-sm font-medium">{state.name}</h6>
<p className="text-xs text-custom-text-200">{state.description}</p>
</div>
{/* Transition count */}
<StateTransitionCount currentTransitionMap={currentTransitionMap} />
</div>
{!disabled && (
<div className="hidden group-hover:flex items-center gap-2">
{/* state mark as default option */}
<div className="flex-shrink-0 text-xs transition-all">
<StateMarksAsDefault
workspaceSlug={workspaceSlug}
projectId={projectId}
stateId={state.id}
isDefault={state.default ? true : false}
/>
</div>
{/* state edit options */}
<div className="flex items-center gap-1 transition-all">
<button
className="flex-shrink-0 w-5 h-5 rounded flex justify-center items-center overflow-hidden transition-colors hover:bg-custom-background-80 cursor-pointer text-custom-text-200 hover:text-custom-text-100"
onClick={() => setUpdateStateModal(true)}
>
<Pencil className="w-3 h-3" />
</button>
<StateDelete workspaceSlug={workspaceSlug} projectId={projectId} totalStates={stateCount} state={state} />
</div>
</div>
)}
</div>
);
});

View file

@ -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 { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { GripVertical, Pencil } from "lucide-react"; // Plane
import { IState, TStateGroups } from "@plane/types"; import { IState, TStateGroups } from "@plane/types";
import { DropIndicator, StateGroupIcon } from "@plane/ui"; import { DropIndicator } from "@plane/ui";
// components // components
import { StateUpdate, StateDelete, StateMarksAsDefault } from "@/components/project-states"; import { StateUpdate } from "@/components/project-states";
// helpers // helpers
import { TDraggableData } from "@/constants/state"; import { TDraggableData } from "@/constants/state";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getCurrentStateSequence } from "@/helpers/state.helper"; import { getCurrentStateSequence } from "@/helpers/state.helper";
// hooks // hooks
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
// Plane-web
import { StateItemChild } from "@/plane-web/components/workflow";
type TStateItem = { type TStateItem = {
workspaceSlug: string; workspaceSlug: string;
@ -126,58 +128,19 @@ export const StateItem: FC<TStateItem> = observer((props) => {
<div <div
ref={draggableElementRef} ref={draggableElementRef}
className={cn( className={cn(
"relative border border-custom-border-100 rounded p-3 px-3.5 flex items-center gap-2 group my-1", "relative border border-custom-border-100 rounded group",
isDragging ? `opacity-50` : `opacity-100`, isDragging ? `opacity-50` : `opacity-100`,
totalStates === 1 ? `cursor-auto` : `cursor-grab` totalStates === 1 ? `cursor-auto` : `cursor-grab`
)} )}
> >
{/* draggable indicator */} <StateItemChild
{!disabled && totalStates != 1 && ( workspaceSlug={workspaceSlug}
<div className="flex-shrink-0 w-3 h-3 rounded-sm absolute left-0 hidden group-hover:flex justify-center items-center transition-colors bg-custom-background-90 cursor-pointer text-custom-text-200 hover:text-custom-text-100"> projectId={projectId}
<GripVertical className="w-3 h-3" /> setUpdateStateModal={setUpdateStateModal}
</div> stateCount={totalStates}
)} disabled={disabled}
state={state}
{/* state icon */} />
<div className="flex-shrink-0">
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
</div>
{/* state title and description */}
<div className="w-full text-sm px-2 min-h-5">
<h6 className="text-sm font-medium">{state.name}</h6>
<p className="text-xs text-custom-text-200">{state.description}</p>
</div>
{!disabled && (
<div className="hidden group-hover:flex items-center gap-2">
{/* state mark as default option */}
<div className="flex-shrink-0 text-xs transition-all">
<StateMarksAsDefault
workspaceSlug={workspaceSlug}
projectId={projectId}
stateId={state.id}
isDefault={state.default ? true : false}
/>
</div>
{/* state edit options */}
<div className="flex items-center gap-1 transition-all">
<button
className="flex-shrink-0 w-5 h-5 rounded flex justify-center items-center overflow-hidden transition-colors hover:bg-custom-background-80 cursor-pointer text-custom-text-200 hover:text-custom-text-100"
onClick={() => setUpdateStateModal(true)}
>
<Pencil className="w-3 h-3" />
</button>
<StateDelete
workspaceSlug={workspaceSlug}
projectId={projectId}
totalStates={totalStates}
state={state}
/>
</div>
</div>
)}
</div> </div>
{/* draggable drop bottom indicator */} {/* draggable drop bottom indicator */}

View file

@ -1,8 +1,8 @@
import { useContext } from "react"; import { useContext } from "react";
// mobx store // mobx store
import { StoreContext } from "@/lib/store-context"; import { StoreContext } from "@/lib/store-context";
// types // Plane-web
import { IStateStore } from "@/store/state.store"; import { IStateStore } from "@/plane-web/store/state.store";
export const useProjectState = (): IStateStore => { export const useProjectState = (): IStateStore => {
const context = useContext(StoreContext); const context = useContext(StoreContext);

View file

@ -51,7 +51,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
const { const {
project: { fetchProjectMembers }, project: { fetchProjectMembers },
} = useMember(); } = useMember();
const { fetchProjectStates } = useProjectState(); const { fetchProjectStates, fetchProjectStateTransitions } = useProjectState();
const { fetchProjectLabels } = useLabel(); const { fetchProjectLabels } = useLabel();
const { getProjectEstimates } = useProjectEstimates(); const { getProjectEstimates } = useProjectEstimates();
// router // router
@ -105,7 +105,12 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// fetching project states // fetching project states
useSWR( useSWR(
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, 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 } { revalidateIfStale: false, revalidateOnFocus: false }
); );
// fetching project estimates // fetching project estimates
@ -169,7 +174,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
layout="screen-detailed" layout="screen-detailed"
primaryButtonOnClick={() => { primaryButtonOnClick={() => {
setTrackElement("Projects page empty state"); setTrackElement("Projects page empty state");
toggleCreateProjectModal(true) toggleCreateProjectModal(true);
}} }}
/> />
</div> </div>

View file

@ -15,7 +15,7 @@ export interface IIssueStoreActions {
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
issueId: string, issueId: string,
issueStatus?: "DEFAULT" | "DRAFT", issueStatus?: "DEFAULT" | "DRAFT"
) => Promise<TIssue>; ) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -146,7 +146,7 @@ export class IssueStore implements IIssueStore {
// fetching states // fetching states
// TODO: check if this function is required // TODO: check if this function is required
this.rootIssueDetailStore.rootIssueStore.state.fetchProjectStates(workspaceSlug, projectId); this.rootIssueDetailStore.rootIssueStore.rootStore.state.fetchProjectStates(workspaceSlug, projectId);
return issue; return issue;
}; };

View file

@ -218,12 +218,12 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
let issueStateGroup: string | undefined = undefined; let issueStateGroup: string | undefined = undefined;
if (oldIssue.state_id) { 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 (state?.group) oldIssueStateGroup = state.group;
} }
if (issueData.state_id) { 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; if (state?.group) issueStateGroup = state.group;
} }
@ -255,7 +255,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
const issue = this.rootIssueDetailStore.issue.getIssueById(issueId); const issue = this.rootIssueDetailStore.issue.getIssueById(issueId);
if (issue && issue.state_id) { if (issue && issue.state_id) {
let issueStateGroup: string | undefined = undefined; 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 (state?.group) issueStateGroup = state.group;
if (issueStateGroup) { if (issueStateGroup) {
@ -290,7 +290,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
const issue = this.rootIssueDetailStore.issue.getIssueById(issueId); const issue = this.rootIssueDetailStore.issue.getIssueById(issueId);
if (issue && issue.state_id) { if (issue && issue.state_id) {
let issueStateGroup: string | undefined = undefined; 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 (state?.group) issueStateGroup = state.group;
if (issueStateGroup) { if (issueStateGroup) {

View file

@ -53,8 +53,6 @@ export interface IIssueRootStore {
issues: IIssueStore; issues: IIssueStore;
state: IStateStore;
issueDetail: IIssueDetail; issueDetail: IIssueDetail;
workspaceIssuesFilter: IWorkspaceIssuesFilter; workspaceIssuesFilter: IWorkspaceIssuesFilter;
@ -111,8 +109,6 @@ export class IssueRootStore implements IIssueRootStore {
issues: IIssueStore; issues: IIssueStore;
state: IStateStore;
issueDetail: IIssueDetail; issueDetail: IIssueDetail;
workspaceIssuesFilter: IWorkspaceIssuesFilter; workspaceIssuesFilter: IWorkspaceIssuesFilter;
@ -191,8 +187,6 @@ export class IssueRootStore implements IIssueRootStore {
this.issues = new IssueStore(); this.issues = new IssueStore();
this.state = new StateStore(rootStore);
this.issueDetail = new IssueDetail(this); this.issueDetail = new IssueDetail(this);
this.workspaceIssuesFilter = new WorkspaceIssuesFilter(this); this.workspaceIssuesFilter = new WorkspaceIssuesFilter(this);

View file

@ -1,4 +1,7 @@
import { enableStaticRendering } from "mobx-react"; 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 // stores
import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store"; import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store";
import { CycleStore, ICycleStore } from "./cycle.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 { IProjectRootStore, ProjectRootStore } from "./project";
import { IProjectViewStore, ProjectViewStore } from "./project-view.store"; import { IProjectViewStore, ProjectViewStore } from "./project-view.store";
import { RouterStore, IRouterStore } from "./router.store"; import { RouterStore, IRouterStore } from "./router.store";
import { IStateStore, StateStore } from "./state.store";
import { ThemeStore, IThemeStore } from "./theme.store"; import { ThemeStore, IThemeStore } from "./theme.store";
import { ITransientStore, TransientStore } from "./transient.store"; import { ITransientStore, TransientStore } from "./transient.store";
import { IUserStore, UserStore } from "./user"; import { IUserStore, UserStore } from "./user";
@ -72,8 +74,8 @@ export class CoreRootStore {
this.moduleFilter = new ModuleFilterStore(this); this.moduleFilter = new ModuleFilterStore(this);
this.projectView = new ProjectViewStore(this); this.projectView = new ProjectViewStore(this);
this.globalView = new GlobalViewStore(this); this.globalView = new GlobalViewStore(this);
this.issue = new IssueRootStore(this); this.issue = new IssueRootStore(this as unknown as RootStore);
this.state = new StateStore(this); this.state = new StateStore(this as unknown as RootStore);
this.label = new LabelStore(this); this.label = new LabelStore(this);
this.dashboard = new DashboardStore(this); this.dashboard = new DashboardStore(this);
this.eventTracker = new EventTrackerStore(this); this.eventTracker = new EventTrackerStore(this);
@ -103,8 +105,8 @@ export class CoreRootStore {
this.moduleFilter = new ModuleFilterStore(this); this.moduleFilter = new ModuleFilterStore(this);
this.projectView = new ProjectViewStore(this); this.projectView = new ProjectViewStore(this);
this.globalView = new GlobalViewStore(this); this.globalView = new GlobalViewStore(this);
this.issue = new IssueRootStore(this); this.issue = new IssueRootStore(this as unknown as RootStore);
this.state = new StateStore(this); this.state = new StateStore(this as unknown as RootStore);
this.label = new LabelStore(this); this.label = new LabelStore(this);
this.dashboard = new DashboardStore(this); this.dashboard = new DashboardStore(this);
this.eventTracker = new EventTrackerStore(this); this.eventTracker = new EventTrackerStore(this);

View file

@ -5,11 +5,11 @@ import { computedFn } from "mobx-utils";
// types // types
import { IState } from "@plane/types"; import { IState } from "@plane/types";
// helpers // helpers
import { convertStringArrayToBooleanObject } from "@/helpers/array.helper";
import { sortStates } from "@/helpers/state.helper"; import { sortStates } from "@/helpers/state.helper";
// services // plane web
import { ProjectStateService } from "@/services/project"; import { ProjectStateService } from "@/plane-web/services/project/project-state.service";
// plane web store import { RootStore } from "@/plane-web/store/root.store";
import { CoreRootStore } from "./root.store";
export interface IStateStore { export interface IStateStore {
//Loaders //Loaders
@ -23,6 +23,10 @@ export interface IStateStore {
// computed actions // computed actions
getStateById: (stateId: string | null | undefined) => IState | undefined; getStateById: (stateId: string | null | undefined) => IState | undefined;
getProjectStates: (projectId: 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 // fetch actions
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<IState[]>; fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<IState[]>;
fetchWorkspaceStates: (workspaceSlug: string) => Promise<IState[]>; fetchWorkspaceStates: (workspaceSlug: string) => Promise<IState[]>;
@ -42,16 +46,19 @@ export interface IStateStore {
stateId: string, stateId: string,
payload: Partial<IState> payload: Partial<IState>
) => Promise<void>; ) => Promise<void>;
//Dummy method
fetchProjectStateTransitions: (workspaceSlug: string, projectId: string) => void;
} }
export class StateStore implements IStateStore { export class StateStore implements IStateStore {
stateMap: Record<string, IState> = {}; stateMap: Record<string, IState> = {};
//loaders //loaders
fetchedMap: Record<string, boolean> = {}; fetchedMap: Record<string, boolean> = {};
rootStore: RootStore;
router; router;
stateService; stateService: ProjectStateService;
constructor(_rootStore: CoreRootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
// observables // observables
stateMap: observable, stateMap: observable,
@ -71,6 +78,7 @@ export class StateStore implements IStateStore {
}); });
this.stateService = new ProjectStateService(); this.stateService = new ProjectStateService();
this.router = _rootStore.router; 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)); 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 * fetches the stateMap of a project
* @param workspaceSlug * @param workspaceSlug
@ -261,4 +283,7 @@ export class StateStore implements IStateStore {
}); });
} }
}; };
// Dummy method
fetchProjectStateTransitions = (workspaceSlug: string, projectId: string) => {};
} }

View file

@ -0,0 +1 @@
export * from "ce/components/workflow";

View file

@ -0,0 +1 @@
export * from "@/services/project/project-state.service";

View file

@ -0,0 +1 @@
export * from "@/store/state.store";

View file

@ -1,2 +1,3 @@
export * from "./projects"; export * from "./projects";
export * from "./issue-types"; export * from "./issue-types";
export * from "ce/types/state.d";

View file

@ -102,3 +102,18 @@ export const getValidKeysFromObject = (obj: any) => {
return Object.keys(obj).filter((key) => !!obj[key]); 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;
};