Kanban new store implementation for project issues

This commit is contained in:
rahulramesha 2023-12-15 17:57:43 +05:30
parent 78bc9f615f
commit fd1f262bf7
21 changed files with 672 additions and 248 deletions

View File

@ -6,17 +6,21 @@ import { Button } from "@plane/ui";
// hooks
import useToast from "hooks/use-toast";
// types
import type { IIssue } from "types";
import { useIssues } from "hooks/store/use-issues";
type Props = {
isOpen: boolean;
handleClose: () => void;
data: IIssue;
dataId: string;
onSubmit?: () => Promise<void>;
};
export const DeleteIssueModal: React.FC<Props> = (props) => {
const { data, isOpen, handleClose, onSubmit } = props;
const { dataId, isOpen, handleClose, onSubmit } = props;
const { issueMap } = useIssues();
const issue = issueMap.allIssues[dataId];
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@ -93,7 +97,7 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{data?.project_detail?.identifier}-{data?.sequence_id}
{issue?.project_detail?.identifier}-{issue?.sequence_id}
</span>
{""}? All of the data related to the issue will be permanently removed. This action cannot be
undone.

View File

@ -12,35 +12,26 @@ import { IIssue } from "types";
import { EIssueActions } from "../types";
import {
ICycleIssuesFilterStore,
ICycleIssuesStore,
IModuleIssuesFilterStore,
IModuleIssuesStore,
IProfileIssuesFilterStore,
IProfileIssuesStore,
IProjectDraftIssuesStore,
IProjectIssuesFilterStore,
IProjectIssuesStore,
IViewIssuesFilterStore,
IViewIssuesStore,
} from "store_legacy/issues";
import { IQuickActionProps } from "../list/list-view-types";
import { IIssueKanBanViewStore } from "store_legacy/issue";
import { IProjectIssues } from "store/issue/project";
//components
import { KanBan } from "./default";
import { KanBanSwimLanes } from "./swimlanes";
import { EProjectStore } from "store_legacy/command-palette.store";
import { DeleteIssueModal, IssuePeekOverview } from "components/issues";
import { EUserProjectRoles } from "constants/project";
import { useIssues } from "hooks/store/use-issues";
import { handleDragDrop } from "./utils";
import { IIssueKanBanViewStore } from "store/issue/issue_kanban_view.store";
export interface IBaseKanBanLayout {
issueStore:
| IProjectIssuesStore
| IModuleIssuesStore
| ICycleIssuesStore
| IViewIssuesStore
| IProjectDraftIssuesStore
| IProfileIssuesStore;
issuesFilterStore:
issues: IProjectIssues;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
@ -56,14 +47,6 @@ export interface IBaseKanBanLayout {
showLoader?: boolean;
viewId?: string;
currentStore?: EProjectStore;
handleDragDrop?: (
source: any,
destination: any,
subGroupBy: string | null,
groupBy: string | null,
issues: any,
issueWithIds: any
) => Promise<IIssue | undefined>;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
}
@ -76,33 +59,31 @@ type KanbanDragState = {
export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => {
const {
issueStore,
issuesFilterStore,
issues,
issuesFilter,
kanbanViewStore,
QuickActions,
issueActions,
showLoader,
viewId,
currentStore,
handleDragDrop,
addIssuesToView,
canEditPropertiesBasedOnProject,
} = props;
// router
const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query;
// store hooks
const {
membership: { currentProjectRole },
} = useUser();
const { issueMap } = useIssues();
// toast alert
const { setToastAlert } = useToast();
const issues = issueStore?.getIssues || {};
const issueIds = issueStore?.getIssuesIds || [];
const issueIds = issues?.getIssuesIds || [];
const displayFilters = issuesFilterStore?.issueFilters?.displayFilters;
const displayProperties = issuesFilterStore?.issueFilters?.displayProperties || null;
const displayFilters = issuesFilter?.issueFilters?.displayFilters;
const sub_group_by: string | null = displayFilters?.sub_group_by || null;
const group_by: string | null = displayFilters?.group_by || null;
@ -111,7 +92,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
// states
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
@ -161,15 +142,23 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
});
setDeleteIssueModal(true);
} else {
await handleDragDrop(result.source, result.destination, sub_group_by, group_by, issues, issueIds).catch(
(err) => {
setToastAlert({
title: "Error",
type: "error",
message: err.detail ?? "Failed to perform this action",
});
}
);
await handleDragDrop(
result.source,
result.destination,
workspaceSlug.toString(),
projectId.toString(),
issues,
sub_group_by,
group_by,
issueMap,
issueIds
).catch((err) => {
setToastAlert({
title: "Error",
type: "error",
message: err.detail ?? "Failed to perform this action",
});
});
}
}
};
@ -202,12 +191,20 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const handleDeleteIssue = async () => {
if (!handleDragDrop) return;
await handleDragDrop(dragState.source, dragState.destination, sub_group_by, group_by, issues, issueIds).finally(
() => {
setDeleteIssueModal(false);
setDragState({});
}
);
await handleDragDrop(
dragState.source,
dragState.destination,
workspaceSlug.toString(),
projectId.toString(),
issues,
sub_group_by,
group_by,
issueMap,
issueIds
).finally(() => {
setDeleteIssueModal(false);
setDragState({});
});
};
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
@ -217,13 +214,13 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
return (
<>
<DeleteIssueModal
data={dragState.draggedIssueId ? issues[dragState.draggedIssueId] : ({} as IIssue)}
dataId={dragState.draggedIssueId}
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDeleteIssue}
/>
{showLoader && issueStore?.loader === "init-loader" && (
{showLoader && issues?.loader === "init-loader" && (
<div className="fixed right-2 top-16 z-30 flex h-10 w-10 items-center justify-center rounded bg-custom-background-80 shadow-custom-shadow-sm">
<Spinner className="h-5 w-5" />
</div>
@ -254,18 +251,18 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</div>
<KanBanView
issues={issues}
issueMap={issueMap}
issueIds={issueIds}
issuesFilter={issuesFilter}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={renderQuickActions}
displayProperties={displayProperties}
kanBanToggle={kanbanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
quickAddCallback={issueStore?.quickAddIssue}
quickAddCallback={issues?.quickAddIssue}
viewId={viewId}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
canEditProperties={canEditProperties}

View File

@ -1,39 +1,57 @@
import { memo } from "react";
import { Draggable } from "@hello-pangea/dnd";
import isEqual from "lodash/isEqual";
// components
import { KanBanProperties } from "./properties";
// ui
import { Tooltip } from "@plane/ui";
// types
import { IIssueDisplayProperties, IIssue } from "types";
import { IIssue } from "types";
import { EIssueActions } from "../types";
import { useRouter } from "next/router";
import {
ICycleIssuesFilterStore,
IModuleIssuesFilterStore,
IProfileIssuesFilterStore,
IProjectIssuesFilterStore,
IViewIssuesFilterStore,
} from "store_legacy/issues";
import { IssuesFilter } from "store/issue/base-issue-filter.store";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
interface IssueBlockProps {
sub_group_id: string;
columnId: string;
index: number;
issue: IIssue;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
isDragDisabled: boolean;
showEmptyGroup: boolean;
handleIssues: (issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
canEditProperties: (projectId: string | undefined) => boolean;
}
interface IssueDetailsBlockProps {
issue: IIssue;
showEmptyGroup: boolean;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
handleIssues: (issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
isReadOnly: boolean;
}
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
const { issue, showEmptyGroup, handleIssues, quickActions, displayProperties, isReadOnly } = props;
const { issue, showEmptyGroup, handleIssues, quickActions, isReadOnly, issuesFilter } = props;
const router = useRouter();
@ -52,14 +70,17 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
return (
<>
{displayProperties && displayProperties?.key && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties?.key}
>
<div className="relative">
<div className="line-clamp-1 text-xs text-custom-text-300">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">{quickActions(issue)}</div>
</div>
)}
</WithDisplayPropertiesHOC>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="line-clamp-2 text-sm font-medium text-custom-text-100" onClick={handleIssuePeekOverview}>
{issue.name}
@ -68,8 +89,8 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
<div>
<KanBanProperties
issue={issue}
issuesFilter={issuesFilter}
handleIssues={updateIssue}
displayProperties={displayProperties}
showEmptyGroup={showEmptyGroup}
isReadOnly={isReadOnly}
/>
@ -78,15 +99,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
);
};
const validateMemo = (prevProps: IssueDetailsBlockProps, nextProps: IssueDetailsBlockProps) => {
if (prevProps.issue !== nextProps.issue) return false;
if (!isEqual(prevProps.displayProperties, nextProps.displayProperties)) {
return false;
}
return true;
};
const KanbanIssueMemoBlock = memo(KanbanIssueDetailsBlock, validateMemo);
const KanbanIssueMemoBlock = memo(KanbanIssueDetailsBlock);
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
const {
@ -94,11 +107,11 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
columnId,
index,
issue,
issuesFilter,
isDragDisabled,
showEmptyGroup,
handleIssues,
quickActions,
displayProperties,
canEditProperties,
} = props;
@ -128,10 +141,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
>
<KanbanIssueMemoBlock
issue={issue}
issuesFilter={issuesFilter}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
isReadOnly={!canEditIssueProperties}
/>
</div>

View File

@ -1,21 +1,33 @@
import { memo } from "react";
//types
import { IIssueDisplayProperties, IIssue } from "types";
import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IIssueResponse } from "store_legacy/issues/types";
// components
import { KanbanIssueBlock } from "components/issues";
import { IIssueStore } from "store/issue/issue.store";
import {
ICycleIssuesFilterStore,
IModuleIssuesFilterStore,
IProfileIssuesFilterStore,
IProjectIssuesFilterStore,
IViewIssuesFilterStore,
} from "store_legacy/issues";
interface IssueBlocksListProps {
sub_group_id: string;
columnId: string;
issues: IIssueResponse;
issueMap: IIssueStore;
issueIds: string[];
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
isDragDisabled: boolean;
showEmptyGroup: boolean;
handleIssues: (issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
canEditProperties: (projectId: string | undefined) => boolean;
}
@ -23,13 +35,13 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
const {
sub_group_id,
columnId,
issues,
issueMap,
issueIds,
issuesFilter,
showEmptyGroup,
isDragDisabled,
handleIssues,
quickActions,
displayProperties,
canEditProperties,
} = props;
@ -38,19 +50,19 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
{issueIds && issueIds.length > 0 ? (
<>
{issueIds.map((issueId, index) => {
if (!issues[issueId]) return null;
const issue = issueMap.allIssues[issueId];
const issue = issues[issueId];
if (!issue) return null;
return (
<KanbanIssueBlock
key={`kanban-issue-block-${issue.id}`}
index={index}
issue={issue}
issuesFilter={issuesFilter}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
columnId={columnId}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}

View File

@ -5,16 +5,31 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group";
// types
import { IIssueDisplayProperties, IIssue } from "types";
import { IIssue } from "types";
// constants
import { columnTypes, getKanbanColumns, IKanbanColumn } from "./utils";
import { EIssueActions } from "../types";
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store_legacy/issues/types";
import { IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store_legacy/issues/types";
import { EProjectStore } from "store_legacy/command-palette.store";
import { IIssueStore } from "store/issue/issue.store";
import {
ICycleIssuesFilterStore,
IModuleIssuesFilterStore,
IProfileIssuesFilterStore,
IProjectIssuesFilterStore,
IViewIssuesFilterStore,
} from "store_legacy/issues";
import { useLabel, useProject, useProjectState } from "hooks/store";
export interface IGroupByKanBan {
issues: IIssueResponse;
issueMap: IIssueStore;
issueIds: any;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
sub_group_by: string | null;
group_by: string | null;
sub_group_id: string;
@ -22,7 +37,6 @@ export interface IGroupByKanBan {
handleIssues: (issue: IIssue, action: EIssueActions) => void;
showEmptyGroup: boolean;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
kanBanToggle: any;
handleKanBanToggle: any;
enableQuickIssueCreate?: boolean;
@ -41,8 +55,9 @@ export interface IGroupByKanBan {
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const {
issues,
issueMap,
issueIds,
issuesFilter,
sub_group_by,
group_by,
sub_group_id = "null",
@ -50,7 +65,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
handleIssues,
showEmptyGroup,
quickActions,
displayProperties,
kanBanToggle,
handleKanBanToggle,
enableQuickIssueCreate,
@ -62,9 +76,12 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
canEditProperties,
} = props;
const { project, projectLabel, projectMember, projectState } = useMobxStore();
//const { projectMember } = useMobxStore();
const project = useProject();
const projectLabel = useLabel();
const projectState = useProjectState();
const list = getKanbanColumns(group_by as columnTypes, project, projectLabel, projectMember, projectState);
const list = getKanbanColumns(group_by as columnTypes, project, projectLabel, projectState);
if (!list) return null;
@ -99,8 +116,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
)}
<KanbanGroup
groupId={_list.id}
issues={issues}
issueMap={issueMap}
issueIds={issueIds}
issuesFilter={issuesFilter}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={sub_group_id}
@ -108,7 +126,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
handleIssues={handleIssues}
showEmptyGroup={showEmptyGroup}
quickActions={quickActions}
displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
@ -124,14 +141,19 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
});
export interface IKanBan {
issues: IIssueResponse;
issueMap: IIssueStore;
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
sub_group_by: string | null;
group_by: string | null;
sub_group_id?: string;
handleIssues: (issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
kanBanToggle: any;
handleKanBanToggle: any;
showEmptyGroup: boolean;
@ -151,14 +173,14 @@ export interface IKanBan {
export const KanBan: React.FC<IKanBan> = observer((props) => {
const {
issues,
issueMap,
issueIds,
issuesFilter,
sub_group_by,
group_by,
sub_group_id = "null",
handleIssues,
quickActions,
displayProperties,
kanBanToggle,
handleKanBanToggle,
showEmptyGroup,
@ -176,7 +198,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
return (
<div className="relative h-full w-full">
<GroupByKanBan
issues={issues}
issueMap={issueMap}
issuesFilter={issuesFilter}
issueIds={issueIds}
group_by={group_by}
sub_group_by={sub_group_by}
@ -185,7 +208,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}

View File

@ -1,14 +1,28 @@
import { Droppable } from "@hello-pangea/dnd";
//types
import { IIssue, IIssueDisplayProperties, IIssueResponse } from "types";
import { IIssue } from "types";
import { EIssueActions } from "../types";
//components
import { KanBanQuickAddIssueForm, KanbanIssueBlocksList } from ".";
import { IIssueStore } from "store/issue/issue.store";
import {
ICycleIssuesFilterStore,
IModuleIssuesFilterStore,
IProfileIssuesFilterStore,
IProjectIssuesFilterStore,
IViewIssuesFilterStore,
} from "store_legacy/issues";
interface IKanbanGroup {
groupId: string;
issues: IIssueResponse;
issueMap: IIssueStore;
issueIds: any;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
sub_group_by: string | null;
group_by: string | null;
sub_group_id: string;
@ -16,7 +30,6 @@ interface IKanbanGroup {
handleIssues: (issue: IIssue, action: EIssueActions) => void;
showEmptyGroup: boolean;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
@ -36,14 +49,14 @@ export const KanbanGroup = (props: IKanbanGroup) => {
sub_group_id,
group_by,
sub_group_by,
issues,
issueMap,
issuesFilter,
verticalPosition,
issueIds,
isDragDisabled,
showEmptyGroup,
handleIssues,
quickActions,
displayProperties,
canEditProperties,
enableQuickIssueCreate,
disableIssueCreation,
@ -62,17 +75,17 @@ export const KanbanGroup = (props: IKanbanGroup) => {
{...provided.droppableProps}
ref={provided.innerRef}
>
{issues && !verticalPosition ? (
{!verticalPosition ? (
<KanbanIssueBlocksList
sub_group_id={sub_group_id}
columnId={groupId}
issues={issues}
issueMap={issueMap}
issueIds={issueIds?.[groupId] || []}
issuesFilter={issuesFilter}
isDragDisabled={isDragDisabled}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
canEditProperties={canEditProperties}
/>
) : null}

View File

@ -10,18 +10,31 @@ import { IssuePropertyAssignee } from "../properties/assignee";
import { IssuePropertyEstimates } from "../properties/estimates";
import { IssuePropertyDate } from "../properties/date";
import { Tooltip } from "@plane/ui";
import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types";
import { IIssue, IState, TIssuePriorities } from "types";
import {
ICycleIssuesFilterStore,
IModuleIssuesFilterStore,
IProfileIssuesFilterStore,
IProjectIssuesFilterStore,
IViewIssuesFilterStore,
} from "store_legacy/issues";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
export interface IKanBanProperties {
issue: IIssue;
handleIssues: (issue: IIssue) => void;
displayProperties: IIssueDisplayProperties | null;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
showEmptyGroup: boolean;
isReadOnly: boolean;
}
export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) => {
const { issue, handleIssues, displayProperties, isReadOnly } = props;
const { issue, handleIssues, issuesFilter, isReadOnly } = props;
const handleState = (state: IState) => {
handleIssues({ ...issue, state: state.id });
@ -55,7 +68,10 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
<div className="flex flex-wrap items-center gap-2 whitespace-nowrap">
{/* basic properties */}
{/* state */}
{displayProperties && displayProperties?.state && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties && displayProperties?.state}
>
<IssuePropertyState
projectId={issue?.project_detail?.id || null}
value={issue?.state || null}
@ -64,20 +80,27 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
disabled={isReadOnly}
hideDropdownArrow
/>
)}
</WithDisplayPropertiesHOC>
{/* priority */}
{displayProperties && displayProperties?.priority && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties && displayProperties?.priority}
>
<IssuePropertyPriority
value={issue?.priority || null}
onChange={handlePriority}
disabled={isReadOnly}
hideDropdownArrow
/>
)}
</WithDisplayPropertiesHOC>
{/* label */}
{displayProperties && displayProperties?.labels && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties && displayProperties?.labels}
>
<IssuePropertyLabels
projectId={issue?.project_detail?.id || null}
value={issue?.labels || null}
@ -86,30 +109,39 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
disabled={isReadOnly}
hideDropdownArrow
/>
)}
</WithDisplayPropertiesHOC>
{/* start date */}
{displayProperties && displayProperties?.start_date && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties?.start_date}
>
<IssuePropertyDate
value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)}
disabled={isReadOnly}
type="start_date"
/>
)}
</WithDisplayPropertiesHOC>
{/* target/due date */}
{displayProperties && displayProperties?.due_date && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties?.due_date}
>
<IssuePropertyDate
value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)}
disabled={isReadOnly}
type="target_date"
/>
)}
</WithDisplayPropertiesHOC>
{/* assignee */}
{displayProperties && displayProperties?.assignee && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties?.assignee}
>
<IssuePropertyAssignee
projectId={issue?.project_detail?.id || null}
value={issue?.assignees || null}
@ -119,10 +151,13 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
disabled={isReadOnly}
multiple
/>
)}
</WithDisplayPropertiesHOC>
{/* estimates */}
{displayProperties && displayProperties?.estimate && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties?.estimate}
>
<IssuePropertyEstimates
projectId={issue?.project_detail?.id || null}
value={issue?.estimate_point || null}
@ -130,38 +165,49 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
disabled={isReadOnly}
hideDropdownArrow
/>
)}
</WithDisplayPropertiesHOC>
{/* extra render properties */}
{/* sub-issues */}
{displayProperties && displayProperties?.sub_issue_count && !!issue?.sub_issues_count && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties?.sub_issue_count && !!issue?.sub_issues_count}
>
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
<Layers className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.sub_issues_count}</div>
</div>
</Tooltip>
)}
</WithDisplayPropertiesHOC>
{/* attachments */}
{displayProperties && displayProperties?.attachment_count && !!issue?.attachment_count && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) =>
displayProperties?.attachment_count && !!issue?.attachment_count
}
>
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.attachment_count}</div>
</div>
</Tooltip>
)}
</WithDisplayPropertiesHOC>
{/* link */}
{displayProperties && displayProperties?.link && !!issue?.link_count && (
<WithDisplayPropertiesHOC
issuesFilter={issuesFilter}
getShouldRenderProperty={(displayProperties) => displayProperties?.link && !!issue?.link_count}
>
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1">
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.link_count}</div>
</div>
</Tooltip>
)}
</WithDisplayPropertiesHOC>
</div>
);
});

