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);
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 (
<ChartContext.Provider
value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}
>
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
{children}
</ChartContext.Provider>
);

View File

@ -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) => (
<Draggable
draggableId={`${sub_group_id}-${columnId}-${issue.id}`}
index={index}
key={`issue-blocks-${sub_group_id}-${columnId}-${issue.id}`}
>
{(provided: any, snapshot: any) => (
export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: IssueBlockProps) => (
<>
{issues && issues.length > 0 ? (
<>
{issues.map((issue: any, index: any) => (
<Draggable
key={`issue-blocks-${sub_group_id}-${columnId}-${issue.id}`}
draggableId={`${issue.id}`}
index={index}
isDragDisabled={isDragDisabled}
>
{(provided: any, snapshot: any) => (
<div
key={issue.id}
className="p-1.5 hover:cursor-default"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
<div
key={issue.id}
className="p-1.5 hover:cursor-default"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
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`
}`}
>
<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 ${
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="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div>
<div className="h-[22px]">Footer</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="min-h-[22px]">
<KanBanProperties />
</div>
</div>
)}
</Draggable>
))}
</>
) : (
<div>No issues are available.</div>
)}
</>
);
};
</div>
)}
</Draggable>
))}
</>
) : (
!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 { 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<IKanBan> = observer(({ issues, sub_group_id = "null" }) => {
const { project: projectStore, issueFilter: issueFilterStore }: RootStore = useMobxStore();
const GroupByKanBan: React.FC<IGroupByKanBan> = 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 (
<div className="relative w-full h-full">
{group_by && group_by === "state" && (
<div className="relative w-full h-full flex">
{projectStore?.projectStates &&
projectStore?.projectStates.length > 0 &&
projectStore?.projectStates.map((state) => (
<div className="flex-shrink-0 flex flex-col w-[340px]">
{sub_group_by === null && (
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2]">
<KanBanGroupByHeaderRoot column_id={state?.id} />
</div>
)}
return (
<div className="relative w-full h-full flex">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div className={`flex-shrink-0 flex flex-col ${!verticalAlignPosition(_list) ? `w-[340px]` : ``}`}>
{sub_group_by === null && (
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2]">
<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 className="w-full h-full">
<Droppable droppableId={`${sub_group_id}-${state?.id}`}>
{!verticalAlignPosition(_list) && (
<div className="w-full min-h-[150px] h-full">
<Droppable droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}>
{(provided: any, snapshot: any) => (
<div
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}
ref={provided.innerRef}
>
{issues && (
<IssueBlock sub_group_id={sub_group_id} columnId={state?.id} issues={issues[state?.id]} />
{issues ? (
<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}
</div>
)}
</Droppable>
</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" && (
<div className="relative w-full h-full flex">
{ISSUE_STATE_GROUPS &&
ISSUE_STATE_GROUPS.length > 0 &&
ISSUE_STATE_GROUPS.map((stateGroup) => (
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
{sub_group_by === null && (
<div className="flex-shrink-0 w-full">
<KanBanGroupByHeaderRoot column_id={stateGroup?.key} />
</div>
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
<GroupByKanBan
issues={issues}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={ISSUE_STATE_GROUPS}
listKey={`key`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/>
)}
{group_by && group_by === "priority" && (
<div className="relative w-full h-full flex">
{ISSUE_PRIORITIES &&
ISSUE_PRIORITIES.length > 0 &&
ISSUE_PRIORITIES.map((priority) => (
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
{sub_group_by === null && (
<div className="flex-shrink-0 w-full">
<KanBanGroupByHeaderRoot column_id={priority?.key} />
</div>
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
<GroupByKanBan
issues={issues}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={ISSUE_PRIORITIES}
listKey={`key`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/>
)}
{group_by && group_by === "labels" && (
<div className="relative w-full h-full flex">
{projectStore?.projectLabels &&
projectStore?.projectLabels.length > 0 &&
projectStore?.projectLabels.map((label) => (
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
{sub_group_by === null && (
<div className="flex-shrink-0 w-full">
<KanBanGroupByHeaderRoot column_id={label?.id} />
</div>
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
<GroupByKanBan
issues={issues}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={projectStore?.projectLabels}
listKey={`id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/>
)}
{group_by && group_by === "assignees" && (
<div className="relative w-full h-full flex">
{projectStore?.projectMembers &&
projectStore?.projectMembers.length > 0 &&
projectStore?.projectMembers.map((member) => (
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
{sub_group_by === null && (
<div className="flex-shrink-0 w-full">
<KanBanGroupByHeaderRoot column_id={member?.id} />
</div>
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
<GroupByKanBan
issues={issues}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={projectStore?.projectMembers}
listKey={`member.id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/>
)}
{group_by && group_by === "created_by" && (
<div className="relative w-full h-full flex">
{projectStore?.projectMembers &&
projectStore?.projectMembers.length > 0 &&
projectStore?.projectMembers.map((member) => (
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
{sub_group_by === null && (
<div className="flex-shrink-0 w-full">
<KanBanGroupByHeaderRoot column_id={member?.id} />
</div>
)}
<div className="w-full h-full">content</div>
</div>
))}
</div>
<GroupByKanBan
issues={issues}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={projectStore?.projectMembers}
listKey={`member.id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/>
)}
</div>
);

View File

@ -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<IAssigneesHeader> = observer(({ column_id }) => {
const { project: projectStore }: RootStore = useMobxStore();
export const Icon = ({ user }: any) => <Avatar user={user} height="22px" width="22px" fontSize="12px" />;
const assignee = (column_id && projectStore?.getProjectMemberById(column_id)) ?? null;
export const AssigneesHeader: React.FC<IAssigneesHeader> = observer(
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
const { project: projectStore }: RootStore = useMobxStore();
return <>{assignee && <HeaderCard title={assignee?.member?.display_name || ""} />}</>;
});
const assignee = (column_id && projectStore?.getProjectMemberByUserId(column_id)) ?? null;
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
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<ICreatedByHeader> = observer(({ column_id }) => {
const { project: projectStore }: RootStore = useMobxStore();
export const CreatedByHeader: React.FC<ICreatedByHeader> = 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 && <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 {
column_id: string;
sub_group_by: string | null;
group_by: string | null;
issues_count: number;
}
export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = observer(({ column_id }) => {
const { issueFilter: issueFilterStore }: RootStore = useMobxStore();
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
return (
export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = observer(
({ column_id, sub_group_by, group_by, issues_count }) => (
<>
{group_by && group_by === "state" && <StateHeader column_id={column_id} />}
{group_by && group_by === "state_detail.group" && <StateGroupHeader column_id={column_id} />}
{group_by && group_by === "priority" && <PriorityHeader column_id={column_id} />}
{group_by && group_by === "labels" && <LabelHeader column_id={column_id} />}
{group_by && group_by === "assignees" && <AssigneesHeader column_id={column_id} />}
{group_by && group_by === "created_by" && <CreatedByHeader column_id={column_id} />}
{group_by && group_by === "state" && (
<StateHeader
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 === "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
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<ILabelHeader> = observer(({ column_id }) => {
const { project: projectStore }: RootStore = useMobxStore();
const Icon = ({ color }: any) => (
<div className="w-[12px] h-[12px] rounded-full" style={{ backgroundColor: color ? color : "#666" }} />
);
const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null;
export const LabelHeader: React.FC<ILabelHeader> = observer(
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
const { project: projectStore }: RootStore = useMobxStore();
return <>{label && <HeaderCard title={label?.name || ""} />}</>;
});
const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null;
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
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<IPriorityHeader> = observer(({ column_id }) => {
const {}: RootStore = useMobxStore();
const Icon = ({ priority }: any) => (
<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
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<IStateGroupHeader> = observer(({ column_id }) => {
const {}: RootStore = useMobxStore();
export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => (
<div className="w-[14px] h-[14px] rounded-full">
<StateGroupIcon stateGroup={stateGroup} color={color || null} width="14" height="14" />
</div>
);
const stateGroup = column_id && issueStateGroupByKey(column_id);
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);
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
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<IStateHeader> = observer(({ column_id }) => {
const { project: projectStore }: RootStore = useMobxStore();
export const StateHeader: React.FC<IStateHeader> = 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 && <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
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<IKanBanSubGroupByHeaderRoot> = 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<IKanBanSubGroupByHeaderRoot> = observer(
({ column_id, sub_group_by, group_by, issues_count }) => (
<>
{sub_group_by && sub_group_by === "state" && <StateHeader column_id={column_id} />}
{sub_group_by && sub_group_by === "state_detail.group" && <StateGroupHeader column_id={column_id} />}
{sub_group_by && sub_group_by === "priority" && <PriorityHeader column_id={column_id} />}
{sub_group_by && sub_group_by === "labels" && <LabelHeader column_id={column_id} />}
{sub_group_by && sub_group_by === "assignees" && <AssigneesHeader column_id={column_id} />}
{sub_group_by && sub_group_by === "created_by" && <CreatedByHeader column_id={column_id} />}
{sub_group_by && sub_group_by === "state" && (
<StateHeader
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 === "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";
// 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<IKanBanLayout> = 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<IKanBanLayout> = 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 (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90`}>
<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>
</div>
);

View File

@ -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<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;
};
return (
<div className="relative w-full min-h-full h-max flex items-center">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div className="flex-shrink-0 flex flex-col w-[340px]">
<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>
);
};
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">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div className="flex-shrink-0 flex flex-col">
<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">
<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 className="w-full border-b border-custom-border-400 border-dashed" />
</div>
{!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes(
getValueFromObject(_list, listKey) as string
) && (
<div className="relative">
<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>
);
});
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) => (
<div className="relative w-full min-h-full h-max flex items-center">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div className="flex-shrink-0 flex flex-col w-[340px]">
<KanBanGroupByHeaderRoot column_id={_list?.[_key]} />
</div>
))}
</div>
);
const SubGroupSwimlane = ({ issues, list, _key }: any) => (
<div className="relative w-full min-h-full h-max">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<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="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2">
<KanBanSubGroupByHeaderRoot column_id={_list?.[_key]} />
</div>
<div className="w-full border-b border-custom-border-400 border-dashed" />
</div>
<div className="relative">
<KanBan issues={issues} sub_group_id={_list?.[_key]} />
</div>
</div>
))}
</div>
);
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = 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<IKanBanSwimLanes> = observer(({ issues, sub_group_by, group_by }) => {
const { project: projectStore }: RootStore = useMobxStore();
return (
<div className="relative">
<div className="sticky top-0 z-[2] bg-custom-background-90 h-[30px]">
{group_by && group_by === "state" && <SubGroupSwimlaneHeader list={projectStore?.projectStates} _key={"id"} />}
<div className="sticky top-0 z-[2] bg-custom-background-90 h-[50px]">
{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" && (
<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" && (
<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" && (
<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>
{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" && (
<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" && (
<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" && (
<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" && (
<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" && (
<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>
);

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.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;

View File

@ -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<IIssue>,
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;

View File

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

View File

@ -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<void>;
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
@ -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;