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
This commit is contained in:
guru_sainath 2023-09-29 12:30:54 +05:30 committed by GitHub
parent f60dcdc599
commit b70047b1d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1275 additions and 406 deletions

View File

@ -6,10 +6,7 @@ import { allViewsWithData, currentViewDataWithView } from "../data";
export const ChartContext = createContext<ChartContextReducer | undefined>(undefined); export const ChartContext = createContext<ChartContextReducer | undefined>(undefined);
const chartReducer = ( const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => {
state: ChartContextData,
action: ChartContextActionPayload
): ChartContextData => {
switch (action.type) { switch (action.type) {
case "CURRENT_VIEW": case "CURRENT_VIEW":
return { ...state, currentView: action.payload }; return { ...state, currentView: action.payload };
@ -50,9 +47,7 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
}; };
return ( return (
<ChartContext.Provider <ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}
>
{children} {children}
</ChartContext.Provider> </ChartContext.Provider>
); );

View File

@ -1,24 +1,25 @@
// react beautiful dnd // react beautiful dnd
import { Draggable } from "@hello-pangea/dnd"; import { Draggable } from "@hello-pangea/dnd";
// components
import { KanBanProperties } from "./properties";
interface IssueBlockProps { interface IssueBlockProps {
sub_group_id: string; sub_group_id: string;
columnId: string; columnId: string;
issues: any; issues: any;
isDragDisabled: boolean;
} }
export const IssueBlock = ({ sub_group_id, columnId, issues }: IssueBlockProps) => { export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: IssueBlockProps) => (
console.log();
return (
<> <>
{issues && issues.length > 0 ? ( {issues && issues.length > 0 ? (
<> <>
{issues.map((issue: any, index: any) => ( {issues.map((issue: any, index: any) => (
<Draggable <Draggable
draggableId={`${sub_group_id}-${columnId}-${issue.id}`}
index={index}
key={`issue-blocks-${sub_group_id}-${columnId}-${issue.id}`} key={`issue-blocks-${sub_group_id}-${columnId}-${issue.id}`}
draggableId={`${issue.id}`}
index={index}
isDragDisabled={isDragDisabled}
> >
{(provided: any, snapshot: any) => ( {(provided: any, snapshot: any) => (
<div <div
@ -29,13 +30,15 @@ export const IssueBlock = ({ sub_group_id, columnId, issues }: IssueBlockProps)
ref={provided.innerRef} ref={provided.innerRef}
> >
<div <div
className={`min-h-[106px] text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[4px] border transition-all bg-custom-background-100 hover:cursor-grab ${ className={`min-h-[106px] text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 hover:cursor-grab ${
snapshot.isDragging ? `border-custom-primary-100` : `border-transparent` snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`
}`} }`}
> >
<div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div> <div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div>
<div className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div> <div className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div>
<div className="h-[22px]">Footer</div> <div className="min-h-[22px]">
<KanBanProperties />
</div>
</div> </div>
</div> </div>
)} )}
@ -43,8 +46,11 @@ export const IssueBlock = ({ sub_group_id, columnId, issues }: IssueBlockProps)
))} ))}
</> </>
) : ( ) : (
<div>No issues are available.</div> !isDragDisabled && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)} )}
</> </>
); );
};

View File

@ -5,42 +5,51 @@ import { Droppable } from "@hello-pangea/dnd";
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { IssueBlock } from "./block"; import { IssueBlock } from "./block";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
// mobx // mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx // mobx
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
export interface IKanBan { export interface IGroupByKanBan {
issues?: any; issues: any;
sub_group_by: string | null;
group_by: string | null;
sub_group_id: string;
list: any;
listKey: string;
handleIssues?: () => void; handleIssues?: () => void;
handleDragDrop?: (result: any) => void | undefined; isDragDisabled: boolean;
sub_group_id?: string;
} }
export const KanBan: React.FC<IKanBan> = observer(({ issues, sub_group_id = "null" }) => { const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
const { project: projectStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); ({ 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 verticalAlignPosition = (_list: any) =>
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; issueKanBanViewStore.kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
return ( return (
<div className="relative w-full h-full">
{group_by && group_by === "state" && (
<div className="relative w-full h-full flex"> <div className="relative w-full h-full flex">
{projectStore?.projectStates && {list &&
projectStore?.projectStates.length > 0 && list.length > 0 &&
projectStore?.projectStates.map((state) => ( list.map((_list: any) => (
<div className="flex-shrink-0 flex flex-col w-[340px]"> <div className={`flex-shrink-0 flex flex-col ${!verticalAlignPosition(_list) ? `w-[340px]` : ``}`}>
{sub_group_by === null && ( {sub_group_by === null && (
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2]"> <div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2]">
<KanBanGroupByHeaderRoot column_id={state?.id} /> <KanBanGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string}
sub_group_by={sub_group_by}
group_by={group_by}
issues_count={issues?.[getValueFromObject(_list, listKey) as string].length || 0}
/>
</div> </div>
)} )}
<div className="w-full h-full"> {!verticalAlignPosition(_list) && (
<Droppable droppableId={`${sub_group_id}-${state?.id}`}> <div className="w-full min-h-[150px] h-full">
<Droppable droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}>
{(provided: any, snapshot: any) => ( {(provided: any, snapshot: any) => (
<div <div
className={`w-full h-full relative transition-all ${ className={`w-full h-full relative transition-all ${
@ -49,102 +58,117 @@ export const KanBan: React.FC<IKanBan> = observer(({ issues, sub_group_id = "nul
{...provided.droppableProps} {...provided.droppableProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
{issues && ( {issues ? (
<IssueBlock sub_group_id={sub_group_id} columnId={state?.id} issues={issues[state?.id]} /> <IssueBlock
sub_group_id={sub_group_id}
columnId={getValueFromObject(_list, listKey) as string}
issues={issues[getValueFromObject(_list, listKey) as string]}
isDragDisabled={isDragDisabled}
/>
) : (
isDragDisabled && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)} )}
{provided.placeholder} {provided.placeholder}
</div> </div>
)} )}
</Droppable> </Droppable>
</div> </div>
)}
</div> </div>
))} ))}
</div> </div>
);
}
);
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<IKanBan> = observer(({ issues, sub_group_by, group_by, sub_group_id = "null" }) => {
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
return (
<div className="relative w-full h-full">
{group_by && group_by === "state" && (
<GroupByKanBan
issues={issues}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={projectStore?.projectStates}
listKey={`id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/>
)} )}
{group_by && group_by === "state_detail.group" && ( {group_by && group_by === "state_detail.group" && (
<div className="relative w-full h-full flex"> <GroupByKanBan
{ISSUE_STATE_GROUPS && issues={issues}
ISSUE_STATE_GROUPS.length > 0 && group_by={group_by}
ISSUE_STATE_GROUPS.map((stateGroup) => ( sub_group_by={sub_group_by}
<div className="flex-shrink-0 flex flex-col w-[300px] h-full"> sub_group_id={sub_group_id}
{sub_group_by === null && ( list={ISSUE_STATE_GROUPS}
<div className="flex-shrink-0 w-full"> listKey={`key`}
<KanBanGroupByHeaderRoot column_id={stateGroup?.key} /> isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
</div> />
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
)} )}
{group_by && group_by === "priority" && ( {group_by && group_by === "priority" && (
<div className="relative w-full h-full flex"> <GroupByKanBan
{ISSUE_PRIORITIES && issues={issues}
ISSUE_PRIORITIES.length > 0 && group_by={group_by}
ISSUE_PRIORITIES.map((priority) => ( sub_group_by={sub_group_by}
<div className="flex-shrink-0 flex flex-col w-[300px] h-full"> sub_group_id={sub_group_id}
{sub_group_by === null && ( list={ISSUE_PRIORITIES}
<div className="flex-shrink-0 w-full"> listKey={`key`}
<KanBanGroupByHeaderRoot column_id={priority?.key} /> isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
</div> />
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
)} )}
{group_by && group_by === "labels" && ( {group_by && group_by === "labels" && (
<div className="relative w-full h-full flex"> <GroupByKanBan
{projectStore?.projectLabels && issues={issues}
projectStore?.projectLabels.length > 0 && group_by={group_by}
projectStore?.projectLabels.map((label) => ( sub_group_by={sub_group_by}
<div className="flex-shrink-0 flex flex-col w-[300px] h-full"> sub_group_id={sub_group_id}
{sub_group_by === null && ( list={projectStore?.projectLabels}
<div className="flex-shrink-0 w-full"> listKey={`id`}
<KanBanGroupByHeaderRoot column_id={label?.id} /> isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
</div> />
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
)} )}
{group_by && group_by === "assignees" && ( {group_by && group_by === "assignees" && (
<div className="relative w-full h-full flex"> <GroupByKanBan
{projectStore?.projectMembers && issues={issues}
projectStore?.projectMembers.length > 0 && group_by={group_by}
projectStore?.projectMembers.map((member) => ( sub_group_by={sub_group_by}
<div className="flex-shrink-0 flex flex-col w-[300px] h-full"> sub_group_id={sub_group_id}
{sub_group_by === null && ( list={projectStore?.projectMembers}
<div className="flex-shrink-0 w-full"> listKey={`member.id`}
<KanBanGroupByHeaderRoot column_id={member?.id} /> isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
</div> />
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
)} )}
{group_by && group_by === "created_by" && ( {group_by && group_by === "created_by" && (
<div className="relative w-full h-full flex"> <GroupByKanBan
{projectStore?.projectMembers && issues={issues}
projectStore?.projectMembers.length > 0 && group_by={group_by}
projectStore?.projectMembers.map((member) => ( sub_group_by={sub_group_by}
<div className="flex-shrink-0 flex flex-col w-[300px] h-full"> sub_group_id={sub_group_id}
{sub_group_by === null && ( list={projectStore?.projectMembers}
<div className="flex-shrink-0 w-full"> listKey={`member.id`}
<KanBanGroupByHeaderRoot column_id={member?.id} /> isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
</div> />
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
)} )}
</div> </div>
); );

View File

@ -1,19 +1,50 @@
// components
import { HeaderCard } from "./card";
// mobx // mobx
import { observer } from "mobx-react-lite"; 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 // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
export interface IAssigneesHeader { export interface IAssigneesHeader {
column_id: string; 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<IAssigneesHeader> = observer(({ column_id }) => { export const Icon = ({ user }: any) => <Avatar user={user} height="22px" width="22px" fontSize="12px" />;
export const AssigneesHeader: React.FC<IAssigneesHeader> = observer(
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
const { project: projectStore }: RootStore = useMobxStore(); const { project: projectStore }: RootStore = useMobxStore();
const assignee = (column_id && projectStore?.getProjectMemberById(column_id)) ?? null; const assignee = (column_id && projectStore?.getProjectMemberByUserId(column_id)) ?? null;
return <>{assignee && <HeaderCard title={assignee?.member?.display_name || ""} />}</>; return (
}); <>
{assignee &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon user={assignee?.member} />}
title={assignee?.member?.display_name || ""}
count={issues_count}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon user={assignee?.member} />}
title={assignee?.member?.display_name || ""}
count={issues_count}
/>
))}
</>
);
}
);

View File

@ -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 (
<div
className={`flex-shrink-0 relative flex gap-0.5 rounded-sm ${
position
? `flex-col items-center w-[44px] border border-custom-border-100 bg-custom-background-80 shadow-custom-shadow-sm`
: `flex-row items-center w-full`
}`}
>
<div className="flex-shrink-0 w-[26px] h-[26px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all">
{icon ? icon : <Circle width={14} strokeWidth={2} />}
</div>
<div className={`capitalize flex items-center gap-1 ${position ? `flex-col` : `flex-row w-full`}`}>
<div className={`font-medium line-clamp-1 ${position ? `vertical-lr` : ``}`}>{title}</div>
<div className="text-xs">(0)</div>
</div>
<div className="flex-shrink-0 w-[26px] h-[26px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all">
{position ? <Maximize2 width={14} strokeWidth={2} /> : <Minimize2 width={14} strokeWidth={2} />}
</div>
<div className="flex-shrink-0 w-[26px] h-[26px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all">
<Plus width={14} strokeWidth={2} />
</div>
</div>
);
};

View File

@ -1,19 +1,48 @@
// components
import { HeaderCard } from "./card";
// mobx // mobx
import { observer } from "mobx-react-lite"; 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 // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
export interface ICreatedByHeader { export interface ICreatedByHeader {
column_id: string; 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<ICreatedByHeader> = observer(({ column_id }) => { export const CreatedByHeader: React.FC<ICreatedByHeader> = observer(
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
const { project: projectStore }: RootStore = useMobxStore(); 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 && <HeaderCard title={createdBy?.member?.display_name || ""} />}</>; return (
}); <>
{createdBy &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon user={createdBy?.member} />}
title={createdBy?.member?.display_name || ""}
count={issues_count}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon user={createdBy?.member} />}
title={createdBy?.member?.display_name || ""}
count={issues_count}
/>
))}
</>
);
}
);

View File

@ -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 (
<div
className={`flex-shrink-0 relative flex gap-2 p-1.5 ${
verticalAlignPosition ? `flex-col items-center w-[44px]` : `flex-row items-center w-full`
}`}
>
<div className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center">
{icon ? icon : <Circle width={14} strokeWidth={2} />}
</div>
<div className={`flex items-center gap-1 ${verticalAlignPosition ? `flex-col` : `flex-row w-full`}`}>
<div
className={`font-medium line-clamp-1 text-custom-text-100 ${verticalAlignPosition ? `vertical-lr` : ``}`}
>
{title}
</div>
<div className={`text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}>
{count || 0}
</div>
</div>
{sub_group_by === null && (
<div
className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all"
onClick={() => issueKanBanViewStore?.handleKanBanToggle("groupByHeaderMinMax", column_id)}
>
{verticalAlignPosition ? (
<Maximize2 width={14} strokeWidth={2} />
) : (
<Minimize2 width={14} strokeWidth={2} />
)}
</div>
)}
{/* <div className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all">
<Plus width={14} strokeWidth={2} />
</div> */}
</div>
);
}
);

View File

@ -13,20 +13,68 @@ import { RootStore } from "store/root";
export interface IKanBanGroupByHeaderRoot { export interface IKanBanGroupByHeaderRoot {
column_id: string; column_id: string;
sub_group_by: string | null;
group_by: string | null;
issues_count: number;
} }
export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = observer(({ column_id }) => { export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = observer(
const { issueFilter: issueFilterStore }: RootStore = useMobxStore(); ({ column_id, sub_group_by, group_by, issues_count }) => (
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
return (
<> <>
{group_by && group_by === "state" && <StateHeader column_id={column_id} />} {group_by && group_by === "state" && (
{group_by && group_by === "state_detail.group" && <StateGroupHeader column_id={column_id} />} <StateHeader
{group_by && group_by === "priority" && <PriorityHeader column_id={column_id} />} column_id={column_id}
{group_by && group_by === "labels" && <LabelHeader column_id={column_id} />} sub_group_by={sub_group_by}
{group_by && group_by === "assignees" && <AssigneesHeader column_id={column_id} />} group_by={group_by}
{group_by && group_by === "created_by" && <CreatedByHeader column_id={column_id} />} header_type={`group_by`}
issues_count={issues_count}
/>
)}
{group_by && group_by === "state_detail.group" && (
<StateGroupHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
/>
)}
{group_by && group_by === "priority" && (
<PriorityHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
/>
)}
{group_by && group_by === "labels" && (
<LabelHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
/>
)}
{group_by && group_by === "assignees" && (
<AssigneesHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
/>
)}
{group_by && group_by === "created_by" && (
<CreatedByHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
/>
)}
</> </>
)
); );
});

View File

@ -1,19 +1,51 @@
// components
import { HeaderCard } from "./card";
// mobx // mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-group-by-card";
// store // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
export interface ILabelHeader { export interface ILabelHeader {
column_id: string; 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<ILabelHeader> = observer(({ column_id }) => { const Icon = ({ color }: any) => (
<div className="w-[12px] h-[12px] rounded-full" style={{ backgroundColor: color ? color : "#666" }} />
);
export const LabelHeader: React.FC<ILabelHeader> = observer(
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
const { project: projectStore }: RootStore = useMobxStore(); const { project: projectStore }: RootStore = useMobxStore();
const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null; const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null;
return <>{label && <HeaderCard title={label?.name || ""} />}</>; return (
}); <>
{label &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon color={label?.color} />}
title={label?.name || ""}
count={issues_count}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon />}
title={label?.name || ""}
count={issues_count}
/>
))}
</>
);
}
);

View File

@ -1,22 +1,72 @@
import React from "react";
// components
import { HeaderCard } from "./card";
// constants
import { issuePriorityByKey } from "constants/issue";
// mobx // mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx // lucide icons
import { useMobxStore } from "lib/mobx/store-provider"; import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
import { RootStore } from "store/root"; // components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-group-by-card";
// constants
import { issuePriorityByKey } from "constants/issue";
export interface IPriorityHeader { export interface IPriorityHeader {
column_id: string; 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<IPriorityHeader> = observer(({ column_id }) => { const Icon = ({ priority }: any) => (
const {}: RootStore = useMobxStore(); <div className="w-full h-full">
{priority === "urgent" ? (
<div className="border border-red-500 bg-red-500 text-white w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<AlertCircle size={14} strokeWidth={2} />
</div>
) : priority === "high" ? (
<div className="border border-red-500/20 bg-red-500/10 text-red-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<SignalHigh size={14} strokeWidth={2} className="pl-[3px]" />
</div>
) : priority === "medium" ? (
<div className="border border-orange-500/20 bg-orange-500/10 text-orange-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<SignalMedium size={14} strokeWidth={2} className="pl-[3px]" />
</div>
) : priority === "low" ? (
<div className="border border-green-500/20 bg-green-500/10 text-green-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<SignalLow size={14} strokeWidth={2} className="pl-[3px]" />
</div>
) : (
<div className="border border-custom-border-400/20 bg-custom-text-400/10 text-custom-text-400 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<Ban size={14} strokeWidth={2} />
</div>
)}
</div>
);
const stateGroup = column_id && issuePriorityByKey(column_id); export const PriorityHeader: React.FC<IPriorityHeader> = observer(
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
const priority = column_id && issuePriorityByKey(column_id);
return <>{stateGroup && <HeaderCard title={stateGroup?.title || ""} />}</>; return (
}); <>
{priority &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon priority={priority?.key} />}
title={priority?.key || ""}
count={issues_count}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon priority={priority?.key} />}
title={priority?.key || ""}
count={issues_count}
/>
))}
</>
);
}
);

View File

@ -1,23 +1,53 @@
import React from "react";
// components
import { HeaderCard } from "./card";
// constants
import { issueStateGroupByKey } from "constants/issue";
// mobx // mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx // components
import { useMobxStore } from "lib/mobx/store-provider"; import { HeaderGroupByCard } from "./group-by-card";
import { RootStore } from "store/root"; import { HeaderSubGroupByCard } from "./sub-group-by-card";
import { StateGroupIcon } from "components/icons";
// constants
import { issueStateGroupByKey } from "constants/issue";
export interface IStateGroupHeader { export interface IStateGroupHeader {
column_id: string; 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<IStateGroupHeader> = observer(({ column_id }) => { export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => (
const {}: RootStore = useMobxStore(); <div className="w-[14px] h-[14px] rounded-full">
<StateGroupIcon stateGroup={stateGroup} color={color || null} width="14" height="14" />
</div>
);
export const StateGroupHeader: React.FC<IStateGroupHeader> = observer(
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
const stateGroup = column_id && issueStateGroupByKey(column_id); const stateGroup = column_id && issueStateGroupByKey(column_id);
return <>{stateGroup && <HeaderCard title={stateGroup?.title || ""} />}</>; console.log("stateGroup", stateGroup);
});
return (
<>
{stateGroup &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon stateGroup={stateGroup?.key} />}
title={stateGroup?.key || ""}
count={issues_count}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon stateGroup={stateGroup?.key} />}
title={stateGroup?.key || ""}
count={issues_count}
/>
))}
</>
);
}
);

View File

@ -1,19 +1,48 @@
// components
import { HeaderCard } from "./card";
// mobx // mobx
import { observer } from "mobx-react-lite"; 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 // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
export interface IStateHeader { export interface IStateHeader {
column_id: string; 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<IStateHeader> = observer(({ column_id }) => { export const StateHeader: React.FC<IStateHeader> = observer(
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
const { project: projectStore }: RootStore = useMobxStore(); 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 && <HeaderCard title={state?.name || ""} />}</>; return (
}); <>
{state &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon stateGroup={state?.group} color={state?.color} />}
title={state?.name || ""}
count={issues_count}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon stateGroup={state?.group} color={state?.color} />}
title={state?.name || ""}
count={issues_count}
/>
))}
</>
);
}
);

View File

@ -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 (
<div className={`flex-shrink-0 relative flex gap-2 rounded-sm flex-row items-center w-full p-1.5`}>
<div
className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all"
onClick={() => issueKanBanViewStore?.handleKanBanToggle("subgroupByIssuesVisibility", column_id)}
>
{issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes(column_id) ? (
<ChevronDown width={14} strokeWidth={2} />
) : (
<ChevronUp width={14} strokeWidth={2} />
)}
</div>
<div className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center">
{icon ? icon : <Circle width={14} strokeWidth={2} />}
</div>
<div className="flex-shrink-0 flex items-center gap-1 text-sm">
<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>
</div>
);
});

View File

@ -1,3 +1,5 @@
// mobx
import { observer } from "mobx-react-lite";
// components // components
import { StateHeader } from "./state"; import { StateHeader } from "./state";
import { StateGroupHeader } from "./state-group"; import { StateGroupHeader } from "./state-group";
@ -5,28 +7,71 @@ import { AssigneesHeader } from "./assignee";
import { PriorityHeader } from "./priority"; import { PriorityHeader } from "./priority";
import { LabelHeader } from "./label"; import { LabelHeader } from "./label";
import { CreatedByHeader } from "./created_by"; 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 { export interface IKanBanSubGroupByHeaderRoot {
column_id: string; column_id: string;
sub_group_by: string | null;
group_by: string | null;
issues_count: number;
} }
export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> = observer(({ column_id }) => { export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> = observer(
const { issueFilter: issueFilterStore }: RootStore = useMobxStore(); ({ column_id, sub_group_by, group_by, issues_count }) => (
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
return (
<> <>
{sub_group_by && sub_group_by === "state" && <StateHeader column_id={column_id} />} {sub_group_by && sub_group_by === "state" && (
{sub_group_by && sub_group_by === "state_detail.group" && <StateGroupHeader column_id={column_id} />} <StateHeader
{sub_group_by && sub_group_by === "priority" && <PriorityHeader column_id={column_id} />} column_id={column_id}
{sub_group_by && sub_group_by === "labels" && <LabelHeader column_id={column_id} />} sub_group_by={sub_group_by}
{sub_group_by && sub_group_by === "assignees" && <AssigneesHeader column_id={column_id} />} group_by={group_by}
{sub_group_by && sub_group_by === "created_by" && <CreatedByHeader column_id={column_id} />} header_type={`sub_group_by`}
issues_count={issues_count}
/>
)}
{sub_group_by && sub_group_by === "state_detail.group" && (
<StateGroupHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
/>
)}
{sub_group_by && sub_group_by === "priority" && (
<PriorityHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
/>
)}
{sub_group_by && sub_group_by === "labels" && (
<LabelHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
/>
)}
{sub_group_by && sub_group_by === "assignees" && (
<AssigneesHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
/>
)}
{sub_group_by && sub_group_by === "created_by" && (
<CreatedByHeader
column_id={column_id}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
/>
)}
</> </>
)
); );
});

View File

@ -0,0 +1,91 @@
// lucide icons
import { Circle } from "lucide-react";
export const KanBanProperties = () => {
console.log("properties");
return (
<div className="relative flex gap-2 overflow-hidden overflow-x-auto whitespace-nowrap">
{/* basic properties */}
{/* state */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">state</div>
</div>
{/* priority */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">priority</div>
</div>
{/* label */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">label</div>
</div>
{/* assignee */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">assignee</div>
</div>
{/* start date */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">start date</div>
</div>
{/* target/due date */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">target/due date</div>
</div>
{/* extra render properties */}
{/* estimate */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">0</div>
</div>
{/* sub-issues */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">0</div>
</div>
{/* attachments */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">0</div>
</div>
{/* link */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">0</div>
</div>
</div>
);
};

View File

@ -3,26 +3,32 @@ import React from "react";
import { DragDropContext } from "@hello-pangea/dnd"; import { DragDropContext } from "@hello-pangea/dnd";
// mobx // mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components
import { KanBanSwimLanes } from "./swimlanes";
import { KanBan } from "./default";
// store // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
import { KanBanSwimLanes } from "./swimlanes";
import { KanBan } from "./default";
export interface IKanBanLayout { export interface IKanBanLayout {}
issues?: any;
handleIssues?: () => void; export const KanBanLayout: React.FC = observer(() => {
handleDragDrop?: (result: any) => void; 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<IKanBanLayout> = observer(({}) => {
const { issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore();
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
? "swimlanes" ? "swimlanes"
: "default"; : "default";
const issues = issueStore?.getIssues;
const onDragEnd = (result: any) => { const onDragEnd = (result: any) => {
if (!result) return; if (!result) return;
@ -34,14 +40,19 @@ export const KanBanLayout: React.FC<IKanBanLayout> = observer(({}) => {
) )
return; return;
console.log("result", result); currentKanBanView === "default"
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination); ? issueKanBanViewStore?.handleDragDrop(result.source, result.destination)
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
}; };
return ( return (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90`}> <div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90`}>
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? <KanBan issues={issues} /> : <KanBanSwimLanes issues={issues} />} {currentKanBanView === "default" ? (
<KanBan issues={issues} sub_group_by={sub_group_by} group_by={group_by} />
) : (
<KanBanSwimLanes issues={issues} sub_group_by={sub_group_by} group_by={group_by} />
)}
</DragDropContext> </DragDropContext>
</div> </div>
); );

View File

@ -4,99 +4,236 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root"; import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
import { KanBan } from "./default"; import { KanBan } from "./default";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
// mobx // mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx // mobx
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
export interface IKanBanSwimLanes { interface ISubGroupSwimlaneHeader {
issues?: any; issues: any;
handleIssues?: () => void; sub_group_by: string | null;
handleDragDrop?: () => void; group_by: string | null;
list: any;
listKey: string;
} }
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
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;
};
const SubGroupSwimlaneHeader = ({ list, _key }: any) => ( return (
<div className="relative w-full min-h-full h-max flex items-center"> <div className="relative w-full min-h-full h-max flex items-center">
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: any) => ( list.map((_list: any) => (
<div className="flex-shrink-0 flex flex-col w-[340px]"> <div className="flex-shrink-0 flex flex-col w-[340px]">
<KanBanGroupByHeaderRoot column_id={_list?.[_key]} /> <KanBanGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string}
sub_group_by={sub_group_by}
group_by={group_by}
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
/>
</div> </div>
))} ))}
</div> </div>
); );
};
const SubGroupSwimlane = ({ issues, list, _key }: any) => ( interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
issues: any;
}
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = 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 (
<div className="relative w-full min-h-full h-max"> <div className="relative w-full min-h-full h-max">
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: any) => ( list.map((_list: any) => (
<div className="flex-shrink-0 flex flex-col"> <div className="flex-shrink-0 flex flex-col">
<div className="sticky top-[30px] w-full z-[1] bg-custom-background-90 flex items-center py-1"> <div className="sticky top-[50px] w-full z-[1] bg-custom-background-90 flex items-center py-1">
<div className="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2"> <div className="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2">
<KanBanSubGroupByHeaderRoot column_id={_list?.[_key]} /> <KanBanSubGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string}
sub_group_by={sub_group_by}
group_by={group_by}
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
/>
</div> </div>
<div className="w-full border-b border-custom-border-400 border-dashed" /> <div className="w-full border-b border-custom-border-400 border-dashed" />
</div> </div>
{!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes(
getValueFromObject(_list, listKey) as string
) && (
<div className="relative"> <div className="relative">
<KanBan issues={issues} sub_group_id={_list?.[_key]} /> <KanBan
issues={issues?.[getValueFromObject(_list, listKey) as string]}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={getValueFromObject(_list, listKey) as string}
/>
</div> </div>
)}
</div> </div>
))} ))}
</div> </div>
); );
});
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer(({ issues }) => { export interface IKanBanSwimLanes {
const { project: projectStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); issues: any;
sub_group_by: string | null;
group_by: string | null;
handleIssues?: () => void;
}
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer(({ issues, sub_group_by, group_by }) => {
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; const { project: projectStore }: RootStore = useMobxStore();
console.log("sub_group_by", sub_group_by);
return ( return (
<div className="relative"> <div className="relative">
<div className="sticky top-0 z-[2] bg-custom-background-90 h-[30px]"> <div className="sticky top-0 z-[2] bg-custom-background-90 h-[50px]">
{group_by && group_by === "state" && <SubGroupSwimlaneHeader list={projectStore?.projectStates} _key={"id"} />} {group_by && group_by === "state" && (
<SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectStates}
listKey={`id`}
/>
)}
{group_by && group_by === "state_detail.group" && ( {group_by && group_by === "state_detail.group" && (
<SubGroupSwimlaneHeader list={ISSUE_STATE_GROUPS} _key={"key"} /> <SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={ISSUE_STATE_GROUPS}
listKey={`key`}
/>
)} )}
{group_by && group_by === "priority" && <SubGroupSwimlaneHeader list={ISSUE_PRIORITIES} _key={"key"} />}
{group_by && group_by === "labels" && <SubGroupSwimlaneHeader list={projectStore?.projectLabels} _key={"id"} />} {group_by && group_by === "priority" && (
<SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={ISSUE_PRIORITIES}
listKey={`key`}
/>
)}
{group_by && group_by === "labels" && (
<SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectLabels}
listKey={`id`}
/>
)}
{group_by && group_by === "assignees" && ( {group_by && group_by === "assignees" && (
<SubGroupSwimlaneHeader list={projectStore?.projectMembers} _key={"id"} /> <SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectMembers}
listKey={`member.id`}
/>
)} )}
{group_by && group_by === "created_by" && ( {group_by && group_by === "created_by" && (
<SubGroupSwimlaneHeader list={projectStore?.projectMembers} _key={"id"} /> <SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectMembers}
listKey={`member.id`}
/>
)} )}
</div> </div>
{sub_group_by && sub_group_by === "state" && ( {sub_group_by && sub_group_by === "state" && (
<SubGroupSwimlane issues={issues} list={projectStore?.projectStates} _key={"id"} /> <SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectStates}
listKey={`id`}
/>
)} )}
{sub_group_by && sub_group_by === "state_detail.group" && ( {sub_group_by && sub_group_by === "state_detail.group" && (
<SubGroupSwimlane issues={issues} list={ISSUE_STATE_GROUPS} _key={"key"} /> <SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={ISSUE_STATE_GROUPS}
listKey={`key`}
/>
)} )}
{sub_group_by && sub_group_by === "priority" && ( {sub_group_by && sub_group_by === "priority" && (
<SubGroupSwimlane issues={issues} list={ISSUE_PRIORITIES} _key={"key"} /> <SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={ISSUE_PRIORITIES}
listKey={`key`}
/>
)} )}
{sub_group_by && sub_group_by === "labels" && ( {sub_group_by && sub_group_by === "labels" && (
<SubGroupSwimlane issues={issues} list={projectStore?.projectLabels} _key={"id"} /> <SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectLabels}
listKey={`id`}
/>
)} )}
{sub_group_by && sub_group_by === "assignees" && ( {sub_group_by && sub_group_by === "assignees" && (
<SubGroupSwimlane issues={issues} list={projectStore?.projectMembers} _key={"id"} /> <SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectMembers}
listKey={`member.id`}
/>
)} )}
{sub_group_by && sub_group_by === "created_by" && ( {sub_group_by && sub_group_by === "created_by" && (
<SubGroupSwimlane issues={issues} list={projectStore?.projectMembers} _key={"id"} /> <SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectMembers}
listKey={`member.id`}
/>
)} )}
</div> </div>
); );

View File

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

View File

@ -142,12 +142,7 @@ class IssueStore implements IIssueStore {
this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); this.rootStore.workspace.setWorkspaceSlug(workspaceSlug);
this.rootStore.project.setProjectId(projectId); this.rootStore.project.setProjectId(projectId);
// TODO: replace this once the issue filter is completed const params = this.rootStore?.issueFilter?.appliedFilters;
const params = {
group_by: "target_date",
order_by: "-created_at",
target_date: "2023-09-01;after,2023-09-30;before",
};
const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params); const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params);
const issueType = this.getIssueType; const issueType = this.getIssueType;

View File

@ -115,8 +115,6 @@ class IssueDetailStore implements IIssueDetailStore {
const response = await this.issueService.createIssues(workspaceId, projectId, data, user); const response = await this.issueService.createIssues(workspaceId, projectId, data, user);
if (response) this.rootStore.issue.addIssueToIssuesStore(projectId, response);
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = null; this.error = null;
@ -137,7 +135,7 @@ class IssueDetailStore implements IIssueDetailStore {
projectId: string, projectId: string,
issueId: string, issueId: string,
data: Partial<IIssue>, data: Partial<IIssue>,
user: ICurrentUserResponse user: ICurrentUserResponse | undefined
) => { ) => {
const newIssues = { ...this.issues }; const newIssues = { ...this.issues };
newIssues[issueId] = { newIssues[issueId] = {
@ -154,8 +152,6 @@ class IssueDetailStore implements IIssueDetailStore {
const response = await this.issueService.patchIssue(workspaceId, projectId, issueId, data, user); const response = await this.issueService.patchIssue(workspaceId, projectId, issueId, data, user);
if (response) this.rootStore.issue.updateIssueInIssuesStore(projectId, response);
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = null; this.error = null;
@ -192,8 +188,6 @@ class IssueDetailStore implements IIssueDetailStore {
await this.issueService.deleteIssue(workspaceId, projectId, issueId, user); await this.issueService.deleteIssue(workspaceId, projectId, issueId, user);
this.rootStore.issue.deleteIssueFromIssuesStore(projectId, issueId);
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = null; this.error = null;

View File

@ -1,24 +1,42 @@
import { action, computed, makeObservable } from "mobx"; import { action, computed, makeObservable, observable, runInAction } from "mobx";
// types // types
import { RootStore } from "./root"; import { RootStore } from "./root";
import { IIssueType } from "./issue"; import { IIssueType } from "./issue";
export interface IIssueKanBanViewStore { 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; handleDragDrop: (source: any, destination: any) => void;
} }
class IssueKanBanViewStore implements IIssueKanBanViewStore { class IssueKanBanViewStore implements IIssueKanBanViewStore {
kanBanToggle: {
groupByHeaderMinMax: string[];
subgroupByIssuesVisibility: string[];
} = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] };
// root store // root store
rootStore; rootStore;
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
kanBanToggle: observable,
// computed // computed
canUserDragDrop: computed, canUserDragDrop: computed,
canUserDragDropVertically: computed, canUserDragDropVertically: computed,
canUserDragDropHorizontally: computed, canUserDragDropHorizontally: computed,
// actions // actions
handleKanBanToggle: action,
handleSwimlaneDragDrop: action,
handleDragDrop: action, handleDragDrop: action,
}); });
@ -27,50 +45,83 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore {
get canUserDragDrop() { get canUserDragDrop() {
if ( if (
this.rootStore?.issueFilter?.userDisplayFilters?.group_by &&
this.rootStore?.issueFilter?.userDisplayFilters?.order_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)
) { ) {
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 true;
} }
return false; return false;
} }
get canUserDragDropVertically() { get canUserDragDropVertically() {
return true; return false;
}
get canUserDragDropHorizontally() {
return true;
} }
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 workspaceSlug = this.rootStore?.workspace?.workspaceSlug;
const projectId = this.rootStore?.project?.projectId; const projectId = this.rootStore?.project?.projectId;
const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType;
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
const sortOrderDefaultValue = 10000;
if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && this.rootStore.issue.getIssues) {
const currentIssues: any = 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 = { let updateIssue: any = {
workspaceSlug: workspaceSlug, workspaceSlug: workspaceSlug,
projectId: projectId, projectId: projectId,
}; };
// user can drag the issues from any direction // source, destination group and sub group id
if (this.canUserDragDrop) { let droppableSourceColumnId = source.droppableId;
// vertical droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null;
if (source.droppableId === destination.droppableId) { let droppableDestinationColumnId = destination.droppableId;
const _columnId = source.droppableId; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null;
const _issues = currentIssues[_columnId]; 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 // update the sort order
if (destination.index === 0) { 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) { } 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 { } else {
updateIssue = { updateIssue = {
...updateIssue, ...updateIssue,
@ -78,34 +129,26 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore {
}; };
} }
// update the mobx state array
const [removed] = _issues.splice(source.index, 1); const [removed] = _issues.splice(source.index, 1);
_issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order });
updateIssue = { ...updateIssue, issueId: removed?.id }; updateIssue = { ...updateIssue, issueId: removed?.id };
currentIssues[source_sub_group_id][source_group_id] = _issues;
currentIssues[_columnId] = _issues;
} }
// horizontal if (source_group_id != destination_group_id) {
if (source.droppableId != destination.droppableId) { const _sourceIssues = currentIssues[source_sub_group_id][source_group_id];
const _sourceColumnId = source.droppableId; let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || [];
const _destinationColumnId = destination.droppableId;
const _sourceIssues = currentIssues[_sourceColumnId]; if (_destinationIssues && _destinationIssues.length > 0) {
const _destinationIssues = currentIssues[_destinationColumnId];
if (_destinationIssues.length > 0) {
if (destination.index === 0) { if (destination.index === 0) {
updateIssue = { updateIssue = {
...updateIssue, ...updateIssue,
sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, 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 = {
...updateIssue, ...updateIssue,
sort_order: _destinationIssues[destination.index].sort_order + sortOrderDefaultValue, sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue,
state: destination?.droppableId,
}; };
} else { } else {
updateIssue = { updateIssue = {
@ -114,39 +157,84 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore {
(_destinationIssues[destination.index - 1].sort_order + (_destinationIssues[destination.index - 1].sort_order +
_destinationIssues[destination.index].sort_order) / _destinationIssues[destination.index].sort_order) /
2, 2,
state: destination?.droppableId,
}; };
} }
} else { } else {
updateIssue = { updateIssue = {
...updateIssue, ...updateIssue,
sort_order: sortOrderDefaultValue, sort_order: sortOrderDefaultValue,
state: destination?.droppableId,
}; };
} }
const [removed] = _sourceIssues.splice(source.index, 1); const [removed] = _sourceIssues.splice(source.index, 1);
if (_destinationIssues && _destinationIssues.length > 0)
_destinationIssues.splice(destination.index, 0, { _destinationIssues.splice(destination.index, 0, {
...removed, ...removed,
state: destination?.droppableId,
sort_order: updateIssue.sort_order, sort_order: updateIssue.sort_order,
}); });
else _destinationIssues = [..._destinationIssues, { ...removed, sort_order: updateIssue.sort_order }];
updateIssue = { ...updateIssue, issueId: removed?.id }; updateIssue = { ...updateIssue, issueId: removed?.id };
currentIssues[_sourceColumnId] = _sourceIssues; // if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state")
currentIssues[_destinationColumnId] = _destinationIssues; // 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 (source_sub_group_id != destination_sub_group_id) {
if (this.canUserDragDropVertically && source.droppableId === destination.droppableId) { 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,
};
} }
// user can drag the issues only horizontally const [removed] = _sourceIssues.splice(source.index, 1);
if (this.canUserDragDropHorizontally && source.droppableId != destination.droppableId) { 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 };
} }
this.rootStore.issue.issues = { const reorderedIssues = {
...this.rootStore?.issue.issues, ...this.rootStore?.issue.issues,
[projectId]: { [projectId]: {
...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.workspaceSlug,
// updateIssue.projectId, // updateIssue.projectId,
// updateIssue.issueId, // 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; export default IssueKanBanViewStore;

View File

@ -48,6 +48,7 @@ export interface IProjectStore {
getProjectStateById: (stateId: string) => IState | null; getProjectStateById: (stateId: string) => IState | null;
getProjectLabelById: (labelId: string) => IIssueLabels | null; getProjectLabelById: (labelId: string) => IIssueLabels | null;
getProjectMemberById: (memberId: string) => IProjectMember | null; getProjectMemberById: (memberId: string) => IProjectMember | null;
getProjectMemberByUserId: (memberId: string) => IProjectMember | null;
fetchProjects: (workspaceSlug: string) => Promise<void>; fetchProjects: (workspaceSlug: string) => Promise<void>;
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>; fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
@ -269,6 +270,14 @@ class ProjectStore implements IProjectStore {
return memberInfo; 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) => { fetchProjectStates = async (workspaceSlug: string, projectSlug: string) => {
try { try {
this.loader = true; this.loader = true;