View File

@ -11,67 +11,45 @@ import { IIssue } from "types";
// constants
import { EIssueActions } from "../../types";
import { EProjectStore } from "store_legacy/command-palette.store";
import { IGroupedIssues, IIssueResponse, ISubGroupedIssues, TUnGroupedIssues } from "store_legacy/issues/types";
import { EIssuesStoreType } from "constants/issue";
import { useIssues } from "hooks/store/use-issues";
export interface IKanBanLayout {}
export const KanBanLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string };
const {
projectIssues: issueStore,
projectIssuesFilter: issuesFilterStore,
issueKanBanView: issueKanBanViewStore,
kanBanHelpers: kanBanHelperStore,
} = useMobxStore();
const { issueKanBanView: issueKanBanViewStore } = useMobxStore();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const issueActions = useMemo(
() => ({
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await issueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
await issues.updateIssue(workspaceSlug, issue.project, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await issueStore.removeIssue(workspaceSlug, issue.project, issue.id);
await issues.removeIssue(workspaceSlug, issue.project, issue.id);
},
}),
[issueStore]
[issues]
);
const handleDragDrop = async (
source: any,
destination: any,
subGroupBy: string | null,
groupBy: string | null,
issues: IIssueResponse | undefined,
issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined
) =>
await kanBanHelperStore.handleDragDrop(
source,
destination,
workspaceSlug,
projectId,
issueStore,
subGroupBy,
groupBy,
issues,
issueWithIds
);
return (
<BaseKanBanRoot
issueActions={issueActions}
issuesFilterStore={issuesFilterStore}
issueStore={issueStore}
issues={issues}
issuesFilter={issuesFilter}
kanbanViewStore={issueKanBanViewStore}
showLoader={true}
QuickActions={ProjectIssueQuickActions}
currentStore={EProjectStore.PROJECT}
handleDragDrop={handleDragDrop}
/>
);
});

