refactor project states to ake way for new features (#6156)
This commit is contained in:
parent
3bccda0c86
commit
66652a5d71
38 changed files with 462 additions and 161 deletions
|
|
@ -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<ITooltipProps> = ({
|
|||
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<HTMLDivElement | null>(null);
|
||||
|
||||
const [shouldRender, setShouldRender] = useState(renderByDefault);
|
||||
|
|
@ -79,6 +81,9 @@ export const Tooltip: React.FC<ITooltipProps> = ({
|
|||
hoverOpenDelay={openDelay}
|
||||
hoverCloseDelay={closeDelay}
|
||||
content={
|
||||
jsxContent ? (
|
||||
<>{jsxContent}</>
|
||||
) : (
|
||||
<div
|
||||
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",
|
||||
|
|
@ -91,6 +96,7 @@ export const Tooltip: React.FC<ITooltipProps> = ({
|
|||
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
|
||||
{tooltipContent}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
position={position}
|
||||
renderTarget={({
|
||||
|
|
|
|||
20
web/ce/components/workflow/add-state-transition.tsx
Normal file
20
web/ce/components/workflow/add-state-transition.tsx
Normal 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>
|
||||
);
|
||||
6
web/ce/components/workflow/index.tsx
Normal file
6
web/ce/components/workflow/index.tsx
Normal 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";
|
||||
35
web/ce/components/workflow/state-item-child.tsx
Normal file
35
web/ce/components/workflow/state-item-child.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
36
web/ce/components/workflow/state-option.tsx
Normal file
36
web/ce/components/workflow/state-option.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
7
web/ce/components/workflow/state-transition-count.tsx
Normal file
7
web/ce/components/workflow/state-transition-count.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { IStateWorkFlow } from "@/plane-web/types";
|
||||
|
||||
type Props = {
|
||||
currentTransitionMap?: IStateWorkFlow;
|
||||
};
|
||||
|
||||
export const StateTransitionCount = (props: Props) => <></>;
|
||||
15
web/ce/components/workflow/use-workflow-drag-n-drop.ts
Normal file
15
web/ce/components/workflow/use-workflow-drag-n-drop.ts
Normal 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
|
||||
) => {},
|
||||
});
|
||||
6
web/ce/components/workflow/workflow-disabled-message.tsx
Normal file
6
web/ce/components/workflow/workflow-disabled-message.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
type Props = {
|
||||
parentStateId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const WorkFlowDisabledMessage = (props: Props) => <></>;
|
||||
8
web/ce/components/workflow/workflow-group-tree.tsx
Normal file
8
web/ce/components/workflow/workflow-group-tree.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { TIssueGroupByOptions } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
groupBy?: TIssueGroupByOptions;
|
||||
groupId: string | undefined;
|
||||
};
|
||||
|
||||
export const WorkFlowGroupTree = (props: Props) => <></>;
|
||||
1
web/ce/services/project/project-state.service.ts
Normal file
1
web/ce/services/project/project-state.service.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "@/services/project/project-state.service";
|
||||
1
web/ce/store/state.store.ts
Normal file
1
web/ce/store/state.store.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "@/store/state.store";
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./projects";
|
||||
export * from "./issue-types";
|
||||
export * from "./gantt-chart";
|
||||
export * from "./state.d";
|
||||
|
|
|
|||
8
web/ce/types/state.d.ts
vendored
Normal file
8
web/ce/types/state.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface IStateTransition {
|
||||
transition_state_id: string;
|
||||
actors: string[];
|
||||
}
|
||||
|
||||
export interface IStateWorkFlow {
|
||||
[transitionId: string]: IStateTransition;
|
||||
}
|
||||
|
|
@ -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<Props> = observer((props) => {
|
||||
|
|
@ -52,6 +56,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||
tabIndex,
|
||||
value,
|
||||
renderByDefault = true,
|
||||
stateIds,
|
||||
filterAvailableStateIds = true,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
|
|
@ -78,16 +84,18 @@ export const StateDropdown: React.FC<Props> = 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: (
|
||||
<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>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -226,22 +234,14 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
<StateOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`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 }) => (
|
||||
<>
|
||||
<span className="flex-grow truncate">{option.content}</span>
|
||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
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"
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="px-1.5 py-1 italic text-custom-text-400">No matches found</p>
|
||||
|
|
|
|||
|
|
@ -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,6 +42,9 @@ export const GroupDragOverlay = (props: Props) => {
|
|||
{ hidden: !shouldOverlayBeVisible }
|
||||
)}
|
||||
>
|
||||
{workflowDisabledSource ? (
|
||||
<WorkFlowDisabledMessage parentStateId={workflowDisabledSource} className="my-2" />
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"p-3 my-8 flex flex-col rounded items-center",
|
||||
|
|
@ -62,6 +72,7 @@ export const GroupDragOverlay = (props: Props) => {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<IHeaderGroupByCard> = observer((props) => {
|
||||
const {
|
||||
group_by,
|
||||
sub_group_by,
|
||||
column_id,
|
||||
icon,
|
||||
|
|
@ -130,6 +132,8 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<WorkFlowGroupTree groupBy={group_by} groupId={column_id} />
|
||||
|
||||
{sub_group_by === null && (
|
||||
<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"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-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
|
||||
|
||||
interface IHeaderSubGroupByCard {
|
||||
|
|
@ -10,11 +13,12 @@ interface IHeaderSubGroupByCard {
|
|||
count: number;
|
||||
column_id: string;
|
||||
collapsedGroups: TIssueKanbanFilters;
|
||||
sub_group_by: TIssueGroupByOptions | undefined;
|
||||
handleCollapsedGroups: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
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="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
|
||||
</div>
|
||||
|
||||
<WorkFlowGroupTree groupBy={sub_group_by} groupId={column_id} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
<GroupDragOverlay
|
||||
dragColumnOrientation={sub_group_by ? "justify-start" : "justify-center"}
|
||||
canOverlayBeVisible={canOverlayBeVisible}
|
||||
isDropDisabled={isDropDisabled}
|
||||
isDropDisabled={isWorkflowDropDisabled || isDropDisabled}
|
||||
workflowDisabledSource={workflowDisabledSource}
|
||||
dropErrorMessage={dropErrorMessage}
|
||||
orderBy={orderBy}
|
||||
isDraggingOverColumn={isDraggingOverColumn}
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||
count={issueCount}
|
||||
collapsedGroups={collapsedGroups}
|
||||
handleCollapsedGroups={handleCollapsedGroups}
|
||||
sub_group_by={sub_group_by}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
>
|
||||
<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>
|
||||
<WorkFlowGroupTree groupBy={groupBy} groupId={groupID} />
|
||||
</div>
|
||||
|
||||
{!disableIssueCreation &&
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement | null>(null);
|
||||
|
||||
const { projectId } = useParams();
|
||||
|
|
@ -105,6 +107,8 @@ export const ListGroup = observer((props: Props) => {
|
|||
|
||||
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(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) => {
|
|||
>
|
||||
<HeaderGroupByCard
|
||||
groupID={group.id}
|
||||
groupBy={group_by}
|
||||
icon={group.icon}
|
||||
title={group.name || ""}
|
||||
count={groupIssueCount}
|
||||
|
|
@ -261,7 +276,8 @@ export const ListGroup = observer((props: Props) => {
|
|||
<GroupDragOverlay
|
||||
dragColumnOrientation={dragColumnOrientation}
|
||||
canOverlayBeVisible={canOverlayBeVisible}
|
||||
isDropDisabled={!!group.isDropDisabled}
|
||||
isDropDisabled={isWorkflowDropDisabled || !!group.isDropDisabled}
|
||||
workflowDisabledSource={workflowDisabledSource}
|
||||
dropErrorMessage={group.dropErrorMessage}
|
||||
orderBy={orderBy}
|
||||
isDraggingOverColumn={isDraggingOverColumn}
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ export * from "./options";
|
|||
export * from "./loader";
|
||||
export * from "./create-update";
|
||||
export * from "./state-delete-modal";
|
||||
export * from "./state-item-title";
|
||||
|
|
|
|||
|
|
@ -16,11 +16,17 @@ type TProjectState = {
|
|||
export const ProjectStateRoot: FC<TProjectState> = 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
|
||||
|
|
|
|||
73
web/core/components/project-states/state-item-title.tsx
Normal file
73
web/core/components/project-states/state-item-title.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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,59 +128,20 @@ export const StateItem: FC<TStateItem> = observer((props) => {
|
|||
<div
|
||||
ref={draggableElementRef}
|
||||
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`,
|
||||
totalStates === 1 ? `cursor-auto` : `cursor-grab`
|
||||
)}
|
||||
>
|
||||
{/* draggable indicator */}
|
||||
{!disabled && totalStates != 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="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
|
||||
<StateItemChild
|
||||
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}
|
||||
setUpdateStateModal={setUpdateStateModal}
|
||||
stateCount={totalStates}
|
||||
disabled={disabled}
|
||||
state={state}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* draggable drop bottom indicator */}
|
||||
<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = 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<IProjectAuthWrapper> = 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<IProjectAuthWrapper> = observer((props) => {
|
|||
layout="screen-detailed"
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Projects page empty state");
|
||||
toggleCreateProjectModal(true)
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export interface IIssueStoreActions {
|
|||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
issueStatus?: "DEFAULT" | "DRAFT",
|
||||
issueStatus?: "DEFAULT" | "DRAFT"
|
||||
) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<IState[]>;
|
||||
fetchWorkspaceStates: (workspaceSlug: string) => Promise<IState[]>;
|
||||
|
|
@ -42,16 +46,19 @@ export interface IStateStore {
|
|||
stateId: string,
|
||||
payload: Partial<IState>
|
||||
) => Promise<void>;
|
||||
//Dummy method
|
||||
fetchProjectStateTransitions: (workspaceSlug: string, projectId: string) => void;
|
||||
}
|
||||
|
||||
export class StateStore implements IStateStore {
|
||||
stateMap: Record<string, IState> = {};
|
||||
//loaders
|
||||
fetchedMap: Record<string, boolean> = {};
|
||||
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) => {};
|
||||
}
|
||||
|
|
|
|||
1
web/ee/components/workflow/index.ts
Normal file
1
web/ee/components/workflow/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/workflow";
|
||||
1
web/ee/services/project/project-state.service.ts
Normal file
1
web/ee/services/project/project-state.service.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "@/services/project/project-state.service";
|
||||
1
web/ee/store/state.store.ts
Normal file
1
web/ee/store/state.store.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "@/store/state.store";
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./projects";
|
||||
export * from "./issue-types";
|
||||
export * from "ce/types/state.d";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue