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);
|
export const ChartContext = createContext<ChartContextReducer | undefined>(undefined);
|
||||||
|
|
||||||
const chartReducer = (
|
const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => {
|
||||||
state: ChartContextData,
|
|
||||||
action: ChartContextActionPayload
|
|
||||||
): ChartContextData => {
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "CURRENT_VIEW":
|
case "CURRENT_VIEW":
|
||||||
return { ...state, currentView: action.payload };
|
return { ...state, currentView: action.payload };
|
||||||
@ -50,9 +47,7 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider
|
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
|
||||||
value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
// react beautiful dnd
|
// react beautiful dnd
|
||||||
import { Draggable } from "@hello-pangea/dnd";
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
|
// components
|
||||||
|
import { KanBanProperties } from "./properties";
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
sub_group_id: string;
|
sub_group_id: string;
|
||||||
columnId: string;
|
columnId: string;
|
||||||
issues: any;
|
issues: any;
|
||||||
|
isDragDisabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueBlock = ({ sub_group_id, columnId, issues }: IssueBlockProps) => {
|
export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: IssueBlockProps) => (
|
||||||
console.log();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
{issues && issues.length > 0 ? (
|
{issues && issues.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{issues.map((issue: any, index: any) => (
|
{issues.map((issue: any, index: any) => (
|
||||||
<Draggable
|
<Draggable
|
||||||
draggableId={`${sub_group_id}-${columnId}-${issue.id}`}
|
|
||||||
index={index}
|
|
||||||
key={`issue-blocks-${sub_group_id}-${columnId}-${issue.id}`}
|
key={`issue-blocks-${sub_group_id}-${columnId}-${issue.id}`}
|
||||||
|
draggableId={`${issue.id}`}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={isDragDisabled}
|
||||||
>
|
>
|
||||||
{(provided: any, snapshot: any) => (
|
{(provided: any, snapshot: any) => (
|
||||||
<div
|
<div
|
||||||
@ -29,13 +30,15 @@ export const IssueBlock = ({ sub_group_id, columnId, issues }: IssueBlockProps)
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`min-h-[106px] text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[4px] border transition-all bg-custom-background-100 hover:cursor-grab ${
|
className={`min-h-[106px] text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 hover:cursor-grab ${
|
||||||
snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`
|
snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div>
|
<div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div>
|
||||||
<div className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div>
|
<div className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div>
|
||||||
<div className="h-[22px]">Footer</div>
|
<div className="min-h-[22px]">
|
||||||
|
<KanBanProperties />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -43,8 +46,11 @@ export const IssueBlock = ({ sub_group_id, columnId, issues }: IssueBlockProps)
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div>No issues are available.</div>
|
!isDragDisabled && (
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||||
|
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
@ -5,42 +5,51 @@ import { Droppable } from "@hello-pangea/dnd";
|
|||||||
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
||||||
import { IssueBlock } from "./block";
|
import { IssueBlock } from "./block";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx
|
// mobx
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
export interface IKanBan {
|
export interface IGroupByKanBan {
|
||||||
issues?: any;
|
issues: any;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
sub_group_id: string;
|
||||||
|
list: any;
|
||||||
|
listKey: string;
|
||||||
handleIssues?: () => void;
|
handleIssues?: () => void;
|
||||||
handleDragDrop?: (result: any) => void | undefined;
|
isDragDisabled: boolean;
|
||||||
sub_group_id?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBan: React.FC<IKanBan> = observer(({ issues, sub_group_id = "null" }) => {
|
const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
|
||||||
const { project: projectStore, issueFilter: issueFilterStore }: RootStore = useMobxStore();
|
({ issues, sub_group_by, group_by, sub_group_id = "null", list, listKey, isDragDisabled }) => {
|
||||||
|
const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
const verticalAlignPosition = (_list: any) =>
|
||||||
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
|
issueKanBanViewStore.kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full">
|
|
||||||
{group_by && group_by === "state" && (
|
|
||||||
<div className="relative w-full h-full flex">
|
<div className="relative w-full h-full flex">
|
||||||
{projectStore?.projectStates &&
|
{list &&
|
||||||
projectStore?.projectStates.length > 0 &&
|
list.length > 0 &&
|
||||||
projectStore?.projectStates.map((state) => (
|
list.map((_list: any) => (
|
||||||
<div className="flex-shrink-0 flex flex-col w-[340px]">
|
<div className={`flex-shrink-0 flex flex-col ${!verticalAlignPosition(_list) ? `w-[340px]` : ``}`}>
|
||||||
{sub_group_by === null && (
|
{sub_group_by === null && (
|
||||||
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2]">
|
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2]">
|
||||||
<KanBanGroupByHeaderRoot column_id={state?.id} />
|
<KanBanGroupByHeaderRoot
|
||||||
|
column_id={getValueFromObject(_list, listKey) as string}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
issues_count={issues?.[getValueFromObject(_list, listKey) as string].length || 0}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-full h-full">
|
{!verticalAlignPosition(_list) && (
|
||||||
<Droppable droppableId={`${sub_group_id}-${state?.id}`}>
|
<div className="w-full min-h-[150px] h-full">
|
||||||
|
<Droppable droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}>
|
||||||
{(provided: any, snapshot: any) => (
|
{(provided: any, snapshot: any) => (
|
||||||
<div
|
<div
|
||||||
className={`w-full h-full relative transition-all ${
|
className={`w-full h-full relative transition-all ${
|
||||||
@ -49,102 +58,117 @@ export const KanBan: React.FC<IKanBan> = observer(({ issues, sub_group_id = "nul
|
|||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{issues && (
|
{issues ? (
|
||||||
<IssueBlock sub_group_id={sub_group_id} columnId={state?.id} issues={issues[state?.id]} />
|
<IssueBlock
|
||||||
|
sub_group_id={sub_group_id}
|
||||||
|
columnId={getValueFromObject(_list, listKey) as string}
|
||||||
|
issues={issues[getValueFromObject(_list, listKey) as string]}
|
||||||
|
isDragDisabled={isDragDisabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
isDragDisabled && (
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||||
|
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface IKanBan {
|
||||||
|
issues: any;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
sub_group_id?: string;
|
||||||
|
handleIssues?: () => void;
|
||||||
|
handleDragDrop?: (result: any) => void | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KanBan: React.FC<IKanBan> = observer(({ issues, sub_group_by, group_by, sub_group_id = "null" }) => {
|
||||||
|
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
{group_by && group_by === "state" && (
|
||||||
|
<GroupByKanBan
|
||||||
|
issues={issues}
|
||||||
|
group_by={group_by}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
sub_group_id={sub_group_id}
|
||||||
|
list={projectStore?.projectStates}
|
||||||
|
listKey={`id`}
|
||||||
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{group_by && group_by === "state_detail.group" && (
|
{group_by && group_by === "state_detail.group" && (
|
||||||
<div className="relative w-full h-full flex">
|
<GroupByKanBan
|
||||||
{ISSUE_STATE_GROUPS &&
|
issues={issues}
|
||||||
ISSUE_STATE_GROUPS.length > 0 &&
|
group_by={group_by}
|
||||||
ISSUE_STATE_GROUPS.map((stateGroup) => (
|
sub_group_by={sub_group_by}
|
||||||
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
|
sub_group_id={sub_group_id}
|
||||||
{sub_group_by === null && (
|
list={ISSUE_STATE_GROUPS}
|
||||||
<div className="flex-shrink-0 w-full">
|
listKey={`key`}
|
||||||
<KanBanGroupByHeaderRoot column_id={stateGroup?.key} />
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
<div className="w-full h-full">content</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{group_by && group_by === "priority" && (
|
{group_by && group_by === "priority" && (
|
||||||
<div className="relative w-full h-full flex">
|
<GroupByKanBan
|
||||||
{ISSUE_PRIORITIES &&
|
issues={issues}
|
||||||
ISSUE_PRIORITIES.length > 0 &&
|
group_by={group_by}
|
||||||
ISSUE_PRIORITIES.map((priority) => (
|
sub_group_by={sub_group_by}
|
||||||
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
|
sub_group_id={sub_group_id}
|
||||||
{sub_group_by === null && (
|
list={ISSUE_PRIORITIES}
|
||||||
<div className="flex-shrink-0 w-full">
|
listKey={`key`}
|
||||||
<KanBanGroupByHeaderRoot column_id={priority?.key} />
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
<div className="w-full h-full">content</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{group_by && group_by === "labels" && (
|
{group_by && group_by === "labels" && (
|
||||||
<div className="relative w-full h-full flex">
|
<GroupByKanBan
|
||||||
{projectStore?.projectLabels &&
|
issues={issues}
|
||||||
projectStore?.projectLabels.length > 0 &&
|
group_by={group_by}
|
||||||
projectStore?.projectLabels.map((label) => (
|
sub_group_by={sub_group_by}
|
||||||
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
|
sub_group_id={sub_group_id}
|
||||||
{sub_group_by === null && (
|
list={projectStore?.projectLabels}
|
||||||
<div className="flex-shrink-0 w-full">
|
listKey={`id`}
|
||||||
<KanBanGroupByHeaderRoot column_id={label?.id} />
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
<div className="w-full h-full">content</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{group_by && group_by === "assignees" && (
|
{group_by && group_by === "assignees" && (
|
||||||
<div className="relative w-full h-full flex">
|
<GroupByKanBan
|
||||||
{projectStore?.projectMembers &&
|
issues={issues}
|
||||||
projectStore?.projectMembers.length > 0 &&
|
group_by={group_by}
|
||||||
projectStore?.projectMembers.map((member) => (
|
sub_group_by={sub_group_by}
|
||||||
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
|
sub_group_id={sub_group_id}
|
||||||
{sub_group_by === null && (
|
list={projectStore?.projectMembers}
|
||||||
<div className="flex-shrink-0 w-full">
|
listKey={`member.id`}
|
||||||
<KanBanGroupByHeaderRoot column_id={member?.id} />
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
<div className="w-full h-full">content</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{group_by && group_by === "created_by" && (
|
{group_by && group_by === "created_by" && (
|
||||||
<div className="relative w-full h-full flex">
|
<GroupByKanBan
|
||||||
{projectStore?.projectMembers &&
|
issues={issues}
|
||||||
projectStore?.projectMembers.length > 0 &&
|
group_by={group_by}
|
||||||
projectStore?.projectMembers.map((member) => (
|
sub_group_by={sub_group_by}
|
||||||
<div className="flex-shrink-0 flex flex-col w-[300px] h-full">
|
sub_group_id={sub_group_id}
|
||||||
{sub_group_by === null && (
|
list={projectStore?.projectMembers}
|
||||||
<div className="flex-shrink-0 w-full">
|
listKey={`member.id`}
|
||||||
<KanBanGroupByHeaderRoot column_id={member?.id} />
|
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
<div className="w-full h-full">content</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,19 +1,50 @@
|
|||||||
// components
|
|
||||||
import { HeaderCard } from "./card";
|
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { HeaderGroupByCard } from "./group-by-card";
|
||||||
|
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||||
|
import { Avatar } from "components/ui";
|
||||||
// store
|
// store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
export interface IAssigneesHeader {
|
export interface IAssigneesHeader {
|
||||||
column_id: string;
|
column_id: string;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
header_type: "group_by" | "sub_group_by";
|
||||||
|
issues_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AssigneesHeader: React.FC<IAssigneesHeader> = observer(({ column_id }) => {
|
export const Icon = ({ user }: any) => <Avatar user={user} height="22px" width="22px" fontSize="12px" />;
|
||||||
|
|
||||||
|
export const AssigneesHeader: React.FC<IAssigneesHeader> = observer(
|
||||||
|
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
|
||||||
const { project: projectStore }: RootStore = useMobxStore();
|
const { project: projectStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
const assignee = (column_id && projectStore?.getProjectMemberById(column_id)) ?? null;
|
const assignee = (column_id && projectStore?.getProjectMemberByUserId(column_id)) ?? null;
|
||||||
|
|
||||||
return <>{assignee && <HeaderCard title={assignee?.member?.display_name || ""} />}</>;
|
return (
|
||||||
});
|
<>
|
||||||
|
{assignee &&
|
||||||
|
(sub_group_by && header_type === "sub_group_by" ? (
|
||||||
|
<HeaderSubGroupByCard
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon user={assignee?.member} />}
|
||||||
|
title={assignee?.member?.display_name || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeaderGroupByCard
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon user={assignee?.member} />}
|
||||||
|
title={assignee?.member?.display_name || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -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
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { HeaderGroupByCard } from "./group-by-card";
|
||||||
|
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||||
|
import { Icon } from "./assignee";
|
||||||
// store
|
// store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
export interface ICreatedByHeader {
|
export interface ICreatedByHeader {
|
||||||
column_id: string;
|
column_id: string;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
header_type: "group_by" | "sub_group_by";
|
||||||
|
issues_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreatedByHeader: React.FC<ICreatedByHeader> = observer(({ column_id }) => {
|
export const CreatedByHeader: React.FC<ICreatedByHeader> = observer(
|
||||||
|
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
|
||||||
const { project: projectStore }: RootStore = useMobxStore();
|
const { project: projectStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
const createdBy = (column_id && projectStore?.getProjectMemberById(column_id)) ?? null;
|
const createdBy = (column_id && projectStore?.getProjectMemberByUserId(column_id)) ?? null;
|
||||||
|
|
||||||
return <>{createdBy && <HeaderCard title={createdBy?.member?.display_name || ""} />}</>;
|
return (
|
||||||
});
|
<>
|
||||||
|
{createdBy &&
|
||||||
|
(sub_group_by && header_type === "sub_group_by" ? (
|
||||||
|
<HeaderSubGroupByCard
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon user={createdBy?.member} />}
|
||||||
|
title={createdBy?.member?.display_name || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeaderGroupByCard
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon user={createdBy?.member} />}
|
||||||
|
title={createdBy?.member?.display_name || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -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 {
|
export interface IKanBanGroupByHeaderRoot {
|
||||||
column_id: string;
|
column_id: string;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
issues_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = observer(({ column_id }) => {
|
export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = observer(
|
||||||
const { issueFilter: issueFilterStore }: RootStore = useMobxStore();
|
({ column_id, sub_group_by, group_by, issues_count }) => (
|
||||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
{group_by && group_by === "state" && <StateHeader column_id={column_id} />}
|
{group_by && group_by === "state" && (
|
||||||
{group_by && group_by === "state_detail.group" && <StateGroupHeader column_id={column_id} />}
|
<StateHeader
|
||||||
{group_by && group_by === "priority" && <PriorityHeader column_id={column_id} />}
|
column_id={column_id}
|
||||||
{group_by && group_by === "labels" && <LabelHeader column_id={column_id} />}
|
sub_group_by={sub_group_by}
|
||||||
{group_by && group_by === "assignees" && <AssigneesHeader column_id={column_id} />}
|
group_by={group_by}
|
||||||
{group_by && group_by === "created_by" && <CreatedByHeader column_id={column_id} />}
|
header_type={`group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{group_by && group_by === "state_detail.group" && (
|
||||||
|
<StateGroupHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{group_by && group_by === "priority" && (
|
||||||
|
<PriorityHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{group_by && group_by === "labels" && (
|
||||||
|
<LabelHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{group_by && group_by === "assignees" && (
|
||||||
|
<AssigneesHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{group_by && group_by === "created_by" && (
|
||||||
|
<CreatedByHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
@ -1,19 +1,51 @@
|
|||||||
// components
|
|
||||||
import { HeaderCard } from "./card";
|
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { HeaderGroupByCard } from "./group-by-card";
|
||||||
|
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||||
// store
|
// store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
export interface ILabelHeader {
|
export interface ILabelHeader {
|
||||||
column_id: string;
|
column_id: string;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
header_type: "group_by" | "sub_group_by";
|
||||||
|
issues_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LabelHeader: React.FC<ILabelHeader> = observer(({ column_id }) => {
|
const Icon = ({ color }: any) => (
|
||||||
|
<div className="w-[12px] h-[12px] rounded-full" style={{ backgroundColor: color ? color : "#666" }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LabelHeader: React.FC<ILabelHeader> = observer(
|
||||||
|
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
|
||||||
const { project: projectStore }: RootStore = useMobxStore();
|
const { project: projectStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null;
|
const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null;
|
||||||
|
|
||||||
return <>{label && <HeaderCard title={label?.name || ""} />}</>;
|
return (
|
||||||
});
|
<>
|
||||||
|
{label &&
|
||||||
|
(sub_group_by && header_type === "sub_group_by" ? (
|
||||||
|
<HeaderSubGroupByCard
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon color={label?.color} />}
|
||||||
|
title={label?.name || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeaderGroupByCard
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon />}
|
||||||
|
title={label?.name || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -1,22 +1,72 @@
|
|||||||
import React from "react";
|
|
||||||
// components
|
|
||||||
import { HeaderCard } from "./card";
|
|
||||||
// constants
|
|
||||||
import { issuePriorityByKey } from "constants/issue";
|
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx
|
// lucide icons
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
|
||||||
import { RootStore } from "store/root";
|
// components
|
||||||
|
import { HeaderGroupByCard } from "./group-by-card";
|
||||||
|
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||||
|
// constants
|
||||||
|
import { issuePriorityByKey } from "constants/issue";
|
||||||
|
|
||||||
export interface IPriorityHeader {
|
export interface IPriorityHeader {
|
||||||
column_id: string;
|
column_id: string;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
header_type: "group_by" | "sub_group_by";
|
||||||
|
issues_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PriorityHeader: React.FC<IPriorityHeader> = observer(({ column_id }) => {
|
const Icon = ({ priority }: any) => (
|
||||||
const {}: RootStore = useMobxStore();
|
<div className="w-full h-full">
|
||||||
|
{priority === "urgent" ? (
|
||||||
|
<div className="border border-red-500 bg-red-500 text-white w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
|
||||||
|
<AlertCircle size={14} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
) : priority === "high" ? (
|
||||||
|
<div className="border border-red-500/20 bg-red-500/10 text-red-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
|
||||||
|
<SignalHigh size={14} strokeWidth={2} className="pl-[3px]" />
|
||||||
|
</div>
|
||||||
|
) : priority === "medium" ? (
|
||||||
|
<div className="border border-orange-500/20 bg-orange-500/10 text-orange-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
|
||||||
|
<SignalMedium size={14} strokeWidth={2} className="pl-[3px]" />
|
||||||
|
</div>
|
||||||
|
) : priority === "low" ? (
|
||||||
|
<div className="border border-green-500/20 bg-green-500/10 text-green-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
|
||||||
|
<SignalLow size={14} strokeWidth={2} className="pl-[3px]" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="border border-custom-border-400/20 bg-custom-text-400/10 text-custom-text-400 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
|
||||||
|
<Ban size={14} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const stateGroup = column_id && issuePriorityByKey(column_id);
|
export const PriorityHeader: React.FC<IPriorityHeader> = observer(
|
||||||
|
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
|
||||||
|
const priority = column_id && issuePriorityByKey(column_id);
|
||||||
|
|
||||||
return <>{stateGroup && <HeaderCard title={stateGroup?.title || ""} />}</>;
|
return (
|
||||||
});
|
<>
|
||||||
|
{priority &&
|
||||||
|
(sub_group_by && header_type === "sub_group_by" ? (
|
||||||
|
<HeaderSubGroupByCard
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon priority={priority?.key} />}
|
||||||
|
title={priority?.key || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeaderGroupByCard
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon priority={priority?.key} />}
|
||||||
|
title={priority?.key || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -1,23 +1,53 @@
|
|||||||
import React from "react";
|
|
||||||
// components
|
|
||||||
import { HeaderCard } from "./card";
|
|
||||||
// constants
|
|
||||||
import { issueStateGroupByKey } from "constants/issue";
|
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx
|
// components
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { HeaderGroupByCard } from "./group-by-card";
|
||||||
import { RootStore } from "store/root";
|
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||||
|
import { StateGroupIcon } from "components/icons";
|
||||||
|
// constants
|
||||||
|
import { issueStateGroupByKey } from "constants/issue";
|
||||||
|
|
||||||
export interface IStateGroupHeader {
|
export interface IStateGroupHeader {
|
||||||
column_id: string;
|
column_id: string;
|
||||||
swimlanes?: boolean;
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
header_type: "group_by" | "sub_group_by";
|
||||||
|
issues_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StateGroupHeader: React.FC<IStateGroupHeader> = observer(({ column_id }) => {
|
export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => (
|
||||||
const {}: RootStore = useMobxStore();
|
<div className="w-[14px] h-[14px] rounded-full">
|
||||||
|
<StateGroupIcon stateGroup={stateGroup} color={color || null} width="14" height="14" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const StateGroupHeader: React.FC<IStateGroupHeader> = observer(
|
||||||
|
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
|
||||||
const stateGroup = column_id && issueStateGroupByKey(column_id);
|
const stateGroup = column_id && issueStateGroupByKey(column_id);
|
||||||
|
|
||||||
return <>{stateGroup && <HeaderCard title={stateGroup?.title || ""} />}</>;
|
console.log("stateGroup", stateGroup);
|
||||||
});
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{stateGroup &&
|
||||||
|
(sub_group_by && header_type === "sub_group_by" ? (
|
||||||
|
<HeaderSubGroupByCard
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon stateGroup={stateGroup?.key} />}
|
||||||
|
title={stateGroup?.key || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeaderGroupByCard
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon stateGroup={stateGroup?.key} />}
|
||||||
|
title={stateGroup?.key || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -1,19 +1,48 @@
|
|||||||
// components
|
|
||||||
import { HeaderCard } from "./card";
|
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { HeaderGroupByCard } from "./group-by-card";
|
||||||
|
import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||||
|
import { Icon } from "./state-group";
|
||||||
// store
|
// store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
export interface IStateHeader {
|
export interface IStateHeader {
|
||||||
column_id: string;
|
column_id: string;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
header_type: "group_by" | "sub_group_by";
|
||||||
|
issues_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StateHeader: React.FC<IStateHeader> = observer(({ column_id }) => {
|
export const StateHeader: React.FC<IStateHeader> = observer(
|
||||||
|
({ column_id, sub_group_by, group_by, header_type, issues_count }) => {
|
||||||
const { project: projectStore }: RootStore = useMobxStore();
|
const { project: projectStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
const state = (column_id && projectStore?.getProjectStateById(column_id)) ?? null;
|
const state = (column_id && projectStore?.getProjectStateById(column_id)) ?? null;
|
||||||
|
|
||||||
return <>{state && <HeaderCard title={state?.name || ""} />}</>;
|
return (
|
||||||
});
|
<>
|
||||||
|
{state &&
|
||||||
|
(sub_group_by && header_type === "sub_group_by" ? (
|
||||||
|
<HeaderSubGroupByCard
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon stateGroup={state?.group} color={state?.color} />}
|
||||||
|
title={state?.name || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeaderGroupByCard
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
column_id={column_id}
|
||||||
|
icon={<Icon stateGroup={state?.group} color={state?.color} />}
|
||||||
|
title={state?.name || ""}
|
||||||
|
count={issues_count}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -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
|
// components
|
||||||
import { StateHeader } from "./state";
|
import { StateHeader } from "./state";
|
||||||
import { StateGroupHeader } from "./state-group";
|
import { StateGroupHeader } from "./state-group";
|
||||||
@ -5,28 +7,71 @@ import { AssigneesHeader } from "./assignee";
|
|||||||
import { PriorityHeader } from "./priority";
|
import { PriorityHeader } from "./priority";
|
||||||
import { LabelHeader } from "./label";
|
import { LabelHeader } from "./label";
|
||||||
import { CreatedByHeader } from "./created_by";
|
import { CreatedByHeader } from "./created_by";
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// mobx
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
|
|
||||||
export interface IKanBanSubGroupByHeaderRoot {
|
export interface IKanBanSubGroupByHeaderRoot {
|
||||||
column_id: string;
|
column_id: string;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
issues_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> = observer(({ column_id }) => {
|
export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> = observer(
|
||||||
const { issueFilter: issueFilterStore }: RootStore = useMobxStore();
|
({ column_id, sub_group_by, group_by, issues_count }) => (
|
||||||
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
{sub_group_by && sub_group_by === "state" && <StateHeader column_id={column_id} />}
|
{sub_group_by && sub_group_by === "state" && (
|
||||||
{sub_group_by && sub_group_by === "state_detail.group" && <StateGroupHeader column_id={column_id} />}
|
<StateHeader
|
||||||
{sub_group_by && sub_group_by === "priority" && <PriorityHeader column_id={column_id} />}
|
column_id={column_id}
|
||||||
{sub_group_by && sub_group_by === "labels" && <LabelHeader column_id={column_id} />}
|
sub_group_by={sub_group_by}
|
||||||
{sub_group_by && sub_group_by === "assignees" && <AssigneesHeader column_id={column_id} />}
|
group_by={group_by}
|
||||||
{sub_group_by && sub_group_by === "created_by" && <CreatedByHeader column_id={column_id} />}
|
header_type={`sub_group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{sub_group_by && sub_group_by === "state_detail.group" && (
|
||||||
|
<StateGroupHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`sub_group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{sub_group_by && sub_group_by === "priority" && (
|
||||||
|
<PriorityHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`sub_group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{sub_group_by && sub_group_by === "labels" && (
|
||||||
|
<LabelHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`sub_group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{sub_group_by && sub_group_by === "assignees" && (
|
||||||
|
<AssigneesHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`sub_group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{sub_group_by && sub_group_by === "created_by" && (
|
||||||
|
<CreatedByHeader
|
||||||
|
column_id={column_id}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
header_type={`sub_group_by`}
|
||||||
|
issues_count={issues_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
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";
|
import { DragDropContext } from "@hello-pangea/dnd";
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
|
import { KanBan } from "./default";
|
||||||
// store
|
// store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
|
||||||
import { KanBan } from "./default";
|
|
||||||
|
|
||||||
export interface IKanBanLayout {
|
export interface IKanBanLayout {}
|
||||||
issues?: any;
|
|
||||||
handleIssues?: () => void;
|
export const KanBanLayout: React.FC = observer(() => {
|
||||||
handleDragDrop?: (result: any) => void;
|
const {
|
||||||
}
|
issue: issueStore,
|
||||||
|
issueFilter: issueFilterStore,
|
||||||
|
issueKanBanView: issueKanBanViewStore,
|
||||||
|
}: RootStore = useMobxStore();
|
||||||
|
|
||||||
|
const issues = issueStore?.getIssues;
|
||||||
|
|
||||||
|
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
|
||||||
|
|
||||||
|
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
||||||
|
|
||||||
export const KanBanLayout: React.FC<IKanBanLayout> = observer(({}) => {
|
|
||||||
const { issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore();
|
|
||||||
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
|
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
|
||||||
? "swimlanes"
|
? "swimlanes"
|
||||||
: "default";
|
: "default";
|
||||||
|
|
||||||
const issues = issueStore?.getIssues;
|
|
||||||
|
|
||||||
const onDragEnd = (result: any) => {
|
const onDragEnd = (result: any) => {
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
@ -34,14 +40,19 @@ export const KanBanLayout: React.FC<IKanBanLayout> = observer(({}) => {
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
console.log("result", result);
|
currentKanBanView === "default"
|
||||||
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
|
? issueKanBanViewStore?.handleDragDrop(result.source, result.destination)
|
||||||
|
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90`}>
|
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90`}>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
{currentKanBanView === "default" ? <KanBan issues={issues} /> : <KanBanSwimLanes issues={issues} />}
|
{currentKanBanView === "default" ? (
|
||||||
|
<KanBan issues={issues} sub_group_by={sub_group_by} group_by={group_by} />
|
||||||
|
) : (
|
||||||
|
<KanBanSwimLanes issues={issues} sub_group_by={sub_group_by} group_by={group_by} />
|
||||||
|
)}
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -4,99 +4,236 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
|||||||
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
|
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx
|
// mobx
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
export interface IKanBanSwimLanes {
|
interface ISubGroupSwimlaneHeader {
|
||||||
issues?: any;
|
issues: any;
|
||||||
handleIssues?: () => void;
|
sub_group_by: string | null;
|
||||||
handleDragDrop?: () => void;
|
group_by: string | null;
|
||||||
|
list: any;
|
||||||
|
listKey: string;
|
||||||
}
|
}
|
||||||
|
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||||
|
issues,
|
||||||
|
sub_group_by,
|
||||||
|
group_by,
|
||||||
|
list,
|
||||||
|
listKey,
|
||||||
|
}) => {
|
||||||
|
const calculateIssueCount = (column_id: string) => {
|
||||||
|
let issueCount = 0;
|
||||||
|
issues &&
|
||||||
|
Object.keys(issues)?.forEach((_issueKey: any) => {
|
||||||
|
issueCount += issues?.[_issueKey]?.[column_id]?.length || 0;
|
||||||
|
});
|
||||||
|
return issueCount;
|
||||||
|
};
|
||||||
|
|
||||||
const SubGroupSwimlaneHeader = ({ list, _key }: any) => (
|
return (
|
||||||
<div className="relative w-full min-h-full h-max flex items-center">
|
<div className="relative w-full min-h-full h-max flex items-center">
|
||||||
{list &&
|
{list &&
|
||||||
list.length > 0 &&
|
list.length > 0 &&
|
||||||
list.map((_list: any) => (
|
list.map((_list: any) => (
|
||||||
<div className="flex-shrink-0 flex flex-col w-[340px]">
|
<div className="flex-shrink-0 flex flex-col w-[340px]">
|
||||||
<KanBanGroupByHeaderRoot column_id={_list?.[_key]} />
|
<KanBanGroupByHeaderRoot
|
||||||
|
column_id={getValueFromObject(_list, listKey) as string}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SubGroupSwimlane = ({ issues, list, _key }: any) => (
|
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||||
|
issues: any;
|
||||||
|
}
|
||||||
|
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer(({ issues, sub_group_by, group_by, list, listKey }) => {
|
||||||
|
const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
|
const calculateIssueCount = (column_id: string) => {
|
||||||
|
let issueCount = 0;
|
||||||
|
issues?.[column_id] &&
|
||||||
|
Object.keys(issues?.[column_id])?.forEach((_list: any) => {
|
||||||
|
issueCount += issues?.[column_id]?.[_list]?.length || 0;
|
||||||
|
});
|
||||||
|
return issueCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="relative w-full min-h-full h-max">
|
<div className="relative w-full min-h-full h-max">
|
||||||
{list &&
|
{list &&
|
||||||
list.length > 0 &&
|
list.length > 0 &&
|
||||||
list.map((_list: any) => (
|
list.map((_list: any) => (
|
||||||
<div className="flex-shrink-0 flex flex-col">
|
<div className="flex-shrink-0 flex flex-col">
|
||||||
<div className="sticky top-[30px] w-full z-[1] bg-custom-background-90 flex items-center py-1">
|
<div className="sticky top-[50px] w-full z-[1] bg-custom-background-90 flex items-center py-1">
|
||||||
<div className="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2">
|
<div className="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2">
|
||||||
<KanBanSubGroupByHeaderRoot column_id={_list?.[_key]} />
|
<KanBanSubGroupByHeaderRoot
|
||||||
|
column_id={getValueFromObject(_list, listKey) as string}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full border-b border-custom-border-400 border-dashed" />
|
<div className="w-full border-b border-custom-border-400 border-dashed" />
|
||||||
</div>
|
</div>
|
||||||
|
{!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes(
|
||||||
|
getValueFromObject(_list, listKey) as string
|
||||||
|
) && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<KanBan issues={issues} sub_group_id={_list?.[_key]} />
|
<KanBan
|
||||||
|
issues={issues?.[getValueFromObject(_list, listKey) as string]}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
sub_group_id={getValueFromObject(_list, listKey) as string}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer(({ issues }) => {
|
export interface IKanBanSwimLanes {
|
||||||
const { project: projectStore, issueFilter: issueFilterStore }: RootStore = useMobxStore();
|
issues: any;
|
||||||
|
sub_group_by: string | null;
|
||||||
|
group_by: string | null;
|
||||||
|
handleIssues?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer(({ issues, sub_group_by, group_by }) => {
|
||||||
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
|
const { project: projectStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
console.log("sub_group_by", sub_group_by);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="sticky top-0 z-[2] bg-custom-background-90 h-[30px]">
|
<div className="sticky top-0 z-[2] bg-custom-background-90 h-[50px]">
|
||||||
{group_by && group_by === "state" && <SubGroupSwimlaneHeader list={projectStore?.projectStates} _key={"id"} />}
|
{group_by && group_by === "state" && (
|
||||||
|
<SubGroupSwimlaneHeader
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={projectStore?.projectStates}
|
||||||
|
listKey={`id`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{group_by && group_by === "state_detail.group" && (
|
{group_by && group_by === "state_detail.group" && (
|
||||||
<SubGroupSwimlaneHeader list={ISSUE_STATE_GROUPS} _key={"key"} />
|
<SubGroupSwimlaneHeader
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={ISSUE_STATE_GROUPS}
|
||||||
|
listKey={`key`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{group_by && group_by === "priority" && <SubGroupSwimlaneHeader list={ISSUE_PRIORITIES} _key={"key"} />}
|
|
||||||
{group_by && group_by === "labels" && <SubGroupSwimlaneHeader list={projectStore?.projectLabels} _key={"id"} />}
|
{group_by && group_by === "priority" && (
|
||||||
|
<SubGroupSwimlaneHeader
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={ISSUE_PRIORITIES}
|
||||||
|
listKey={`key`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{group_by && group_by === "labels" && (
|
||||||
|
<SubGroupSwimlaneHeader
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={projectStore?.projectLabels}
|
||||||
|
listKey={`id`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{group_by && group_by === "assignees" && (
|
{group_by && group_by === "assignees" && (
|
||||||
<SubGroupSwimlaneHeader list={projectStore?.projectMembers} _key={"id"} />
|
<SubGroupSwimlaneHeader
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={projectStore?.projectMembers}
|
||||||
|
listKey={`member.id`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{group_by && group_by === "created_by" && (
|
{group_by && group_by === "created_by" && (
|
||||||
<SubGroupSwimlaneHeader list={projectStore?.projectMembers} _key={"id"} />
|
<SubGroupSwimlaneHeader
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={projectStore?.projectMembers}
|
||||||
|
listKey={`member.id`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sub_group_by && sub_group_by === "state" && (
|
{sub_group_by && sub_group_by === "state" && (
|
||||||
<SubGroupSwimlane issues={issues} list={projectStore?.projectStates} _key={"id"} />
|
<SubGroupSwimlane
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={projectStore?.projectStates}
|
||||||
|
listKey={`id`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sub_group_by && sub_group_by === "state_detail.group" && (
|
{sub_group_by && sub_group_by === "state_detail.group" && (
|
||||||
<SubGroupSwimlane issues={issues} list={ISSUE_STATE_GROUPS} _key={"key"} />
|
<SubGroupSwimlane
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={ISSUE_STATE_GROUPS}
|
||||||
|
listKey={`key`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sub_group_by && sub_group_by === "priority" && (
|
{sub_group_by && sub_group_by === "priority" && (
|
||||||
<SubGroupSwimlane issues={issues} list={ISSUE_PRIORITIES} _key={"key"} />
|
<SubGroupSwimlane
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={ISSUE_PRIORITIES}
|
||||||
|
listKey={`key`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sub_group_by && sub_group_by === "labels" && (
|
{sub_group_by && sub_group_by === "labels" && (
|
||||||
<SubGroupSwimlane issues={issues} list={projectStore?.projectLabels} _key={"id"} />
|
<SubGroupSwimlane
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={projectStore?.projectLabels}
|
||||||
|
listKey={`id`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sub_group_by && sub_group_by === "assignees" && (
|
{sub_group_by && sub_group_by === "assignees" && (
|
||||||
<SubGroupSwimlane issues={issues} list={projectStore?.projectMembers} _key={"id"} />
|
<SubGroupSwimlane
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={projectStore?.projectMembers}
|
||||||
|
listKey={`member.id`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sub_group_by && sub_group_by === "created_by" && (
|
{sub_group_by && sub_group_by === "created_by" && (
|
||||||
<SubGroupSwimlane issues={issues} list={projectStore?.projectMembers} _key={"id"} />
|
<SubGroupSwimlane
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
list={projectStore?.projectMembers}
|
||||||
|
listKey={`member.id`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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.workspace.setWorkspaceSlug(workspaceSlug);
|
||||||
this.rootStore.project.setProjectId(projectId);
|
this.rootStore.project.setProjectId(projectId);
|
||||||
|
|
||||||
// TODO: replace this once the issue filter is completed
|
const params = this.rootStore?.issueFilter?.appliedFilters;
|
||||||
const params = {
|
|
||||||
group_by: "target_date",
|
|
||||||
order_by: "-created_at",
|
|
||||||
target_date: "2023-09-01;after,2023-09-30;before",
|
|
||||||
};
|
|
||||||
const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params);
|
const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params);
|
||||||
|
|
||||||
const issueType = this.getIssueType;
|
const issueType = this.getIssueType;
|
||||||
|
@ -115,8 +115,6 @@ class IssueDetailStore implements IIssueDetailStore {
|
|||||||
|
|
||||||
const response = await this.issueService.createIssues(workspaceId, projectId, data, user);
|
const response = await this.issueService.createIssues(workspaceId, projectId, data, user);
|
||||||
|
|
||||||
if (response) this.rootStore.issue.addIssueToIssuesStore(projectId, response);
|
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
@ -137,7 +135,7 @@ class IssueDetailStore implements IIssueDetailStore {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
issueId: string,
|
issueId: string,
|
||||||
data: Partial<IIssue>,
|
data: Partial<IIssue>,
|
||||||
user: ICurrentUserResponse
|
user: ICurrentUserResponse | undefined
|
||||||
) => {
|
) => {
|
||||||
const newIssues = { ...this.issues };
|
const newIssues = { ...this.issues };
|
||||||
newIssues[issueId] = {
|
newIssues[issueId] = {
|
||||||
@ -154,8 +152,6 @@ class IssueDetailStore implements IIssueDetailStore {
|
|||||||
|
|
||||||
const response = await this.issueService.patchIssue(workspaceId, projectId, issueId, data, user);
|
const response = await this.issueService.patchIssue(workspaceId, projectId, issueId, data, user);
|
||||||
|
|
||||||
if (response) this.rootStore.issue.updateIssueInIssuesStore(projectId, response);
|
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
@ -192,8 +188,6 @@ class IssueDetailStore implements IIssueDetailStore {
|
|||||||
|
|
||||||
await this.issueService.deleteIssue(workspaceId, projectId, issueId, user);
|
await this.issueService.deleteIssue(workspaceId, projectId, issueId, user);
|
||||||
|
|
||||||
this.rootStore.issue.deleteIssueFromIssuesStore(projectId, issueId);
|
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
@ -1,24 +1,42 @@
|
|||||||
import { action, computed, makeObservable } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "./root";
|
import { RootStore } from "./root";
|
||||||
import { IIssueType } from "./issue";
|
import { IIssueType } from "./issue";
|
||||||
|
|
||||||
export interface IIssueKanBanViewStore {
|
export interface IIssueKanBanViewStore {
|
||||||
|
kanBanToggle: {
|
||||||
|
groupByHeaderMinMax: string[];
|
||||||
|
subgroupByIssuesVisibility: string[];
|
||||||
|
};
|
||||||
|
// computed
|
||||||
|
canUserDragDrop: boolean;
|
||||||
|
canUserDragDropVertically: boolean;
|
||||||
|
canUserDragDropHorizontally: boolean;
|
||||||
|
// actions
|
||||||
|
handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void;
|
||||||
|
handleSwimlaneDragDrop: (source: any, destination: any) => void;
|
||||||
handleDragDrop: (source: any, destination: any) => void;
|
handleDragDrop: (source: any, destination: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
||||||
|
kanBanToggle: {
|
||||||
|
groupByHeaderMinMax: string[];
|
||||||
|
subgroupByIssuesVisibility: string[];
|
||||||
|
} = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] };
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
|
kanBanToggle: observable,
|
||||||
// computed
|
// computed
|
||||||
canUserDragDrop: computed,
|
canUserDragDrop: computed,
|
||||||
canUserDragDropVertically: computed,
|
canUserDragDropVertically: computed,
|
||||||
canUserDragDropHorizontally: computed,
|
canUserDragDropHorizontally: computed,
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
|
handleKanBanToggle: action,
|
||||||
|
handleSwimlaneDragDrop: action,
|
||||||
handleDragDrop: action,
|
handleDragDrop: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -27,50 +45,83 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
|||||||
|
|
||||||
get canUserDragDrop() {
|
get canUserDragDrop() {
|
||||||
if (
|
if (
|
||||||
this.rootStore?.issueFilter?.userDisplayFilters?.group_by &&
|
|
||||||
this.rootStore?.issueFilter?.userDisplayFilters?.order_by &&
|
this.rootStore?.issueFilter?.userDisplayFilters?.order_by &&
|
||||||
["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.group_by) &&
|
this.rootStore?.issueFilter?.userDisplayFilters?.order_by === "sort_order" &&
|
||||||
this.rootStore?.issueFilter?.userDisplayFilters?.order_by === "sort_order"
|
this.rootStore?.issueFilter?.userDisplayFilters?.group_by &&
|
||||||
|
["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.group_by)
|
||||||
) {
|
) {
|
||||||
|
if (this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by === null) return true;
|
||||||
|
if (
|
||||||
|
this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by &&
|
||||||
|
["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by)
|
||||||
|
)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canUserDragDropVertically() {
|
get canUserDragDropVertically() {
|
||||||
return true;
|
return false;
|
||||||
}
|
|
||||||
get canUserDragDropHorizontally() {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragDrop = async (source: any, destination: any) => {
|
get canUserDragDropHorizontally() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||||
|
this.kanBanToggle = {
|
||||||
|
...this.kanBanToggle,
|
||||||
|
[toggle]: this.kanBanToggle[toggle].includes(value)
|
||||||
|
? this.kanBanToggle[toggle].filter((v) => v !== value)
|
||||||
|
: [...this.kanBanToggle[toggle], value],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSwimlaneDragDrop = async (source: any, destination: any) => {
|
||||||
const workspaceSlug = this.rootStore?.workspace?.workspaceSlug;
|
const workspaceSlug = this.rootStore?.workspace?.workspaceSlug;
|
||||||
const projectId = this.rootStore?.project?.projectId;
|
const projectId = this.rootStore?.project?.projectId;
|
||||||
const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType;
|
const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType;
|
||||||
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
|
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
|
||||||
|
|
||||||
const sortOrderDefaultValue = 10000;
|
|
||||||
|
|
||||||
if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && this.rootStore.issue.getIssues) {
|
|
||||||
const currentIssues: any = this.rootStore.issue.getIssues;
|
const currentIssues: any = this.rootStore.issue.getIssues;
|
||||||
|
|
||||||
|
const sortOrderDefaultValue = 65535;
|
||||||
|
|
||||||
|
if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) {
|
||||||
|
// update issue payload
|
||||||
let updateIssue: any = {
|
let updateIssue: any = {
|
||||||
workspaceSlug: workspaceSlug,
|
workspaceSlug: workspaceSlug,
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// user can drag the issues from any direction
|
// source, destination group and sub group id
|
||||||
if (this.canUserDragDrop) {
|
let droppableSourceColumnId = source.droppableId;
|
||||||
// vertical
|
droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null;
|
||||||
if (source.droppableId === destination.droppableId) {
|
let droppableDestinationColumnId = destination.droppableId;
|
||||||
const _columnId = source.droppableId;
|
droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null;
|
||||||
const _issues = currentIssues[_columnId];
|
if (!droppableSourceColumnId || !droppableDestinationColumnId) return null;
|
||||||
|
|
||||||
|
const source_group_id: string = droppableSourceColumnId[0];
|
||||||
|
const source_sub_group_id: string = droppableSourceColumnId[1] === "null" ? null : droppableSourceColumnId[1];
|
||||||
|
|
||||||
|
const destination_group_id: string = droppableDestinationColumnId[0];
|
||||||
|
const destination_sub_group_id: string =
|
||||||
|
droppableDestinationColumnId[1] === "null" ? null : droppableDestinationColumnId[1];
|
||||||
|
|
||||||
|
if (source_sub_group_id === destination_sub_group_id) {
|
||||||
|
if (source_group_id === destination_group_id) {
|
||||||
|
const _issues = currentIssues[source_sub_group_id][source_group_id];
|
||||||
|
|
||||||
// update the sort order
|
// update the sort order
|
||||||
if (destination.index === 0) {
|
if (destination.index === 0) {
|
||||||
updateIssue = { ...updateIssue, sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue };
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue,
|
||||||
|
};
|
||||||
} else if (destination.index === _issues.length - 1) {
|
} else if (destination.index === _issues.length - 1) {
|
||||||
updateIssue = { ...updateIssue, sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue };
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
updateIssue = {
|
updateIssue = {
|
||||||
...updateIssue,
|
...updateIssue,
|
||||||
@ -78,34 +129,26 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the mobx state array
|
|
||||||
const [removed] = _issues.splice(source.index, 1);
|
const [removed] = _issues.splice(source.index, 1);
|
||||||
_issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order });
|
_issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order });
|
||||||
updateIssue = { ...updateIssue, issueId: removed?.id };
|
updateIssue = { ...updateIssue, issueId: removed?.id };
|
||||||
|
currentIssues[source_sub_group_id][source_group_id] = _issues;
|
||||||
currentIssues[_columnId] = _issues;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// horizontal
|
if (source_group_id != destination_group_id) {
|
||||||
if (source.droppableId != destination.droppableId) {
|
const _sourceIssues = currentIssues[source_sub_group_id][source_group_id];
|
||||||
const _sourceColumnId = source.droppableId;
|
let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || [];
|
||||||
const _destinationColumnId = destination.droppableId;
|
|
||||||
|
|
||||||
const _sourceIssues = currentIssues[_sourceColumnId];
|
if (_destinationIssues && _destinationIssues.length > 0) {
|
||||||
const _destinationIssues = currentIssues[_destinationColumnId];
|
|
||||||
|
|
||||||
if (_destinationIssues.length > 0) {
|
|
||||||
if (destination.index === 0) {
|
if (destination.index === 0) {
|
||||||
updateIssue = {
|
updateIssue = {
|
||||||
...updateIssue,
|
...updateIssue,
|
||||||
sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue,
|
sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue,
|
||||||
state: destination?.droppableId,
|
|
||||||
};
|
};
|
||||||
} else if (destination.index === _destinationIssues.length - 1) {
|
} else if (destination.index === _destinationIssues.length) {
|
||||||
updateIssue = {
|
updateIssue = {
|
||||||
...updateIssue,
|
...updateIssue,
|
||||||
sort_order: _destinationIssues[destination.index].sort_order + sortOrderDefaultValue,
|
sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue,
|
||||||
state: destination?.droppableId,
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
updateIssue = {
|
updateIssue = {
|
||||||
@ -114,39 +157,84 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
|||||||
(_destinationIssues[destination.index - 1].sort_order +
|
(_destinationIssues[destination.index - 1].sort_order +
|
||||||
_destinationIssues[destination.index].sort_order) /
|
_destinationIssues[destination.index].sort_order) /
|
||||||
2,
|
2,
|
||||||
state: destination?.droppableId,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateIssue = {
|
updateIssue = {
|
||||||
...updateIssue,
|
...updateIssue,
|
||||||
sort_order: sortOrderDefaultValue,
|
sort_order: sortOrderDefaultValue,
|
||||||
state: destination?.droppableId,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [removed] = _sourceIssues.splice(source.index, 1);
|
const [removed] = _sourceIssues.splice(source.index, 1);
|
||||||
|
if (_destinationIssues && _destinationIssues.length > 0)
|
||||||
_destinationIssues.splice(destination.index, 0, {
|
_destinationIssues.splice(destination.index, 0, {
|
||||||
...removed,
|
...removed,
|
||||||
state: destination?.droppableId,
|
|
||||||
sort_order: updateIssue.sort_order,
|
sort_order: updateIssue.sort_order,
|
||||||
});
|
});
|
||||||
|
else _destinationIssues = [..._destinationIssues, { ...removed, sort_order: updateIssue.sort_order }];
|
||||||
updateIssue = { ...updateIssue, issueId: removed?.id };
|
updateIssue = { ...updateIssue, issueId: removed?.id };
|
||||||
|
|
||||||
currentIssues[_sourceColumnId] = _sourceIssues;
|
// if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state")
|
||||||
currentIssues[_destinationColumnId] = _destinationIssues;
|
// updateIssue = { ...updateIssue, state: destination_group_id };
|
||||||
|
// if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority")
|
||||||
|
// updateIssue = { ...updateIssue, priority: destination_group_id };
|
||||||
|
|
||||||
|
currentIssues[source_sub_group_id][source_group_id] = _sourceIssues;
|
||||||
|
currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// user can drag the issues only vertically
|
if (source_sub_group_id != destination_sub_group_id) {
|
||||||
if (this.canUserDragDropVertically && source.droppableId === destination.droppableId) {
|
const _sourceIssues = currentIssues[source_sub_group_id][source_group_id];
|
||||||
|
let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || [];
|
||||||
|
|
||||||
|
if (_destinationIssues && _destinationIssues.length > 0) {
|
||||||
|
if (destination.index === 0) {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
} else if (destination.index === _destinationIssues.length) {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order:
|
||||||
|
(_destinationIssues[destination.index - 1].sort_order +
|
||||||
|
_destinationIssues[destination.index].sort_order) /
|
||||||
|
2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: sortOrderDefaultValue,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// user can drag the issues only horizontally
|
const [removed] = _sourceIssues.splice(source.index, 1);
|
||||||
if (this.canUserDragDropHorizontally && source.droppableId != destination.droppableId) {
|
if (_destinationIssues && _destinationIssues.length > 0)
|
||||||
|
_destinationIssues.splice(destination.index, 0, {
|
||||||
|
...removed,
|
||||||
|
sort_order: updateIssue.sort_order,
|
||||||
|
});
|
||||||
|
else _destinationIssues = [..._destinationIssues, { ...removed, sort_order: updateIssue.sort_order }];
|
||||||
|
|
||||||
|
updateIssue = { ...updateIssue, issueId: removed?.id };
|
||||||
|
currentIssues[source_sub_group_id][source_group_id] = _sourceIssues;
|
||||||
|
currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues;
|
||||||
|
|
||||||
|
// if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state")
|
||||||
|
// updateIssue = { ...updateIssue, state: destination_group_id };
|
||||||
|
// if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority")
|
||||||
|
// updateIssue = { ...updateIssue, priority: destination_group_id };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rootStore.issue.issues = {
|
const reorderedIssues = {
|
||||||
...this.rootStore?.issue.issues,
|
...this.rootStore?.issue.issues,
|
||||||
[projectId]: {
|
[projectId]: {
|
||||||
...this.rootStore?.issue.issues?.[projectId],
|
...this.rootStore?.issue.issues?.[projectId],
|
||||||
@ -157,14 +245,160 @@ class IssueKanBanViewStore implements IIssueKanBanViewStore {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// this.rootStore.issueDetail?.updateIssueAsync(
|
runInAction(() => {
|
||||||
|
this.rootStore.issue.issues = { ...reorderedIssues };
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log("updateIssue", updateIssue);
|
||||||
|
|
||||||
|
// this.rootStore.issueDetail?.updateIssue(
|
||||||
// updateIssue.workspaceSlug,
|
// updateIssue.workspaceSlug,
|
||||||
// updateIssue.projectId,
|
// updateIssue.projectId,
|
||||||
// updateIssue.issueId,
|
// updateIssue.issueId,
|
||||||
// updateIssue
|
// updateIssue,
|
||||||
|
// undefined
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleDragDrop = async (source: any, destination: any) => {
|
||||||
|
const workspaceSlug = this.rootStore?.workspace?.workspaceSlug;
|
||||||
|
const projectId = this.rootStore?.project?.projectId;
|
||||||
|
const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType;
|
||||||
|
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
|
||||||
|
const currentIssues: any = this.rootStore.issue.getIssues;
|
||||||
|
|
||||||
|
const sortOrderDefaultValue = 65535;
|
||||||
|
|
||||||
|
if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) {
|
||||||
|
// update issue payload
|
||||||
|
let updateIssue: any = {
|
||||||
|
workspaceSlug: workspaceSlug,
|
||||||
|
projectId: projectId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// source, destination group and sub group id
|
||||||
|
let droppableSourceColumnId = source.droppableId;
|
||||||
|
droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null;
|
||||||
|
let droppableDestinationColumnId = destination.droppableId;
|
||||||
|
droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null;
|
||||||
|
if (!droppableSourceColumnId || !droppableDestinationColumnId) return null;
|
||||||
|
|
||||||
|
const source_group_id: string = droppableSourceColumnId[0];
|
||||||
|
const destination_group_id: string = droppableDestinationColumnId[0];
|
||||||
|
|
||||||
|
if (this.canUserDragDrop) {
|
||||||
|
// vertical
|
||||||
|
if (source_group_id === destination_group_id) {
|
||||||
|
const _issues = currentIssues[source_group_id];
|
||||||
|
|
||||||
|
// update the sort order
|
||||||
|
if (destination.index === 0) {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
} else if (destination.index === _issues.length - 1) {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [removed] = _issues.splice(source.index, 1);
|
||||||
|
_issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order });
|
||||||
|
updateIssue = { ...updateIssue, issueId: removed?.id };
|
||||||
|
currentIssues[source_group_id] = _issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// horizontal
|
||||||
|
if (source_group_id != destination_group_id) {
|
||||||
|
const _sourceIssues = currentIssues[source_group_id];
|
||||||
|
let _destinationIssues = currentIssues[destination_group_id] || [];
|
||||||
|
|
||||||
|
if (_destinationIssues && _destinationIssues.length > 0) {
|
||||||
|
if (destination.index === 0) {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
} else if (destination.index === _destinationIssues.length) {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order:
|
||||||
|
(_destinationIssues[destination.index - 1].sort_order +
|
||||||
|
_destinationIssues[destination.index].sort_order) /
|
||||||
|
2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateIssue = {
|
||||||
|
...updateIssue,
|
||||||
|
sort_order: sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [removed] = _sourceIssues.splice(source.index, 1);
|
||||||
|
if (_destinationIssues && _destinationIssues.length > 0)
|
||||||
|
_destinationIssues.splice(destination.index, 0, {
|
||||||
|
...removed,
|
||||||
|
sort_order: updateIssue.sort_order,
|
||||||
|
});
|
||||||
|
else _destinationIssues = [..._destinationIssues, { ...removed, sort_order: updateIssue.sort_order }];
|
||||||
|
updateIssue = { ...updateIssue, issueId: removed?.id };
|
||||||
|
|
||||||
|
currentIssues[source_group_id] = _sourceIssues;
|
||||||
|
currentIssues[destination_group_id] = _destinationIssues;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state")
|
||||||
|
updateIssue = { ...updateIssue, state: destination_group_id };
|
||||||
|
if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority")
|
||||||
|
updateIssue = { ...updateIssue, priority: destination_group_id };
|
||||||
|
}
|
||||||
|
|
||||||
|
// user can drag the issues only vertically
|
||||||
|
if (this.canUserDragDropVertically && destination_group_id === destination_group_id) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// user can drag the issues only horizontally
|
||||||
|
if (this.canUserDragDropHorizontally && destination_group_id != destination_group_id) {
|
||||||
|
}
|
||||||
|
|
||||||
|
const reorderedIssues = {
|
||||||
|
...this.rootStore?.issue.issues,
|
||||||
|
[projectId]: {
|
||||||
|
...this.rootStore?.issue.issues?.[projectId],
|
||||||
|
[issueType]: {
|
||||||
|
...this.rootStore?.issue.issues?.[projectId]?.[issueType],
|
||||||
|
[issueType]: currentIssues,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.rootStore.issue.issues = { ...reorderedIssues };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore.issueDetail?.updateIssue(
|
||||||
|
updateIssue.workspaceSlug,
|
||||||
|
updateIssue.projectId,
|
||||||
|
updateIssue.issueId,
|
||||||
|
updateIssue,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IssueKanBanViewStore;
|
export default IssueKanBanViewStore;
|
||||||
|
@ -48,6 +48,7 @@ export interface IProjectStore {
|
|||||||
getProjectStateById: (stateId: string) => IState | null;
|
getProjectStateById: (stateId: string) => IState | null;
|
||||||
getProjectLabelById: (labelId: string) => IIssueLabels | null;
|
getProjectLabelById: (labelId: string) => IIssueLabels | null;
|
||||||
getProjectMemberById: (memberId: string) => IProjectMember | null;
|
getProjectMemberById: (memberId: string) => IProjectMember | null;
|
||||||
|
getProjectMemberByUserId: (memberId: string) => IProjectMember | null;
|
||||||
|
|
||||||
fetchProjects: (workspaceSlug: string) => Promise<void>;
|
fetchProjects: (workspaceSlug: string) => Promise<void>;
|
||||||
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
|
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
@ -269,6 +270,14 @@ class ProjectStore implements IProjectStore {
|
|||||||
return memberInfo;
|
return memberInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getProjectMemberByUserId = (memberId: string) => {
|
||||||
|
if (!this.projectId) return null;
|
||||||
|
const members = this.projectMembers;
|
||||||
|
if (!members) return null;
|
||||||
|
const memberInfo: IProjectMember | null = members.find((member) => member.member.id === memberId) || null;
|
||||||
|
return memberInfo;
|
||||||
|
};
|
||||||
|
|
||||||
fetchProjectStates = async (workspaceSlug: string, projectSlug: string) => {
|
fetchProjectStates = async (workspaceSlug: string, projectSlug: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
|
Loading…
Reference in New Issue
Block a user