View File

@ -1,17 +1,24 @@
import { observer } from "mobx-react-lite";
//mobx
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { KanBan } from "./default";
import { HeaderSubGroupByCard } from "./headers/sub-group-by-card";
import { HeaderGroupByCard } from "./headers/group-by-card";
// types
import { IIssue, IIssueDisplayProperties } from "types";
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store_legacy/issues/types";
import { IIssue } from "types";
import { IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store_legacy/issues/types";
// constants
import { EIssueActions } from "../types";
import { EProjectStore } from "store_legacy/command-palette.store";
import { IKanbanColumn, columnTypes, getKanbanColumns } from "./utils";
import { IIssueStore } from "store/issue/issue.store";
import { useLabel, useProject, useProjectState } from "hooks/store";
import {
ICycleIssuesFilterStore,
IModuleIssuesFilterStore,
IProfileIssuesFilterStore,
IProjectIssuesFilterStore,
IViewIssuesFilterStore,
} from "store_legacy/issues";
interface ISubGroupSwimlaneHeader {
issueIds: any;
@ -51,12 +58,17 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
);
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
issues: IIssueResponse;
issueMap: IIssueStore;
issueIds: any;
showEmptyGroup: boolean;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
handleIssues: (issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
kanBanToggle: any;
handleKanBanToggle: any;
isDragStarted?: boolean;
@ -75,14 +87,14 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
}
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const {
issues,
issueMap,
issueIds,
sub_group_by,
group_by,
list,
handleIssues,
quickActions,
displayProperties,
issuesFilter,
kanBanToggle,
handleKanBanToggle,
showEmptyGroup,
@ -124,14 +136,14 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
{!kanBanToggle?.subgroupByIssuesVisibility.includes(_list.id) && (
<div className="relative">
<KanBan
issues={issues}
issueMap={issueMap}
issueIds={issueIds?.[_list.id]}
issuesFilter={issuesFilter}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
@ -150,13 +162,18 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
});
export interface IKanBanSwimLanes {
issues: IIssueResponse;
issueMap: IIssueStore;
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
sub_group_by: string | null;
group_by: string | null;
handleIssues: (issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
kanBanToggle: any;
handleKanBanToggle: any;
showEmptyGroup: boolean;
@ -177,13 +194,13 @@ export interface IKanBanSwimLanes {
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
const {
issues,
issueMap,
issueIds,
issuesFilter,
sub_group_by,
group_by,
handleIssues,
quickActions,
displayProperties,
kanBanToggle,
handleKanBanToggle,
showEmptyGroup,
@ -196,16 +213,12 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
viewId,
} = props;
const { project, projectLabel, projectMember, projectState } = useMobxStore();
const project = useProject();
const projectLabel = useLabel();
const projectState = useProjectState();
const groupByList = getKanbanColumns(group_by as columnTypes, project, projectLabel, projectMember, projectState);
const subGroupByList = getKanbanColumns(
sub_group_by as columnTypes,
project,
projectLabel,
projectMember,
projectState
);
const groupByList = getKanbanColumns(group_by as columnTypes, project, projectLabel, projectState);
const subGroupByList = getKanbanColumns(sub_group_by as columnTypes, project, projectLabel, projectState);
if (!groupByList || !subGroupByList) return null;
@ -224,14 +237,14 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{sub_group_by && (
<SubGroupSwimlane
issues={issues}
issueMap={issueMap}
list={subGroupByList}
issueIds={issueIds}
issuesFilter={issuesFilter}
group_by={group_by}
sub_group_by={sub_group_by}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}

View File

@ -1,9 +1,14 @@
import { DraggableLocation } from "@hello-pangea/dnd";
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { ISSUE_PRIORITIES, ISSUE_STATE_GROUPS } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper";
import { ReactElement } from "react";
import { IProjectLabelStore, IProjectMemberStore, IProjectStateStore, IProjectStore } from "store_legacy/project";
import { IIssue } from "types";
import { IIssueStore } from "store/issue/issue.store";
import { IProjectIssues } from "store/issue/project";
import { ILabelRootStore } from "store/label";
import { IProjectStore } from "store/project/project.store";
import { IStateStore } from "store/state.store";
import { IGroupedIssues, IIssue, ISubGroupedIssues, TUnGroupedIssues } from "types";
export type columnTypes =
| "project"
@ -24,9 +29,9 @@ export interface IKanbanColumn {
export const getKanbanColumns = (
groupBy: columnTypes | null,
project: IProjectStore,
projectLabel: IProjectLabelStore,
projectMember: IProjectMemberStore,
projectState: IProjectStateStore
projectLabel: ILabelRootStore,
projectState: IStateStore
//projectMember?: IProjectMemberStore,
): IKanbanColumn[] | undefined => {
switch (groupBy) {
case "project":
@ -39,27 +44,32 @@ export const getKanbanColumns = (
return getPriorityColumns();
case "labels":
return getLabelsColumns(projectLabel);
case "assignees":
return getAssigneeColumns(projectMember);
case "created_by":
return getCreatedByColumns(projectMember);
// case "assignees":
// return getAssigneeColumns(projectMember);
// case "created_by":
// return getCreatedByColumns(projectMember);
}
};
const getProjectColumns = (project: IProjectStore): IKanbanColumn[] | undefined => {
const { workspaceProjects: projects } = project;
const { workspaceProjects: projectIds, projectMap } = project;
if (!projects) return;
if (!projectIds) return;
return projects.map((project) => ({
id: project.id,
name: project.name,
Icon: <div className="w-6 h-6">{renderEmoji(project.emoji || "")}</div>,
payload: { project: project.id },
}));
return projectIds.map((projectId) => {
const project = projectMap[projectId];
if (project)
return {
id: project.id,
name: project.name,
Icon: <div className="w-6 h-6">{renderEmoji(project.emoji || "")}</div>,
payload: { project: project.id },
};
});
};
const getStateColumns = (projectState: IProjectStateStore): IKanbanColumn[] | undefined => {
const getStateColumns = (projectState: IStateStore): IKanbanColumn[] | undefined => {
const { projectStates } = projectState;
if (!projectStates) return;
@ -101,8 +111,10 @@ const getPriorityColumns = () => {
}));
};
const getLabelsColumns = (projectLabel: IProjectLabelStore) => {
const { projectLabels } = projectLabel;
const getLabelsColumns = (projectLabel: ILabelRootStore) => {
const {
project: { projectLabels },
} = projectLabel;
if (!projectLabels) return;
@ -118,32 +130,185 @@ const getLabelsColumns = (projectLabel: IProjectLabelStore) => {
}));
};
const getAssigneeColumns = (projectMember: IProjectMemberStore) => {
const { projectMembers: users } = projectMember;
if (!users) return;
// const getAssigneeColumns = (projectMember: IProjectMemberStore) => {
// const { projectMembers: users } = projectMember;
// if (!users) return;
return users.map((user) => {
const member = user.member;
return {
id: member?.id,
name: member?.display_name || "",
Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
payload: { assignees: [member?.id] },
};
});
// return users.map((user) => {
// const member = user.member;
// return {
// id: member?.id,
// name: member?.display_name || "",
// Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
// payload: { assignees: [member?.id] },
// };
// });
// };
// const getCreatedByColumns = (projectMember: IProjectMemberStore) => {
// const { projectMembers: users } = projectMember;
// if (!users) return;
// return users.map((user) => {
// const member = user.member;
// return {
// id: member?.id,
// name: member?.display_name || "",
// Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
// payload: { created_by: member?.id },
// };
// });
// };
export const handleDragDrop = async (
source: DraggableLocation | null,
destination: DraggableLocation | null,
workspaceSlug: string,
projectId: string, // projectId for all views or user id in profile issues
store: IProjectIssues,
subGroupBy: string | null,
groupBy: string | null,
issueMap: IIssueStore | undefined,
issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined,
viewId: string | null = null // it can be moduleId, cycleId
) => {
if (!issueMap || !issueWithIds || !source || !destination) return;
let updateIssue: any = {};
const sourceColumnId = (source?.droppableId && source?.droppableId.split("__")) || null;
const destinationColumnId = (destination?.droppableId && destination?.droppableId.split("__")) || null;
if (!sourceColumnId || !destinationColumnId) return;
const sourceGroupByColumnId = sourceColumnId[0] || null;
const destinationGroupByColumnId = destinationColumnId[0] || null;
const sourceSubGroupByColumnId = sourceColumnId[1] || null;
const destinationSubGroupByColumnId = destinationColumnId[1] || null;
if (
!workspaceSlug ||
!projectId ||
!groupBy ||
!sourceGroupByColumnId ||
!destinationGroupByColumnId ||
!sourceSubGroupByColumnId ||
!sourceGroupByColumnId
)
return;
if (destinationGroupByColumnId === "issue-trash-box") {
const sourceIssues: string[] = subGroupBy
? (issueWithIds as ISubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId]
: (issueWithIds as IGroupedIssues)[sourceGroupByColumnId];
const [removed] = sourceIssues.splice(source.index, 1);
if (removed) {
if (viewId) return await store?.removeIssue(workspaceSlug, projectId, removed); //, viewId);
else return await store?.removeIssue(workspaceSlug, projectId, removed);
}
} else {
const sourceIssues = subGroupBy
? (issueWithIds as ISubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId]
: (issueWithIds as IGroupedIssues)[sourceGroupByColumnId];
const destinationIssues = subGroupBy
? (issueWithIds as ISubGroupedIssues)[sourceSubGroupByColumnId][destinationGroupByColumnId]
: (issueWithIds as IGroupedIssues)[destinationGroupByColumnId];
const [removed] = sourceIssues.splice(source.index, 1);
const removedIssueDetail = issueMap.allIssues[removed];
if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) {
updateIssue = {
id: removedIssueDetail?.id,
};
// for both horizontal and vertical dnd
updateIssue = {
...updateIssue,
...handleSortOrder(destinationIssues, destination.index, issueMap),
};
if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) {
if (sourceGroupByColumnId != destinationGroupByColumnId) {
if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId };
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
}
} else {
if (subGroupBy === "state")
updateIssue = {
...updateIssue,
state: destinationSubGroupByColumnId,
priority: destinationGroupByColumnId,
};
if (subGroupBy === "priority")
updateIssue = {
...updateIssue,
state: destinationGroupByColumnId,
priority: destinationSubGroupByColumnId,
};
}
} else {
updateIssue = {
id: removedIssueDetail?.id,
};
// for both horizontal and vertical dnd
updateIssue = {
...updateIssue,
...handleSortOrder(destinationIssues, destination.index, issueMap),
};
// for horizontal dnd
if (sourceColumnId != destinationColumnId) {
if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId };
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
}
}
if (updateIssue && updateIssue?.id) {
if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); //, viewId);
else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue);
}
}
};
const getCreatedByColumns = (projectMember: IProjectMemberStore) => {
const { projectMembers: users } = projectMember;
if (!users) return;
const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueStore) => {
const sortOrderDefaultValue = 65535;
let currentIssueState = {};
return users.map((user) => {
const member = user.member;
return {
id: member?.id,
name: member?.display_name || "",
Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
payload: { created_by: member?.id },
if (destinationIssues && destinationIssues.length > 0) {
if (destinationIndex === 0) {
const destinationIssueId = destinationIssues[destinationIndex];
currentIssueState = {
...currentIssueState,
sort_order: issueMap.allIssues[destinationIssueId].sort_order - sortOrderDefaultValue,
};
} else if (destinationIndex === destinationIssues.length) {
const destinationIssueId = destinationIssues[destinationIndex - 1];
currentIssueState = {
...currentIssueState,
sort_order: issueMap.allIssues[destinationIssueId].sort_order + sortOrderDefaultValue,
};
} else {
const destinationTopIssueId = destinationIssues[destinationIndex - 1];
const destinationBottomIssueId = destinationIssues[destinationIndex];
currentIssueState = {
...currentIssueState,
sort_order:
(issueMap.allIssues[destinationTopIssueId].sort_order +
issueMap.allIssues[destinationBottomIssueId].sort_order) /
2,
};
}
} else {
currentIssueState = {
...currentIssueState,
sort_order: sortOrderDefaultValue,
};
});
}
return currentIssueState;
};

View File

@ -0,0 +1,32 @@
import { observer } from "mobx-react-lite";
import { ReactNode } from "react";
import {
ICycleIssuesFilterStore,
IModuleIssuesFilterStore,
IProfileIssuesFilterStore,
IProjectIssuesFilterStore,
IViewIssuesFilterStore,
} from "store_legacy/issues";
import { IIssueDisplayProperties } from "types";
interface IWithDisplayPropertiesHOC {
issuesFilter:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
getShouldRenderProperty: (displayProperties: IIssueDisplayProperties) => boolean;
children: ReactNode;
}
export const WithDisplayPropertiesHOC = observer(
({ issuesFilter, getShouldRenderProperty, children }: IWithDisplayPropertiesHOC) => {
const displayProperties = issuesFilter.issueFilters.displayProperties;
const shouldRenderProperty = getShouldRenderProperty(displayProperties);
if (!shouldRenderProperty) return null;
return <>{children}</>;
}
);

View File

@ -1,11 +1,8 @@
import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// TODO: update this
import useStoreIssues from "hooks/use-store-issues";
// components
import {
ListLayout,
@ -17,6 +14,8 @@ import {
ProjectEmptyState,
} from "components/issues";
import { Spinner } from "@plane/ui";
import { useIssues } from "hooks/store/use-issues";
import { EIssuesStoreType } from "constants/issue";
// hooks
export const ProjectLayoutRoot: React.FC = observer(() => {
@ -25,16 +24,20 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const {
projectIssues: { loader, getIssues, fetchIssues },
projectIssuesFilter: { issueFilters, fetchFilters },
} = useMobxStore();
issues: { loader, getIssues, fetchIssues },
issuesFilter: { issueFilters, fetchFilters },
} = useIssues(EIssuesStoreType.PROJECT);
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
if (workspaceSlug && projectId) {
await fetchFilters(workspaceSlug, projectId);
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
}
});
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId) {
await fetchFilters(workspaceSlug, projectId);
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
}
},
{ revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true }
);
// TODO: update this
// const {

