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 {
|
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={({
|
||||||
|
|
|
||||||
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,2 +1,2 @@
|
||||||
export * from "./project";
|
export * from "./project";
|
||||||
export * from "./workspace.service";
|
export * from "./workspace.service";
|
||||||
|
|
|
||||||
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 "./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
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";
|
"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>
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
>
|
||||||
<span>{dropErrorMessage}</span>
|
{dropErrorMessage ? (
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
) : (
|
<AlertCircle width={13} height={13} />
|
||||||
<>
|
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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 { 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 */}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) => {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 "./projects";
|
||||||
export * from "./issue-types";
|
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]);
|
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