Merge branch 'feat/pagination' of github.com:makeplane/plane into feat/pagination

This commit is contained in:
pablohashescobar 2024-03-22 17:28:14 +05:30
commit 33a64fc67b
61 changed files with 1699 additions and 998 deletions

View File

@ -12,16 +12,26 @@ export * from "./activity/base";
export type TLoader = "init-loader" | "mutation" | "pagination" | undefined; export type TLoader = "init-loader" | "mutation" | "pagination" | undefined;
export type TIssueGroup = { issueIds: string[]; issueCount: number };
export type TGroupedIssues = { export type TGroupedIssues = {
[group_id: string]: TIssueGroup; [group_id: string]: string[];
}; };
export type TSubGroupedIssues = { export type TSubGroupedIssues = {
[sub_grouped_id: string]: TGroupedIssues; [sub_grouped_id: string]: TGroupedIssues;
}; };
export type TUnGroupedIssues = {
"All Issues": TIssueGroup; export type TIssues = TGroupedIssues | TSubGroupedIssues;
export type TPaginationData = {
nextCursor: string;
prevCursor: string;
nextPageResults: boolean;
}; };
export type TIssues = TGroupedIssues | TUnGroupedIssues; export type TIssuePaginationData = {
[group_id: string]: TPaginationData;
};
export type TGroupedIssueCount = {
[group_id: string]: number;
};

View File