View File

@ -408,3 +408,13 @@ export const groupReactionEmojis = (reactions: any) => {
return _groupedEmojis;
};
export enum EIssuesStoreType {
MODULE,
CYClE,
PROJECT,
VIEW,
DRAFT,
ARCHIVED,
GLOBAL,
}

View File

@ -0,0 +1,20 @@
import { useContext } from "react";
// mobx store
import { MobxStoreContext } from "lib/mobx/store-provider";
// types
import { EIssuesStoreType } from "constants/issue";
export const useIssues = (storeType?: EIssuesStoreType) => {
const context = useContext(MobxStoreContext);
if (context === undefined) throw new Error("useMobxStore must be used within MobxStoreProvider");
switch (storeType) {
case EIssuesStoreType.PROJECT:
return {
issues: context.issue.projectIssues,
issuesFilter: context.issue.projectIssuesFilter,
issueMap: context.issue.issues,
};
default:
return { issueMap: context.issue.issues };
}
};

View File

@ -1,6 +1,6 @@
import { createContext, useContext } from "react";
// mobx store
import { RootStore } from "store/root.store";
// mobx store
let rootStore: RootStore = new RootStore();

View File

@ -26,7 +26,7 @@ export class IssueStore implements IIssueStore {
makeObservable(this, {
// observable
allIssues: observable.ref,
allIssues: observable,
// actions
addIssue: action,
updateIssue: action,

View File

@ -0,0 +1,76 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { IssueRootStore } from "./root.store";
// types
export interface IIssueKanBanViewStore {
kanBanToggle: {
groupByHeaderMinMax: string[];
subgroupByIssuesVisibility: string[];
};
// computed
canUserDragDrop: boolean;
canUserDragDropVertically: boolean;
canUserDragDropHorizontally: boolean;
// actions
handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void;
}
export class IssueKanBanViewStore implements IIssueKanBanViewStore {
kanBanToggle: {
groupByHeaderMinMax: string[];
subgroupByIssuesVisibility: string[];
} = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] };
// root store
rootStore;
constructor(_rootStore: IssueRootStore) {
makeObservable(this, {
kanBanToggle: observable,
// computed
canUserDragDrop: computed,
canUserDragDropVertically: computed,
canUserDragDropHorizontally: computed,
// actions
handleKanBanToggle: action,
});
this.rootStore = _rootStore;
}
get canUserDragDrop() {
return true;
if (this.rootStore.issueDetail.peekId) return false;
if (
this.rootStore?.issueFilter?.userDisplayFilters?.order_by &&
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) return true;
if (
this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by &&
["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by)
)
return true;
}
return false;
}
get canUserDragDropVertically() {
return false;
}
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],
};
};
}

