From b70047b1d5e76d2e53d3fd886068f164ffa59bee Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Fri, 29 Sep 2023 12:30:54 +0530 Subject: [PATCH] chore: issues grouped kanban and swimlanes UI and functionality (#2294) * chore: updated the all the group_by and sub_group_by UI and functionality render in kanban * chore: kanban sorting in mobx and ui updates * chore: ui changes and drag and drop functionality changes in kanban * chore: issues count render in kanban default and swimlanes * chore: Added icons to the group_by and sub_group_by in kanban and swimlanes --- web/components/gantt-chart/contexts/index.tsx | 9 +- .../issues/issue-layouts/kanban/block.tsx | 80 ++-- .../issues/issue-layouts/kanban/default.tsx | 222 ++++++----- .../issue-layouts/kanban/headers/assignee.tsx | 45 ++- .../issue-layouts/kanban/headers/card.tsx | 39 -- .../kanban/headers/created_by.tsx | 43 ++- .../kanban/headers/group-by-card.tsx | 65 ++++ .../kanban/headers/group-by-root.tsx | 74 +++- .../issue-layouts/kanban/headers/label.tsx | 46 ++- .../issue-layouts/kanban/headers/priority.tsx | 76 +++- .../kanban/headers/state-group.tsx | 58 ++- .../issue-layouts/kanban/headers/state.tsx | 43 ++- .../kanban/headers/sub-group-by-card.tsx | 43 +++ .../kanban/headers/sub-group-by-root.tsx | 81 ++++- .../issue-layouts/kanban/properties.tsx | 91 +++++ .../issues/issue-layouts/kanban/root.tsx | 39 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 249 ++++++++++--- web/constants/issue.ts | 10 + web/store/issue.ts | 7 +- web/store/issue_detail.ts | 8 +- web/store/kanban_view.ts | 344 +++++++++++++++--- web/store/project.ts | 9 + 22 files changed, 1275 insertions(+), 406 deletions(-) delete mode 100644 web/components/issues/issue-layouts/kanban/headers/card.tsx create mode 100644 web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx create mode 100644 web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx create mode 100644 web/components/issues/issue-layouts/kanban/properties.tsx diff --git a/web/components/gantt-chart/contexts/index.tsx b/web/components/gantt-chart/contexts/index.tsx index 05dfbe678..137cc2607 100644 --- a/web/components/gantt-chart/contexts/index.tsx +++ b/web/components/gantt-chart/contexts/index.tsx @@ -6,10 +6,7 @@ import { allViewsWithData, currentViewDataWithView } from "../data"; export const ChartContext = createContext(undefined); -const chartReducer = ( - state: ChartContextData, - action: ChartContextActionPayload -): ChartContextData => { +const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => { switch (action.type) { case "CURRENT_VIEW": return { ...state, currentView: action.payload }; @@ -50,9 +47,7 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ }; return ( - + {children} ); diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 6900e9782..ad424df25 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -1,50 +1,56 @@ // react beautiful dnd import { Draggable } from "@hello-pangea/dnd"; +// components +import { KanBanProperties } from "./properties"; interface IssueBlockProps { sub_group_id: string; columnId: string; issues: any; + isDragDisabled: boolean; } -export const IssueBlock = ({ sub_group_id, columnId, issues }: IssueBlockProps) => { - console.log(); - - return ( - <> - {issues && issues.length > 0 ? ( - <> - {issues.map((issue: any, index: any) => ( - - {(provided: any, snapshot: any) => ( +export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: IssueBlockProps) => ( + <> + {issues && issues.length > 0 ? ( + <> + {issues.map((issue: any, index: any) => ( + + {(provided: any, snapshot: any) => ( +
-
-
ONE-{issue.sequence_id}
-
{issue.name}
-
Footer
+
ONE-{issue.sequence_id}
+
{issue.name}
+
+
- )} - - ))} - - ) : ( -
No issues are available.
- )} - - ); -}; +
+ )} + + ))} + + ) : ( + !isDragDisabled && ( +
+ {/*
Drop here
*/} +
+ ) + )} + +); diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 321105635..842550e40 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -5,42 +5,51 @@ import { Droppable } from "@hello-pangea/dnd"; import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { IssueBlock } from "./block"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; // mobx import { observer } from "mobx-react-lite"; // mobx import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; -export interface IKanBan { - issues?: any; +export interface IGroupByKanBan { + issues: any; + sub_group_by: string | null; + group_by: string | null; + sub_group_id: string; + list: any; + listKey: string; handleIssues?: () => void; - handleDragDrop?: (result: any) => void | undefined; - sub_group_id?: string; + isDragDisabled: boolean; } -export const KanBan: React.FC = observer(({ issues, sub_group_id = "null" }) => { - const { project: projectStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); +const GroupByKanBan: React.FC = observer( + ({ issues, sub_group_by, group_by, sub_group_id = "null", list, listKey, isDragDisabled }) => { + const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; + const verticalAlignPosition = (_list: any) => + issueKanBanViewStore.kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string); - return ( -
- {group_by && group_by === "state" && ( -
- {projectStore?.projectStates && - projectStore?.projectStates.length > 0 && - projectStore?.projectStates.map((state) => ( -
- {sub_group_by === null && ( -
- -
- )} + return ( +
+ {list && + list.length > 0 && + list.map((_list: any) => ( +
+ {sub_group_by === null && ( +
+ +
+ )} -
- + {!verticalAlignPosition(_list) && ( +
+ {(provided: any, snapshot: any) => (
= observer(({ issues, sub_group_id = "nul {...provided.droppableProps} ref={provided.innerRef} > - {issues && ( - + {issues ? ( + + ) : ( + isDragDisabled && ( +
+ {/*
Drop here
*/} +
+ ) )} {provided.placeholder}
)}
-
- ))} -
+ )} +
+ ))} +
+ ); + } +); + +export interface IKanBan { + issues: any; + sub_group_by: string | null; + group_by: string | null; + sub_group_id?: string; + handleIssues?: () => void; + handleDragDrop?: (result: any) => void | undefined; +} + +export const KanBan: React.FC = observer(({ issues, sub_group_by, group_by, sub_group_id = "null" }) => { + const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); + + return ( +
+ {group_by && group_by === "state" && ( + )} {group_by && group_by === "state_detail.group" && ( -
- {ISSUE_STATE_GROUPS && - ISSUE_STATE_GROUPS.length > 0 && - ISSUE_STATE_GROUPS.map((stateGroup) => ( -
- {sub_group_by === null && ( -
- -
- )} -
content
-
- ))} -
+ )} {group_by && group_by === "priority" && ( -
- {ISSUE_PRIORITIES && - ISSUE_PRIORITIES.length > 0 && - ISSUE_PRIORITIES.map((priority) => ( -
- {sub_group_by === null && ( -
- -
- )} -
content
-
- ))} -
+ )} {group_by && group_by === "labels" && ( -
- {projectStore?.projectLabels && - projectStore?.projectLabels.length > 0 && - projectStore?.projectLabels.map((label) => ( -
- {sub_group_by === null && ( -
- -
- )} -
content
-
- ))} -
+ )} {group_by && group_by === "assignees" && ( -
- {projectStore?.projectMembers && - projectStore?.projectMembers.length > 0 && - projectStore?.projectMembers.map((member) => ( -
- {sub_group_by === null && ( -
- -
- )} -
content
-
- ))} -
+ )} {group_by && group_by === "created_by" && ( -
- {projectStore?.projectMembers && - projectStore?.projectMembers.length > 0 && - projectStore?.projectMembers.map((member) => ( -
- {sub_group_by === null && ( -
- -
- )} -
content
-
- ))} -
+ )}
); diff --git a/web/components/issues/issue-layouts/kanban/headers/assignee.tsx b/web/components/issues/issue-layouts/kanban/headers/assignee.tsx index 6824f1853..b913bc85f 100644 --- a/web/components/issues/issue-layouts/kanban/headers/assignee.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/assignee.tsx @@ -1,19 +1,50 @@ -// components -import { HeaderCard } from "./card"; // mobx import { observer } from "mobx-react-lite"; +// components +import { HeaderGroupByCard } from "./group-by-card"; +import { HeaderSubGroupByCard } from "./sub-group-by-card"; +import { Avatar } from "components/ui"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; export interface IAssigneesHeader { column_id: string; + sub_group_by: string | null; + group_by: string | null; + header_type: "group_by" | "sub_group_by"; + issues_count: number; } -export const AssigneesHeader: React.FC = observer(({ column_id }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const Icon = ({ user }: any) => ; - const assignee = (column_id && projectStore?.getProjectMemberById(column_id)) ?? null; +export const AssigneesHeader: React.FC = observer( + ({ column_id, sub_group_by, group_by, header_type, issues_count }) => { + const { project: projectStore }: RootStore = useMobxStore(); - return <>{assignee && }; -}); + const assignee = (column_id && projectStore?.getProjectMemberByUserId(column_id)) ?? null; + + return ( + <> + {assignee && + (sub_group_by && header_type === "sub_group_by" ? ( + } + title={assignee?.member?.display_name || ""} + count={issues_count} + /> + ) : ( + } + title={assignee?.member?.display_name || ""} + count={issues_count} + /> + ))} + + ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/headers/card.tsx b/web/components/issues/issue-layouts/kanban/headers/card.tsx deleted file mode 100644 index 8c35aa99d..000000000 --- a/web/components/issues/issue-layouts/kanban/headers/card.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; -// lucide icons -import { Plus, Minimize2, Maximize2, Circle } from "lucide-react"; - -interface IHeaderCard { - icon?: React.ReactNode; - title: string; -} - -export const HeaderCard = ({ icon, title }: IHeaderCard) => { - const position = false; - - return ( -
-
- {icon ? icon : } -
- -
-
{title}
-
(0)
-
- -
- {position ? : } -
- -
- -
-
- ); -}; diff --git a/web/components/issues/issue-layouts/kanban/headers/created_by.tsx b/web/components/issues/issue-layouts/kanban/headers/created_by.tsx index 3b71bbf67..d7c45e642 100644 --- a/web/components/issues/issue-layouts/kanban/headers/created_by.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/created_by.tsx @@ -1,19 +1,48 @@ -// components -import { HeaderCard } from "./card"; // mobx import { observer } from "mobx-react-lite"; +// components +import { HeaderGroupByCard } from "./group-by-card"; +import { HeaderSubGroupByCard } from "./sub-group-by-card"; +import { Icon } from "./assignee"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; export interface ICreatedByHeader { column_id: string; + sub_group_by: string | null; + group_by: string | null; + header_type: "group_by" | "sub_group_by"; + issues_count: number; } -export const CreatedByHeader: React.FC = observer(({ column_id }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const CreatedByHeader: React.FC = observer( + ({ column_id, sub_group_by, group_by, header_type, issues_count }) => { + const { project: projectStore }: RootStore = useMobxStore(); - const createdBy = (column_id && projectStore?.getProjectMemberById(column_id)) ?? null; + const createdBy = (column_id && projectStore?.getProjectMemberByUserId(column_id)) ?? null; - return <>{createdBy && }; -}); + return ( + <> + {createdBy && + (sub_group_by && header_type === "sub_group_by" ? ( + } + title={createdBy?.member?.display_name || ""} + count={issues_count} + /> + ) : ( + } + title={createdBy?.member?.display_name || ""} + count={issues_count} + /> + ))} + + ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx new file mode 100644 index 000000000..c59d6023f --- /dev/null +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -0,0 +1,65 @@ +import React from "react"; +// lucide icons +import { Plus, Minimize2, Maximize2, Circle } from "lucide-react"; +// mobx +import { observer } from "mobx-react-lite"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +interface IHeaderGroupByCard { + sub_group_by: string | null; + group_by: string | null; + column_id: string; + icon?: React.ReactNode; + title: string; + count: number; +} + +export const HeaderGroupByCard = observer( + ({ sub_group_by, group_by, column_id, icon, title, count }: IHeaderGroupByCard) => { + const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); + + const verticalAlignPosition = issueKanBanViewStore.kanBanToggle?.groupByHeaderMinMax.includes(column_id); + + return ( +
+
+ {icon ? icon : } +
+ +
+
+ {title} +
+
+ {count || 0} +
+
+ + {sub_group_by === null && ( +
issueKanBanViewStore?.handleKanBanToggle("groupByHeaderMinMax", column_id)} + > + {verticalAlignPosition ? ( + + ) : ( + + )} +
+ )} + + {/*
+ +
*/} +
+ ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-root.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-root.tsx index 9412d04f3..bdee36e42 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-root.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-root.tsx @@ -13,20 +13,68 @@ import { RootStore } from "store/root"; export interface IKanBanGroupByHeaderRoot { column_id: string; + sub_group_by: string | null; + group_by: string | null; + issues_count: number; } -export const KanBanGroupByHeaderRoot: React.FC = observer(({ column_id }) => { - const { issueFilter: issueFilterStore }: RootStore = useMobxStore(); - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - - return ( +export const KanBanGroupByHeaderRoot: React.FC = observer( + ({ column_id, sub_group_by, group_by, issues_count }) => ( <> - {group_by && group_by === "state" && } - {group_by && group_by === "state_detail.group" && } - {group_by && group_by === "priority" && } - {group_by && group_by === "labels" && } - {group_by && group_by === "assignees" && } - {group_by && group_by === "created_by" && } + {group_by && group_by === "state" && ( + + )} + {group_by && group_by === "state_detail.group" && ( + + )} + {group_by && group_by === "priority" && ( + + )} + {group_by && group_by === "labels" && ( + + )} + {group_by && group_by === "assignees" && ( + + )} + {group_by && group_by === "created_by" && ( + + )} - ); -}); + ) +); diff --git a/web/components/issues/issue-layouts/kanban/headers/label.tsx b/web/components/issues/issue-layouts/kanban/headers/label.tsx index 00223e515..135a1d139 100644 --- a/web/components/issues/issue-layouts/kanban/headers/label.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/label.tsx @@ -1,19 +1,51 @@ -// components -import { HeaderCard } from "./card"; // mobx import { observer } from "mobx-react-lite"; +// components +import { HeaderGroupByCard } from "./group-by-card"; +import { HeaderSubGroupByCard } from "./sub-group-by-card"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; export interface ILabelHeader { column_id: string; + sub_group_by: string | null; + group_by: string | null; + header_type: "group_by" | "sub_group_by"; + issues_count: number; } -export const LabelHeader: React.FC = observer(({ column_id }) => { - const { project: projectStore }: RootStore = useMobxStore(); +const Icon = ({ color }: any) => ( +
+); - const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null; +export const LabelHeader: React.FC = observer( + ({ column_id, sub_group_by, group_by, header_type, issues_count }) => { + const { project: projectStore }: RootStore = useMobxStore(); - return <>{label && }; -}); + const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null; + + return ( + <> + {label && + (sub_group_by && header_type === "sub_group_by" ? ( + } + title={label?.name || ""} + count={issues_count} + /> + ) : ( + } + title={label?.name || ""} + count={issues_count} + /> + ))} + + ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/headers/priority.tsx b/web/components/issues/issue-layouts/kanban/headers/priority.tsx index 5493f1a36..38e4afbc4 100644 --- a/web/components/issues/issue-layouts/kanban/headers/priority.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/priority.tsx @@ -1,22 +1,72 @@ -import React from "react"; -// components -import { HeaderCard } from "./card"; -// constants -import { issuePriorityByKey } from "constants/issue"; // mobx import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +// lucide icons +import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react"; +// components +import { HeaderGroupByCard } from "./group-by-card"; +import { HeaderSubGroupByCard } from "./sub-group-by-card"; +// constants +import { issuePriorityByKey } from "constants/issue"; export interface IPriorityHeader { column_id: string; + sub_group_by: string | null; + group_by: string | null; + header_type: "group_by" | "sub_group_by"; + issues_count: number; } -export const PriorityHeader: React.FC = observer(({ column_id }) => { - const {}: RootStore = useMobxStore(); +const Icon = ({ priority }: any) => ( +
+ {priority === "urgent" ? ( +
+ +
+ ) : priority === "high" ? ( +
+ +
+ ) : priority === "medium" ? ( +
+ +
+ ) : priority === "low" ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+); - const stateGroup = column_id && issuePriorityByKey(column_id); +export const PriorityHeader: React.FC = observer( + ({ column_id, sub_group_by, group_by, header_type, issues_count }) => { + const priority = column_id && issuePriorityByKey(column_id); - return <>{stateGroup && }; -}); + return ( + <> + {priority && + (sub_group_by && header_type === "sub_group_by" ? ( + } + title={priority?.key || ""} + count={issues_count} + /> + ) : ( + } + title={priority?.key || ""} + count={issues_count} + /> + ))} + + ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/headers/state-group.tsx b/web/components/issues/issue-layouts/kanban/headers/state-group.tsx index 2a406ac94..4baa64dd0 100644 --- a/web/components/issues/issue-layouts/kanban/headers/state-group.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/state-group.tsx @@ -1,23 +1,53 @@ -import React from "react"; -// components -import { HeaderCard } from "./card"; -// constants -import { issueStateGroupByKey } from "constants/issue"; // mobx import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +// components +import { HeaderGroupByCard } from "./group-by-card"; +import { HeaderSubGroupByCard } from "./sub-group-by-card"; +import { StateGroupIcon } from "components/icons"; +// constants +import { issueStateGroupByKey } from "constants/issue"; export interface IStateGroupHeader { column_id: string; - swimlanes?: boolean; + sub_group_by: string | null; + group_by: string | null; + header_type: "group_by" | "sub_group_by"; + issues_count: number; } -export const StateGroupHeader: React.FC = observer(({ column_id }) => { - const {}: RootStore = useMobxStore(); +export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => ( +
+ +
+); - const stateGroup = column_id && issueStateGroupByKey(column_id); +export const StateGroupHeader: React.FC = observer( + ({ column_id, sub_group_by, group_by, header_type, issues_count }) => { + const stateGroup = column_id && issueStateGroupByKey(column_id); - return <>{stateGroup && }; -}); + console.log("stateGroup", stateGroup); + + return ( + <> + {stateGroup && + (sub_group_by && header_type === "sub_group_by" ? ( + } + title={stateGroup?.key || ""} + count={issues_count} + /> + ) : ( + } + title={stateGroup?.key || ""} + count={issues_count} + /> + ))} + + ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/headers/state.tsx b/web/components/issues/issue-layouts/kanban/headers/state.tsx index 4ed95a07c..8f1b35f99 100644 --- a/web/components/issues/issue-layouts/kanban/headers/state.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/state.tsx @@ -1,19 +1,48 @@ -// components -import { HeaderCard } from "./card"; // mobx import { observer } from "mobx-react-lite"; +// components +import { HeaderGroupByCard } from "./group-by-card"; +import { HeaderSubGroupByCard } from "./sub-group-by-card"; +import { Icon } from "./state-group"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; export interface IStateHeader { column_id: string; + sub_group_by: string | null; + group_by: string | null; + header_type: "group_by" | "sub_group_by"; + issues_count: number; } -export const StateHeader: React.FC = observer(({ column_id }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const StateHeader: React.FC = observer( + ({ column_id, sub_group_by, group_by, header_type, issues_count }) => { + const { project: projectStore }: RootStore = useMobxStore(); - const state = (column_id && projectStore?.getProjectStateById(column_id)) ?? null; + const state = (column_id && projectStore?.getProjectStateById(column_id)) ?? null; - return <>{state && }; -}); + return ( + <> + {state && + (sub_group_by && header_type === "sub_group_by" ? ( + } + title={state?.name || ""} + count={issues_count} + /> + ) : ( + } + title={state?.name || ""} + count={issues_count} + /> + ))} + + ); + } +); diff --git a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx new file mode 100644 index 000000000..17545c29b --- /dev/null +++ b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx @@ -0,0 +1,43 @@ +import React from "react"; +// lucide icons +import { Circle, ChevronDown, ChevronUp } from "lucide-react"; +// mobx +import { observer } from "mobx-react-lite"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +interface IHeaderSubGroupByCard { + icon?: React.ReactNode; + title: string; + count: number; + column_id: string; +} + +export const HeaderSubGroupByCard = observer(({ icon, title, count, column_id }: IHeaderSubGroupByCard) => { + const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); + + return ( +
+
issueKanBanViewStore?.handleKanBanToggle("subgroupByIssuesVisibility", column_id)} + > + {issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes(column_id) ? ( + + ) : ( + + )} +
+ +
+ {icon ? icon : } +
+ +
+
{title}
+
{count || 0}
+
+
+ ); +}); diff --git a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-root.tsx b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-root.tsx index f78970fd7..9f86393be 100644 --- a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-root.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-root.tsx @@ -1,3 +1,5 @@ +// mobx +import { observer } from "mobx-react-lite"; // components import { StateHeader } from "./state"; import { StateGroupHeader } from "./state-group"; @@ -5,28 +7,71 @@ import { AssigneesHeader } from "./assignee"; import { PriorityHeader } from "./priority"; import { LabelHeader } from "./label"; import { CreatedByHeader } from "./created_by"; -// mobx -import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; export interface IKanBanSubGroupByHeaderRoot { column_id: string; + sub_group_by: string | null; + group_by: string | null; + issues_count: number; } -export const KanBanSubGroupByHeaderRoot: React.FC = observer(({ column_id }) => { - const { issueFilter: issueFilterStore }: RootStore = useMobxStore(); - const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; - - return ( +export const KanBanSubGroupByHeaderRoot: React.FC = observer( + ({ column_id, sub_group_by, group_by, issues_count }) => ( <> - {sub_group_by && sub_group_by === "state" && } - {sub_group_by && sub_group_by === "state_detail.group" && } - {sub_group_by && sub_group_by === "priority" && } - {sub_group_by && sub_group_by === "labels" && } - {sub_group_by && sub_group_by === "assignees" && } - {sub_group_by && sub_group_by === "created_by" && } + {sub_group_by && sub_group_by === "state" && ( + + )} + {sub_group_by && sub_group_by === "state_detail.group" && ( + + )} + {sub_group_by && sub_group_by === "priority" && ( + + )} + {sub_group_by && sub_group_by === "labels" && ( + + )} + {sub_group_by && sub_group_by === "assignees" && ( + + )} + {sub_group_by && sub_group_by === "created_by" && ( + + )} - ); -}); + ) +); diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx new file mode 100644 index 000000000..b2a8ce8ff --- /dev/null +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -0,0 +1,91 @@ +// lucide icons +import { Circle } from "lucide-react"; + +export const KanBanProperties = () => { + console.log("properties"); + return ( +
+ {/* basic properties */} + {/* state */} +
+
+ +
+
state
+
+ + {/* priority */} +
+
+ +
+
priority
+
+ + {/* label */} +
+
+ +
+
label
+
+ + {/* assignee */} +
+
+ +
+
assignee
+
+ + {/* start date */} +
+
+ +
+
start date
+
+ + {/* target/due date */} +
+
+ +
+
target/due date
+
+ + {/* extra render properties */} + {/* estimate */} +
+
+ +
+
0
+
+ + {/* sub-issues */} +
+
+ +
+
0
+
+ + {/* attachments */} +
+
+ +
+
0
+
+ + {/* link */} +
+
+ +
+
0
+
+
+ ); +}; diff --git a/web/components/issues/issue-layouts/kanban/root.tsx b/web/components/issues/issue-layouts/kanban/root.tsx index 4b3bb5de9..5701a297d 100644 --- a/web/components/issues/issue-layouts/kanban/root.tsx +++ b/web/components/issues/issue-layouts/kanban/root.tsx @@ -3,26 +3,32 @@ import React from "react"; import { DragDropContext } from "@hello-pangea/dnd"; // mobx import { observer } from "mobx-react-lite"; +// components +import { KanBanSwimLanes } from "./swimlanes"; +import { KanBan } from "./default"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; -import { KanBanSwimLanes } from "./swimlanes"; -import { KanBan } from "./default"; -export interface IKanBanLayout { - issues?: any; - handleIssues?: () => void; - handleDragDrop?: (result: any) => void; -} +export interface IKanBanLayout {} + +export const KanBanLayout: React.FC = observer(() => { + const { + issue: issueStore, + issueFilter: issueFilterStore, + issueKanBanView: issueKanBanViewStore, + }: RootStore = useMobxStore(); + + const issues = issueStore?.getIssues; + + const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; + + const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; -export const KanBanLayout: React.FC = observer(({}) => { - const { issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by ? "swimlanes" : "default"; - const issues = issueStore?.getIssues; - const onDragEnd = (result: any) => { if (!result) return; @@ -34,14 +40,19 @@ export const KanBanLayout: React.FC = observer(({}) => { ) return; - console.log("result", result); - // issueKanBanViewStore?.handleDragDrop(result.source, result.destination); + currentKanBanView === "default" + ? issueKanBanViewStore?.handleDragDrop(result.source, result.destination) + : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); }; return (
- {currentKanBanView === "default" ? : } + {currentKanBanView === "default" ? ( + + ) : ( + + )}
); diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 7ac4a4743..5579b33cd 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -4,99 +4,236 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root"; import { KanBan } from "./default"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; // mobx import { observer } from "mobx-react-lite"; // mobx import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; +interface ISubGroupSwimlaneHeader { + issues: any; + sub_group_by: string | null; + group_by: string | null; + list: any; + listKey: string; +} +const SubGroupSwimlaneHeader: React.FC = ({ + issues, + sub_group_by, + group_by, + list, + listKey, +}) => { + const calculateIssueCount = (column_id: string) => { + let issueCount = 0; + issues && + Object.keys(issues)?.forEach((_issueKey: any) => { + issueCount += issues?.[_issueKey]?.[column_id]?.length || 0; + }); + return issueCount; + }; + + return ( +
+ {list && + list.length > 0 && + list.map((_list: any) => ( +
+ +
+ ))} +
+ ); +}; + +interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { + issues: any; +} +const SubGroupSwimlane: React.FC = observer(({ issues, sub_group_by, group_by, list, listKey }) => { + const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); + + const calculateIssueCount = (column_id: string) => { + let issueCount = 0; + issues?.[column_id] && + Object.keys(issues?.[column_id])?.forEach((_list: any) => { + issueCount += issues?.[column_id]?.[_list]?.length || 0; + }); + return issueCount; + }; + + return ( +
+ {list && + list.length > 0 && + list.map((_list: any) => ( +
+
+
+ +
+
+
+ {!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes( + getValueFromObject(_list, listKey) as string + ) && ( +
+ +
+ )} +
+ ))} +
+ ); +}); + export interface IKanBanSwimLanes { - issues?: any; + issues: any; + sub_group_by: string | null; + group_by: string | null; handleIssues?: () => void; - handleDragDrop?: () => void; } -const SubGroupSwimlaneHeader = ({ list, _key }: any) => ( -
- {list && - list.length > 0 && - list.map((_list: any) => ( -
- -
- ))} -
-); - -const SubGroupSwimlane = ({ issues, list, _key }: any) => ( -
- {list && - list.length > 0 && - list.map((_list: any) => ( -
-
-
- -
-
-
- -
- -
-
- ))} -
-); - -export const KanBanSwimLanes: React.FC = observer(({ issues }) => { - const { project: projectStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); - - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; - - console.log("sub_group_by", sub_group_by); +export const KanBanSwimLanes: React.FC = observer(({ issues, sub_group_by, group_by }) => { + const { project: projectStore }: RootStore = useMobxStore(); return (
-
- {group_by && group_by === "state" && } +
+ {group_by && group_by === "state" && ( + + )} + {group_by && group_by === "state_detail.group" && ( - + )} - {group_by && group_by === "priority" && } - {group_by && group_by === "labels" && } + + {group_by && group_by === "priority" && ( + + )} + + {group_by && group_by === "labels" && ( + + )} + {group_by && group_by === "assignees" && ( - + )} + {group_by && group_by === "created_by" && ( - + )}
{sub_group_by && sub_group_by === "state" && ( - + )} {sub_group_by && sub_group_by === "state_detail.group" && ( - + )} {sub_group_by && sub_group_by === "priority" && ( - + )} {sub_group_by && sub_group_by === "labels" && ( - + )} {sub_group_by && sub_group_by === "assignees" && ( - + )} {sub_group_by && sub_group_by === "created_by" && ( - + )}
); diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 52fc6c4db..6a2bb7d63 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -314,3 +314,13 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, }; + +export const getValueFromObject = (object: Object, key: string): string | number | boolean | null => { + const keys = key ? key.split(".") : []; + + let value: any = object; + if (!value || keys.length === 0) return null; + + for (const _key of keys) value = value[_key]; + return value; +}; diff --git a/web/store/issue.ts b/web/store/issue.ts index a8577ebc9..a4374e8ad 100644 --- a/web/store/issue.ts +++ b/web/store/issue.ts @@ -142,12 +142,7 @@ class IssueStore implements IIssueStore { this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); this.rootStore.project.setProjectId(projectId); - // TODO: replace this once the issue filter is completed - const params = { - group_by: "target_date", - order_by: "-created_at", - target_date: "2023-09-01;after,2023-09-30;before", - }; + const params = this.rootStore?.issueFilter?.appliedFilters; const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params); const issueType = this.getIssueType; diff --git a/web/store/issue_detail.ts b/web/store/issue_detail.ts index 31a91d51f..4095099a0 100644 --- a/web/store/issue_detail.ts +++ b/web/store/issue_detail.ts @@ -115,8 +115,6 @@ class IssueDetailStore implements IIssueDetailStore { const response = await this.issueService.createIssues(workspaceId, projectId, data, user); - if (response) this.rootStore.issue.addIssueToIssuesStore(projectId, response); - runInAction(() => { this.loader = false; this.error = null; @@ -137,7 +135,7 @@ class IssueDetailStore implements IIssueDetailStore { projectId: string, issueId: string, data: Partial, - user: ICurrentUserResponse + user: ICurrentUserResponse | undefined ) => { const newIssues = { ...this.issues }; newIssues[issueId] = { @@ -154,8 +152,6 @@ class IssueDetailStore implements IIssueDetailStore { const response = await this.issueService.patchIssue(workspaceId, projectId, issueId, data, user); - if (response) this.rootStore.issue.updateIssueInIssuesStore(projectId, response); - runInAction(() => { this.loader = false; this.error = null; @@ -192,8 +188,6 @@ class IssueDetailStore implements IIssueDetailStore { await this.issueService.deleteIssue(workspaceId, projectId, issueId, user); - this.rootStore.issue.deleteIssueFromIssuesStore(projectId, issueId); - runInAction(() => { this.loader = false; this.error = null; diff --git a/web/store/kanban_view.ts b/web/store/kanban_view.ts index c0d15227a..946a749a7 100644 --- a/web/store/kanban_view.ts +++ b/web/store/kanban_view.ts @@ -1,24 +1,42 @@ -import { action, computed, makeObservable } from "mobx"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // types import { RootStore } from "./root"; import { IIssueType } from "./issue"; export interface IIssueKanBanViewStore { + kanBanToggle: { + groupByHeaderMinMax: string[]; + subgroupByIssuesVisibility: string[]; + }; + // computed + canUserDragDrop: boolean; + canUserDragDropVertically: boolean; + canUserDragDropHorizontally: boolean; + // actions + handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void; + handleSwimlaneDragDrop: (source: any, destination: any) => void; handleDragDrop: (source: any, destination: any) => void; } class IssueKanBanViewStore implements IIssueKanBanViewStore { + kanBanToggle: { + groupByHeaderMinMax: string[]; + subgroupByIssuesVisibility: string[]; + } = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] }; // root store rootStore; constructor(_rootStore: RootStore) { makeObservable(this, { + kanBanToggle: observable, // computed canUserDragDrop: computed, canUserDragDropVertically: computed, canUserDragDropHorizontally: computed, // actions + handleKanBanToggle: action, + handleSwimlaneDragDrop: action, handleDragDrop: action, }); @@ -27,50 +45,83 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore { get canUserDragDrop() { if ( - this.rootStore?.issueFilter?.userDisplayFilters?.group_by && this.rootStore?.issueFilter?.userDisplayFilters?.order_by && - ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.group_by) && - this.rootStore?.issueFilter?.userDisplayFilters?.order_by === "sort_order" + this.rootStore?.issueFilter?.userDisplayFilters?.order_by === "sort_order" && + this.rootStore?.issueFilter?.userDisplayFilters?.group_by && + ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.group_by) ) { - return true; + if (this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by === null) return true; + if ( + this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by && + ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by) + ) + return true; } return false; } + get canUserDragDropVertically() { - return true; - } - get canUserDragDropHorizontally() { - return true; + return false; } - handleDragDrop = async (source: any, destination: any) => { + get canUserDragDropHorizontally() { + return false; + } + + handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { + this.kanBanToggle = { + ...this.kanBanToggle, + [toggle]: this.kanBanToggle[toggle].includes(value) + ? this.kanBanToggle[toggle].filter((v) => v !== value) + : [...this.kanBanToggle[toggle], value], + }; + }; + + handleSwimlaneDragDrop = async (source: any, destination: any) => { const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; const projectId = this.rootStore?.project?.projectId; const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.issue.getIssues; - const sortOrderDefaultValue = 10000; - - if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && this.rootStore.issue.getIssues) { - const currentIssues: any = this.rootStore.issue.getIssues; + const sortOrderDefaultValue = 65535; + if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) { + // update issue payload let updateIssue: any = { workspaceSlug: workspaceSlug, projectId: projectId, }; - // user can drag the issues from any direction - if (this.canUserDragDrop) { - // vertical - if (source.droppableId === destination.droppableId) { - const _columnId = source.droppableId; - const _issues = currentIssues[_columnId]; + // source, destination group and sub group id + let droppableSourceColumnId = source.droppableId; + droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; + let droppableDestinationColumnId = destination.droppableId; + droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; + if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; + + const source_group_id: string = droppableSourceColumnId[0]; + const source_sub_group_id: string = droppableSourceColumnId[1] === "null" ? null : droppableSourceColumnId[1]; + + const destination_group_id: string = droppableDestinationColumnId[0]; + const destination_sub_group_id: string = + droppableDestinationColumnId[1] === "null" ? null : droppableDestinationColumnId[1]; + + if (source_sub_group_id === destination_sub_group_id) { + if (source_group_id === destination_group_id) { + const _issues = currentIssues[source_sub_group_id][source_group_id]; // update the sort order if (destination.index === 0) { - updateIssue = { ...updateIssue, sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue }; + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue, + }; } else if (destination.index === _issues.length - 1) { - updateIssue = { ...updateIssue, sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue }; + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue, + }; } else { updateIssue = { ...updateIssue, @@ -78,34 +129,26 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore { }; } - // update the mobx state array const [removed] = _issues.splice(source.index, 1); _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); updateIssue = { ...updateIssue, issueId: removed?.id }; - - currentIssues[_columnId] = _issues; + currentIssues[source_sub_group_id][source_group_id] = _issues; } - // horizontal - if (source.droppableId != destination.droppableId) { - const _sourceColumnId = source.droppableId; - const _destinationColumnId = destination.droppableId; + if (source_group_id != destination_group_id) { + const _sourceIssues = currentIssues[source_sub_group_id][source_group_id]; + let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || []; - const _sourceIssues = currentIssues[_sourceColumnId]; - const _destinationIssues = currentIssues[_destinationColumnId]; - - if (_destinationIssues.length > 0) { + if (_destinationIssues && _destinationIssues.length > 0) { if (destination.index === 0) { updateIssue = { ...updateIssue, sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, - state: destination?.droppableId, }; - } else if (destination.index === _destinationIssues.length - 1) { + } else if (destination.index === _destinationIssues.length) { updateIssue = { ...updateIssue, - sort_order: _destinationIssues[destination.index].sort_order + sortOrderDefaultValue, - state: destination?.droppableId, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, }; } else { updateIssue = { @@ -114,39 +157,84 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore { (_destinationIssues[destination.index - 1].sort_order + _destinationIssues[destination.index].sort_order) / 2, - state: destination?.droppableId, }; } } else { updateIssue = { ...updateIssue, sort_order: sortOrderDefaultValue, - state: destination?.droppableId, }; } const [removed] = _sourceIssues.splice(source.index, 1); - _destinationIssues.splice(destination.index, 0, { - ...removed, - state: destination?.droppableId, - sort_order: updateIssue.sort_order, - }); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + }); + else _destinationIssues = [..._destinationIssues, { ...removed, sort_order: updateIssue.sort_order }]; updateIssue = { ...updateIssue, issueId: removed?.id }; - currentIssues[_sourceColumnId] = _sourceIssues; - currentIssues[_destinationColumnId] = _destinationIssues; + // if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") + // updateIssue = { ...updateIssue, state: destination_group_id }; + // if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") + // updateIssue = { ...updateIssue, priority: destination_group_id }; + + currentIssues[source_sub_group_id][source_group_id] = _sourceIssues; + currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues; } } - // user can drag the issues only vertically - if (this.canUserDragDropVertically && source.droppableId === destination.droppableId) { + if (source_sub_group_id != destination_sub_group_id) { + const _sourceIssues = currentIssues[source_sub_group_id][source_group_id]; + let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + }); + else _destinationIssues = [..._destinationIssues, { ...removed, sort_order: updateIssue.sort_order }]; + + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_sub_group_id][source_group_id] = _sourceIssues; + currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues; + + // if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") + // updateIssue = { ...updateIssue, state: destination_group_id }; + // if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") + // updateIssue = { ...updateIssue, priority: destination_group_id }; } - // user can drag the issues only horizontally - if (this.canUserDragDropHorizontally && source.droppableId != destination.droppableId) { - } - - this.rootStore.issue.issues = { + const reorderedIssues = { ...this.rootStore?.issue.issues, [projectId]: { ...this.rootStore?.issue.issues?.[projectId], @@ -157,14 +245,160 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore { }, }; - // this.rootStore.issueDetail?.updateIssueAsync( + runInAction(() => { + this.rootStore.issue.issues = { ...reorderedIssues }; + }); + + // console.log("updateIssue", updateIssue); + + // this.rootStore.issueDetail?.updateIssue( // updateIssue.workspaceSlug, // updateIssue.projectId, // updateIssue.issueId, - // updateIssue + // updateIssue, + // undefined // ); } }; + + handleDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.issue.getIssues; + + const sortOrderDefaultValue = 65535; + + if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + // source, destination group and sub group id + let droppableSourceColumnId = source.droppableId; + droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; + let droppableDestinationColumnId = destination.droppableId; + droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; + if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; + + const source_group_id: string = droppableSourceColumnId[0]; + const destination_group_id: string = droppableDestinationColumnId[0]; + + if (this.canUserDragDrop) { + // vertical + if (source_group_id === destination_group_id) { + const _issues = currentIssues[source_group_id]; + + // update the sort order + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _issues.length - 1) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2, + }; + } + + const [removed] = _issues.splice(source.index, 1); + _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_group_id] = _issues; + } + + // horizontal + if (source_group_id != destination_group_id) { + const _sourceIssues = currentIssues[source_group_id]; + let _destinationIssues = currentIssues[destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + }); + else _destinationIssues = [..._destinationIssues, { ...removed, sort_order: updateIssue.sort_order }]; + updateIssue = { ...updateIssue, issueId: removed?.id }; + + currentIssues[source_group_id] = _sourceIssues; + currentIssues[destination_group_id] = _destinationIssues; + } + + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") + updateIssue = { ...updateIssue, state: destination_group_id }; + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") + updateIssue = { ...updateIssue, priority: destination_group_id }; + } + + // user can drag the issues only vertically + if (this.canUserDragDropVertically && destination_group_id === destination_group_id) { + } + + // user can drag the issues only horizontally + if (this.canUserDragDropHorizontally && destination_group_id != destination_group_id) { + } + + const reorderedIssues = { + ...this.rootStore?.issue.issues, + [projectId]: { + ...this.rootStore?.issue.issues?.[projectId], + [issueType]: { + ...this.rootStore?.issue.issues?.[projectId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.issue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue, + undefined + ); + } + }; } export default IssueKanBanViewStore; diff --git a/web/store/project.ts b/web/store/project.ts index f35982b3d..75a3aab04 100644 --- a/web/store/project.ts +++ b/web/store/project.ts @@ -48,6 +48,7 @@ export interface IProjectStore { getProjectStateById: (stateId: string) => IState | null; getProjectLabelById: (labelId: string) => IIssueLabels | null; getProjectMemberById: (memberId: string) => IProjectMember | null; + getProjectMemberByUserId: (memberId: string) => IProjectMember | null; fetchProjects: (workspaceSlug: string) => Promise; fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise; @@ -269,6 +270,14 @@ class ProjectStore implements IProjectStore { return memberInfo; }; + getProjectMemberByUserId = (memberId: string) => { + if (!this.projectId) return null; + const members = this.projectMembers; + if (!members) return null; + const memberInfo: IProjectMember | null = members.find((member) => member.member.id === memberId) || null; + return memberInfo; + }; + fetchProjectStates = async (workspaceSlug: string, projectSlug: string) => { try { this.loader = true;