@ -61,7 +61,14 @@ type TIssueResponseResults =
| TBaseIssue[] | TBaseIssue[]
| { | {
[key: string]: { [key: string]: {
results: TBaseIssue[]; results:
| TBaseIssue[]
| {
[key: string]: {
results: TBaseIssue[];
total_results: number;
};
};
total_results: number; total_results: number;
}; };
}; };
@ -72,6 +79,7 @@ export type TIssuesResponse = {
prev_cursor: string; prev_cursor: string;
next_page_results: boolean; next_page_results: boolean;
prev_page_results: boolean; prev_page_results: boolean;
total_count: number;
count: number; count: number;
total_pages: number; total_pages: number;
extra_stats: null; extra_stats: null;

View File

@ -1,13 +1,14 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
import { CycleGanttBlock } from "components/cycles"; import { CycleGanttBlock } from "components/cycles";
import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar, ChartDataType } from "components/gantt-chart";
import { useCycle } from "hooks/store"; import { useCycle } from "hooks/store";
// components // components
// types // types
import { ICycle } from "@plane/types"; import { ICycle } from "@plane/types";
import { getMonthChartItemPositionWidthInMonth } from "components/gantt-chart/views";
// constants // constants
type Props = { type Props = {
@ -23,6 +24,28 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
// store hooks // store hooks
const { getCycleById, updateCycleDetails } = useCycle(); const { getCycleById, updateCycleDetails } = useCycle();
const getBlockById = useCallback(
(id: string, currentViewData?: ChartDataType | undefined) => {
const cycle = getCycleById(id);
const block = {
data: cycle,
id: cycle?.id ?? "",
sort_order: cycle?.sort_order ?? 0,
start_date: cycle?.start_date ? new Date(cycle?.start_date) : undefined,
target_date: cycle?.end_date ? new Date(cycle?.end_date) : undefined,
};
if (currentViewData) {
return {
...block,
position: getMonthChartItemPositionWidthInMonth(currentViewData, block),
};
}
return block;
},
[getCycleById]
);
const handleCycleUpdate = async (cycle: ICycle, data: IBlockUpdateData) => { const handleCycleUpdate = async (cycle: ICycle, data: IBlockUpdateData) => {
if (!workspaceSlug || !cycle) return; if (!workspaceSlug || !cycle) return;
@ -32,28 +55,13 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
await updateCycleDetails(workspaceSlug.toString(), cycle.project_id, cycle.id, payload); await updateCycleDetails(workspaceSlug.toString(), cycle.project_id, cycle.id, payload);
}; };
const blockFormat = (blocks: (ICycle | null)[]) => {
if (!blocks) return [];
const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date);
const structuredBlocks = filteredBlocks.map((block) => ({
data: block,
id: block?.id ?? "",
sort_order: block?.sort_order ?? 0,
start_date: new Date(block?.start_date ?? ""),
target_date: new Date(block?.end_date ?? ""),
}));
return structuredBlocks;
};
return ( return (
<div className="h-full w-full overflow-y-auto"> <div className="h-full w-full overflow-y-auto">
<GanttChartRoot <GanttChartRoot
title="Cycles" title="Cycles"
loaderTitle="Cycles" loaderTitle="Cycles"
blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null} blockIds={cycleIds}
getBlockById={getBlockById}
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
sidebarToRender={(props) => <CycleGanttSidebar {...props} />} sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
blockToRender={(data: ICycle) => <CycleGanttBlock cycleId={data.id} />} blockToRender={(data: ICycle) => <CycleGanttBlock cycleId={data.id} />}

View File

@ -10,10 +10,12 @@ import { useIssueDetail } from "hooks/store";
import { BLOCK_HEIGHT } from "../constants"; import { BLOCK_HEIGHT } from "../constants";
import { ChartAddBlock, ChartDraggable } from "../helpers"; import { ChartAddBlock, ChartDraggable } from "../helpers";
import { useGanttChart } from "../hooks"; import { useGanttChart } from "../hooks";
import { IBlockUpdateData, IGanttBlock } from "../types"; import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
type Props = { type Props = {
block: IGanttBlock; blockId: string;
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
showAllBlocks: boolean;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any) => React.ReactNode;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableBlockLeftResize: boolean; enableBlockLeftResize: boolean;
@ -25,7 +27,9 @@ type Props = {
export const GanttChartBlock: React.FC<Props> = observer((props) => { export const GanttChartBlock: React.FC<Props> = observer((props) => {
const { const {
block, blockId,
getBlockById,
showAllBlocks,
blockToRender, blockToRender,
blockUpdateHandler, blockUpdateHandler,
enableBlockLeftResize, enableBlockLeftResize,
@ -35,9 +39,14 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
ganttContainerRef, ganttContainerRef,
} = props; } = props;
// store hooks // store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart(); const { currentViewData, updateActiveBlockId, isBlockActive } = useGanttChart();
const { peekIssue } = useIssueDetail(); const { peekIssue } = useIssueDetail();
const block = getBlockById(blockId, currentViewData);
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!block || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
const isBlockVisibleOnChart = block.start_date && block.target_date; const isBlockVisibleOnChart = block.start_date && block.target_date;
const handleChartBlockPosition = ( const handleChartBlockPosition = (
@ -72,7 +81,6 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
return ( return (
<div <div
key={`block-${block.id}`}
className="relative min-w-full w-max" className="relative min-w-full w-max"
style={{ style={{
height: `${BLOCK_HEIGHT}px`, height: `${BLOCK_HEIGHT}px`,
@ -80,11 +88,11 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
> >
<div <div
className={cn("relative h-full", { className={cn("relative h-full", {
"bg-custom-background-80": isBlockActive(block.id), "bg-custom-background-80": isBlockActive(blockId),
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": "rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id, peekIssue?.issueId === block.data.id,
})} })}
onMouseEnter={() => updateActiveBlockId(block.id)} onMouseEnter={() => updateActiveBlockId(blockId)}
onMouseLeave={() => updateActiveBlockId(null)} onMouseLeave={() => updateActiveBlockId(null)}
> >
{isBlockVisibleOnChart ? ( {isBlockVisibleOnChart ? (

View File

@ -1,14 +1,15 @@
import { FC } from "react"; import { FC } from "react";
// components // components
import { HEADER_HEIGHT } from "../constants"; import { HEADER_HEIGHT } from "../constants";
import { IBlockUpdateData, IGanttBlock } from "../types"; import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
import { GanttChartBlock } from "./block"; import { GanttChartBlock } from "./block";
// types // types
// constants // constants
export type GanttChartBlocksProps = { export type GanttChartBlocksProps = {
itemsContainerWidth: number; itemsContainerWidth: number;
blocks: IGanttBlock[] | null; blockIds: string[];
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any) => React.ReactNode;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableBlockLeftResize: boolean; enableBlockLeftResize: boolean;
@ -22,9 +23,10 @@ export type GanttChartBlocksProps = {
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => { export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
const { const {
itemsContainerWidth, itemsContainerWidth,
blocks, blockIds,
blockToRender, blockToRender,
blockUpdateHandler, blockUpdateHandler,
getBlockById,
enableBlockLeftResize, enableBlockLeftResize,
enableBlockRightResize, enableBlockRightResize,
enableBlockMove, enableBlockMove,
@ -41,14 +43,13 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
transform: `translateY(${HEADER_HEIGHT}px)`, transform: `translateY(${HEADER_HEIGHT}px)`,
}} }}
> >
{blocks?.map((block) => { {blockIds?.map((blockId) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
return ( return (
<GanttChartBlock <GanttChartBlock
key={block.id} key={blockId}
block={block} blockId={blockId}
getBlockById={getBlockById}
showAllBlocks={showAllBlocks}
blockToRender={blockToRender} blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler} blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize} enableBlockLeftResize={enableBlockLeftResize}

View File

@ -10,7 +10,7 @@ import { IGanttBlock, TGanttViews } from "../types";
// constants // constants
type Props = { type Props = {
blocks: IGanttBlock[] | null; blockIds: string[];
fullScreenMode: boolean; fullScreenMode: boolean;
handleChartView: (view: TGanttViews) => void; handleChartView: (view: TGanttViews) => void;
handleToday: () => void; handleToday: () => void;
@ -20,7 +20,7 @@ type Props = {
}; };
export const GanttChartHeader: React.FC<Props> = observer((props) => { export const GanttChartHeader: React.FC<Props> = observer((props) => {
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props; const { blockIds, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props;
// chart hook // chart hook
const { currentView } = useGanttChart(); const { currentView } = useGanttChart();
@ -28,7 +28,9 @@ export const GanttChartHeader: React.FC<Props> = observer((props) => {
<div className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2"> <div className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2">
<div className="flex items-center gap-2 text-lg font-medium">{title}</div> <div className="flex items-center gap-2 text-lg font-medium">{title}</div>
<div className="ml-auto"> <div className="ml-auto">
<div className="ml-auto text-sm font-medium">{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}</div> <div className="ml-auto text-sm font-medium">
{blockIds ? `${blockIds.length} ${loaderTitle}` : "Loading..."}
</div>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">

View File

@ -4,6 +4,7 @@ import { observer } from "mobx-react";
// components // components
import { import {
BiWeekChartView, BiWeekChartView,
ChartDataType,
DayChartView, DayChartView,
GanttChartBlocksList, GanttChartBlocksList,
GanttChartSidebar, GanttChartSidebar,
@ -21,11 +22,13 @@ import { cn } from "helpers/common.helper";
import { useGanttChart } from "../hooks/use-gantt-chart"; import { useGanttChart } from "../hooks/use-gantt-chart";
type Props = { type Props = {
blocks: IGanttBlock[] | null; blockIds: string[];
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
canLoadMoreBlocks?: boolean;
loadMoreBlocks?: () => void;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any) => React.ReactNode;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
bottomSpacing: boolean; bottomSpacing: boolean;
chartBlocks: IGanttBlock[] | null;
enableBlockLeftResize: boolean; enableBlockLeftResize: boolean;
enableBlockMove: boolean; enableBlockMove: boolean;
enableBlockRightResize: boolean; enableBlockRightResize: boolean;
@ -41,11 +44,12 @@ type Props = {
export const GanttChartMainContent: React.FC<Props> = observer((props) => { export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const { const {
blocks, blockIds,
getBlockById,
loadMoreBlocks,
blockToRender, blockToRender,
blockUpdateHandler, blockUpdateHandler,
bottomSpacing, bottomSpacing,
chartBlocks,
enableBlockLeftResize, enableBlockLeftResize,
enableBlockMove, enableBlockMove,
enableBlockRightResize, enableBlockRightResize,
@ -55,6 +59,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
showAllBlocks, showAllBlocks,
sidebarToRender, sidebarToRender,
title, title,
canLoadMoreBlocks,
updateCurrentViewRenderPayload, updateCurrentViewRenderPayload,
quickAdd, quickAdd,
} = props; } = props;
@ -104,7 +109,11 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
onScroll={onScroll} onScroll={onScroll}
> >
<GanttChartSidebar <GanttChartSidebar
blocks={blocks} blockIds={blockIds}
getBlockById={getBlockById}
loadMoreBlocks={loadMoreBlocks}
canLoadMoreBlocks={canLoadMoreBlocks}
ganttContainerRef={ganttContainerRef}
blockUpdateHandler={blockUpdateHandler} blockUpdateHandler={blockUpdateHandler}
enableReorder={enableReorder} enableReorder={enableReorder}
sidebarToRender={sidebarToRender} sidebarToRender={sidebarToRender}
@ -116,7 +125,8 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
{currentViewData && ( {currentViewData && (
<GanttChartBlocksList <GanttChartBlocksList
itemsContainerWidth={itemsContainerWidth} itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks} blockIds={blockIds}
getBlockById={getBlockById}
blockToRender={blockToRender} blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler} blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize} enableBlockLeftResize={enableBlockLeftResize}

View File

@ -13,17 +13,13 @@ import { currentViewDataWithView } from "../data";
// constants // constants
import { useGanttChart } from "../hooks/use-gantt-chart"; import { useGanttChart } from "../hooks/use-gantt-chart";
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types"; import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
import { import { generateMonthChart, getNumberOfDaysBetweenTwoDatesInMonth } from "../views";
generateMonthChart,
getNumberOfDaysBetweenTwoDatesInMonth,
getMonthChartItemPositionWidthInMonth,
} from "../views";
type ChartViewRootProps = { type ChartViewRootProps = {
border: boolean; border: boolean;
title: string; title: string;
loaderTitle: string; loaderTitle: string;
blocks: IGanttBlock[] | null; blockIds: string[];
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode;
@ -34,6 +30,9 @@ type ChartViewRootProps = {
enableAddBlock: boolean; enableAddBlock: boolean;
bottomSpacing: boolean; bottomSpacing: boolean;
showAllBlocks: boolean; showAllBlocks: boolean;
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
loadMoreBlocks?: () => void;
canLoadMoreBlocks?: boolean;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
}; };
@ -41,11 +40,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
const { const {
border, border,
title, title,
blocks = null, blockIds,
getBlockById,
loadMoreBlocks,
loaderTitle, loaderTitle,
blockUpdateHandler, blockUpdateHandler,
sidebarToRender, sidebarToRender,
blockToRender, blockToRender,
canLoadMoreBlocks,
enableBlockLeftResize, enableBlockLeftResize,
enableBlockRightResize, enableBlockRightResize,
enableBlockMove, enableBlockMove,
@ -58,25 +60,10 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
// states // states
const [itemsContainerWidth, setItemsContainerWidth] = useState(0); const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
const [fullScreenMode, setFullScreenMode] = useState(false); const [fullScreenMode, setFullScreenMode] = useState(false);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
// hooks // hooks
const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } = const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } =
useGanttChart(); useGanttChart();
// rendering the block structure
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks
? blocks.map((block: any) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
: [];
useEffect(() => {
if (!currentViewData || !blocks) return;
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
}, [currentViewData, blocks]);
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => { const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
const selectedCurrentView: TGanttViews = view; const selectedCurrentView: TGanttViews = view;
const selectedCurrentViewData: ChartDataType | undefined = const selectedCurrentViewData: ChartDataType | undefined =
@ -166,7 +153,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
})} })}
> >
<GanttChartHeader <GanttChartHeader
blocks={blocks} blockIds={blockIds}
fullScreenMode={fullScreenMode} fullScreenMode={fullScreenMode}
toggleFullScreenMode={() => setFullScreenMode((prevData) => !prevData)} toggleFullScreenMode={() => setFullScreenMode((prevData) => !prevData)}
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)} handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
@ -175,11 +162,13 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
title={title} title={title}
/> />
<GanttChartMainContent <GanttChartMainContent
blocks={blocks} blockIds={blockIds}
getBlockById={getBlockById}
loadMoreBlocks={loadMoreBlocks}
canLoadMoreBlocks={canLoadMoreBlocks}
blockToRender={blockToRender} blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler} blockUpdateHandler={blockUpdateHandler}
bottomSpacing={bottomSpacing} bottomSpacing={bottomSpacing}
chartBlocks={chartBlocks}
enableBlockLeftResize={enableBlockLeftResize} enableBlockLeftResize={enableBlockLeftResize}
enableBlockMove={enableBlockMove} enableBlockMove={enableBlockMove}
enableBlockRightResize={enableBlockRightResize} enableBlockRightResize={enableBlockRightResize}

View File

@ -1,6 +1,6 @@
import { FC } from "react"; import { FC } from "react";
// components // components
import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; import { ChartDataType, ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// context // context
import { GanttStoreProvider } from "components/gantt-chart/contexts"; import { GanttStoreProvider } from "components/gantt-chart/contexts";
@ -8,11 +8,14 @@ type GanttChartRootProps = {
border?: boolean; border?: boolean;
title: string; title: string;
loaderTitle: string; loaderTitle: string;
blocks: IGanttBlock[] | null; blockIds: string[];
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode; blockToRender: (data: any) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
canLoadMoreBlocks?: boolean;
loadMoreBlocks?: () => void;
enableBlockLeftResize?: boolean; enableBlockLeftResize?: boolean;
enableBlockRightResize?: boolean; enableBlockRightResize?: boolean;
enableBlockMove?: boolean; enableBlockMove?: boolean;
@ -26,11 +29,14 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
const { const {
border = true, border = true,
title, title,
blocks, blockIds,
loaderTitle = "blocks", loaderTitle = "blocks",
blockUpdateHandler, blockUpdateHandler,
sidebarToRender, sidebarToRender,
blockToRender, blockToRender,
getBlockById,
loadMoreBlocks,
canLoadMoreBlocks,
enableBlockLeftResize = false, enableBlockLeftResize = false,
enableBlockRightResize = false, enableBlockRightResize = false,
enableBlockMove = false, enableBlockMove = false,
@ -46,7 +52,10 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
<ChartViewRoot <ChartViewRoot
border={border} border={border}
title={title} title={title}
blocks={blocks} blockIds={blockIds}
getBlockById={getBlockById}
loadMoreBlocks={loadMoreBlocks}
canLoadMoreBlocks={canLoadMoreBlocks}
loaderTitle={loaderTitle} loaderTitle={loaderTitle}
blockUpdateHandler={blockUpdateHandler} blockUpdateHandler={blockUpdateHandler}
sidebarToRender={sidebarToRender} sidebarToRender={sidebarToRender}

View File

@ -2,22 +2,23 @@ import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; import { ChartDataType, IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
import { CyclesSidebarBlock } from "./block"; import { CyclesSidebarBlock } from "./block";
// types // types
type Props = { type Props = {
title: string; title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null; getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
blockIds: string[];
enableReorder: boolean; enableReorder: boolean;
}; };
export const CycleGanttSidebar: React.FC<Props> = (props) => { export const CycleGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder } = props; const { blockUpdateHandler, blockIds, getBlockById, enableReorder } = props;
const handleOrderChange = (result: DropResult) => { const handleOrderChange = (result: DropResult) => {
if (!blocks) return; if (!blockIds) return;
const { source, destination } = result; const { source, destination } = result;
@ -27,29 +28,30 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
// return if dropped on the same index // return if dropped on the same index
if (source.index === destination.index) return; if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order; let updatedSortOrder = getBlockById(blockIds[source.index]).sort_order;
// update the sort order to the lowest if dropped at the top // update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; if (destination.index === 0) updatedSortOrder = getBlockById(blockIds[0]).sort_order - 1000;
// update the sort order to the highest if dropped at the bottom // update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; else if (destination.index === blockIds.length - 1)
updatedSortOrder = getBlockById(blockIds[blockIds.length - 1]).sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between // update the sort order to the average of the two adjacent blocks if dropped in between
else { else {
const destinationSortingOrder = blocks[destination.index].sort_order; const destinationSortingOrder = getBlockById(blockIds[destination.index]).sort_order;
const relativeDestinationSortingOrder = const relativeDestinationSortingOrder =
source.index < destination.index source.index < destination.index
? blocks[destination.index + 1].sort_order ? getBlockById(blockIds[destination.index + 1]).sort_order
: blocks[destination.index - 1].sort_order; : getBlockById(blockIds[destination.index - 1]).sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
} }
// extract the element from the source index and insert it at the destination index without updating the entire array // extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0]; const removedElement = blockIds.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement); blockIds.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index // call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, { blockUpdateHandler(getBlockById(removedElement).data, {
sort_order: { sort_order: {
destinationIndex: destination.index, destinationIndex: destination.index,
newSortOrder: updatedSortOrder, newSortOrder: updatedSortOrder,
@ -64,24 +66,28 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
{(droppableProvided) => ( {(droppableProvided) => (
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}> <div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<> <>
{blocks ? ( {blockIds ? (
blocks.map((block, index) => ( blockIds.map((blockId, index) => {
<Draggable const block = getBlockById(blockId);
key={`sidebar-block-${block.id}`} if (!block.start_date || !block.target_date) return null;
draggableId={`sidebar-block-${block.id}`} return (
index={index} <Draggable
isDragDisabled={!enableReorder} key={`sidebar-block-${block.id}`}
> draggableId={`sidebar-block-${block.id}`}
{(provided, snapshot) => ( index={index}
<CyclesSidebarBlock isDragDisabled={!enableReorder}
block={block} >
enableReorder={enableReorder} {(provided, snapshot) => (
provided={provided} <CyclesSidebarBlock
snapshot={snapshot} block={block}
/> enableReorder={enableReorder}
)} provided={provided}
</Draggable> snapshot={snapshot}
)) />
)}
</Draggable>
);
})
) : ( ) : (
<Loader className="space-y-3 pr-2"> <Loader className="space-y-3 pr-2">
<Loader.Item height="34px" /> <Loader.Item height="34px" />

View File

@ -0,0 +1,34 @@
import { Draggable } from "@hello-pangea/dnd";
import { observer } from "mobx-react";
import { IssuesSidebarBlock } from "./block";
import { IGanttBlock } from "components/gantt-chart/types";
interface Props {
blockId: string;
enableReorder: boolean;
index: number;
showAllBlocks: boolean;
getBlockById: (blockId: string) => IGanttBlock;
}
export const IssueDraggableBlock = observer((props: Props) => {
const { blockId, enableReorder, index, showAllBlocks, getBlockById } = props;
const block = getBlockById(blockId);
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !isBlockVisibleOnSidebar) return null;
return (
<Draggable
key={`sidebar-block-${blockId}`}
draggableId={`sidebar-block-${blockId}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<IssuesSidebarBlock block={block} enableReorder={enableReorder} provided={provided} snapshot={snapshot} />
)}
</Draggable>
);
});

View File

@ -4,20 +4,40 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// types // types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { IssuesSidebarBlock } from "./block"; import { observer } from "mobx-react";
import { IssueDraggableBlock } from "./issue-draggable-block";
import { useIntersectionObserver } from "hooks/use-intersection-observer";
import { RefObject, useRef } from "react";
type Props = { type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null; getBlockById: (id: string) => IGanttBlock;
canLoadMoreBlocks?: boolean;
loadMoreBlocks?: () => void;
ganttContainerRef: RefObject<HTMLDivElement>;
blockIds: string[];
enableReorder: boolean; enableReorder: boolean;
showAllBlocks?: boolean; showAllBlocks?: boolean;
}; };
export const IssueGanttSidebar: React.FC<Props> = (props) => { export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props; const {
blockUpdateHandler,
blockIds,
getBlockById,
enableReorder,
loadMoreBlocks,
canLoadMoreBlocks,
ganttContainerRef,
showAllBlocks = false,
} = props;
const intersectionRef = useRef<HTMLSpanElement | null>(null);
useIntersectionObserver(ganttContainerRef, intersectionRef, loadMoreBlocks, "50% 0% 50% 0%");
const handleOrderChange = (result: DropResult) => { const handleOrderChange = (result: DropResult) => {
if (!blocks) return; if (!blockIds) return;
const { source, destination } = result; const { source, destination } = result;
@ -27,29 +47,30 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
// return if dropped on the same index // return if dropped on the same index
if (source.index === destination.index) return; if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order; let updatedSortOrder = getBlockById(blockIds[source.index]).sort_order;
// update the sort order to the lowest if dropped at the top // update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; if (destination.index === 0) updatedSortOrder = getBlockById(blockIds[0]).sort_order - 1000;
// update the sort order to the highest if dropped at the bottom // update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; else if (destination.index === blockIds.length - 1)
updatedSortOrder = getBlockById(blockIds[blockIds.length - 1])!.sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between // update the sort order to the average of the two adjacent blocks if dropped in between
else { else {
const destinationSortingOrder = blocks[destination.index].sort_order; const destinationSortingOrder = getBlockById(blockIds[destination.index]).sort_order;
const relativeDestinationSortingOrder = const relativeDestinationSortingOrder =
source.index < destination.index source.index < destination.index
? blocks[destination.index + 1].sort_order ? getBlockById(blockIds[destination.index + 1]).sort_order
: blocks[destination.index - 1].sort_order; : getBlockById(blockIds[destination.index - 1]).sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
} }
// extract the element from the source index and insert it at the destination index without updating the entire array // extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0]; const removedElement = blockIds.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement); blockIds.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index // call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, { blockUpdateHandler(getBlockById(removedElement).data, {
sort_order: { sort_order: {
destinationIndex: destination.index, destinationIndex: destination.index,
newSortOrder: updatedSortOrder, newSortOrder: updatedSortOrder,
@ -64,31 +85,21 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
{(droppableProvided) => ( {(droppableProvided) => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}> <div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<> <>
{blocks ? ( {blockIds ? (
blocks.map((block, index) => { <>
const isBlockVisibleOnSidebar = block.start_date && block.target_date; {blockIds.map((blockId, index) => (
<IssueDraggableBlock
// hide the block if it doesn't have start and target dates and showAllBlocks is false blockId={blockId}
if (!showAllBlocks && !isBlockVisibleOnSidebar) return; enableReorder={enableReorder}
return (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index} index={index}
isDragDisabled={!enableReorder} showAllBlocks={showAllBlocks}
> getBlockById={getBlockById}
{(provided, snapshot) => ( />
<IssuesSidebarBlock ))}
block={block} {canLoadMoreBlocks && (
enableReorder={enableReorder} <span ref={intersectionRef} className="h-5 w-10 bg-custom-background-80 rounded animate-pulse" />
provided={provided} )}
snapshot={snapshot} </>
/>
)}
</Draggable>
);
})
) : ( ) : (
<Loader className="space-y-3 pr-2"> <Loader className="space-y-3 pr-2">
<Loader.Item height="34px" /> <Loader.Item height="34px" />
@ -104,4 +115,4 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
); );
}; });

View File

@ -2,22 +2,23 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; import { ChartDataType, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
import { ModulesSidebarBlock } from "./block"; import { ModulesSidebarBlock } from "./block";
// types // types
type Props = { type Props = {
title: string; title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null; getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
blockIds: string[];
enableReorder: boolean; enableReorder: boolean;
}; };
export const ModuleGanttSidebar: React.FC<Props> = (props) => { export const ModuleGanttSidebar: React.FC<Props> = (props) => {
const { blockUpdateHandler, blocks, enableReorder } = props; const { blockUpdateHandler, blockIds, getBlockById, enableReorder } = props;
const handleOrderChange = (result: DropResult) => { const handleOrderChange = (result: DropResult) => {
if (!blocks) return; if (!blockIds) return;
const { source, destination } = result; const { source, destination } = result;
@ -27,29 +28,30 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
// return if dropped on the same index // return if dropped on the same index
if (source.index === destination.index) return; if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order; let updatedSortOrder = getBlockById(blockIds[source.index]).sort_order;
// update the sort order to the lowest if dropped at the top // update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; if (destination.index === 0) updatedSortOrder = getBlockById(blockIds[0]).sort_order - 1000;
// update the sort order to the highest if dropped at the bottom // update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; else if (destination.index === blockIds.length - 1)
updatedSortOrder = getBlockById(blockIds[blockIds.length - 1]).sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between // update the sort order to the average of the two adjacent blocks if dropped in between
else { else {
const destinationSortingOrder = blocks[destination.index].sort_order; const destinationSortingOrder = getBlockById(blockIds[destination.index]).sort_order;
const relativeDestinationSortingOrder = const relativeDestinationSortingOrder =
source.index < destination.index source.index < destination.index
? blocks[destination.index + 1].sort_order ? getBlockById(blockIds[destination.index + 1]).sort_order
: blocks[destination.index - 1].sort_order; : getBlockById(blockIds[destination.index - 1]).sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
} }
// extract the element from the source index and insert it at the destination index without updating the entire array // extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0]; const removedElement = blockIds.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement); blockIds.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index // call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, { blockUpdateHandler(getBlockById(removedElement).data, {
sort_order: { sort_order: {
destinationIndex: destination.index, destinationIndex: destination.index,
newSortOrder: updatedSortOrder, newSortOrder: updatedSortOrder,
@ -64,24 +66,27 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
{(droppableProvided) => ( {(droppableProvided) => (
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}> <div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<> <>
{blocks ? ( {blockIds ? (
blocks.map((block, index) => ( blockIds.map((blockId, index) => {
<Draggable const block = getBlockById(blockId);
key={`sidebar-block-${block.id}`} return (
draggableId={`sidebar-block-${block.id}`} <Draggable
index={index} key={`sidebar-block-${block.id}`}
isDragDisabled={!enableReorder} draggableId={`sidebar-block-${block.id}`}
> index={index}
{(provided, snapshot) => ( isDragDisabled={!enableReorder}
<ModulesSidebarBlock >
block={block} {(provided, snapshot) => (
enableReorder={enableReorder} <ModulesSidebarBlock
provided={provided} block={block}
snapshot={snapshot} enableReorder={enableReorder}
/> provided={provided}
)} snapshot={snapshot}
</Draggable> />
)) )}
</Draggable>
);
})
) : ( ) : (
<Loader className="space-y-3 pr-2"> <Loader className="space-y-3 pr-2">
<Loader.Item height="34px" /> <Loader.Item height="34px" />

View File

@ -1,19 +1,35 @@
// components // components
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; import { ChartDataType, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// constants // constants
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants"; import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
import { RefObject } from "react";
type Props = { type Props = {
blocks: IGanttBlock[] | null; blockIds: string[];
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
canLoadMoreBlocks?: boolean;
loadMoreBlocks?: () => void;
ganttContainerRef: RefObject<HTMLDivElement>;
enableReorder: boolean; enableReorder: boolean;
sidebarToRender: (props: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode;
title: string; title: string;
getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
}; };
export const GanttChartSidebar: React.FC<Props> = (props) => { export const GanttChartSidebar: React.FC<Props> = (props) => {
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title, quickAdd } = props; const {
blockIds,
blockUpdateHandler,
enableReorder,
sidebarToRender,
getBlockById,
loadMoreBlocks,
canLoadMoreBlocks,
ganttContainerRef,
title,
quickAdd,
} = props;
return ( return (
<div <div
@ -35,7 +51,17 @@ export const GanttChartSidebar: React.FC<Props> = (props) => {
</div> </div>
<div className="min-h-full h-max bg-custom-background-100 overflow-x-hidden overflow-y-auto"> <div className="min-h-full h-max bg-custom-background-100 overflow-x-hidden overflow-y-auto">
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })} {sidebarToRender &&
sidebarToRender({
title,
blockUpdateHandler,
blockIds,
getBlockById,
enableReorder,
canLoadMoreBlocks,
ganttContainerRef,
loadMoreBlocks,
})}
</div> </div>
{quickAdd ? quickAdd : null} {quickAdd ? quickAdd : null}
</div> </div>

View File

@ -6,8 +6,8 @@ export interface IGanttBlock {
width: number; width: number;
}; };
sort_order: number; sort_order: number;
start_date: Date | null; start_date: Date | undefined;
target_date: Date | null; target_date: Date | undefined;
} }
export interface IBlockUpdateData { export interface IBlockUpdateData {

View File

@ -167,7 +167,7 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType,
const { startDate } = chartData.data; const { startDate } = chartData.data;
const { start_date: itemStartDate, target_date: itemTargetDate } = itemData; const { start_date: itemStartDate, target_date: itemTargetDate } = itemData;
if (!itemStartDate || !itemTargetDate) return null; if (!itemStartDate || !itemTargetDate) return;
startDate.setHours(0, 0, 0, 0); startDate.setHours(0, 0, 0, 0);
itemStartDate.setHours(0, 0, 0, 0); itemStartDate.setHours(0, 0, 0, 0);

View File

@ -223,7 +223,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
className="group w-3/5 flex-grow" className="group w-3/5 flex-grow"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${ buttonClassName={`text-sm justify-between ${
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400" issue?.assignee_ids?.length > 0 ? "" : "text-custom-text-400"
}`} }`}
hideIcon={issue.assignee_ids?.length === 0} hideIcon={issue.assignee_ids?.length === 0}
dropdownArrow dropdownArrow

View File

@ -12,7 +12,7 @@ import { useCalendarView, useIssues, useUser } from "hooks/store";
import { useIssuesActions } from "hooks/use-issues-actions"; import { useIssuesActions } from "hooks/use-issues-actions";
// ui // ui
// types // types
import { EIssueLayoutTypes, EIssuesStoreType, IssueGroupByOptions } from "constants/issue"; import { EIssueLayoutTypes, EIssuesStoreType, EIssueGroupByToServerOptions } from "constants/issue";
import { IQuickActionProps } from "../list/list-view-types"; import { IQuickActionProps } from "../list/list-view-types";
import { handleDragDrop } from "./utils"; import { handleDragDrop } from "./utils";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
@ -68,14 +68,14 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
useSWR( useSWR(
startDate && endDate && layout ? `ISSUE_CALENDAR_LAYOUT_${storeType}_${startDate}_${endDate}_${layout}` : null, startDate && endDate && layout ? `ISSUE_CALENDAR_LAYOUT_${storeType}_${startDate}_${endDate}_${layout}` : null,
startDate && endDate startDate && endDate && layout
? () => ? () =>
fetchIssues("init-loader", { fetchIssues("init-loader", {
canGroup: true, canGroup: true,
perPageCount: layout === "month" ? 4 : 30, perPageCount: layout === "month" ? 4 : 30,
before: endDate, before: endDate,
after: startDate, after: startDate,
groupedBy: IssueGroupByOptions["target_date"], groupedBy: EIssueGroupByToServerOptions["target_date"],
}) })
: null, : null,
{ {
@ -112,9 +112,12 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
} }
}; };
const loadMoreIssues = useCallback(() => { const loadMoreIssues = useCallback(
fetchNextIssues(); (dateString: string) => {
}, [fetchNextIssues]); fetchNextIssues(dateString);
},
[fetchNextIssues]
);
return ( return (
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.CALENDAR}> <IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.CALENDAR}>
@ -142,6 +145,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
/> />
)} )}
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
getPaginationData={issues.getPaginationData}
getGroupIssueCount={issues.getGroupIssueCount}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
quickAddCallback={issues.quickAddIssue} quickAddCallback={issues.quickAddIssue}
viewId={viewId} viewId={viewId}

View File

@ -13,6 +13,7 @@ import {
TIssue, TIssue,
TIssueKanbanFilters, TIssueKanbanFilters,
TIssueMap, TIssueMap,
TPaginationData,
} from "@plane/types"; } from "@plane/types";
import { ICalendarWeek } from "./types"; import { ICalendarWeek } from "./types";
// constants // constants
@ -33,7 +34,9 @@ type Props = {
layout: "month" | "week" | undefined; layout: "month" | "week" | undefined;
showWeekends: boolean; showWeekends: boolean;
issueCalendarView: ICalendarStore; issueCalendarView: ICalendarStore;
loadMoreIssues: () => void; loadMoreIssues: (dateString: string) => void;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
@ -63,6 +66,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
quickActions, quickActions,
quickAddCallback, quickAddCallback,
addIssuesToView, addIssuesToView,
getPaginationData,
getGroupIssueCount,
viewId, viewId,
updateFilters, updateFilters,
readOnly = false, readOnly = false,
@ -108,6 +113,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount}
enableQuickIssueCreate enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
quickActions={quickActions} quickActions={quickActions}
@ -126,6 +133,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount}
enableQuickIssueCreate enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
quickActions={quickActions} quickActions={quickActions}

View File

@ -12,14 +12,16 @@ import { ICycleIssuesFilter } from "store/issue/cycle";
import { IModuleIssuesFilter } from "store/issue/module"; import { IModuleIssuesFilter } from "store/issue/module";
import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectIssuesFilter } from "store/issue/project";
import { IProjectViewIssuesFilter } from "store/issue/project-views"; import { IProjectViewIssuesFilter } from "store/issue/project-views";
import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
type Props = { type Props = {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
date: ICalendarDate; date: ICalendarDate;
issues: TIssueMap | undefined; issues: TIssueMap | undefined;
groupedIssueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
loadMoreIssues: () => void; loadMoreIssues: (dateString: string) => void;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
@ -41,6 +43,8 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
issues, issues,
groupedIssueIds, groupedIssueIds,
loadMoreIssues, loadMoreIssues,
getPaginationData,
getGroupIssueCount,
quickActions, quickActions,
enableQuickIssueCreate, enableQuickIssueCreate,
disableIssueCreation, disableIssueCreation,
@ -53,9 +57,12 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
const formattedDatePayload = renderFormattedPayloadDate(date.date); const formattedDatePayload = renderFormattedPayloadDate(date.date);
if (!formattedDatePayload) return null; if (!formattedDatePayload) return null;
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null; const issueIds = groupedIssueIds?.[formattedDatePayload];
const dayIssueCount = getGroupIssueCount(formattedDatePayload);
const nextPageResults = getPaginationData(formattedDatePayload)?.nextPageResults;
const totalIssues = issueIdList?.issueCount ?? 0; const shouldLoadMore =
nextPageResults === undefined && dayIssueCount !== undefined ? issueIds?.length < dayIssueCount : !!nextPageResults;
const isToday = date.date.toDateString() === new Date().toDateString(); const isToday = date.date.toDateString() === new Date().toDateString();
@ -101,7 +108,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
> >
<CalendarIssueBlocks <CalendarIssueBlocks
issues={issues} issues={issues}
issueIdList={issueIdList?.issueIds ?? []} issueIdList={issueIds ?? []}
quickActions={quickActions} quickActions={quickActions}
isDragDisabled={readOnly} isDragDisabled={readOnly}
/> />
@ -121,12 +128,12 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
</div> </div>
)} )}
{totalIssues > (issueIdList?.issueIds?.length ?? 0) && ( {shouldLoadMore && (
<div className="flex items-center px-2.5 py-1"> <div className="flex items-center px-2.5 py-1">
<button <button
type="button" type="button"
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300" className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
onClick={loadMoreIssues} onClick={() => loadMoreIssues(formattedDatePayload)}
> >
Load More Load More
</button> </button>

View File

@ -8,7 +8,7 @@ import { ICycleIssuesFilter } from "store/issue/cycle";
import { IModuleIssuesFilter } from "store/issue/module"; import { IModuleIssuesFilter } from "store/issue/module";
import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectIssuesFilter } from "store/issue/project";
import { IProjectViewIssuesFilter } from "store/issue/project-views"; import { IProjectViewIssuesFilter } from "store/issue/project-views";
import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
import { ICalendarDate, ICalendarWeek } from "./types"; import { ICalendarDate, ICalendarWeek } from "./types";
type Props = { type Props = {
@ -17,7 +17,9 @@ type Props = {
groupedIssueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
week: ICalendarWeek | undefined; week: ICalendarWeek | undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
loadMoreIssues: () => void; loadMoreIssues: (dateString: string) => void;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
quickAddCallback?: ( quickAddCallback?: (
@ -38,6 +40,8 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
groupedIssueIds, groupedIssueIds,
week, week,
loadMoreIssues, loadMoreIssues,
getPaginationData,
getGroupIssueCount,
quickActions, quickActions,
enableQuickIssueCreate, enableQuickIssueCreate,
disableIssueCreation, disableIssueCreation,
@ -69,6 +73,8 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount}
quickActions={quickActions} quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}

View File

@ -1,20 +1,23 @@
import React from "react"; import React, { useCallback } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart"; import { ChartDataType, GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart";
import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { renderIssueBlocksStructure } from "helpers/issue.helper"; import { getIssueBlocksStructure } from "helpers/issue.helper";
import { useIssues, useUser } from "hooks/store"; import { useIssues, useUser } from "hooks/store";
import { useIssuesActions } from "hooks/use-issues-actions"; import { useIssuesActions } from "hooks/use-issues-actions";
// components // components
// helpers // helpers
// types // types
import { TIssue, TUnGroupedIssues } from "@plane/types"; import { TIssue } from "@plane/types";
// constants // constants
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue"; import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { IssueLayoutHOC } from "../issue-layout-HOC"; import { IssueLayoutHOC } from "../issue-layout-HOC";
import useSWR from "swr";
import { getMonthChartItemPositionWidthInMonth } from "components/gantt-chart/views";
import { ALL_ISSUES } from "store/issue/helpers/base-issues.store";
type GanttStoreType = type GanttStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
@ -32,19 +35,42 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { issues, issuesFilter } = useIssues(storeType); const { issues, issuesFilter, issueMap } = useIssues(storeType);
const { updateIssue } = useIssuesActions(storeType); const { fetchIssues, fetchNextIssues, updateIssue } = useIssuesActions(storeType);
// store hooks // store hooks
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { issueMap } = useIssues();
const appliedDisplayFilters = issuesFilter.issueFilters?.displayFilters; const appliedDisplayFilters = issuesFilter.issueFilters?.displayFilters;
const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; useSWR(`ISSUE_GANTT_LAYOUT_${storeType}`, () => fetchIssues("init-loader", { canGroup: false, perPageCount: 100 }), {
revalidateOnFocus: false,
revalidateOnReconnect: false,
});
const issuesIds = (issues.groupedIssueIds?.[ALL_ISSUES] as string[]) ?? [];
const nextPageResults = issues.getPaginationData(ALL_ISSUES)?.nextPageResults;
const { enableIssueCreation } = issues?.viewFlags || {}; const { enableIssueCreation } = issues?.viewFlags || {};
const issuesArray = issueIds.map((id) => issueMap?.[id]); const loadMoreIssues = useCallback(() => {
fetchNextIssues();
}, [fetchNextIssues]);
const getBlockById = useCallback(
(id: string, currentViewData?: ChartDataType | undefined) => {
const issue = issueMap[id];
const block = getIssueBlocksStructure(issue);
if (currentViewData) {
return {
...block,
position: getMonthChartItemPositionWidthInMonth(currentViewData, block),
};
}
return block;
},
[issueMap]
);
const updateIssueBlockStructure = async (issue: TIssue, data: IBlockUpdateData) => { const updateIssueBlockStructure = async (issue: TIssue, data: IBlockUpdateData) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
@ -64,7 +90,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
border={false} border={false}
title="Issues" title="Issues"
loaderTitle="Issues" loaderTitle="Issues"
blocks={issues ? renderIssueBlocksStructure(issuesArray) : null} blockIds={issuesIds}
getBlockById={getBlockById}
blockUpdateHandler={updateIssueBlockStructure} blockUpdateHandler={updateIssueBlockStructure}
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />} blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />}
sidebarToRender={(props) => <IssueGanttSidebar {...props} showAllBlocks />} sidebarToRender={(props) => <IssueGanttSidebar {...props} showAllBlocks />}
@ -78,6 +105,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
<GanttQuickAddIssueForm quickAddCallback={issues.quickAddIssue} viewId={viewId} /> <GanttQuickAddIssueForm quickAddCallback={issues.quickAddIssue} viewId={viewId} />
) : undefined ) : undefined
} }
loadMoreBlocks={loadMoreIssues}
canLoadMoreBlocks={nextPageResults}
showAllBlocks showAllBlocks
/> />
</div> </div>

View File

@ -9,6 +9,7 @@ import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { useIssues } from "hooks/store"; import { useIssues } from "hooks/store";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { IssueLayoutEmptyState } from "./empty-states"; import { IssueLayoutEmptyState } from "./empty-states";
import { ALL_ISSUES } from "store/issue/helpers/base-issues.store";
const ActiveLoader = (props: { layout: EIssueLayoutTypes }) => { const ActiveLoader = (props: { layout: EIssueLayoutTypes }) => {
const { layout } = props; const { layout } = props;
@ -43,7 +44,7 @@ export const IssueLayoutHOC = observer((props: Props) => {
return <ActiveLoader layout={layout} />; return <ActiveLoader layout={layout} />;
} }
if (issues.issueCount === 0) { if (issues.getGroupIssueCount(ALL_ISSUES) === 0) {
return <IssueLayoutEmptyState storeType={storeType} />; return <IssueLayoutEmptyState storeType={storeType} />;
} }

View File

@ -75,27 +75,38 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
updateFilters, updateFilters,
} = useIssuesActions(storeType); } = useIssuesActions(storeType);
useSWR(`ISSUE_KANBAN_LAYOUT_${storeType}`, () => fetchIssues("init-loader", { canGroup: true, perPageCount: 30 }), {
revalidateOnFocus: false,
revalidateOnReconnect: false,
});
const fetchMoreIssues = useCallback(() => {
if (issues.loader !== "pagination") {
fetchNextIssues();
}
}, [fetchNextIssues]);
const debouncedFetchMoreIssues = debounce(() => fetchMoreIssues(), 300, { leading: true, trailing: false });
const issueIds = issues?.groupedIssueIds;
const displayFilters = issuesFilter?.issueFilters?.displayFilters; const displayFilters = issuesFilter?.issueFilters?.displayFilters;
const displayProperties = issuesFilter?.issueFilters?.displayProperties; const displayProperties = issuesFilter?.issueFilters?.displayProperties;
const sub_group_by: string | null = displayFilters?.sub_group_by || null; const sub_group_by: string | null = displayFilters?.sub_group_by || null;
const group_by: string | null = displayFilters?.group_by || null; const group_by: string | null = displayFilters?.group_by || null;
useSWR(
`ISSUE_KANBAN_LAYOUT_${storeType}_${group_by}_${sub_group_by}`,
() => fetchIssues("init-loader", { canGroup: true, perPageCount: 30 }),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
const fetchMoreIssues = useCallback(
(groupId?: string, subgroupId?: string) => {
if (issues.loader !== "pagination") {
fetchNextIssues(groupId, subgroupId);
}
},
[fetchNextIssues]
);
const debouncedFetchMoreIssues = debounce(
(groupId?: string, subgroupId?: string) => fetchMoreIssues(groupId, subgroupId),
300,
{ leading: true, trailing: false }
);
const groupedIssueIds = issues?.groupedIssueIds;
const userDisplayFilters = displayFilters || null; const userDisplayFilters = displayFilters || null;
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan; const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
@ -160,7 +171,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
sub_group_by, sub_group_by,
group_by, group_by,
issueMap, issueMap,
issueIds, groupedIssueIds,
updateIssue, updateIssue,
removeIssue removeIssue
).catch((err) => { ).catch((err) => {
@ -201,7 +212,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
sub_group_by, sub_group_by,
group_by, group_by,
issueMap, issueMap,
issueIds, groupedIssueIds,
updateIssue, updateIssue,
removeIssue removeIssue
).finally(() => { ).finally(() => {
@ -271,7 +282,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
<div className="h-max w-max"> <div className="h-max w-max">
<KanBanView <KanBanView
issuesMap={issueMap} issuesMap={issueMap}
issueIds={issueIds!} groupedIssueIds={groupedIssueIds ?? {}}
getGroupIssueCount={issues.getGroupIssueCount}
displayProperties={displayProperties} displayProperties={displayProperties}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
@ -282,6 +294,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
enableQuickIssueCreate={enableQuickAdd} enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true} showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true}
quickAddCallback={issues?.quickAddIssue} quickAddCallback={issues?.quickAddIssue}
getPaginationData={issues.getPaginationData}
viewId={viewId} viewId={viewId}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}

View File

@ -22,7 +22,9 @@ interface IssueBlockProps {
isDragDisabled: boolean; isDragDisabled: boolean;
draggableId: string; draggableId: string;
index: number; index: number;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue) => React.ReactNode; quickActions: (issue: TIssue) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
@ -33,7 +35,9 @@ interface IssueBlockProps {
interface IssueDetailsBlockProps { interface IssueDetailsBlockProps {
issue: TIssue; issue: TIssue;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue) => React.ReactNode; quickActions: (issue: TIssue) => React.ReactNode;
isReadOnly: boolean; isReadOnly: boolean;
} }

View File

@ -12,7 +12,9 @@ interface IssueBlocksListProps {
issueIds: string[]; issueIds: string[];
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
isDragDisabled: boolean; isDragDisabled: boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;

View File

@ -21,8 +21,8 @@ import {
IIssueDisplayProperties, IIssueDisplayProperties,
IIssueMap, IIssueMap,
TSubGroupedIssues, TSubGroupedIssues,
TUnGroupedIssues,
TIssueKanbanFilters, TIssueKanbanFilters,
TPaginationData,
} from "@plane/types"; } from "@plane/types";
// parent components // parent components
import { getGroupByColumns } from "../utils"; import { getGroupByColumns } from "../utils";
@ -33,17 +33,21 @@ import { KanbanStoreType } from "./base-kanban-root";
export interface IGroupByKanBan { export interface IGroupByKanBan {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
sub_group_id: string; sub_group_id: string;
isDragDisabled: boolean; isDragDisabled: boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: any; handleKanbanFilters: any;
loadMoreIssues: (() => void) | undefined; loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
@ -64,7 +68,9 @@ export interface IGroupByKanBan {
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => { const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const { const {
issuesMap, issuesMap,
issueIds, groupedIssueIds,
getGroupIssueCount,
getPaginationData,
displayProperties, displayProperties,
sub_group_by, sub_group_by,
group_by, group_by,
@ -107,7 +113,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
if (!list) return null; if (!list) return null;
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.issueCount > 0); const groupWithIssues = list.filter((_list) => (getGroupIssueCount(_list.id) ?? 0) > 0);
const groupList = showEmptyGroup ? list : groupWithIssues; const groupList = showEmptyGroup ? list : groupWithIssues;
@ -143,7 +149,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
column_id={_list.id} column_id={_list.id}
icon={_list.icon} icon={_list.icon}
title={_list.name} title={_list.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.issueCount || 0} count={getGroupIssueCount(_list.id) ?? 0}
issuePayload={_list.payload} issuePayload={_list.payload}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy} disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
storeType={storeType} storeType={storeType}
@ -158,7 +164,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
<KanbanGroup <KanbanGroup
groupId={_list.id} groupId={_list.id}
issuesMap={issuesMap} issuesMap={issuesMap}
issueIds={issueIds} groupedIssueIds={groupedIssueIds}
getGroupIssueCount={getGroupIssueCount}
getPaginationData={getPaginationData}
peekIssueId={peekIssue?.issueId ?? ""} peekIssueId={peekIssue?.issueId ?? ""}
displayProperties={displayProperties} displayProperties={displayProperties}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
@ -187,16 +195,20 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
export interface IKanBan { export interface IKanBan {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues; groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
sub_group_id?: string; sub_group_id?: string;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
loadMoreIssues: (() => void) | undefined; loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
showEmptyGroup: boolean; showEmptyGroup: boolean;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: ( quickAddCallback?: (
@ -217,7 +229,9 @@ export interface IKanBan {
export const KanBan: React.FC<IKanBan> = observer((props) => { export const KanBan: React.FC<IKanBan> = observer((props) => {
const { const {
issuesMap, issuesMap,
issueIds, groupedIssueIds,
getGroupIssueCount,
getPaginationData,
displayProperties, displayProperties,
sub_group_by, sub_group_by,
group_by, group_by,
@ -244,7 +258,9 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
return ( return (
<GroupByKanBan <GroupByKanBan
issuesMap={issuesMap} issuesMap={issuesMap}
issueIds={issueIds} groupedIssueIds={groupedIssueIds}
getGroupIssueCount={getGroupIssueCount}
getPaginationData={getPaginationData}
displayProperties={displayProperties} displayProperties={displayProperties}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}

View File

@ -10,23 +10,28 @@ import {
IIssueDisplayProperties, IIssueDisplayProperties,
IIssueMap, IIssueMap,
TSubGroupedIssues, TSubGroupedIssues,
TUnGroupedIssues, TPaginationData,
} from "@plane/types"; } from "@plane/types";
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
import { KanbanIssueBlockLoader } from "components/ui/loader"; import { KanbanIssueBlockLoader } from "components/ui/loader";
import { useIntersectionObserver } from "hooks/use-intersection-observer"; import { useIntersectionObserver } from "hooks/use-intersection-observer";
import { observer } from "mobx-react";
interface IKanbanGroup { interface IKanbanGroup {
groupId: string; groupId: string;
issuesMap: IIssueMap; issuesMap: IIssueMap;
peekIssueId?: string; peekIssueId?: string;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
sub_group_id: string; sub_group_id: string;
isDragDisabled: boolean; isDragDisabled: boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: ( quickAddCallback?: (
@ -35,7 +40,7 @@ interface IKanbanGroup {
data: TIssue, data: TIssue,
viewId?: string viewId?: string
) => Promise<TIssue | undefined>; ) => Promise<TIssue | undefined>;
loadMoreIssues: (() => void) | undefined; loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
viewId?: string; viewId?: string;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
@ -44,7 +49,7 @@ interface IKanbanGroup {
isDragStarted?: boolean; isDragStarted?: boolean;
} }
export const KanbanGroup = (props: IKanbanGroup) => { export const KanbanGroup = observer((props: IKanbanGroup) => {
const { const {
groupId, groupId,
sub_group_id, sub_group_id,
@ -52,7 +57,9 @@ export const KanbanGroup = (props: IKanbanGroup) => {
sub_group_by, sub_group_by,
issuesMap, issuesMap,
displayProperties, displayProperties,
issueIds, groupedIssueIds,
getGroupIssueCount,
getPaginationData,
peekIssueId, peekIssueId,
isDragDisabled, isDragDisabled,
updateIssue, updateIssue,
@ -125,6 +132,23 @@ export const KanbanGroup = (props: IKanbanGroup) => {
return preloadedData; return preloadedData;
}; };
const isSubGroup = !!sub_group_id && sub_group_id !== "null";
const issueIds = isSubGroup
? (groupedIssueIds as TSubGroupedIssues)[groupId][sub_group_id]
: (groupedIssueIds as TGroupedIssues)[groupId];
const groupIssueCount = isSubGroup ? getGroupIssueCount(sub_group_id) : getGroupIssueCount(groupId);
const nextPageResults = isSubGroup
? getPaginationData(sub_group_id)?.nextPageResults
: getPaginationData(groupId)?.nextPageResults;
const shouldLoadMore =
nextPageResults === undefined && groupIssueCount !== undefined
? issueIds?.length < groupIssueCount
: !!nextPageResults;
return ( return (
<div className={`relative w-full h-full transition-all`}> <div className={`relative w-full h-full transition-all`}>
<Droppable droppableId={`${groupId}__${sub_group_id}`}> <Droppable droppableId={`${groupId}__${sub_group_id}`}>
@ -139,7 +163,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
columnId={groupId} columnId={groupId}
issuesMap={issuesMap} issuesMap={issuesMap}
peekIssueId={peekIssueId} peekIssueId={peekIssueId}
issueIds={(issueIds as TGroupedIssues)?.[groupId]?.issueIds || []} issueIds={issueIds}
displayProperties={displayProperties} displayProperties={displayProperties}
isDragDisabled={isDragDisabled} isDragDisabled={isDragDisabled}
updateIssue={updateIssue} updateIssue={updateIssue}
@ -151,7 +175,17 @@ export const KanbanGroup = (props: IKanbanGroup) => {
{provided.placeholder} {provided.placeholder}
{loadMoreIssues && <KanbanIssueBlockLoader ref={intersectionRef} />} {shouldLoadMore && isSubGroup ? (
<div
className="w-full sticky bottom-0 p-3 text-sm text-custom-primary-100 hover:underline cursor-pointer"
onClick={() => loadMoreIssues(groupId, sub_group_id)}
>
{" "}
Load more &darr;
</div>
) : (
<KanbanIssueBlockLoader ref={intersectionRef} />
)}
{enableQuickIssueCreate && !disableIssueCreation && ( {enableQuickIssueCreate && !disableIssueCreation && (
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0"> <div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
@ -172,4 +206,4 @@ export const KanbanGroup = (props: IKanbanGroup) => {
</Droppable> </Droppable>
</div> </div>
); );
}; });

View File

@ -10,8 +10,9 @@ import {
IIssueDisplayProperties, IIssueDisplayProperties,
IIssueMap, IIssueMap,
TSubGroupedIssues, TSubGroupedIssues,
TUnGroupedIssues,
TIssueKanbanFilters, TIssueKanbanFilters,
TGroupedIssueCount,
TPaginationData,
} from "@plane/types"; } from "@plane/types";
import { getGroupByColumns } from "../utils"; import { getGroupByColumns } from "../utils";
import { KanBan } from "./default"; import { KanBan } from "./default";
@ -22,7 +23,7 @@ import { KanbanStoreType } from "./base-kanban-root";
// constants // constants
interface ISubGroupSwimlaneHeader { interface ISubGroupSwimlaneHeader {
issueIds: TGroupedIssues | TSubGroupedIssues; getGroupIssueCount: (groupId: string | undefined) => number | undefined;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
list: IGroupByColumn[]; list: IGroupByColumn[];
@ -31,16 +32,8 @@ interface ISubGroupSwimlaneHeader {
storeType: KanbanStoreType; storeType: KanbanStoreType;
} }
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
let headerCount = 0;
Object.keys(issueIds).map((groupState) => {
headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.issueCount || 0);
});
return headerCount;
};
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issueIds, getGroupIssueCount,
sub_group_by, sub_group_by,
group_by, group_by,
storeType, storeType,
@ -59,7 +52,7 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
column_id={_list.id} column_id={_list.id}
icon={_list.icon} icon={_list.icon}
title={_list.name} title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)} count={getGroupIssueCount(_list?.id) ?? 0}
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload} issuePayload={_list.payload}
@ -72,10 +65,14 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
showEmptyGroup: boolean; showEmptyGroup: boolean;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
@ -93,12 +90,14 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
) => Promise<TIssue | undefined>; ) => Promise<TIssue | undefined>;
viewId?: string; viewId?: string;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
loadMoreIssues: (() => void) | undefined; loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
} }
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => { const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const { const {
issuesMap, issuesMap,
issueIds, groupedIssueIds,
getGroupIssueCount,
getPaginationData,
sub_group_by, sub_group_by,
group_by, group_by,
list, list,
@ -119,22 +118,12 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
isDragStarted, isDragStarted,
} = props; } = props;
const calculateIssueCount = (column_id: string) => {
let issueCount = 0;
const subGroupedIds = issueIds as TSubGroupedIssues;
subGroupedIds?.[column_id] &&
Object.keys(subGroupedIds?.[column_id])?.forEach((_list: any) => {
issueCount += subGroupedIds?.[column_id]?.[_list]?.issueCount || 0;
});
return issueCount;
};
return ( return (
<div className="relative h-max min-h-full w-full"> <div className="relative h-max min-h-full w-full">
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: any, index: number) => { list.map((_list: any) => {
const isLastSubGroup = index === list.length - 1; const issueCount = getGroupIssueCount(_list.id) ?? 0;
return ( return (
<div key={_list.id} className="flex flex-shrink-0 flex-col"> <div key={_list.id} className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1"> <div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
@ -143,7 +132,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
column_id={_list.id} column_id={_list.id}
icon={_list.Icon} icon={_list.Icon}
title={_list.name || ""} title={_list.name || ""}
count={calculateIssueCount(_list.id)} count={issueCount}
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}
/> />
@ -155,7 +144,9 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
<div className="relative"> <div className="relative">
<KanBan <KanBan
issuesMap={issuesMap} issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]} groupedIssueIds={groupedIssueIds}
getGroupIssueCount={getGroupIssueCount}
getPaginationData={getPaginationData}
displayProperties={displayProperties} displayProperties={displayProperties}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
@ -173,7 +164,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
storeType={storeType} storeType={storeType}
loadMoreIssues={isLastSubGroup ? loadMoreIssues : undefined} loadMoreIssues={loadMoreIssues}
/> />
</div> </div>
)} )}
@ -186,15 +177,19 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
export interface IKanBanSwimLanes { export interface IKanBanSwimLanes {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues; groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
loadMoreIssues: (() => void) | undefined; loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
showEmptyGroup: boolean; showEmptyGroup: boolean;
isDragStarted?: boolean; isDragStarted?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
@ -215,7 +210,9 @@ export interface IKanBanSwimLanes {
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => { export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
const { const {
issuesMap, issuesMap,
issueIds, groupedIssueIds,
getGroupIssueCount,
getPaginationData,
displayProperties, displayProperties,
sub_group_by, sub_group_by,
group_by, group_by,
@ -268,7 +265,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
<div className="relative"> <div className="relative">
<div className="sticky top-0 z-[2] h-[50px] bg-custom-background-90"> <div className="sticky top-0 z-[2] h-[50px] bg-custom-background-90">
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issueIds={issueIds} getGroupIssueCount={getGroupIssueCount}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}
@ -282,7 +279,9 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
<SubGroupSwimlane <SubGroupSwimlane
issuesMap={issuesMap} issuesMap={issuesMap}
list={subGroupByList} list={subGroupByList}
issueIds={issueIds} groupedIssueIds={groupedIssueIds}
getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount}
displayProperties={displayProperties} displayProperties={displayProperties}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}

View File

@ -1,5 +1,5 @@
import { DraggableLocation } from "@hello-pangea/dnd"; import { DraggableLocation } from "@hello-pangea/dnd";
import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues, TIssue } from "@plane/types"; import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TIssue } from "@plane/types";
const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => { const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => {
const sortOrderDefaultValue = 65535; const sortOrderDefaultValue = 65535;
@ -44,7 +44,7 @@ export const handleDragDrop = async (
subGroupBy: string | null, subGroupBy: string | null,
groupBy: string | null, groupBy: string | null,
issueMap: IIssueMap, issueMap: IIssueMap,
issueWithIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined, issueWithIds: TGroupedIssues | TSubGroupedIssues | undefined,
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined, updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined,
removeIssue: (projectId: string, issueId: string) => Promise<void> | undefined removeIssue: (projectId: string, issueId: string) => Promise<void> | undefined
) => { ) => {

View File

@ -5,13 +5,14 @@ import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { useIssues, useUser } from "hooks/store"; import { useIssues, useUser } from "hooks/store";
import { TGroupedIssues, TIssue, TUnGroupedIssues } from "@plane/types"; import { TGroupedIssues, TIssue } from "@plane/types";
// components // components
import { List } from "./default"; import { List } from "./default";
import { IQuickActionProps } from "./list-view-types"; import { IQuickActionProps } from "./list-view-types";
import { useIssuesActions } from "hooks/use-issues-actions"; import { useIssuesActions } from "hooks/use-issues-actions";
import { IssueLayoutHOC } from "../issue-layout-HOC"; import { IssueLayoutHOC } from "../issue-layout-HOC";
import useSWR from "swr"; import useSWR from "swr";
import { ALL_ISSUES } from "store/issue/helpers/base-issues.store";
// constants // constants
// hooks // hooks
@ -51,14 +52,24 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
const { issueMap } = useIssues(); const { issueMap } = useIssues();
useSWR(`ISSUE_LIST_LAYOUT_${storeType}`, () => fetchIssues("init-loader", { canGroup: true, perPageCount: 100 }), { const displayFilters = issuesFilter?.issueFilters?.displayFilters;
revalidateOnFocus: false, const displayProperties = issuesFilter?.issueFilters?.displayProperties;
revalidateOnReconnect: false,
}); const group_by = displayFilters?.group_by || null;
const showEmptyGroup = displayFilters?.show_empty_groups ?? false;
useSWR(
`ISSUE_LIST_LAYOUT_${storeType}_${group_by}`,
() => fetchIssues("init-loader", { canGroup: true, perPageCount: 50 }),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const issueIds = issues?.groupedIssueIds as TGroupedIssues | undefined; const groupedIssueIds = issues?.groupedIssueIds as TGroupedIssues | undefined;
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
const canEditProperties = useCallback( const canEditProperties = useCallback(
@ -71,12 +82,6 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
); );
const displayFilters = issuesFilter?.issueFilters?.displayFilters;
const displayProperties = issuesFilter?.issueFilters?.displayProperties;
const group_by = displayFilters?.group_by || null;
const showEmptyGroup = displayFilters?.show_empty_groups ?? false;
const renderQuickActions = useCallback( const renderQuickActions = useCallback(
(issue: TIssue) => ( (issue: TIssue) => (
<QuickActions <QuickActions
@ -93,9 +98,12 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
); );
const loadMoreIssues = useCallback(() => { const loadMoreIssues = useCallback(
fetchNextIssues(); (groupId?: string) => {
}, [fetchNextIssues]); fetchNextIssues(groupId);
},
[fetchNextIssues]
);
return ( return (
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.LIST}> <IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.LIST}>
@ -106,11 +114,12 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
group_by={group_by} group_by={group_by}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={renderQuickActions} quickActions={renderQuickActions}
issueIds={issueIds!} groupedIssueIds={groupedIssueIds ?? {}}
shouldLoadMore={issues.next_page_results}
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
viewId={viewId} viewId={viewId}
getPaginationData={issues.getPaginationData}
getGroupIssueCount={issues.getGroupIssueCount}
quickAddCallback={issues?.quickAddIssue} quickAddCallback={issues?.quickAddIssue}
enableIssueQuickAdd={!!enableQuickAdd} enableIssueQuickAdd={!!enableQuickAdd}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}

View File

@ -13,7 +13,9 @@ import { IssueProperties } from "../properties/all-properties";
interface IssueBlockProps { interface IssueBlockProps {
issueId: string; issueId: string;
issuesMap: TIssueMap; issuesMap: TIssueMap;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue) => React.ReactNode; quickActions: (issue: TIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;

View File

@ -3,19 +3,22 @@ import { FC, MutableRefObject } from "react";
import RenderIfVisible from "components/core/render-if-visible-HOC"; import RenderIfVisible from "components/core/render-if-visible-HOC";
import { IssueBlock } from "components/issues"; import { IssueBlock } from "components/issues";
// types // types
import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
import { observer } from "mobx-react";
interface Props { interface Props {
issueIds: TGroupedIssues | TUnGroupedIssues | any; issueIds: TGroupedIssues | any;
issuesMap: TIssueMap; issuesMap: TIssueMap;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue) => React.ReactNode; quickActions: (issue: TIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
containerRef: MutableRefObject<HTMLDivElement | null>; containerRef: MutableRefObject<HTMLDivElement | null>;
} }
export const IssueBlocksList: FC<Props> = (props) => { export const IssueBlocksList: FC<Props> = observer((props) => {
const { issueIds, issuesMap, updateIssue, quickActions, displayProperties, canEditProperties, containerRef } = props; const { issueIds, issuesMap, updateIssue, quickActions, displayProperties, canEditProperties, containerRef } = props;
return ( return (
@ -47,4 +50,4 @@ export const IssueBlocksList: FC<Props> = (props) => {
)} )}
</div> </div>
); );
}; });

View File

@ -1,4 +1,4 @@
import { useRef } from "react"; import { useCallback, useRef } from "react";
// components // components
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
// hooks // hooks
@ -11,26 +11,31 @@ import {
TIssue, TIssue,
IIssueDisplayProperties, IIssueDisplayProperties,
TIssueMap, TIssueMap,
TUnGroupedIssues,
IGroupByColumn, IGroupByColumn,
TIssueGroup, TPaginationData,
} from "@plane/types"; } from "@plane/types";
import { getGroupByColumns } from "../utils"; import { getGroupByColumns } from "../utils";
import { HeaderGroupByCard } from "./headers/group-by-card"; import { HeaderGroupByCard } from "./headers/group-by-card";
import { EIssuesStoreType } from "constants/issue"; import { EIssuesStoreType } from "constants/issue";
import { ArrowDown } from "lucide-react";
import { ListLoaderItemRow } from "components/ui"; import { ListLoaderItemRow } from "components/ui";
import { useIntersectionObserver } from "hooks/use-intersection-observer"; import { useIntersectionObserver } from "hooks/use-intersection-observer";
import { ALL_ISSUES } from "store/issue/helpers/base-issues.store";
import { observer } from "mobx-react";
import isNil from "lodash/isNil";
export interface IGroupByList { export interface IGroupByList {
issueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
issuesMap: TIssueMap; issuesMap: TIssueMap;
group_by: string | null; group_by: string | null;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue) => React.ReactNode; quickActions: (issue: TIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
enableIssueQuickAdd: boolean; enableIssueQuickAdd: boolean;
showEmptyGroup?: boolean; showEmptyGroup?: boolean;
getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
@ -43,13 +48,12 @@ export interface IGroupByList {
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
viewId?: string; viewId?: string;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
shouldLoadMore: boolean; loadMoreIssues: (groupId?: string) => void;
loadMoreIssues: () => void;
} }
const GroupByList: React.FC<IGroupByList> = (props) => { const GroupByList: React.FC<IGroupByList> = observer((props) => {
const { const {
issueIds, groupedIssueIds,
issuesMap, issuesMap,
group_by, group_by,
updateIssue, updateIssue,
@ -63,7 +67,8 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
disableIssueCreation, disableIssueCreation,
storeType, storeType,
addIssuesToView, addIssuesToView,
shouldLoadMore, getPaginationData,
getGroupIssueCount,
isCompletedCycle = false, isCompletedCycle = false,
loadMoreIssues, loadMoreIssues,
} = props; } = props;
@ -123,9 +128,8 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
return preloadedData; return preloadedData;
}; };
const validateEmptyIssueGroups = (issues: TIssueGroup) => { const validateEmptyIssueGroups = (issueCount: number = 0) => {
const issuesCount = issues?.issueCount || 0; if (!showEmptyGroup && issueCount <= 0) return false;
if (!showEmptyGroup && issuesCount <= 0) return false;
return true; return true;
}; };
@ -139,17 +143,25 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
{groups && {groups &&
groups.length > 0 && groups.length > 0 &&
groups.map((_list: IGroupByColumn) => { groups.map((_list: IGroupByColumn) => {
const issueGroup = issueIds?.[_list.id] as TIssueGroup; const groupIssueIds = groupedIssueIds?.[_list.id];
const groupIssueCount = getGroupIssueCount(_list.id);
const nextPageResults = getPaginationData(_list.id)?.nextPageResults;
const shouldLoadMore =
nextPageResults === undefined && groupIssueCount !== undefined
? groupIssueIds?.length < groupIssueCount
: !!nextPageResults;
return ( return (
issueGroup && groupIssueIds &&
validateEmptyIssueGroups(issueGroup) && ( !isNil(groupIssueCount) &&
validateEmptyIssueGroups(groupIssueCount) && (
<div key={_list.id} className={`flex flex-shrink-0 flex-col`}> <div key={_list.id} className={`flex flex-shrink-0 flex-col`}>
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1"> <div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
<HeaderGroupByCard <HeaderGroupByCard
icon={_list.icon} icon={_list.icon}
title={_list.name || ""} title={_list.name || ""}
count={issueGroup.issueCount} count={groupIssueCount}
issuePayload={_list.payload} issuePayload={_list.payload}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle} disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle}
storeType={storeType} storeType={storeType}
@ -157,9 +169,9 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
/> />
</div> </div>
{issueIds && ( {groupedIssueIds && (
<IssueBlocksList <IssueBlocksList
issueIds={issueGroup.issueIds} issueIds={groupIssueIds}
issuesMap={issuesMap} issuesMap={issuesMap}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={quickActions} quickActions={quickActions}
@ -168,15 +180,13 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
containerRef={containerRef} containerRef={containerRef}
/> />
)} )}
{/* &&
issueGroup.issueIds?.length <= issueGroup.issueCount */}
{shouldLoadMore && {shouldLoadMore &&
(group_by ? ( (group_by ? (
<div <div
className={ className={
"h-11 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm text-custom-primary-100 hover:underline cursor-pointer" "h-11 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm text-custom-primary-100 hover:underline cursor-pointer"
} }
onClick={loadMoreIssues} onClick={() => loadMoreIssues(_list.id)}
> >
Load more &darr; Load more &darr;
</div> </div>
@ -199,19 +209,20 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
})} })}
</div> </div>
); );
}; });
export interface IList { export interface IList {
issueIds: TGroupedIssues | TUnGroupedIssues; groupedIssueIds: TGroupedIssues;
issuesMap: TIssueMap; issuesMap: TIssueMap;
group_by: string | null; group_by: string | null;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | null | undefined, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
quickActions: (issue: TIssue) => React.ReactNode; quickActions: (issue: TIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
showEmptyGroup: boolean; showEmptyGroup: boolean;
enableIssueQuickAdd: boolean; enableIssueQuickAdd: boolean;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
shouldLoadMore: boolean;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -222,13 +233,15 @@ export interface IList {
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
storeType: EIssuesStoreType; storeType: EIssuesStoreType;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
loadMoreIssues: () => void; getPaginationData: (groupId: string | undefined) => TPaginationData | undefined;
getGroupIssueCount: (groupId: string | undefined) => number | undefined;
loadMoreIssues: (groupId?: string) => void;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
} }
export const List: React.FC<IList> = (props) => { export const List: React.FC<IList> = (props) => {
const { const {
issueIds, groupedIssueIds,
issuesMap, issuesMap,
group_by, group_by,
updateIssue, updateIssue,
@ -239,10 +252,11 @@ export const List: React.FC<IList> = (props) => {
showEmptyGroup, showEmptyGroup,
enableIssueQuickAdd, enableIssueQuickAdd,
canEditProperties, canEditProperties,
getPaginationData,
getGroupIssueCount,
disableIssueCreation, disableIssueCreation,
storeType, storeType,
addIssuesToView, addIssuesToView,
shouldLoadMore,
loadMoreIssues, loadMoreIssues,
isCompletedCycle = false, isCompletedCycle = false,
} = props; } = props;
@ -250,10 +264,9 @@ export const List: React.FC<IList> = (props) => {
return ( return (
<div className="relative h-full w-full"> <div className="relative h-full w-full">
<GroupByList <GroupByList
issueIds={issueIds} groupedIssueIds={groupedIssueIds}
issuesMap={issuesMap} issuesMap={issuesMap}
group_by={group_by} group_by={group_by}
shouldLoadMore={shouldLoadMore}
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={quickActions} quickActions={quickActions}
@ -262,6 +275,8 @@ export const List: React.FC<IList> = (props) => {
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
getPaginationData={getPaginationData}
getGroupIssueCount={getGroupIssueCount}
viewId={viewId} viewId={viewId}
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}
storeType={storeType} storeType={storeType}

View File

@ -17,8 +17,15 @@ import { SpreadsheetLayoutLoader } from "components/ui";
import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { TIssue, IIssueDisplayFilterOptions } from "@plane/types";
// constants // constants
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "constants/issue";
import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state";
import { ALL_ISSUES } from "store/issue/helpers/base-issues.store";
import { IssueLayoutHOC } from "../issue-layout-HOC";
export const AllIssueLayoutRoot: React.FC = observer(() => { export const AllIssueLayoutRoot: React.FC = observer(() => {
// router // router
@ -30,7 +37,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const { const {
issuesFilter: { filters, fetchFilters, updateFilters }, issuesFilter: { filters, fetchFilters, updateFilters },
issues: { loader, issueCount: totalIssueCount, groupedIssueIds, fetchIssues, fetchNextIssues }, issues: { loader, getPaginationData, groupedIssueIds, fetchIssues, fetchNextIssues },
} = useIssues(EIssuesStoreType.GLOBAL); } = useIssues(EIssuesStoreType.GLOBAL);
const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL);
@ -153,53 +160,28 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
return <SpreadsheetLayoutLoader />; return <SpreadsheetLayoutLoader />;
} }
const { const issueIds = groupedIssueIds[ALL_ISSUES];
"All Issues": { issueIds, issueCount }, const nextPageResults = getPaginationData(ALL_ISSUES)?.nextPageResults;
} = groupedIssueIds;
const emptyStateType = const emptyStateType =
(workspaceProjectIds ?? []).length > 0 ? `workspace-${globalViewId}` : EmptyStateType.WORKSPACE_NO_PROJECTS; (workspaceProjectIds ?? []).length > 0 ? `workspace-${globalViewId}` : EmptyStateType.WORKSPACE_NO_PROJECTS;
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <IssueLayoutHOC storeType={EIssuesStoreType.GLOBAL} layout={EIssueLayoutTypes.SPREADSHEET}>
<div className="relative h-full w-full flex flex-col"> <SpreadsheetView
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} /> displayProperties={issueFilters?.displayProperties ?? {}}
{!totalIssueCount ? ( displayFilters={issueFilters?.displayFilters ?? {}}
<EmptyState handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} issueIds={Array.isArray(issueIds) ? issueIds : []}
size="sm" quickActions={renderQuickActions}
primaryButtonOnClick={ updateIssue={updateIssue}
(workspaceProjectIds ?? []).length > 0 canEditProperties={canEditProperties}
? globalViewId !== "custom-view" && globalViewId !== "subscribed" viewId={globalViewId.toString()}
? () => { canLoadMoreIssues={!!nextPageResults}
setTrackElement("All issues empty state"); loadMoreIssues={fetchNextPages}
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); />
} {/* peek overview */}
: undefined <IssuePeekOverview />
: () => { </IssueLayoutHOC>
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateProjectModal(true);
}
}
/>
) : (
<Fragment>
<SpreadsheetView
displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={Array.isArray(issueIds) ? issueIds : []}
quickActions={renderQuickActions}
updateIssue={updateIssue}
canEditProperties={canEditProperties}
viewId={globalViewId.toString()}
onEndOfListTrigger={fetchNextPages}
/>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
</div>
</div>
); );
}); });

View File

@ -9,11 +9,12 @@ import { useIssuesActions } from "hooks/use-issues-actions";
// views // views
// types // types
// constants // constants
import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; import { TIssue, IIssueDisplayFilterOptions } from "@plane/types";
import { IQuickActionProps } from "../list/list-view-types"; import { IQuickActionProps } from "../list/list-view-types";
import { SpreadsheetView } from "./spreadsheet-view"; import { SpreadsheetView } from "./spreadsheet-view";
import useSWR from "swr"; import useSWR from "swr";
import { IssueLayoutHOC } from "../issue-layout-HOC"; import { IssueLayoutHOC } from "../issue-layout-HOC";
import { ALL_ISSUES } from "store/issue/helpers/base-issues.store";
export type SpreadsheetStoreType = export type SpreadsheetStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
@ -72,7 +73,8 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
); );
const issueIds = issues.groupedIssueIds?.["All Issues"]?.issueIds ?? []; const issueIds = issues.groupedIssueIds?.[ALL_ISSUES] ?? [];
const nextPageResults = issues.getPaginationData(ALL_ISSUES)?.nextPageResults;
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
@ -118,7 +120,8 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
viewId={viewId} viewId={viewId}
enableQuickCreateIssue={enableQuickAdd} enableQuickCreateIssue={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
onEndOfListTrigger={fetchNextIssues} canLoadMoreIssues={!!nextPageResults}
loadMoreIssues={fetchNextIssues}
/> />
</IssueLayoutHOC> </IssueLayoutHOC>
); );

View File

@ -25,7 +25,8 @@ type Props = {
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
portalElement: React.MutableRefObject<HTMLDivElement | null>; portalElement: React.MutableRefObject<HTMLDivElement | null>;
containerRef: MutableRefObject<HTMLTableElement | null>; containerRef: MutableRefObject<HTMLTableElement | null>;
onEndOfListTrigger: () => void; canLoadMoreIssues: boolean;
loadMoreIssues: () => void;
}; };
export const SpreadsheetTable = observer((props: Props) => { export const SpreadsheetTable = observer((props: Props) => {
@ -39,8 +40,9 @@ export const SpreadsheetTable = observer((props: Props) => {
quickActions, quickActions,
updateIssue, updateIssue,
canEditProperties, canEditProperties,
canLoadMoreIssues,
containerRef, containerRef,
onEndOfListTrigger, loadMoreIssues,
} = props; } = props;
// states // states
@ -80,27 +82,7 @@ export const SpreadsheetTable = observer((props: Props) => {
}; };
}, [handleScroll, containerRef]); }, [handleScroll, containerRef]);
// useEffect(() => { useIntersectionObserver(containerRef, intersectionRef, loadMoreIssues, `50% 0% 50% 0%`);
// if (intersectionRef.current) {
// const observer = new IntersectionObserver(
// (entries) => {
// if (entries[0].isIntersecting) onEndOfListTrigger();
// },
// {
// root: containerRef?.current,
// rootMargin: `50% 0% 50% 0%`,
// }
// );
// observer.observe(intersectionRef.current);
// return () => {
// if (intersectionRef.current) {
// // eslint-disable-next-line react-hooks/exhaustive-deps
// observer.unobserve(intersectionRef.current);
// }
// };
// }
// }, [intersectionRef, containerRef]);
useIntersectionObserver(containerRef, intersectionRef, onEndOfListTrigger, `50% 0% 50% 0%`);
const handleKeyBoardNavigation = useTableKeyboardNavigation(); const handleKeyBoardNavigation = useTableKeyboardNavigation();
@ -130,7 +112,7 @@ export const SpreadsheetTable = observer((props: Props) => {
/> />
))} ))}
</tbody> </tbody>
<tfoot ref={intersectionRef}>Loading...</tfoot> {canLoadMoreIssues && <tfoot ref={intersectionRef}>Loading...</tfoot>}
</table> </table>
); );
}); });

View File

@ -31,7 +31,8 @@ type Props = {
) => Promise<TIssue | undefined>; ) => Promise<TIssue | undefined>;
viewId?: string; viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
onEndOfListTrigger: () => void; canLoadMoreIssues: boolean;
loadMoreIssues: () => void;
enableQuickCreateIssue?: boolean; enableQuickCreateIssue?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
}; };
@ -49,7 +50,8 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
canEditProperties, canEditProperties,
enableQuickCreateIssue, enableQuickCreateIssue,
disableIssueCreation, disableIssueCreation,
onEndOfListTrigger, canLoadMoreIssues,
loadMoreIssues,
} = props; } = props;
// refs // refs
const containerRef = useRef<HTMLTableElement | null>(null); const containerRef = useRef<HTMLTableElement | null>(null);
@ -81,7 +83,8 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
updateIssue={updateIssue} updateIssue={updateIssue}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
containerRef={containerRef} containerRef={containerRef}
onEndOfListTrigger={onEndOfListTrigger} canLoadMoreIssues={canLoadMoreIssues}
loadMoreIssues={loadMoreIssues}
/> />
</div> </div>
<div className="border-t border-custom-border-100"> <div className="border-t border-custom-border-100">

View File

@ -2,11 +2,13 @@ import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx store // mobx store
// components // components
import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "components/gantt-chart"; import { ChartDataType, GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "components/gantt-chart";
import { ModuleGanttBlock } from "components/modules"; import { ModuleGanttBlock } from "components/modules";
import { useModule, useProject } from "hooks/store"; import { useModule, useProject } from "hooks/store";
// types // types
import { IModule } from "@plane/types"; import { IModule } from "@plane/types";
import { useCallback } from "react";
import { getMonthChartItemPositionWidthInMonth } from "components/gantt-chart/views";
export const ModulesListGanttChartView: React.FC = observer(() => { export const ModulesListGanttChartView: React.FC = observer(() => {
// router // router
@ -14,7 +16,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectModuleIds, moduleMap, updateModuleDetails } = useModule(); const { projectModuleIds, getModuleById, updateModuleDetails } = useModule();
const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => { const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => {
if (!workspaceSlug || !module) return; if (!workspaceSlug || !module) return;
@ -25,26 +27,39 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
await updateModuleDetails(workspaceSlug.toString(), module.project_id, module.id, payload); await updateModuleDetails(workspaceSlug.toString(), module.project_id, module.id, payload);
}; };
const blockFormat = (blocks: string[]) => const getBlockById = useCallback(
blocks?.map((blockId) => { (id: string, currentViewData?: ChartDataType | undefined) => {
const block = moduleMap[blockId]; const module = getModuleById(id);
return {
data: block, const block = {
id: block.id, data: module,
sort_order: block.sort_order, id: module?.id ?? "",
start_date: block.start_date ? new Date(block.start_date) : null, sort_order: module?.sort_order ?? 0,
target_date: block.target_date ? new Date(block.target_date) : null, start_date: module?.start_date ? new Date(module.start_date) : undefined,
target_date: module?.target_date ? new Date(module.target_date) : undefined,
}; };
}); if (currentViewData) {
return {
...block,
position: getMonthChartItemPositionWidthInMonth(currentViewData, block),
};
}
return block;
},
[getModuleById]
);
const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15; const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15;
if (!projectModuleIds) return null;
return ( return (
<div className="h-full w-full overflow-y-auto"> <div className="h-full w-full overflow-y-auto">
<GanttChartRoot <GanttChartRoot
title="Modules" title="Modules"
loaderTitle="Modules" loaderTitle="Modules"
blocks={projectModuleIds ? blockFormat(projectModuleIds) : null} blockIds={projectModuleIds}
getBlockById={getBlockById}
sidebarToRender={(props) => <ModuleGanttSidebar {...props} />} sidebarToRender={(props) => <ModuleGanttSidebar {...props} />}
blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)} blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)}
blockToRender={(data: IModule) => <ModuleGanttBlock moduleId={data.id} />} blockToRender={(data: IModule) => <ModuleGanttBlock moduleId={data.id} />}

View File

@ -436,7 +436,7 @@ export const groupReactionEmojis = (reactions: any) => {
}; };
export enum IssueGroupByOptions { export enum EIssueGroupByToServerOptions {
"state" = "state_id", "state" = "state_id",
"priority" = "priority", "priority" = "priority",
"labels" = "labels__id", "labels" = "labels__id",
@ -448,3 +448,16 @@ export enum IssueGroupByOptions {
"project" = "project_id", "project" = "project_id",
"created_by" = "created_by", "created_by" = "created_by",
} }
export enum EServerGroupByToFilterOptions {
"state_id" = "state",
"priority" = "priority",
"labels__id" = "labels",
"state__group" = "state_group",
"assignees__id" = "assignees",
"cycle_id" = "cycle",
"modules__id" = "module",
"target_date" = "target_date",
"project_id" = "project",
"created_by" = "created_by",
}

View File

@ -156,14 +156,15 @@ export const shouldHighlightIssueDueDate = (
// if the issue is overdue, highlight the due date // if the issue is overdue, highlight the due date
return targetDateDistance <= 0; return targetDateDistance <= 0;
}; };
export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => export const getIssueBlocksStructure = (block: TIssue): IGanttBlock => {
blocks?.map((block) => ({ return {
data: block, data: block,
id: block.id, id: block.id,
sort_order: block.sort_order, sort_order: block.sort_order,
start_date: block.start_date ? new Date(block.start_date) : null, start_date: block.start_date ? new Date(block.start_date) : undefined,
target_date: block.target_date ? new Date(block.target_date) : null, target_date: block.target_date ? new Date(block.target_date) : undefined,
})); };
};
export function getChangedIssuefields(formData: Partial<TIssue>, dirtyFields: { [key: string]: boolean | undefined }) { export function getChangedIssuefields(formData: Partial<TIssue>, dirtyFields: { [key: string]: boolean | undefined }) {
const changedFields: Partial<TIssue> = {}; const changedFields: Partial<TIssue> = {};

View File

@ -18,7 +18,7 @@ interface IssueActions {
options: IssuePaginationOptions, options: IssuePaginationOptions,
userViewId?: "assigned" | "created" | "subscribed" userViewId?: "assigned" | "created" | "subscribed"
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: () => Promise<TIssuesResponse | undefined>; fetchNextIssues: (groupId?: string, subGroupId?: string) => Promise<TIssuesResponse | undefined>;
removeIssue: (projectId: string | undefined | null, issueId: string) => Promise<void>; removeIssue: (projectId: string | undefined | null, issueId: string) => Promise<void>;
createIssue?: (projectId: string | undefined | null, data: Partial<TIssue>) => Promise<TIssue | undefined>; createIssue?: (projectId: string | undefined | null, data: Partial<TIssue>) => Promise<TIssue | undefined>;
updateIssue?: (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue?: (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -77,10 +77,13 @@ const useProjectIssueActions = () => {
}, },
[issues.fetchIssues, workspaceSlug, projectId] [issues.fetchIssues, workspaceSlug, projectId]
); );
const fetchNextIssues = useCallback(async () => { const fetchNextIssues = useCallback(
if (!workspaceSlug || !projectId) return; async (groupId?: string, subGroupId?: string) => {
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString()); if (!workspaceSlug || !projectId) return;
}, [issues.fetchIssues, workspaceSlug, projectId]); return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId);
},
[issues.fetchIssues, workspaceSlug, projectId]
);
const createIssue = useCallback( const createIssue = useCallback(
async (projectId: string | undefined | null, data: Partial<TIssue>) => { async (projectId: string | undefined | null, data: Partial<TIssue>) => {
@ -151,10 +154,19 @@ const useCycleIssueActions = () => {
}, },
[issues.fetchIssues, workspaceSlug, projectId, cycleId] [issues.fetchIssues, workspaceSlug, projectId, cycleId]
); );
const fetchNextIssues = useCallback(async () => { const fetchNextIssues = useCallback(
if (!workspaceSlug || !projectId || !cycleId) return; async (groupId?: string, subGroupId?: string) => {
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); if (!workspaceSlug || !projectId || !cycleId) return;
}, [issues.fetchIssues, workspaceSlug, projectId, cycleId]); return issues.fetchNextIssues(
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString(),
groupId,
subGroupId
);
},
[issues.fetchIssues, workspaceSlug, projectId, cycleId]
);
const createIssue = useCallback( const createIssue = useCallback(
async (projectId: string | undefined | null, data: Partial<TIssue>) => { async (projectId: string | undefined | null, data: Partial<TIssue>) => {
@ -242,10 +254,19 @@ const useModuleIssueActions = () => {
}, },
[issues.fetchIssues, workspaceSlug, projectId, moduleId] [issues.fetchIssues, workspaceSlug, projectId, moduleId]
); );
const fetchNextIssues = useCallback(async () => { const fetchNextIssues = useCallback(
if (!workspaceSlug || !projectId || !moduleId) return; async (groupId?: string, subGroupId?: string) => {
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); if (!workspaceSlug || !projectId || !moduleId) return;
}, [issues.fetchIssues, workspaceSlug, projectId, moduleId]); return issues.fetchNextIssues(
workspaceSlug.toString(),
projectId.toString(),
moduleId.toString(),
groupId,
subGroupId
);
},
[issues.fetchIssues, workspaceSlug, projectId, moduleId]
);
const createIssue = useCallback( const createIssue = useCallback(
async (projectId: string | undefined | null, data: Partial<TIssue>) => { async (projectId: string | undefined | null, data: Partial<TIssue>) => {
@ -324,10 +345,13 @@ const useProfileIssueActions = () => {
}, },
[issues.fetchIssues, workspaceSlug, userId] [issues.fetchIssues, workspaceSlug, userId]
); );
const fetchNextIssues = useCallback(async () => { const fetchNextIssues = useCallback(
if (!workspaceSlug || !userId) return; async (groupId?: string, subGroupId?: string) => {
return issues.fetchNextIssues(workspaceSlug.toString(), userId.toString()); if (!workspaceSlug || !userId) return;
}, [issues.fetchIssues, workspaceSlug, userId]); return issues.fetchNextIssues(workspaceSlug.toString(), userId.toString(), groupId, subGroupId);
},
[issues.fetchIssues, workspaceSlug, userId]
);
const createIssue = useCallback( const createIssue = useCallback(
async (projectId: string | undefined | null, data: Partial<TIssue>) => { async (projectId: string | undefined | null, data: Partial<TIssue>) => {
@ -398,10 +422,13 @@ const useProjectViewIssueActions = () => {
}, },
[issues.fetchIssues, workspaceSlug, projectId] [issues.fetchIssues, workspaceSlug, projectId]
); );
const fetchNextIssues = useCallback(async () => { const fetchNextIssues = useCallback(
if (!workspaceSlug || !projectId) return; async (groupId?: string, subGroupId?: string) => {
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString()); if (!workspaceSlug || !projectId) return;
}, [issues.fetchIssues, workspaceSlug, projectId]); return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId);
},
[issues.fetchIssues, workspaceSlug, projectId]
);
const createIssue = useCallback( const createIssue = useCallback(
async (projectId: string | undefined | null, data: Partial<TIssue>) => { async (projectId: string | undefined | null, data: Partial<TIssue>) => {
@ -472,10 +499,13 @@ const useDraftIssueActions = () => {
}, },
[issues.fetchIssues, workspaceSlug, projectId] [issues.fetchIssues, workspaceSlug, projectId]
); );
const fetchNextIssues = useCallback(async () => { const fetchNextIssues = useCallback(
if (!workspaceSlug || !projectId) return; async (groupId?: string, subGroupId?: string) => {
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString()); if (!workspaceSlug || !projectId) return;
}, [issues.fetchIssues, workspaceSlug, projectId]); return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId);
},
[issues.fetchIssues, workspaceSlug, projectId]
);
const createIssue = useCallback( const createIssue = useCallback(
async (projectId: string | undefined | null, data: Partial<TIssue>) => { async (projectId: string | undefined | null, data: Partial<TIssue>) => {
@ -538,10 +568,13 @@ const useArchivedIssueActions = () => {
}, },
[issues.fetchIssues, workspaceSlug, projectId] [issues.fetchIssues, workspaceSlug, projectId]
); );
const fetchNextIssues = useCallback(async () => { const fetchNextIssues = useCallback(
if (!workspaceSlug || !projectId) return; async (groupId?: string, subGroupId?: string) => {
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString()); if (!workspaceSlug || !projectId) return;
}, [issues.fetchIssues, workspaceSlug, projectId]); return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId);
},
[issues.fetchIssues, workspaceSlug, projectId]
);
const removeIssue = useCallback( const removeIssue = useCallback(
async (projectId: string | undefined | null, issueId: string) => { async (projectId: string | undefined | null, issueId: string) => {
@ -595,10 +628,13 @@ const useGlobalIssueActions = () => {
}, },
[issues.fetchIssues, workspaceSlug, globalViewId] [issues.fetchIssues, workspaceSlug, globalViewId]
); );
const fetchNextIssues = useCallback(async () => { const fetchNextIssues = useCallback(
if (!workspaceSlug || !globalViewId) return; async (groupId?: string, subGroupId?: string) => {
return issues.fetchNextIssues(workspaceSlug.toString(), globalViewId.toString()); if (!workspaceSlug || !globalViewId) return;
}, [issues.fetchIssues, workspaceSlug, globalViewId]); return issues.fetchNextIssues(workspaceSlug.toString(), globalViewId.toString(), groupId, subGroupId);
},
[issues.fetchIssues, workspaceSlug, globalViewId]
);
const createIssue = useCallback( const createIssue = useCallback(
async (projectId: string | undefined | null, data: Partial<TIssue>) => { async (projectId: string | undefined | null, data: Partial<TIssue>) => {

View File

@ -28,7 +28,9 @@ export interface IArchivedIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor: string | undefined cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
@ -94,21 +96,19 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
return filteredRouteParams; return filteredRouteParams;
} }
getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { getFilterParams = computedFn(
const filterParams = this.appliedFilters; (
options: IssuePaginationOptions,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.appliedFilters;
const paginationOptions: Partial<Record<TIssueParams, string | boolean>> = { const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
...filterParams, return paginationParams;
cursor: cursor ? cursor : `${options.perPageCount}:0:0`,
per_page: options.perPageCount.toString(),
};
if (options.groupedBy) {
paginationOptions.group_by = options.groupedBy;
} }
);
return paginationOptions;
});
fetchFilters = async (workspaceSlug: string, projectId: string) => { fetchFilters = async (workspaceSlug: string, projectId: string) => {
try { try {

View File

@ -6,7 +6,7 @@ import { TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse } from "@pl
// types // types
import { IIssueRootStore } from "../root.store"; import { IIssueRootStore } from "../root.store";
import { IArchivedIssuesFilter } from "./filter.store"; import { IArchivedIssuesFilter } from "./filter.store";
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store"; import { BaseIssuesStore, EIssueGroupedAction, IBaseIssuesStore } from "../helpers/base-issues.store";
export interface IArchivedIssues extends IBaseIssuesStore { export interface IArchivedIssues extends IBaseIssuesStore {
// observable // observable
@ -23,7 +23,12 @@ export interface IArchivedIssues extends IBaseIssuesStore {
projectId: string, projectId: string,
loadType: TLoader loadType: TLoader
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (
workspaceSlug: string,
projectId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -66,7 +71,7 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(options, undefined); const params = this.issueFilterStore?.getFilterParams(options, undefined, undefined, undefined);
const response = await this.issueArchiveService.getArchivedIssues(workspaceSlug, projectId, params); const response = await this.issueArchiveService.getArchivedIssues(workspaceSlug, projectId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -77,15 +82,21 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
} }
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => {
if (!this.paginationOptions || !this.next_page_results) return; const cursorObject = this.getPaginationData(subGroupId ?? groupId);
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); const params = this.issueFilterStore?.getFilterParams(
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); this.paginationOptions,
cursorObject?.nextCursor,
groupId,
subGroupId
);
const response = await this.issueArchiveService.getArchivedIssues(workspaceSlug, projectId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response, groupId, subGroupId);
return response; return response;
} catch (error) { } catch (error) {
this.loader = undefined; this.loader = undefined;
@ -110,7 +121,7 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
this.rootIssueStore.issues.updateIssue(issueId, { this.rootIssueStore.issues.updateIssue(issueId, {
archived_at: null, archived_at: null,
}); });
this.issues && pull(this.issues, issueId); this.removeIssueFromList(issueId);
}); });
return response; return response;

View File

@ -28,7 +28,9 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor: string | undefined cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
@ -97,21 +99,19 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
return filteredRouteParams; return filteredRouteParams;
} }
getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { getFilterParams = computedFn(
const filterParams = this.appliedFilters; (
options: IssuePaginationOptions,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.appliedFilters;
const paginationOptions: Partial<Record<TIssueParams, string | boolean>> = { const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
...filterParams, return paginationParams;
cursor: cursor ? cursor : `${options.perPageCount}:0:0`,
per_page: options.perPageCount.toString(),
};
if (options.groupedBy) {
paginationOptions.group_by = options.groupedBy;
} }
);
return paginationOptions;
});
fetchFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => { fetchFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => {
try { try {

View File

@ -9,7 +9,7 @@ import { CycleService } from "services/cycle.service";
// types // types
import { TIssue, TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse } from "@plane/types"; import { TIssue, TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse } from "@plane/types";
import { IIssueRootStore } from "../root.store"; import { IIssueRootStore } from "../root.store";
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store"; import { BaseIssuesStore, EIssueGroupedAction, IBaseIssuesStore } from "../helpers/base-issues.store";
import { ICycleIssuesFilter } from "./filter.store"; import { ICycleIssuesFilter } from "./filter.store";
export const ACTIVE_CYCLE_ISSUES = "ACTIVE_CYCLE_ISSUES"; export const ACTIVE_CYCLE_ISSUES = "ACTIVE_CYCLE_ISSUES";
@ -30,7 +30,13 @@ export interface ICycleIssues extends IBaseIssuesStore {
loadType: TLoader, loadType: TLoader,
cycleId: string cycleId: string
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (
workspaceSlug: string,
projectId: string,
cycleId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>, cycleId: string) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>, cycleId: string) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -111,7 +117,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
this.cycleId = cycleId; this.cycleId = cycleId;
const params = this.issueFilterStore?.getFilterParams(options, undefined); const params = this.issueFilterStore?.getFilterParams(options, undefined, undefined, undefined);
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params); const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -122,15 +128,27 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
} }
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => { fetchNextIssues = async (
if (!this.paginationOptions || !this.next_page_results) return; workspaceSlug: string,
projectId: string,
cycleId: string,
groupId?: string,
subGroupId?: string
) => {
const cursorObject = this.getPaginationData(subGroupId ?? groupId);
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); const params = this.issueFilterStore?.getFilterParams(
this.paginationOptions,
cursorObject?.nextCursor,
groupId,
subGroupId
);
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params); const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response, groupId, subGroupId);
return response; return response;
} catch (error) { } catch (error) {
this.loader = undefined; this.loader = undefined;
@ -175,8 +193,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds);
runInAction(() => { runInAction(() => {
this.cycleId === cycleId && this.cycleId === cycleId && issueIds.forEach((issueId) => this.addIssueToList(issueId));
update(this, "issues", (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds)));
}); });
issueIds.forEach((issueId) => { issueIds.forEach((issueId) => {
@ -193,7 +210,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
try { try {
await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
runInAction(() => { runInAction(() => {
this.issues && this.cycleId === cycleId && pull(this.issues, issueId); this.cycleId === cycleId && this.removeIssueFromList(issueId);
}); });
this.rootIssueStore.issues.updateIssue(issueId, { cycle_id: null }); this.rootIssueStore.issues.updateIssue(issueId, { cycle_id: null });

View File

@ -28,7 +28,9 @@ export interface IDraftIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor: string | undefined cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
@ -94,25 +96,19 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
return filteredRouteParams; return filteredRouteParams;
} }
getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { getFilterParams = computedFn(
const filterParams = this.appliedFilters; (
options: IssuePaginationOptions,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.appliedFilters;
const paginationOptions: Partial<Record<TIssueParams, string | boolean>> = { const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
...filterParams, return paginationParams;
cursor: cursor ? cursor : `${options.perPageCount}:0:0`,
per_page: options.perPageCount.toString(),
};
if (options.groupedBy) {
paginationOptions.group_by = options.groupedBy;
} }
);
if (options.after && options.before) {
paginationOptions["target_date"] = `${options.after};after,${options.before};before`;
}
return paginationOptions;
});
fetchFilters = async (workspaceSlug: string, projectId: string) => { fetchFilters = async (workspaceSlug: string, projectId: string) => {
try { try {

View File

@ -24,7 +24,12 @@ export interface IDraftIssues extends IBaseIssuesStore {
loadType: TLoader loadType: TLoader
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (
workspaceSlug: string,
projectId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -63,7 +68,7 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(options, undefined); const params = this.issueFilterStore?.getFilterParams(options, undefined, undefined, undefined);
const response = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params); const response = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -74,15 +79,21 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
} }
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => {
if (!this.paginationOptions || !this.next_page_results) return; const cursorObject = this.getPaginationData(subGroupId ?? groupId);
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); const params = this.issueFilterStore?.getFilterParams(
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); this.paginationOptions,
cursorObject?.nextCursor,
groupId,
subGroupId
);
const response = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response, groupId, subGroupId);
return response; return response;
} catch (error) { } catch (error) {
this.loader = undefined; this.loader = undefined;

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,18 @@ import {
IIssueFilterOptions, IIssueFilterOptions,
IIssueFilters, IIssueFilters,
IIssueFiltersResponse, IIssueFiltersResponse,
IssuePaginationOptions,
TIssueKanbanFilters, TIssueKanbanFilters,
TIssueParams, TIssueParams,
TStaticViewTypes, TStaticViewTypes,
} from "@plane/types"; } from "@plane/types";
// constants // constants
import { EIssueFilterType, EIssuesStoreType, IssueGroupByOptions } from "constants/issue"; import {
EIssueFilterType,
EIssuesStoreType,
EIssueGroupByToServerOptions,
EServerGroupByToFilterOptions,
} from "constants/issue";
// lib // lib
import { storage } from "lib/local-storage"; import { storage } from "lib/local-storage";
@ -89,7 +95,10 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
project: filters?.project || undefined, project: filters?.project || undefined,
subscriber: filters?.subscriber || undefined, subscriber: filters?.subscriber || undefined,
// display filters // display filters
group_by: displayFilters?.group_by ? IssueGroupByOptions[displayFilters.group_by] : undefined, group_by: displayFilters?.group_by ? EIssueGroupByToServerOptions[displayFilters.group_by] : undefined,
sub_group_by: displayFilters?.sub_group_by
? EIssueGroupByToServerOptions[displayFilters.sub_group_by]
: undefined,
order_by: displayFilters?.order_by || undefined, order_by: displayFilters?.order_by || undefined,
type: displayFilters?.type || undefined, type: displayFilters?.type || undefined,
sub_issue: displayFilters?.sub_issue ?? true, sub_issue: displayFilters?.sub_issue ?? true,
@ -209,7 +218,6 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
cycle: displayProperties?.cycle ?? true, cycle: displayProperties?.cycle ?? true,
}); });
handleIssuesLocalFilters = { handleIssuesLocalFilters = {
fetchFiltersFromStorage: () => { fetchFiltersFromStorage: () => {
const _filters = storage.get("issue_local_filters"); const _filters = storage.get("issue_local_filters");
@ -272,4 +280,64 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
storage.set("issue_local_filters", JSON.stringify(storageFilters)); storage.set("issue_local_filters", JSON.stringify(storageFilters));
}, },
}; };
/**
* This Method returns true if the display properties changed requires a server side update
* @param displayFilters
* @returns
*/
requiresServerUpdate = (displayFilters: IIssueDisplayFilterOptions) => {
const NON_SERVER_DISPLAY_FILTERS = ["order_by", "sub_issue", "type"];
const displayFilterKeys = Object.keys(displayFilters);
return NON_SERVER_DISPLAY_FILTERS.some((serverDisplayfilter: string) =>
displayFilterKeys.includes(serverDisplayfilter)
);
};
getPaginationParams(
filterParams: Partial<Record<TIssueParams, string | boolean>> | undefined,
options: IssuePaginationOptions,
cursor: string | undefined,
groupId?: string,
subGroupId?: string
) {
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount * 2}:0:0` : `${options.perPageCount}:0:0`;
const paginationParams: Partial<Record<TIssueParams, string | boolean>> = {
...filterParams,
cursor: pageCursor,
per_page: (groupId ? options.perPageCount * 2 : options.perPageCount).toString(),
};
if (options.groupedBy) {
paginationParams.group_by = options.groupedBy;
}
if (options.after && options.before) {
paginationParams["target_date"] = `${options.after};after,${options.before};before`;
}
if (groupId) {
const groupBy = paginationParams["group_by"] as EIssueGroupByToServerOptions | undefined;
delete paginationParams["group_by"];
if (groupBy) {
const groupByFilterOption = EServerGroupByToFilterOptions[groupBy];
paginationParams[groupByFilterOption] = groupId;
}
}
if (subGroupId) {
const subGroupBy = paginationParams["sub_group_by"] as EIssueGroupByToServerOptions | undefined;
delete paginationParams["sub_group_by"];
if (subGroupBy) {
const subGroupByFilterOption = EServerGroupByToFilterOptions[subGroupBy];
paginationParams[subGroupByFilterOption] = subGroupId;
}
}
return paginationParams;
}
} }

View File

@ -18,7 +18,7 @@ export type IIssueStore = {
removeIssue(issueId: string): void; removeIssue(issueId: string): void;
// helper methods // helper methods
getIssueById(issueId: string): undefined | TIssue; getIssueById(issueId: string): undefined | TIssue;
getIssuesByIds(issueIds: string[], type: "archived" | "un-archived"): undefined | Record<string, TIssue>; // Record defines issue_id as key and TIssue as value getIssuesByIds(issueIds: string[], type: "archived" | "un-archived"): TIssue[]; // Record defines issue_id as key and TIssue as value
}; };
export class IssueStore implements IIssueStore { export class IssueStore implements IIssueStore {
@ -112,15 +112,16 @@ export class IssueStore implements IIssueStore {
* @returns {Record<string, TIssue> | undefined} * @returns {Record<string, TIssue> | undefined}
*/ */
getIssuesByIds = computedFn((issueIds: string[], type: "archived" | "un-archived") => { getIssuesByIds = computedFn((issueIds: string[], type: "archived" | "un-archived") => {
if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return undefined; if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return [];
const filteredIssues: { [key: string]: TIssue } = {}; const filteredIssues: TIssue[] = [];
Object.values(this.issuesMap).forEach((issue) => { Object.values(issueIds).forEach((issueId) => {
// if type is archived then check archived_at is not null // if type is archived then check archived_at is not null
// if type is un-archived then check archived_at is null // if type is un-archived then check archived_at is null
if ((type === "archived" && issue.archived_at) || (type === "un-archived" && !issue.archived_at)) { const issue = this.issuesMap[issueId];
if (issueIds.includes(issue.id)) filteredIssues[issue.id] = issue; if ((issue && type === "archived" && issue.archived_at) || (type === "un-archived" && !issue?.archived_at)) {
filteredIssues.push(issue);
} }
}); });
return isEmpty(filteredIssues) ? undefined : filteredIssues; return filteredIssues;
}); });
} }

View File

@ -28,7 +28,9 @@ export interface IModuleIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor: string | undefined cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
@ -97,21 +99,19 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul
return filteredRouteParams; return filteredRouteParams;
} }
getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { getFilterParams = computedFn(
const filterParams = this.appliedFilters; (
options: IssuePaginationOptions,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.appliedFilters;
const paginationOptions: Partial<Record<TIssueParams, string | boolean>> = { const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
...filterParams, return paginationParams;
cursor: cursor ? cursor : `${options.perPageCount}:0:0`,
per_page: options.perPageCount.toString(),
};
if (options.groupedBy) {
paginationOptions.group_by = options.groupedBy;
} }
);
return paginationOptions;
});
fetchFilters = async (workspaceSlug: string, projectId: string, moduleId: string) => { fetchFilters = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try { try {

View File

@ -28,7 +28,13 @@ export interface IModuleIssues extends IBaseIssuesStore {
loadType: TLoader, loadType: TLoader,
moduleId: string moduleId: string
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (
workspaceSlug: string,
projectId: string,
moduleId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>, moduleId: string) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>, moduleId: string) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -111,7 +117,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
this.moduleId = moduleId; this.moduleId = moduleId;
const params = this.issueFilterStore?.getFilterParams(options, undefined); const params = this.issueFilterStore?.getFilterParams(options, undefined, undefined, undefined);
const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params); const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -122,15 +128,27 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
} }
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => { fetchNextIssues = async (
if (!this.paginationOptions || !this.next_page_results) return; workspaceSlug: string,
projectId: string,
moduleId: string,
groupId?: string,
subGroupId?: string
) => {
const cursorObject = this.getPaginationData(subGroupId ?? groupId);
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); const params = this.issueFilterStore?.getFilterParams(
this.paginationOptions,
cursorObject?.nextCursor,
groupId,
subGroupId
);
const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params); const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response, groupId, subGroupId);
return response; return response;
} catch (error) { } catch (error) {
this.loader = undefined; this.loader = undefined;
@ -177,11 +195,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds);
runInAction(() => { runInAction(() => {
this.moduleId === moduleId && this.moduleId === moduleId && issueIds.forEach((issueId) => this.addIssueToList(issueId));
update(this, "issues", (moduleIssueIds = []) => {
if (!moduleIssueIds) return [...issueIds];
else return uniq(concat(moduleIssueIds, issueIds));
});
}); });
issueIds.forEach((issueId) => { issueIds.forEach((issueId) => {
@ -207,10 +221,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
); );
runInAction(() => { runInAction(() => {
this.moduleId === moduleId && this.moduleId === moduleId && issueIds.forEach((issueId) => this.removeIssueFromList(issueId));
issueIds.forEach((issueId) => {
this.issues && pull(this.issues, issueId);
});
}); });
runInAction(() => { runInAction(() => {
@ -238,11 +249,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
runInAction(() => { runInAction(() => {
moduleIds.forEach((moduleId) => { moduleIds.forEach((moduleId) => {
this.moduleId === moduleId && this.moduleId === moduleId && this.addIssueToList(issueId);
update(this, "issues", (moduleIssueIds = []) => {
if (moduleIssueIds.includes(issueId)) return moduleIssueIds;
else return uniq(concat(moduleIssueIds, [issueId]));
});
}); });
update(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => update(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
uniq(concat(issueModuleIds, moduleIds)) uniq(concat(issueModuleIds, moduleIds))
@ -259,11 +266,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
try { try {
runInAction(() => { runInAction(() => {
moduleIds.forEach((moduleId) => { moduleIds.forEach((moduleId) => {
this.moduleId === moduleId && this.moduleId === moduleId && this.removeIssueFromList(issueId);
update(this, "issues", (moduleIssueIds = []) => {
if (moduleIssueIds.includes(issueId)) return pull(moduleIssueIds, issueId);
else return uniq(concat(moduleIssueIds, [issueId]));
});
update(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => update(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
pull(issueModuleIds, moduleId) pull(issueModuleIds, moduleId)
); );
@ -286,7 +289,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
try { try {
runInAction(() => { runInAction(() => {
this.issues && this.moduleId === this.moduleId && pull(this.issues, issueId); this.moduleId === this.moduleId && this.removeIssueFromList(issueId);
update(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => update(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) =>
pull(issueModuleIds, moduleId) pull(issueModuleIds, moduleId)
); );

View File

@ -30,7 +30,9 @@ export interface IProfileIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor: string | undefined cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, userId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, userId: string) => Promise<void>;
@ -99,21 +101,19 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf
return filteredRouteParams; return filteredRouteParams;
} }
getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { getFilterParams = computedFn(
const filterParams = this.appliedFilters; (
options: IssuePaginationOptions,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.appliedFilters;
const paginationOptions: Partial<Record<TIssueParams, string | boolean>> = { const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
...filterParams, return paginationParams;
cursor: cursor ? cursor : `${options.perPageCount}:0:0`,
per_page: options.perPageCount.toString(),
};
if (options.groupedBy) {
paginationOptions.group_by = options.groupedBy;
} }
);
return paginationOptions;
});
fetchFilters = async (workspaceSlug: string, userId: string) => { fetchFilters = async (workspaceSlug: string, userId: string) => {
try { try {

View File

@ -27,7 +27,12 @@ export interface IProfileIssues extends IBaseIssuesStore {
userId: string, userId: string,
loadType: TLoader loadType: TLoader
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, userId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (
workspaceSlug: string,
userId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -95,7 +100,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
this.setViewId(view); this.setViewId(view);
let params = this.issueFilterStore?.getFilterParams(options, undefined); let params = this.issueFilterStore?.getFilterParams(options, undefined, undefined, undefined);
params = { params = {
...params, ...params,
assignees: undefined, assignees: undefined,
@ -116,12 +121,18 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
} }
}; };
fetchNextIssues = async (workspaceSlug: string, userId: string) => { fetchNextIssues = async (workspaceSlug: string, userId: string, groupId?: string, subGroupId?: string) => {
if (!this.paginationOptions || !this.currentView || !this.next_page_results) return; const cursorObject = this.getPaginationData(subGroupId ?? groupId);
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
let params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); let params = this.issueFilterStore?.getFilterParams(
this.paginationOptions,
cursorObject?.nextCursor,
groupId,
subGroupId
);
params = { params = {
...params, ...params,
assignees: undefined, assignees: undefined,
@ -134,7 +145,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
const response = await this.userService.getUserProfileIssues(workspaceSlug, userId, params); const response = await this.userService.getUserProfileIssues(workspaceSlug, userId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response, groupId, subGroupId);
return response; return response;
} catch (error) { } catch (error) {
this.loader = undefined; this.loader = undefined;

View File

@ -28,7 +28,9 @@ export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor: string | undefined cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>;
@ -95,21 +97,19 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
return filteredRouteParams; return filteredRouteParams;
} }
getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { getFilterParams = computedFn(
const filterParams = this.appliedFilters; (
options: IssuePaginationOptions,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.appliedFilters;
const paginationOptions: Partial<Record<TIssueParams, string | boolean>> = { const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
...filterParams, return paginationParams;
cursor: cursor ? cursor : `${options.perPageCount}:0:0`,
per_page: options.perPageCount.toString(),
};
if (options.groupedBy) {
paginationOptions.group_by = options.groupedBy;
} }
);
return paginationOptions;
});
fetchFilters = async (workspaceSlug: string, projectId: string, viewId: string) => { fetchFilters = async (workspaceSlug: string, projectId: string, viewId: string) => {
try { try {

View File

@ -21,7 +21,12 @@ export interface IProjectViewIssues extends IBaseIssuesStore {
projectId: string, projectId: string,
loadType: TLoader loadType: TLoader
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (
workspaceSlug: string,
projectId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -62,7 +67,7 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(options, undefined); const params = this.issueFilterStore?.getFilterParams(options, undefined, undefined, undefined);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -73,15 +78,21 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
} }
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => {
if (!this.paginationOptions || !this.next_page_results) return; const cursorObject = this.getPaginationData(subGroupId ?? groupId);
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); let params = this.issueFilterStore?.getFilterParams(
this.paginationOptions,
cursorObject?.nextCursor,
groupId,
subGroupId
);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response, groupId, subGroupId);
return response; return response;
} catch (error) { } catch (error) {
this.loader = undefined; this.loader = undefined;

View File

@ -20,7 +20,7 @@ import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-
// types // types
import { IIssueRootStore } from "../root.store"; import { IIssueRootStore } from "../root.store";
// constants // constants
import { EIssueFilterType, EIssuesStoreType, IssueGroupByOptions } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
// services // services
@ -28,7 +28,9 @@ export interface IProjectIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor: string | undefined cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
@ -94,25 +96,18 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
return filteredRouteParams; return filteredRouteParams;
} }
getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { getFilterParams = computedFn(
const filterParams = this.appliedFilters; (
options: IssuePaginationOptions,
const paginationOptions: Partial<Record<TIssueParams, string | boolean>> = { cursor: string | undefined,
...filterParams, groupId: string | undefined,
cursor: cursor ? cursor : `${options.perPageCount}:0:0`, subGroupId: string | undefined
per_page: options.perPageCount.toString(), ) => {
}; const filterParams = this.appliedFilters;
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
if (options.groupedBy) { return paginationParams;
paginationOptions.group_by = options.groupedBy;
} }
);
if (options.after && options.before) {
paginationOptions["target_date"] = `${options.after};after,${options.before};before`;
}
return paginationOptions;
});
fetchFilters = async (workspaceSlug: string, projectId: string) => { fetchFilters = async (workspaceSlug: string, projectId: string) => {
try { try {
@ -221,7 +216,8 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
}); });
}); });
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation"); if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, { await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, {
display_filters: _filters.displayFilters, display_filters: _filters.displayFilters,

View File

@ -22,7 +22,12 @@ export interface IProjectIssues extends IBaseIssuesStore {
projectId: string, projectId: string,
loadType: TLoader loadType: TLoader
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (
workspaceSlug: string,
projectId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -65,7 +70,7 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(options, undefined); const params = this.issueFilterStore?.getFilterParams(options, undefined, undefined, undefined);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -76,15 +81,21 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
} }
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => {
if (!this.paginationOptions || !this.next_page_results) return; const cursorObject = this.getPaginationData(subGroupId ?? groupId);
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); const params = this.issueFilterStore?.getFilterParams(
this.paginationOptions,
cursorObject?.nextCursor,
groupId,
subGroupId
);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response, groupId, subGroupId);
return response; return response;
} catch (error) { } catch (error) {
this.loader = undefined; this.loader = undefined;

View File

@ -43,7 +43,9 @@ export interface IWorkspaceIssuesFilter extends IBaseIssueFilterStore {
getFilterParams: ( getFilterParams: (
viewId: string, viewId: string,
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor?: string cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
} }
@ -102,21 +104,20 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
return filteredRouteParams; return filteredRouteParams;
}; };
getFilterParams = computedFn((viewId: string, options: IssuePaginationOptions, cursor: string | undefined) => { getFilterParams = computedFn(
const filterParams = this.getAppliedFilters(viewId); (
viewId: string,
options: IssuePaginationOptions,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.getAppliedFilters(viewId);
const paginationOptions: Partial<Record<TIssueParams, string | boolean>> = { const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
...filterParams, return paginationParams;
cursor: cursor ? cursor : `${options.perPageCount}:0:0`,
per_page: options.perPageCount.toString(),
};
if (options.groupedBy) {
paginationOptions.group_by = options.groupedBy;
} }
);
return paginationOptions;
});
get issueFilters() { get issueFilters() {
const viewId = this.rootIssueStore.globalViewId; const viewId = this.rootIssueStore.globalViewId;
@ -237,7 +238,7 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
}); });
}); });
this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation"); this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation");
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, { this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {

View File

@ -1,7 +1,7 @@
import { action, makeObservable, runInAction } from "mobx"; import { action, makeObservable, runInAction } from "mobx";
// base class // base class
import { WorkspaceService } from "services/workspace.service"; import { WorkspaceService } from "services/workspace.service";
import { IssuePaginationOptions, TIssue, TIssuesResponse, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssuePaginationOptions, TIssue, TIssuesResponse, TLoader, ViewFlags } from "@plane/types";
// services // services
// types // types
import { IIssueRootStore } from "../root.store"; import { IIssueRootStore } from "../root.store";
@ -23,7 +23,12 @@ export interface IWorkspaceIssues extends IBaseIssuesStore {
viewId: string, viewId: string,
loadType: TLoader loadType: TLoader
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, viewId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (
workspaceSlug: string,
viewId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
@ -64,7 +69,7 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(viewId, options, undefined); const params = this.issueFilterStore?.getFilterParams(viewId, options, undefined, undefined, undefined);
const response = await this.workspaceService.getViewIssues(workspaceSlug, params); const response = await this.workspaceService.getViewIssues(workspaceSlug, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -75,15 +80,22 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
} }
}; };
fetchNextIssues = async (workspaceSlug: string, viewId: string) => { fetchNextIssues = async (workspaceSlug: string, viewId: string, groupId?: string, subGroupId?: string) => {
if (!this.paginationOptions) return; const cursorObject = this.getPaginationData(subGroupId ?? groupId);
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(viewId, this.paginationOptions, this.nextCursor); const params = this.issueFilterStore?.getFilterParams(
viewId,
this.paginationOptions,
cursorObject?.nextCursor,
groupId,
subGroupId
);
const response = await this.workspaceService.getViewIssues(workspaceSlug, params); const response = await this.workspaceService.getViewIssues(workspaceSlug, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response, groupId, subGroupId);
return response; return response;
} catch (error) { } catch (error) {
this.loader = undefined; this.loader = undefined;