mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
f60dcdc599
commit
b70047b1d5
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
|
91
web/components/issues/issue-layouts/kanban/properties.tsx
Normal file
91
web/components/issues/issue-layouts/kanban/properties.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user