View File

@ -14,6 +14,7 @@ import {
TIssueGroupByOptions,
TLoader,
TUnGroupedIssues,
ViewFlags,
} from "types";
export interface IProjectIssues {
@ -28,6 +29,7 @@ export interface IProjectIssues {
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>) => Promise<IIssue>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
quickAddIssue: (workspaceSlug: string, projectId: string, data: IIssue) => Promise<IIssue>;
viewFlags: ViewFlags;
}
export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
@ -38,6 +40,12 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
issueService;
// root store
rootIssueStore: IIssueRootStore;
//viewData
viewFlags = {
enableQuickAdd: true,
enableIssueCreation: true,
enableInlineEditing: true,
};
constructor(_rootStore: IIssueRootStore) {
super(_rootStore);

View File

@ -18,6 +18,7 @@ import {
} from "./project-views";
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft";
import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store";
export interface IIssueRootStore {
currentUserId: string | undefined;
@ -59,6 +60,8 @@ export interface IIssueRootStore {
draftIssuesFilter: IDraftIssuesFilter;
draftIssues: IDraftIssues;
issueKanBanView: IIssueKanBanViewStore;
}
export class IssueRootStore {
@ -102,6 +105,8 @@ export class IssueRootStore {
draftIssuesFilter: IDraftIssuesFilter;
draftIssues: IDraftIssues;
issueKanBanView: IIssueKanBanViewStore;
constructor(rootStore: RootStore) {
makeObservable(this, {
currentUserId: observable.ref,
@ -160,5 +165,7 @@ export class IssueRootStore {
this.draftIssuesFilter = new DraftIssuesFilter(this);
this.draftIssues = new DraftIssues();
this.issueKanBanView = new IssueKanBanViewStore(this);
}
}

View File

@ -11,7 +11,6 @@ import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
import { IStateStore, StateStore } from "./state.store";
import { IPageStore, PageStore } from "./page.store";
import { ILabelRootStore, LabelRootStore } from "./label";
enableStaticRendering(typeof window === "undefined");
export class RootStore {

View File

@ -272,3 +272,9 @@ export type TUnGroupedIssues = string[];
export interface IIssueResponse {
[issue_id: string]: IIssue;
}
export interface ViewFlags {
enableQuickAdd: boolean;
enableIssueCreation: boolean;
enableInlineEditing: boolean;
}