diff --git a/packages/types/src/issues/base.d.ts b/packages/types/src/issues/base.d.ts index 81ee13a57..ad4df9547 100644 --- a/packages/types/src/issues/base.d.ts +++ b/packages/types/src/issues/base.d.ts @@ -12,16 +12,26 @@ export * from "./activity/base"; export type TLoader = "init-loader" | "mutation" | "pagination" | undefined; -export type TIssueGroup = { issueIds: string[]; issueCount: number }; export type TGroupedIssues = { - [group_id: string]: TIssueGroup; + [group_id: string]: string[]; }; export type TSubGroupedIssues = { [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; +}; diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index 310a34396..df3a56eea 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -61,7 +61,14 @@ type TIssueResponseResults = | TBaseIssue[] | { [key: string]: { - results: TBaseIssue[]; + results: + | TBaseIssue[] + | { + [key: string]: { + results: TBaseIssue[]; + total_results: number; + }; + }; total_results: number; }; }; @@ -72,6 +79,7 @@ export type TIssuesResponse = { prev_cursor: string; next_page_results: boolean; prev_page_results: boolean; + total_count: number; count: number; total_pages: number; extra_stats: null; diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx index 914ffbb47..4cc4313ab 100644 --- a/web/components/gantt-chart/chart/main-content.tsx +++ b/web/components/gantt-chart/chart/main-content.tsx @@ -24,6 +24,7 @@ import { useGanttChart } from "../hooks/use-gantt-chart"; type Props = { blockIds: string[]; getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; + canLoadMoreBlocks?: boolean; loadMoreBlocks?: () => void; blockToRender: (data: any) => React.ReactNode; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; @@ -58,6 +59,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { showAllBlocks, sidebarToRender, title, + canLoadMoreBlocks, updateCurrentViewRenderPayload, quickAdd, } = props; @@ -110,6 +112,8 @@ export const GanttChartMainContent: React.FC = observer((props) => { blockIds={blockIds} getBlockById={getBlockById} loadMoreBlocks={loadMoreBlocks} + canLoadMoreBlocks={canLoadMoreBlocks} + ganttContainerRef={ganttContainerRef} blockUpdateHandler={blockUpdateHandler} enableReorder={enableReorder} sidebarToRender={sidebarToRender} diff --git a/web/components/gantt-chart/chart/root.tsx b/web/components/gantt-chart/chart/root.tsx index 056b8872d..8bbb55547 100644 --- a/web/components/gantt-chart/chart/root.tsx +++ b/web/components/gantt-chart/chart/root.tsx @@ -32,6 +32,7 @@ type ChartViewRootProps = { showAllBlocks: boolean; getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; loadMoreBlocks?: () => void; + canLoadMoreBlocks?: boolean; quickAdd?: React.JSX.Element | undefined; }; @@ -46,6 +47,7 @@ export const ChartViewRoot: FC = observer((props) => { blockUpdateHandler, sidebarToRender, blockToRender, + canLoadMoreBlocks, enableBlockLeftResize, enableBlockRightResize, enableBlockMove, @@ -163,6 +165,7 @@ export const ChartViewRoot: FC = observer((props) => { blockIds={blockIds} getBlockById={getBlockById} loadMoreBlocks={loadMoreBlocks} + canLoadMoreBlocks={canLoadMoreBlocks} blockToRender={blockToRender} blockUpdateHandler={blockUpdateHandler} bottomSpacing={bottomSpacing} diff --git a/web/components/gantt-chart/root.tsx b/web/components/gantt-chart/root.tsx index 86700068d..687eae58a 100644 --- a/web/components/gantt-chart/root.tsx +++ b/web/components/gantt-chart/root.tsx @@ -14,6 +14,7 @@ type GanttChartRootProps = { sidebarToRender: (props: any) => React.ReactNode; quickAdd?: React.JSX.Element | undefined; getBlockById: (id: string, currentViewData?: ChartDataType | undefined) => IGanttBlock; + canLoadMoreBlocks?: boolean; loadMoreBlocks?: () => void; enableBlockLeftResize?: boolean; enableBlockRightResize?: boolean; @@ -35,6 +36,7 @@ export const GanttChartRoot: FC = (props) => { blockToRender, getBlockById, loadMoreBlocks, + canLoadMoreBlocks, enableBlockLeftResize = false, enableBlockRightResize = false, enableBlockMove = false, @@ -53,6 +55,7 @@ export const GanttChartRoot: FC = (props) => { blockIds={blockIds} getBlockById={getBlockById} loadMoreBlocks={loadMoreBlocks} + canLoadMoreBlocks={canLoadMoreBlocks} loaderTitle={loaderTitle} blockUpdateHandler={blockUpdateHandler} sidebarToRender={sidebarToRender} diff --git a/web/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/components/gantt-chart/sidebar/issues/sidebar.tsx index 3c808ba34..73316e8df 100644 --- a/web/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -7,23 +7,34 @@ import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; import { observer } from "mobx-react"; import { IssueDraggableBlock } from "./issue-draggable-block"; import { useIntersectionObserver } from "hooks/use-intersection-observer"; -import { useRef } from "react"; +import { RefObject, useRef } from "react"; type Props = { blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; getBlockById: (id: string) => IGanttBlock; + canLoadMoreBlocks?: boolean; loadMoreBlocks?: () => void; + ganttContainerRef: RefObject; blockIds: string[]; enableReorder: boolean; showAllBlocks?: boolean; }; export const IssueGanttSidebar: React.FC = observer((props) => { - const { blockUpdateHandler, blockIds, getBlockById, enableReorder, loadMoreBlocks, showAllBlocks = false } = props; + const { + blockUpdateHandler, + blockIds, + getBlockById, + enableReorder, + loadMoreBlocks, + canLoadMoreBlocks, + ganttContainerRef, + showAllBlocks = false, + } = props; const intersectionRef = useRef(null); - useIntersectionObserver(undefined, intersectionRef, loadMoreBlocks); + useIntersectionObserver(ganttContainerRef, intersectionRef, loadMoreBlocks, "50% 0% 50% 0%"); const handleOrderChange = (result: DropResult) => { if (!blockIds) return; @@ -85,7 +96,9 @@ export const IssueGanttSidebar: React.FC = observer((props) => { getBlockById={getBlockById} /> ))} - + {canLoadMoreBlocks && ( + + )} ) : ( diff --git a/web/components/gantt-chart/sidebar/root.tsx b/web/components/gantt-chart/sidebar/root.tsx index 15b464d1f..fa818a96c 100644 --- a/web/components/gantt-chart/sidebar/root.tsx +++ b/web/components/gantt-chart/sidebar/root.tsx @@ -2,11 +2,14 @@ import { ChartDataType, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; // constants import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants"; +import { RefObject } from "react"; type Props = { blockIds: string[]; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + canLoadMoreBlocks?: boolean; loadMoreBlocks?: () => void; + ganttContainerRef: RefObject; enableReorder: boolean; sidebarToRender: (props: any) => React.ReactNode; title: string; @@ -22,6 +25,8 @@ export const GanttChartSidebar: React.FC = (props) => { sidebarToRender, getBlockById, loadMoreBlocks, + canLoadMoreBlocks, + ganttContainerRef, title, quickAdd, } = props; @@ -47,7 +52,16 @@ export const GanttChartSidebar: React.FC = (props) => {
{sidebarToRender && - sidebarToRender({ title, blockUpdateHandler, blockIds, getBlockById, enableReorder, loadMoreBlocks })} + sidebarToRender({ + title, + blockUpdateHandler, + blockIds, + getBlockById, + enableReorder, + canLoadMoreBlocks, + ganttContainerRef, + loadMoreBlocks, + })}
{quickAdd ? quickAdd : null} diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index 7f1a7a0d1..f32bee66b 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -223,7 +223,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" 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} dropdownArrow diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index a3706e113..1f7afe5e0 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -12,7 +12,7 @@ import { useCalendarView, useIssues, useUser } from "hooks/store"; import { useIssuesActions } from "hooks/use-issues-actions"; // ui // types -import { EIssueLayoutTypes, EIssuesStoreType, IssueGroupByOptions } from "constants/issue"; +import { EIssueLayoutTypes, EIssuesStoreType, EIssueGroupByToServerOptions } from "constants/issue"; import { IQuickActionProps } from "../list/list-view-types"; import { handleDragDrop } from "./utils"; import { EUserProjectRoles } from "constants/project"; @@ -68,14 +68,14 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { useSWR( startDate && endDate && layout ? `ISSUE_CALENDAR_LAYOUT_${storeType}_${startDate}_${endDate}_${layout}` : null, - startDate && endDate + startDate && endDate && layout ? () => fetchIssues("init-loader", { canGroup: true, perPageCount: layout === "month" ? 4 : 30, before: endDate, after: startDate, - groupedBy: IssueGroupByOptions["target_date"], + groupedBy: EIssueGroupByToServerOptions["target_date"], }) : null, { @@ -112,9 +112,12 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { } }; - const loadMoreIssues = useCallback(() => { - fetchNextIssues(); - }, [fetchNextIssues]); + const loadMoreIssues = useCallback( + (dateString: string) => { + fetchNextIssues(dateString); + }, + [fetchNextIssues] + ); return ( @@ -142,6 +145,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { /> )} loadMoreIssues={loadMoreIssues} + getPaginationData={issues.getPaginationData} + getGroupIssueCount={issues.getGroupIssueCount} addIssuesToView={addIssuesToView} quickAddCallback={issues.quickAddIssue} viewId={viewId} diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 9494ff721..e13a68ab8 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -13,6 +13,7 @@ import { TIssue, TIssueKanbanFilters, TIssueMap, + TPaginationData, } from "@plane/types"; import { ICalendarWeek } from "./types"; // constants @@ -33,7 +34,9 @@ type Props = { layout: "month" | "week" | undefined; showWeekends: boolean; 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; quickAddCallback?: ( workspaceSlug: string, @@ -63,6 +66,8 @@ export const CalendarChart: React.FC = observer((props) => { quickActions, quickAddCallback, addIssuesToView, + getPaginationData, + getGroupIssueCount, viewId, updateFilters, readOnly = false, @@ -108,6 +113,8 @@ export const CalendarChart: React.FC = observer((props) => { issues={issues} groupedIssueIds={groupedIssueIds} loadMoreIssues={loadMoreIssues} + getPaginationData={getPaginationData} + getGroupIssueCount={getGroupIssueCount} enableQuickIssueCreate disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} @@ -126,6 +133,8 @@ export const CalendarChart: React.FC = observer((props) => { issues={issues} groupedIssueIds={groupedIssueIds} loadMoreIssues={loadMoreIssues} + getPaginationData={getPaginationData} + getGroupIssueCount={getGroupIssueCount} enableQuickIssueCreate disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index f8feb00ca..649ba0bb6 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -12,14 +12,16 @@ import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; +import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; date: ICalendarDate; issues: TIssueMap | undefined; 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; enableQuickIssueCreate?: boolean; disableIssueCreation?: boolean; @@ -41,6 +43,8 @@ export const CalendarDayTile: React.FC = observer((props) => { issues, groupedIssueIds, loadMoreIssues, + getPaginationData, + getGroupIssueCount, quickActions, enableQuickIssueCreate, disableIssueCreation, @@ -53,9 +57,12 @@ export const CalendarDayTile: React.FC = observer((props) => { const formattedDatePayload = renderFormattedPayloadDate(date.date); 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(); @@ -101,7 +108,7 @@ export const CalendarDayTile: React.FC = observer((props) => { > @@ -121,12 +128,12 @@ export const CalendarDayTile: React.FC = observer((props) => { )} - {totalIssues > (issueIdList?.issueIds?.length ?? 0) && ( + {shouldLoadMore && (
diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 644efe8d5..472b9ef38 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -8,7 +8,7 @@ import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; 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"; type Props = { @@ -17,7 +17,9 @@ type Props = { groupedIssueIds: TGroupedIssues; week: ICalendarWeek | undefined; 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; disableIssueCreation?: boolean; quickAddCallback?: ( @@ -38,6 +40,8 @@ export const CalendarWeekDays: React.FC = observer((props) => { groupedIssueIds, week, loadMoreIssues, + getPaginationData, + getGroupIssueCount, quickActions, enableQuickIssueCreate, disableIssueCreation, @@ -69,6 +73,8 @@ export const CalendarWeekDays: React.FC = observer((props) => { issues={issues} groupedIssueIds={groupedIssueIds} loadMoreIssues={loadMoreIssues} + getPaginationData={getPaginationData} + getGroupIssueCount={getGroupIssueCount} quickActions={quickActions} enableQuickIssueCreate={enableQuickIssueCreate} disableIssueCreation={disableIssueCreation} diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 693d9f677..518e33c46 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -11,12 +11,13 @@ import { useIssuesActions } from "hooks/use-issues-actions"; // components // helpers // types -import { TIssue, TIssueGroup } from "@plane/types"; +import { TIssue } from "@plane/types"; // constants import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue"; 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 = | EIssuesStoreType.PROJECT @@ -47,7 +48,9 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan revalidateOnReconnect: false, }); - const issuesObject = issues.groupedIssueIds?.["All Issues"] as TIssueGroup; + const issuesIds = (issues.groupedIssueIds?.[ALL_ISSUES] as string[]) ?? []; + const nextPageResults = issues.getPaginationData(ALL_ISSUES)?.nextPageResults; + const { enableIssueCreation } = issues?.viewFlags || {}; const loadMoreIssues = useCallback(() => { @@ -87,7 +90,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan border={false} title="Issues" loaderTitle="Issues" - blockIds={issuesObject?.issueIds} + blockIds={issuesIds} getBlockById={getBlockById} blockUpdateHandler={updateIssueBlockStructure} blockToRender={(data: TIssue) => } @@ -103,6 +106,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan ) : undefined } loadMoreBlocks={loadMoreIssues} + canLoadMoreBlocks={nextPageResults} showAllBlocks />
diff --git a/web/components/issues/issue-layouts/issue-layout-HOC.tsx b/web/components/issues/issue-layouts/issue-layout-HOC.tsx index ae9582ccf..dd6384b4f 100644 --- a/web/components/issues/issue-layouts/issue-layout-HOC.tsx +++ b/web/components/issues/issue-layouts/issue-layout-HOC.tsx @@ -9,6 +9,7 @@ import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; import { observer } from "mobx-react"; import { IssueLayoutEmptyState } from "./empty-states"; +import { ALL_ISSUES } from "store/issue/helpers/base-issues.store"; const ActiveLoader = (props: { layout: EIssueLayoutTypes }) => { const { layout } = props; @@ -43,7 +44,7 @@ export const IssueLayoutHOC = observer((props: Props) => { return ; } - if (issues.issueCount === 0) { + if (issues.getGroupIssueCount(ALL_ISSUES) === 0) { return ; } diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 7212ebd14..f596fb117 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -75,27 +75,38 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas updateFilters, } = 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 displayProperties = issuesFilter?.issueFilters?.displayProperties; const sub_group_by: string | null = displayFilters?.sub_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 KanBanView = sub_group_by ? KanBanSwimLanes : KanBan; @@ -160,7 +171,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas sub_group_by, group_by, issueMap, - issueIds, + groupedIssueIds, updateIssue, removeIssue ).catch((err) => { @@ -201,7 +212,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas sub_group_by, group_by, issueMap, - issueIds, + groupedIssueIds, updateIssue, removeIssue ).finally(() => { @@ -271,7 +282,8 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas
= observer((props: IBas enableQuickIssueCreate={enableQuickAdd} showEmptyGroup={userDisplayFilters?.show_empty_groups ?? true} quickAddCallback={issues?.quickAddIssue} + getPaginationData={issues.getPaginationData} viewId={viewId} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} canEditProperties={canEditProperties} diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 0ac680fdd..d8528425e 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -22,7 +22,9 @@ interface IssueBlockProps { isDragDisabled: boolean; draggableId: string; index: number; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -33,7 +35,9 @@ interface IssueBlockProps { interface IssueDetailsBlockProps { issue: TIssue; displayProperties: IIssueDisplayProperties | undefined; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue) => React.ReactNode; isReadOnly: boolean; } diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index 7a58a4933..607a642e1 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -12,7 +12,9 @@ interface IssueBlocksListProps { issueIds: string[]; displayProperties: IIssueDisplayProperties | undefined; isDragDisabled: boolean; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 19283ef63..be8820124 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -21,8 +21,8 @@ import { IIssueDisplayProperties, IIssueMap, TSubGroupedIssues, - TUnGroupedIssues, TIssueKanbanFilters, + TPaginationData, } from "@plane/types"; // parent components import { getGroupByColumns } from "../utils"; @@ -33,17 +33,21 @@ import { KanbanStoreType } from "./base-kanban-root"; export interface IGroupByKanBan { issuesMap: IIssueMap; - issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + getGroupIssueCount: (groupId: string | undefined) => number | undefined; + getPaginationData: (groupId: string | undefined) => TPaginationData | undefined; displayProperties: IIssueDisplayProperties | undefined; sub_group_by: string | null; group_by: string | null; sub_group_id: string; isDragDisabled: boolean; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: any; - loadMoreIssues: (() => void) | undefined; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; enableQuickIssueCreate?: boolean; quickAddCallback?: ( workspaceSlug: string, @@ -64,7 +68,9 @@ export interface IGroupByKanBan { const GroupByKanBan: React.FC = observer((props) => { const { issuesMap, - issueIds, + groupedIssueIds, + getGroupIssueCount, + getPaginationData, displayProperties, sub_group_by, group_by, @@ -107,7 +113,7 @@ const GroupByKanBan: React.FC = observer((props) => { 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; @@ -143,7 +149,7 @@ const GroupByKanBan: React.FC = observer((props) => { column_id={_list.id} icon={_list.icon} title={_list.name} - count={(issueIds as TGroupedIssues)?.[_list.id]?.issueCount || 0} + count={getGroupIssueCount(_list.id) ?? 0} issuePayload={_list.payload} disableIssueCreation={disableIssueCreation || isGroupByCreatedBy} storeType={storeType} @@ -158,7 +164,9 @@ const GroupByKanBan: React.FC = observer((props) => { = observer((props) => { export interface IKanBan { issuesMap: IIssueMap; - issueIds: TGroupedIssues | TSubGroupedIssues; + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + getGroupIssueCount: (groupId: string | undefined) => number | undefined; + getPaginationData: (groupId: string | undefined) => TPaginationData | undefined; displayProperties: IIssueDisplayProperties | undefined; sub_group_by: string | null; group_by: string | null; sub_group_id?: string; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; - loadMoreIssues: (() => void) | undefined; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; showEmptyGroup: boolean; enableQuickIssueCreate?: boolean; quickAddCallback?: ( @@ -217,7 +229,9 @@ export interface IKanBan { export const KanBan: React.FC = observer((props) => { const { issuesMap, - issueIds, + groupedIssueIds, + getGroupIssueCount, + getPaginationData, displayProperties, sub_group_by, group_by, @@ -244,7 +258,9 @@ export const KanBan: React.FC = observer((props) => { return ( number | undefined; + getPaginationData: (groupId: string | undefined) => TPaginationData | undefined; displayProperties: IIssueDisplayProperties | undefined; sub_group_by: string | null; group_by: string | null; sub_group_id: string; isDragDisabled: boolean; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; quickAddCallback?: ( @@ -35,7 +40,7 @@ interface IKanbanGroup { data: TIssue, viewId?: string ) => Promise; - loadMoreIssues: (() => void) | undefined; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; viewId?: string; disableIssueCreation?: boolean; canEditProperties: (projectId: string | undefined) => boolean; @@ -44,7 +49,7 @@ interface IKanbanGroup { isDragStarted?: boolean; } -export const KanbanGroup = (props: IKanbanGroup) => { +export const KanbanGroup = observer((props: IKanbanGroup) => { const { groupId, sub_group_id, @@ -52,7 +57,9 @@ export const KanbanGroup = (props: IKanbanGroup) => { sub_group_by, issuesMap, displayProperties, - issueIds, + groupedIssueIds, + getGroupIssueCount, + getPaginationData, peekIssueId, isDragDisabled, updateIssue, @@ -125,6 +132,23 @@ export const KanbanGroup = (props: IKanbanGroup) => { 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 (
@@ -139,7 +163,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { columnId={groupId} issuesMap={issuesMap} peekIssueId={peekIssueId} - issueIds={(issueIds as TGroupedIssues)?.[groupId]?.issueIds || []} + issueIds={issueIds} displayProperties={displayProperties} isDragDisabled={isDragDisabled} updateIssue={updateIssue} @@ -151,7 +175,17 @@ export const KanbanGroup = (props: IKanbanGroup) => { {provided.placeholder} - {loadMoreIssues && } + {shouldLoadMore && isSubGroup ? ( +
loadMoreIssues(groupId, sub_group_id)} + > + {" "} + Load more ↓ +
+ ) : ( + + )} {enableQuickIssueCreate && !disableIssueCreation && (
@@ -172,4 +206,4 @@ export const KanbanGroup = (props: IKanbanGroup) => {
); -}; +}); diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 9688bc6ac..3fe0770ad 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -10,8 +10,9 @@ import { IIssueDisplayProperties, IIssueMap, TSubGroupedIssues, - TUnGroupedIssues, TIssueKanbanFilters, + TGroupedIssueCount, + TPaginationData, } from "@plane/types"; import { getGroupByColumns } from "../utils"; import { KanBan } from "./default"; @@ -22,7 +23,7 @@ import { KanbanStoreType } from "./base-kanban-root"; // constants interface ISubGroupSwimlaneHeader { - issueIds: TGroupedIssues | TSubGroupedIssues; + getGroupIssueCount: (groupId: string | undefined) => number | undefined; sub_group_by: string | null; group_by: string | null; list: IGroupByColumn[]; @@ -31,16 +32,8 @@ interface ISubGroupSwimlaneHeader { 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 = ({ - issueIds, + getGroupIssueCount, sub_group_by, group_by, storeType, @@ -59,7 +52,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ column_id={_list.id} icon={_list.icon} title={_list.name} - count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)} + count={getGroupIssueCount(_list?.id) ?? 0} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} issuePayload={_list.payload} @@ -72,10 +65,14 @@ const SubGroupSwimlaneHeader: React.FC = ({ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { issuesMap: IIssueMap; - issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + getGroupIssueCount: (groupId: string | undefined) => number | undefined; + getPaginationData: (groupId: string | undefined) => TPaginationData | undefined; showEmptyGroup: boolean; displayProperties: IIssueDisplayProperties | undefined; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; @@ -93,12 +90,14 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { ) => Promise; viewId?: string; scrollableContainerRef?: MutableRefObject; - loadMoreIssues: (() => void) | undefined; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; } const SubGroupSwimlane: React.FC = observer((props) => { const { issuesMap, - issueIds, + groupedIssueIds, + getGroupIssueCount, + getPaginationData, sub_group_by, group_by, list, @@ -119,22 +118,12 @@ const SubGroupSwimlane: React.FC = observer((props) => { isDragStarted, } = 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 (
{list && list.length > 0 && - list.map((_list: any, index: number) => { - const isLastSubGroup = index === list.length - 1; + list.map((_list: any) => { + const issueCount = getGroupIssueCount(_list.id) ?? 0; return (
@@ -143,7 +132,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { column_id={_list.id} icon={_list.Icon} title={_list.name || ""} - count={calculateIssueCount(_list.id)} + count={issueCount} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} /> @@ -155,7 +144,9 @@ const SubGroupSwimlane: React.FC = observer((props) => {
= observer((props) => { scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} storeType={storeType} - loadMoreIssues={isLastSubGroup ? loadMoreIssues : undefined} + loadMoreIssues={loadMoreIssues} />
)} @@ -186,15 +177,19 @@ const SubGroupSwimlane: React.FC = observer((props) => { export interface IKanBanSwimLanes { issuesMap: IIssueMap; - issueIds: TGroupedIssues | TSubGroupedIssues; + groupedIssueIds: TGroupedIssues | TSubGroupedIssues; + getGroupIssueCount: (groupId: string | undefined) => number | undefined; + getPaginationData: (groupId: string | undefined) => TPaginationData | undefined; displayProperties: IIssueDisplayProperties | undefined; sub_group_by: string | null; group_by: string | null; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; - loadMoreIssues: (() => void) | undefined; + loadMoreIssues: (groupId?: string, subGroupId?: string) => void; showEmptyGroup: boolean; isDragStarted?: boolean; disableIssueCreation?: boolean; @@ -215,7 +210,9 @@ export interface IKanBanSwimLanes { export const KanBanSwimLanes: React.FC = observer((props) => { const { issuesMap, - issueIds, + groupedIssueIds, + getGroupIssueCount, + getPaginationData, displayProperties, sub_group_by, group_by, @@ -268,7 +265,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => {
= observer((props) => { { const sortOrderDefaultValue = 65535; @@ -44,7 +44,7 @@ export const handleDragDrop = async ( subGroupBy: string | null, groupBy: string | null, issueMap: IIssueMap, - issueWithIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined, + issueWithIds: TGroupedIssues | TSubGroupedIssues | undefined, updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined, removeIssue: (projectId: string, issueId: string) => Promise | undefined ) => { diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index dcfd63000..794f3a26c 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -5,13 +5,14 @@ import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; import { useIssues, useUser } from "hooks/store"; -import { TGroupedIssues, TIssue, TUnGroupedIssues } from "@plane/types"; +import { TGroupedIssues, TIssue } from "@plane/types"; // components import { List } from "./default"; import { IQuickActionProps } from "./list-view-types"; import { useIssuesActions } from "hooks/use-issues-actions"; import { IssueLayoutHOC } from "../issue-layout-HOC"; import useSWR from "swr"; +import { ALL_ISSUES } from "store/issue/helpers/base-issues.store"; // constants // hooks @@ -51,14 +52,24 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const { issueMap } = useIssues(); - useSWR(`ISSUE_LIST_LAYOUT_${storeType}`, () => fetchIssues("init-loader", { canGroup: true, perPageCount: 100 }), { - revalidateOnFocus: false, - revalidateOnReconnect: false, - }); + const displayFilters = issuesFilter?.issueFilters?.displayFilters; + const displayProperties = issuesFilter?.issueFilters?.displayProperties; + + 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 issueIds = issues?.groupedIssueIds as TGroupedIssues | undefined; + const groupedIssueIds = issues?.groupedIssueIds as TGroupedIssues | undefined; const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; const canEditProperties = useCallback( @@ -71,12 +82,6 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { [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( (issue: TIssue) => ( { [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); - const loadMoreIssues = useCallback(() => { - fetchNextIssues(); - }, [fetchNextIssues]); + const loadMoreIssues = useCallback( + (groupId?: string) => { + fetchNextIssues(groupId); + }, + [fetchNextIssues] + ); return ( @@ -106,11 +114,12 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { group_by={group_by} updateIssue={updateIssue} quickActions={renderQuickActions} - issueIds={issueIds!} - shouldLoadMore={issues.next_page_results} + groupedIssueIds={groupedIssueIds ?? {}} loadMoreIssues={loadMoreIssues} showEmptyGroup={showEmptyGroup} viewId={viewId} + getPaginationData={issues.getPaginationData} + getGroupIssueCount={issues.getGroupIssueCount} quickAddCallback={issues?.quickAddIssue} enableIssueQuickAdd={!!enableQuickAdd} canEditProperties={canEditProperties} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index c5cb498b3..8dd7bd5f6 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -13,7 +13,9 @@ import { IssueProperties } from "../properties/all-properties"; interface IssueBlockProps { issueId: string; issuesMap: TIssueMap; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; canEditProperties: (projectId: string | undefined) => boolean; diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index a5e0ff53f..836e01b7b 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -3,19 +3,22 @@ import { FC, MutableRefObject } from "react"; import RenderIfVisible from "components/core/render-if-visible-HOC"; import { IssueBlock } from "components/issues"; // 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 { - issueIds: TGroupedIssues | TUnGroupedIssues | any; + issueIds: TGroupedIssues | any; issuesMap: TIssueMap; canEditProperties: (projectId: string | undefined) => boolean; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; containerRef: MutableRefObject; } -export const IssueBlocksList: FC = (props) => { +export const IssueBlocksList: FC = observer((props) => { const { issueIds, issuesMap, updateIssue, quickActions, displayProperties, canEditProperties, containerRef } = props; return ( @@ -47,4 +50,4 @@ export const IssueBlocksList: FC = (props) => { )}
); -}; +}); diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 599974356..06376b37a 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useCallback, useRef } from "react"; // components import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; // hooks @@ -11,26 +11,31 @@ import { TIssue, IIssueDisplayProperties, TIssueMap, - TUnGroupedIssues, IGroupByColumn, - TIssueGroup, + TPaginationData, } from "@plane/types"; import { getGroupByColumns } from "../utils"; import { HeaderGroupByCard } from "./headers/group-by-card"; import { EIssuesStoreType } from "constants/issue"; -import { ArrowDown } from "lucide-react"; import { ListLoaderItemRow } from "components/ui"; 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 { - issueIds: TGroupedIssues; + groupedIssueIds: TGroupedIssues; issuesMap: TIssueMap; group_by: string | null; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; enableIssueQuickAdd: boolean; showEmptyGroup?: boolean; + getPaginationData: (groupId: string | undefined) => TPaginationData | undefined; + getGroupIssueCount: (groupId: string | undefined) => number | undefined; canEditProperties: (projectId: string | undefined) => boolean; quickAddCallback?: ( workspaceSlug: string, @@ -43,13 +48,12 @@ export interface IGroupByList { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; - shouldLoadMore: boolean; - loadMoreIssues: () => void; + loadMoreIssues: (groupId?: string) => void; } -const GroupByList: React.FC = (props) => { +const GroupByList: React.FC = observer((props) => { const { - issueIds, + groupedIssueIds, issuesMap, group_by, updateIssue, @@ -63,7 +67,8 @@ const GroupByList: React.FC = (props) => { disableIssueCreation, storeType, addIssuesToView, - shouldLoadMore, + getPaginationData, + getGroupIssueCount, isCompletedCycle = false, loadMoreIssues, } = props; @@ -123,9 +128,8 @@ const GroupByList: React.FC = (props) => { return preloadedData; }; - const validateEmptyIssueGroups = (issues: TIssueGroup) => { - const issuesCount = issues?.issueCount || 0; - if (!showEmptyGroup && issuesCount <= 0) return false; + const validateEmptyIssueGroups = (issueCount: number = 0) => { + if (!showEmptyGroup && issueCount <= 0) return false; return true; }; @@ -139,17 +143,25 @@ const GroupByList: React.FC = (props) => { {groups && groups.length > 0 && 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 ( - issueGroup && - validateEmptyIssueGroups(issueGroup) && ( + groupIssueIds && + !isNil(groupIssueCount) && + validateEmptyIssueGroups(groupIssueCount) && (
= (props) => { />
- {issueIds && ( + {groupedIssueIds && ( = (props) => { containerRef={containerRef} /> )} - {/* && - issueGroup.issueIds?.length <= issueGroup.issueCount */} {shouldLoadMore && (group_by ? (
loadMoreIssues(_list.id)} > Load more ↓
@@ -199,19 +209,20 @@ const GroupByList: React.FC = (props) => { })}
); -}; +}); export interface IList { - issueIds: TGroupedIssues | TUnGroupedIssues; + groupedIssueIds: TGroupedIssues; issuesMap: TIssueMap; group_by: string | null; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null | undefined, issueId: string, data: Partial) => Promise) + | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; showEmptyGroup: boolean; enableIssueQuickAdd: boolean; canEditProperties: (projectId: string | undefined) => boolean; - shouldLoadMore: boolean; quickAddCallback?: ( workspaceSlug: string, projectId: string, @@ -222,13 +233,15 @@ export interface IList { disableIssueCreation?: boolean; storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; - loadMoreIssues: () => void; + getPaginationData: (groupId: string | undefined) => TPaginationData | undefined; + getGroupIssueCount: (groupId: string | undefined) => number | undefined; + loadMoreIssues: (groupId?: string) => void; isCompletedCycle?: boolean; } export const List: React.FC = (props) => { const { - issueIds, + groupedIssueIds, issuesMap, group_by, updateIssue, @@ -239,10 +252,11 @@ export const List: React.FC = (props) => { showEmptyGroup, enableIssueQuickAdd, canEditProperties, + getPaginationData, + getGroupIssueCount, disableIssueCreation, storeType, addIssuesToView, - shouldLoadMore, loadMoreIssues, isCompletedCycle = false, } = props; @@ -250,10 +264,9 @@ export const List: React.FC = (props) => { return (
= (props) => { showEmptyGroup={showEmptyGroup} canEditProperties={canEditProperties} quickAddCallback={quickAddCallback} + getPaginationData={getPaginationData} + getGroupIssueCount={getGroupIssueCount} viewId={viewId} disableIssueCreation={disableIssueCreation} storeType={storeType} diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 265e15df8..5e886d554 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -17,8 +17,15 @@ import { SpreadsheetLayoutLoader } from "components/ui"; import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; // constants 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 { ALL_ISSUES } from "store/issue/helpers/base-issues.store"; +import { IssueLayoutHOC } from "../issue-layout-HOC"; export const AllIssueLayoutRoot: React.FC = observer(() => { // router @@ -30,7 +37,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const { commandPalette: commandPaletteStore } = useApplication(); const { issuesFilter: { filters, fetchFilters, updateFilters }, - issues: { loader, issueCount: totalIssueCount, groupedIssueIds, fetchIssues, fetchNextIssues }, + issues: { loader, getPaginationData, groupedIssueIds, fetchIssues, fetchNextIssues }, } = useIssues(EIssuesStoreType.GLOBAL); const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); @@ -153,53 +160,28 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { return ; } - const { - "All Issues": { issueIds, issueCount }, - } = groupedIssueIds; + const issueIds = groupedIssueIds[ALL_ISSUES]; + const nextPageResults = getPaginationData(ALL_ISSUES)?.nextPageResults; const emptyStateType = (workspaceProjectIds ?? []).length > 0 ? `workspace-${globalViewId}` : EmptyStateType.WORKSPACE_NO_PROJECTS; return ( -
-
- - {!totalIssueCount ? ( - 0 - ? globalViewId !== "custom-view" && globalViewId !== "subscribed" - ? () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - } - : undefined - : () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - } - } - /> - ) : ( - - - {/* peek overview */} - - - )} -
-
+ + + {/* peek overview */} + + ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 6c2a85213..3f1008300 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -9,11 +9,12 @@ import { useIssuesActions } from "hooks/use-issues-actions"; // views // types // constants -import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; +import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; import { SpreadsheetView } from "./spreadsheet-view"; import useSWR from "swr"; import { IssueLayoutHOC } from "../issue-layout-HOC"; +import { ALL_ISSUES } from "store/issue/helpers/base-issues.store"; export type SpreadsheetStoreType = | EIssuesStoreType.PROJECT @@ -72,7 +73,8 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { [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( (updatedDisplayFilter: Partial) => { @@ -118,7 +120,8 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { viewId={viewId} enableQuickCreateIssue={enableQuickAdd} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} - onEndOfListTrigger={fetchNextIssues} + canLoadMoreIssues={!!nextPageResults} + loadMoreIssues={fetchNextIssues} /> ); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx index 52de26d6a..26991beed 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx @@ -25,7 +25,8 @@ type Props = { canEditProperties: (projectId: string | undefined) => boolean; portalElement: React.MutableRefObject; containerRef: MutableRefObject; - onEndOfListTrigger: () => void; + canLoadMoreIssues: boolean; + loadMoreIssues: () => void; }; export const SpreadsheetTable = observer((props: Props) => { @@ -39,8 +40,9 @@ export const SpreadsheetTable = observer((props: Props) => { quickActions, updateIssue, canEditProperties, + canLoadMoreIssues, containerRef, - onEndOfListTrigger, + loadMoreIssues, } = props; // states @@ -80,27 +82,7 @@ export const SpreadsheetTable = observer((props: Props) => { }; }, [handleScroll, containerRef]); - // useEffect(() => { - // 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%`); + useIntersectionObserver(containerRef, intersectionRef, loadMoreIssues, `50% 0% 50% 0%`); const handleKeyBoardNavigation = useTableKeyboardNavigation(); @@ -130,7 +112,7 @@ export const SpreadsheetTable = observer((props: Props) => { /> ))} - Loading... + {canLoadMoreIssues && Loading...} ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index a2873776b..f23850f73 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -31,7 +31,8 @@ type Props = { ) => Promise; viewId?: string; canEditProperties: (projectId: string | undefined) => boolean; - onEndOfListTrigger: () => void; + canLoadMoreIssues: boolean; + loadMoreIssues: () => void; enableQuickCreateIssue?: boolean; disableIssueCreation?: boolean; }; @@ -49,7 +50,8 @@ export const SpreadsheetView: React.FC = observer((props) => { canEditProperties, enableQuickCreateIssue, disableIssueCreation, - onEndOfListTrigger, + canLoadMoreIssues, + loadMoreIssues, } = props; // refs const containerRef = useRef(null); @@ -81,7 +83,8 @@ export const SpreadsheetView: React.FC = observer((props) => { updateIssue={updateIssue} canEditProperties={canEditProperties} containerRef={containerRef} - onEndOfListTrigger={onEndOfListTrigger} + canLoadMoreIssues={canLoadMoreIssues} + loadMoreIssues={loadMoreIssues} />
diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 21bc8fb6a..20ca9a1e1 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -436,7 +436,7 @@ export const groupReactionEmojis = (reactions: any) => { }; -export enum IssueGroupByOptions { +export enum EIssueGroupByToServerOptions { "state" = "state_id", "priority" = "priority", "labels" = "labels__id", @@ -448,3 +448,16 @@ export enum IssueGroupByOptions { "project" = "project_id", "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", +} \ No newline at end of file diff --git a/web/hooks/use-issues-actions.tsx b/web/hooks/use-issues-actions.tsx index b4be6d3fe..a41ceee5d 100644 --- a/web/hooks/use-issues-actions.tsx +++ b/web/hooks/use-issues-actions.tsx @@ -18,7 +18,7 @@ interface IssueActions { options: IssuePaginationOptions, userViewId?: "assigned" | "created" | "subscribed" ) => Promise; - fetchNextIssues: () => Promise; + fetchNextIssues: (groupId?: string, subGroupId?: string) => Promise; removeIssue: (projectId: string | undefined | null, issueId: string) => Promise; createIssue?: (projectId: string | undefined | null, data: Partial) => Promise; updateIssue?: (projectId: string | undefined | null, issueId: string, data: Partial) => Promise; @@ -77,10 +77,13 @@ const useProjectIssueActions = () => { }, [issues.fetchIssues, workspaceSlug, projectId] ); - const fetchNextIssues = useCallback(async () => { - if (!workspaceSlug || !projectId) return; - return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString()); - }, [issues.fetchIssues, workspaceSlug, projectId]); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !projectId) return; + return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId); + }, + [issues.fetchIssues, workspaceSlug, projectId] + ); const createIssue = useCallback( async (projectId: string | undefined | null, data: Partial) => { @@ -151,10 +154,19 @@ const useCycleIssueActions = () => { }, [issues.fetchIssues, workspaceSlug, projectId, cycleId] ); - const fetchNextIssues = useCallback(async () => { - if (!workspaceSlug || !projectId || !cycleId) return; - return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); - }, [issues.fetchIssues, workspaceSlug, projectId, cycleId]); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !projectId || !cycleId) return; + return issues.fetchNextIssues( + workspaceSlug.toString(), + projectId.toString(), + cycleId.toString(), + groupId, + subGroupId + ); + }, + [issues.fetchIssues, workspaceSlug, projectId, cycleId] + ); const createIssue = useCallback( async (projectId: string | undefined | null, data: Partial) => { @@ -242,10 +254,19 @@ const useModuleIssueActions = () => { }, [issues.fetchIssues, workspaceSlug, projectId, moduleId] ); - const fetchNextIssues = useCallback(async () => { - if (!workspaceSlug || !projectId || !moduleId) return; - return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); - }, [issues.fetchIssues, workspaceSlug, projectId, moduleId]); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !projectId || !moduleId) return; + return issues.fetchNextIssues( + workspaceSlug.toString(), + projectId.toString(), + moduleId.toString(), + groupId, + subGroupId + ); + }, + [issues.fetchIssues, workspaceSlug, projectId, moduleId] + ); const createIssue = useCallback( async (projectId: string | undefined | null, data: Partial) => { @@ -324,10 +345,13 @@ const useProfileIssueActions = () => { }, [issues.fetchIssues, workspaceSlug, userId] ); - const fetchNextIssues = useCallback(async () => { - if (!workspaceSlug || !userId) return; - return issues.fetchNextIssues(workspaceSlug.toString(), userId.toString()); - }, [issues.fetchIssues, workspaceSlug, userId]); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !userId) return; + return issues.fetchNextIssues(workspaceSlug.toString(), userId.toString(), groupId, subGroupId); + }, + [issues.fetchIssues, workspaceSlug, userId] + ); const createIssue = useCallback( async (projectId: string | undefined | null, data: Partial) => { @@ -398,10 +422,13 @@ const useProjectViewIssueActions = () => { }, [issues.fetchIssues, workspaceSlug, projectId] ); - const fetchNextIssues = useCallback(async () => { - if (!workspaceSlug || !projectId) return; - return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString()); - }, [issues.fetchIssues, workspaceSlug, projectId]); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !projectId) return; + return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId); + }, + [issues.fetchIssues, workspaceSlug, projectId] + ); const createIssue = useCallback( async (projectId: string | undefined | null, data: Partial) => { @@ -472,10 +499,13 @@ const useDraftIssueActions = () => { }, [issues.fetchIssues, workspaceSlug, projectId] ); - const fetchNextIssues = useCallback(async () => { - if (!workspaceSlug || !projectId) return; - return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString()); - }, [issues.fetchIssues, workspaceSlug, projectId]); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !projectId) return; + return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId); + }, + [issues.fetchIssues, workspaceSlug, projectId] + ); const createIssue = useCallback( async (projectId: string | undefined | null, data: Partial) => { @@ -538,10 +568,13 @@ const useArchivedIssueActions = () => { }, [issues.fetchIssues, workspaceSlug, projectId] ); - const fetchNextIssues = useCallback(async () => { - if (!workspaceSlug || !projectId) return; - return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString()); - }, [issues.fetchIssues, workspaceSlug, projectId]); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !projectId) return; + return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId); + }, + [issues.fetchIssues, workspaceSlug, projectId] + ); const removeIssue = useCallback( async (projectId: string | undefined | null, issueId: string) => { @@ -595,10 +628,13 @@ const useGlobalIssueActions = () => { }, [issues.fetchIssues, workspaceSlug, globalViewId] ); - const fetchNextIssues = useCallback(async () => { - if (!workspaceSlug || !globalViewId) return; - return issues.fetchNextIssues(workspaceSlug.toString(), globalViewId.toString()); - }, [issues.fetchIssues, workspaceSlug, globalViewId]); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !globalViewId) return; + return issues.fetchNextIssues(workspaceSlug.toString(), globalViewId.toString(), groupId, subGroupId); + }, + [issues.fetchIssues, workspaceSlug, globalViewId] + ); const createIssue = useCallback( async (projectId: string | undefined | null, data: Partial) => { diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index 461477531..aa1987ddb 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -28,7 +28,9 @@ export interface IArchivedIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor: string | undefined + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string) => Promise; @@ -94,21 +96,19 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc return filteredRouteParams; } - getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { - const filterParams = this.appliedFilters; + getFilterParams = computedFn( + ( + options: IssuePaginationOptions, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.appliedFilters; - const paginationOptions: Partial> = { - ...filterParams, - cursor: cursor ? cursor : `${options.perPageCount}:0:0`, - per_page: options.perPageCount.toString(), - }; - - if (options.groupedBy) { - paginationOptions.group_by = options.groupedBy; + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; } - - return paginationOptions; - }); + ); fetchFilters = async (workspaceSlug: string, projectId: string) => { try { diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index 96bb4040e..0252742ed 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -6,7 +6,7 @@ import { TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse } from "@pl // types import { IIssueRootStore } from "../root.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 { // observable @@ -23,7 +23,12 @@ export interface IArchivedIssues extends IBaseIssuesStore { projectId: string, loadType: TLoader ) => Promise; - fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + projectId: string, + groupId?: string, + subGroupId?: string + ) => Promise; restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -66,7 +71,7 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues { this.loader = loadType; }); 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); this.onfetchIssues(response, options); @@ -77,15 +82,21 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues { } }; - fetchNextIssues = async (workspaceSlug: string, projectId: string) => { - if (!this.paginationOptions || !this.next_page_results) return; + fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => { + const cursorObject = this.getPaginationData(subGroupId ?? groupId); + if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; try { this.loader = "pagination"; - const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); - const response = await this.issueService.getIssues(workspaceSlug, projectId, params); + const params = this.issueFilterStore?.getFilterParams( + 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; } catch (error) { this.loader = undefined; @@ -110,7 +121,7 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues { this.rootIssueStore.issues.updateIssue(issueId, { archived_at: null, }); - this.issues && pull(this.issues, issueId); + this.removeIssueFromList(issueId); }); return response; diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index 753413a27..c16013772 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -28,7 +28,9 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor: string | undefined + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; @@ -97,21 +99,19 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI return filteredRouteParams; } - getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { - const filterParams = this.appliedFilters; + getFilterParams = computedFn( + ( + options: IssuePaginationOptions, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.appliedFilters; - const paginationOptions: Partial> = { - ...filterParams, - cursor: cursor ? cursor : `${options.perPageCount}:0:0`, - per_page: options.perPageCount.toString(), - }; - - if (options.groupedBy) { - paginationOptions.group_by = options.groupedBy; + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; } - - return paginationOptions; - }); + ); fetchFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => { try { diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 43e3fed35..275022fa3 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -9,7 +9,7 @@ import { CycleService } from "services/cycle.service"; // types import { TIssue, TLoader, ViewFlags, IssuePaginationOptions, TIssuesResponse } from "@plane/types"; 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"; export const ACTIVE_CYCLE_ISSUES = "ACTIVE_CYCLE_ISSUES"; @@ -30,7 +30,13 @@ export interface ICycleIssues extends IBaseIssuesStore { loadType: TLoader, cycleId: string ) => Promise; - fetchNextIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + projectId: string, + cycleId: string, + groupId?: string, + subGroupId?: string + ) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial, cycleId: string) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; @@ -111,7 +117,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues { 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); this.onfetchIssues(response, options); @@ -122,15 +128,27 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues { } }; - fetchNextIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => { - if (!this.paginationOptions || !this.next_page_results) return; + fetchNextIssues = async ( + workspaceSlug: string, + projectId: string, + cycleId: string, + groupId?: string, + subGroupId?: string + ) => { + const cursorObject = this.getPaginationData(subGroupId ?? groupId); + if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; try { 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); - this.onfetchNexIssues(response); + this.onfetchNexIssues(response, groupId, subGroupId); return response; } catch (error) { this.loader = undefined; @@ -175,8 +193,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues { if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); runInAction(() => { - this.cycleId === cycleId && - update(this, "issues", (cycleIssueIds = []) => uniq(concat(cycleIssueIds, issueIds))); + this.cycleId === cycleId && issueIds.forEach((issueId) => this.addIssueToList(issueId)); }); issueIds.forEach((issueId) => { @@ -193,7 +210,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues { try { await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); runInAction(() => { - this.issues && this.cycleId === cycleId && pull(this.issues, issueId); + this.cycleId === cycleId && this.removeIssueFromList(issueId); }); this.rootIssueStore.issues.updateIssue(issueId, { cycle_id: null }); diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index fb92aa3e9..07cdd069c 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -28,7 +28,9 @@ export interface IDraftIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor: string | undefined + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string) => Promise; @@ -94,25 +96,19 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI return filteredRouteParams; } - getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { - const filterParams = this.appliedFilters; + getFilterParams = computedFn( + ( + options: IssuePaginationOptions, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.appliedFilters; - const paginationOptions: Partial> = { - ...filterParams, - cursor: cursor ? cursor : `${options.perPageCount}:0:0`, - per_page: options.perPageCount.toString(), - }; - - if (options.groupedBy) { - paginationOptions.group_by = options.groupedBy; + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; } - - if (options.after && options.before) { - paginationOptions["target_date"] = `${options.after};after,${options.before};before`; - } - - return paginationOptions; - }); + ); fetchFilters = async (workspaceSlug: string, projectId: string) => { try { diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index e4f7330ee..e55162d83 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -24,7 +24,12 @@ export interface IDraftIssues extends IBaseIssuesStore { loadType: TLoader ) => Promise; - fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + projectId: string, + groupId?: string, + subGroupId?: string + ) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; @@ -63,7 +68,7 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues { this.loader = loadType; }); 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); this.onfetchIssues(response, options); @@ -74,15 +79,21 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues { } }; - fetchNextIssues = async (workspaceSlug: string, projectId: string) => { - if (!this.paginationOptions || !this.next_page_results) return; + fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => { + const cursorObject = this.getPaginationData(subGroupId ?? groupId); + if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; try { this.loader = "pagination"; - const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor); - const response = await this.issueService.getIssues(workspaceSlug, projectId, params); + const params = this.issueFilterStore?.getFilterParams( + 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; } catch (error) { this.loader = undefined; diff --git a/web/store/issue/helpers/base-issues.store.ts b/web/store/issue/helpers/base-issues.store.ts index 0f97a8e0e..2b2e90869 100644 --- a/web/store/issue/helpers/base-issues.store.ts +++ b/web/store/issue/helpers/base-issues.store.ts @@ -4,81 +4,68 @@ import uniq from "lodash/uniq"; import concat from "lodash/concat"; import pull from "lodash/pull"; import orderBy from "lodash/orderBy"; -import get from "lodash/get"; +import clone from "lodash/clone"; import indexOf from "lodash/indexOf"; import isEmpty from "lodash/isEmpty"; -import values from "lodash/values"; // types import { TIssue, - TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions, TGroupedIssues, TSubGroupedIssues, - TUnGroupedIssues, TLoader, IssuePaginationOptions, TIssuesResponse, + TIssues, + TIssuePaginationData, + TGroupedIssueCount, + TPaginationData, } from "@plane/types"; import { IIssueRootStore } from "../root.store"; import { IBaseIssueFilterStore } from "./issue-filter-helper.store"; // constants -import { EIssueLayoutTypes, ISSUE_PRIORITIES } from "constants/issue"; +import { ISSUE_PRIORITIES } from "constants/issue"; import { STATE_GROUPS } from "constants/state"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // services import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue"; +import set from "lodash/set"; +import { get } from "lodash"; +import { computedFn } from "mobx-utils"; export type TIssueDisplayFilterOptions = Exclude | "target_date"; +export enum EIssueGroupedAction { + ADD, + DELETE, +} + +export const ALL_ISSUES = "All Issues"; + export interface IBaseIssuesStore { // observable loader: TLoader; - issues: string[] | undefined; - - nextCursor: string | undefined; - prevCursor: string | undefined; - issueCount: number | undefined; - pageCount: number | undefined; - - next_page_results: boolean; - prev_page_results: boolean; - - groupedIssueCount: Record | undefined; - - // computed - groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; + groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined; + groupedIssueCount: TGroupedIssueCount; + issuePaginationData: TIssuePaginationData; //actions removeIssue(workspaceSlug: string, projectId: string, issueId: string): Promise; // helper methods - groupedIssues( - groupBy: TIssueDisplayFilterOptions, - orderBy: TIssueOrderByOptions, - issues: TIssueMap, - groupedIssueCount: Record, - isCalendarIssues?: boolean - ): TGroupedIssues; - subGroupedIssues( - subGroupBy: TIssueDisplayFilterOptions, - groupBy: TIssueDisplayFilterOptions, - orderBy: TIssueOrderByOptions, - issues: TIssueMap, - groupedIssueCount: Record - ): TSubGroupedIssues; - unGroupedIssues(orderBy: TIssueOrderByOptions, issues: TIssueMap, count: number): TUnGroupedIssues; issueDisplayFiltersDefaultData(groupBy: string | null): string[]; - issuesSortWithOrderBy(issueObject: TIssueMap, key: Partial): TIssue[]; + issuesSortWithOrderBy(issueIds: string[], key: Partial): string[]; getGroupArray(value: boolean | number | string | string[] | null, isDate?: boolean): string[]; + getPaginationData(groupId: string | undefined): TPaginationData | undefined; + getGroupIssueCount: (groupId: string | undefined) => number | undefined; } const ISSUE_FILTER_DEFAULT_DATA: Record = { project: "project_id", state: "state_id", - "state_detail.group": "state_group" as keyof TIssue, // state_detail.group is only being used for state_group display, + "state_detail.group": "state_id" as keyof TIssue, // state_detail.group is only being used for state_group display, priority: "priority", labels: "label_ids", created_by: "created_by", @@ -90,17 +77,10 @@ const ISSUE_FILTER_DEFAULT_DATA: Record | undefined = undefined; + groupedIssueIds: TIssues | undefined = undefined; + issuePaginationData: TIssuePaginationData = {}; - nextCursor: string | undefined = undefined; - prevCursor: string | undefined = undefined; - - issueCount: number | undefined = undefined; - pageCount: number | undefined = undefined; - - next_page_results: boolean = true; - prev_page_results: boolean = false; + groupedIssueCount: TGroupedIssueCount = {}; paginationOptions: IssuePaginationOptions | undefined = undefined; @@ -118,25 +98,24 @@ export class BaseIssuesStore implements IBaseIssuesStore { makeObservable(this, { // observable loader: observable.ref, + groupedIssueIds: observable, + issuePaginationData: observable, groupedIssueCount: observable, - issues: observable, - - nextCursor: observable.ref, - prevCursor: observable.ref, - issueCount: observable.ref, - pageCount: observable.ref, - next_page_results: observable.ref, - prev_page_results: observable.ref, paginationOptions: observable, // computed - groupedIssueIds: computed, + orderBy: computed, + groupBy: computed, + subGroupBy: computed, + issueGroupKey: computed, + issueSubGroupKey: computed, // action storePreviousPaginationValues: action.bound, onfetchIssues: action.bound, onfetchNexIssues: action.bound, clear: action.bound, + getPaginationData: action.bound, createIssue: action, updateIssue: action, @@ -154,60 +133,48 @@ export class BaseIssuesStore implements IBaseIssuesStore { this.issueDraftService = new IssueDraftService(); } - storePreviousPaginationValues = (issuesResponse: TIssuesResponse, options?: IssuePaginationOptions) => { - if (options) this.paginationOptions = options; - - this.nextCursor = issuesResponse.next_cursor; - this.prevCursor = issuesResponse.prev_cursor; - - this.issueCount = issuesResponse.count; - this.pageCount = issuesResponse.total_pages; - - this.next_page_results = issuesResponse.next_page_results; - this.prev_page_results = issuesResponse.prev_page_results; - }; - - get groupedIssueIds() { + get orderBy() { const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters; - if (!displayFilters) return undefined; + if (!displayFilters) return; - const subGroupBy = displayFilters?.sub_group_by; - const groupBy = displayFilters?.group_by; - const orderBy = displayFilters?.order_by; - const layout = displayFilters?.layout; + return displayFilters?.order_by; + } - if (!this.issues) return; + get groupBy() { + const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters; + if (!displayFilters) return; - const currentIssues = this.rootIssueStore.issues.getIssuesByIds( - this.issues, - this.isArchived ? "archived" : "un-archived" - ); - if (!currentIssues) return { "All Issues": { issueIds: [], issueCount: 0 } }; + return displayFilters?.group_by; + } - let groupedIssues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = {}; + get subGroupBy() { + const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters; + if (!displayFilters || displayFilters.group_by === displayFilters.sub_group_by) return; - if (layout === EIssueLayoutTypes.LIST && orderBy) { - if (groupBy) groupedIssues = this.groupedIssues(groupBy, orderBy, currentIssues, this.groupedIssueCount); - else groupedIssues = this.unGroupedIssues(orderBy, currentIssues, this.issueCount); - } else if (layout === EIssueLayoutTypes.KANBAN && groupBy && orderBy) { - if (subGroupBy) - groupedIssues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, currentIssues, this.groupedIssueCount); - else groupedIssues = this.groupedIssues(groupBy, orderBy, currentIssues, this.groupedIssueCount); - } else if (layout === EIssueLayoutTypes.CALENDAR) - groupedIssues = this.groupedIssues("target_date", "target_date", currentIssues, this.groupedIssueCount, true); - else if (layout === EIssueLayoutTypes.SPREADSHEET) - groupedIssues = this.unGroupedIssues(orderBy ?? "-created_at", currentIssues, this.issueCount); - else if (layout === EIssueLayoutTypes.GANTT) - groupedIssues = this.unGroupedIssues(orderBy ?? "sort_order", currentIssues, this.issueCount); + return displayFilters?.sub_group_by; + } - return groupedIssues; + get issueGroupKey() { + const groupBy = this.groupBy; + + if (!groupBy) return; + + return ISSUE_FILTER_DEFAULT_DATA[groupBy]; + } + + get issueSubGroupKey() { + const subGroupBy = this.subGroupBy; + + if (!subGroupBy) return; + + return ISSUE_FILTER_DEFAULT_DATA[subGroupBy]; } onfetchIssues(issuesResponse: TIssuesResponse, options: IssuePaginationOptions) { - const { issueList, groupedIssueCount } = this.processIssueResponse(issuesResponse); + const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse); runInAction(() => { - this.issues = issueList.map((issue) => issue.id); + this.groupedIssueIds = groupedIssues; this.groupedIssueCount = groupedIssueCount; this.loader = undefined; }); @@ -217,22 +184,68 @@ export class BaseIssuesStore implements IBaseIssuesStore { this.storePreviousPaginationValues(issuesResponse, options); } - onfetchNexIssues(issuesResponse: TIssuesResponse) { - const { issueList, groupedIssueCount } = this.processIssueResponse(issuesResponse); - const newIssueIds = issueList.map((issue) => issue.id); - - runInAction(() => { - update(this, "issues", (issueIds: string[] = []) => { - return uniq(concat(issueIds, newIssueIds)); - }); - - this.groupedIssueCount = groupedIssueCount; - this.loader = undefined; - }); + onfetchNexIssues(issuesResponse: TIssuesResponse, groupId?: string, subGroupId?: string) { + const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse); this.rootIssueStore.issues.addIssue(issueList); - this.storePreviousPaginationValues(issuesResponse); + runInAction(() => { + this.updateGroupedIssueIds(groupedIssues, groupedIssueCount, groupId, subGroupId); + this.loader = undefined; + }); + + this.storePreviousPaginationValues(issuesResponse, undefined, groupId, subGroupId); + } + + updateGroupedIssueIds( + groupedIssues: TIssues, + groupedIssueCount: TGroupedIssueCount, + groupId?: string, + subGroupId?: string + ) { + if (groupId && groupedIssues[ALL_ISSUES] && Array.isArray(groupedIssues[ALL_ISSUES])) { + const issueGroup = groupedIssues[ALL_ISSUES]; + const issueGroupCount = groupedIssueCount[ALL_ISSUES]; + const issuesPath = [groupId]; + + if (!subGroupId) { + set(this.groupedIssueCount, [groupId], issueGroupCount); + } + + this.updateIssueGroup(issueGroup, issuesPath); + return; + } + + for (const groupId in groupedIssues) { + const issueGroup = groupedIssues[groupId]; + const issueGroupCount = groupedIssueCount[groupId]; + + set(this.groupedIssueCount, [groupId], issueGroupCount); + const shouldContinue = this.updateIssueGroup(issueGroup, [groupId]); + if (shouldContinue) continue; + + for (const subGroupId in issueGroup) { + const issueSubGroup = (issueGroup as TGroupedIssues)[subGroupId]; + const issueSubGroupCount = groupedIssueCount[subGroupId]; + + set(this.groupedIssueCount, [subGroupId], issueSubGroupCount); + this.updateIssueGroup(issueSubGroup, [groupId, subGroupId]); + } + } + } + + updateIssueGroup(groupedIssueIds: TGroupedIssues | string[], issuePath: string[]): boolean { + if (!groupedIssueIds) return true; + + if (groupedIssueIds && Array.isArray(groupedIssueIds)) { + update(this, ["groupedIssueIds", ...issuePath], (issueIds: string[] = []) => { + return this.issuesSortWithOrderBy(uniq(concat(issueIds, groupedIssueIds as string[])), this.orderBy); + }); + + return true; + } + + return false; } async createIssue( @@ -254,13 +267,15 @@ export class BaseIssuesStore implements IBaseIssuesStore { } async updateIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial) { - const issueBeforeUpdate = { ...this.rootIssueStore.issues.getIssueById(issueId) }; + const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId)); try { this.rootIssueStore.issues.updateIssue(issueId, data); + this.updateIssueList({ ...issueBeforeUpdate, ...data } as TIssue, issueBeforeUpdate); await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); } catch (error) { - this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate); + this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate ?? {}); + this.updateIssueList(issueBeforeUpdate, { ...issueBeforeUpdate, ...data } as TIssue); throw error; } } @@ -278,13 +293,15 @@ export class BaseIssuesStore implements IBaseIssuesStore { } async updateDraftIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial) { - const issueBeforeUpdate = { ...this.rootIssueStore.issues.getIssueById(issueId) }; + const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId)); try { this.rootIssueStore.issues.updateIssue(issueId, data); + this.updateIssueList({ ...issueBeforeUpdate, ...data } as TIssue, issueBeforeUpdate); await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data); } catch (error) { - this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate); + this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate ?? {}); + this.updateIssueList(issueBeforeUpdate, { ...issueBeforeUpdate, ...data } as TIssue); throw error; } } @@ -294,7 +311,7 @@ export class BaseIssuesStore implements IBaseIssuesStore { await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); runInAction(() => { - if (this.issues) pull(this.issues, issueId); + this.removeIssueFromList(issueId); }); this.rootIssueStore.issues.removeIssue(issueId); @@ -311,7 +328,7 @@ export class BaseIssuesStore implements IBaseIssuesStore { this.rootIssueStore.issues.updateIssue(issueId, { archived_at: response.archived_at, }); - if (this.issues) pull(this.issues, issueId); + this.removeIssueFromList(issueId); }); } catch (error) { throw error; @@ -319,7 +336,6 @@ export class BaseIssuesStore implements IBaseIssuesStore { } async issueQuickAdd(workspaceSlug: string, projectId: string, data: TIssue) { - if (!this.issues) this.issues = []; try { this.addIssue(data); @@ -328,25 +344,20 @@ export class BaseIssuesStore implements IBaseIssuesStore { } catch (error) { throw error; } finally { - if (!this.issues) return; - const quickAddIssueIndex = this.issues.findIndex((currentIssueId) => currentIssueId === data.id); - if (quickAddIssueIndex >= 0) - runInAction(() => { - this.issues!.splice(quickAddIssueIndex, 1); - this.rootIssueStore.issues.removeIssue(data.id); - }); + runInAction(() => { + this.removeIssueFromList(data.id); + this.rootIssueStore.issues.removeIssue(data.id); + }); } } async removeBulkIssues(workspaceSlug: string, projectId: string, issueIds: string[]) { try { - if (!this.issues) return; - const response = await this.issueService.bulkDeleteIssues(workspaceSlug, projectId, { issue_ids: issueIds }); runInAction(() => { issueIds.forEach((issueId) => { - pull(this.issues!, issueId); + this.removeIssueFromList(issueId); this.rootIssueStore.issues.removeIssue(issueId); }); }); @@ -358,120 +369,210 @@ export class BaseIssuesStore implements IBaseIssuesStore { addIssue(issue: TIssue) { runInAction(() => { - if (!this.issues) this.issues = []; - this.issues.push(issue.id); this.rootIssueStore.issues.addIssue([issue]); }); + + this.updateIssueList(issue, undefined, EIssueGroupedAction.ADD); } clear() { runInAction(() => { - this.issues = undefined; - this.groupedIssueCount = undefined; - this.groupedIssueCount = undefined; - - this.nextCursor = undefined; - this.prevCursor = undefined; - - this.issueCount = undefined; - this.pageCount = undefined; - + this.groupedIssueIds = undefined; + this.issuePaginationData = {}; + this.groupedIssueCount = {}; this.paginationOptions = undefined; }); } - groupedIssues = ( - groupBy: TIssueDisplayFilterOptions, - orderBy: TIssueOrderByOptions, - issues: TIssueMap, - groupedIssueCount: Record | undefined, - isCalendarIssues: boolean = false - ) => { - const currentIssues: TGroupedIssues = {}; - if (!groupBy || !groupedIssueCount) return currentIssues; + addIssueToList(issueId: string) { + const issue = this.rootIssueStore.issues.getIssueById(issueId); + this.updateIssueList(issue, undefined, EIssueGroupedAction.ADD); + } - this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { - currentIssues[group] = { issueIds: [], issueCount: groupedIssueCount[group] }; - }); + removeIssueFromList(issueId: string) { + const issue = this.rootIssueStore.issues.getIssueById(issueId); + this.updateIssueList(issue, undefined, EIssueGroupedAction.DELETE); + } - const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); + updateIssueList(issue?: TIssue, issueBeforeUpdate?: TIssue, action?: EIssueGroupedAction) { + if (!issue) return; + const issueUpdates = this.getUpdateDetails(issue, issueBeforeUpdate, action); + runInAction(() => { + for (const issueUpdate of issueUpdates) { + //if update is add, add it at a particular path + if (issueUpdate.action === EIssueGroupedAction.ADD) { + update(this, ["groupedIssueIds", ...issueUpdate.path], (issueIds: string[] = []) => { + return this.issuesSortWithOrderBy(uniq(concat(issueIds, issue.id)), this.orderBy); + }); + this.updateIssueCount(issueUpdate.path, 1); + } - for (const issue in projectIssues) { - const currentIssue = projectIssues[issue]; - let groupArray = []; - - if (groupBy === "state_detail.group" && currentIssue?.state_id) { - // if groupBy state_detail.group is coming from the project level the we are using stateDetails from root store else we are looping through the stateMap - const state_group = (this.rootIssueStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None"; - groupArray = [state_group]; - } else { - const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); - groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : ["None"]; - } - - for (const group of groupArray) { - if (!currentIssues[group]) currentIssues[group] = { issueIds: [], issueCount: groupedIssueCount[group] }; - if (group) currentIssues[group].issueIds.push(currentIssue.id); - } - } - - return currentIssues; - }; - - subGroupedIssues = ( - subGroupBy: TIssueDisplayFilterOptions, - groupBy: TIssueDisplayFilterOptions, - orderBy: TIssueOrderByOptions, - issues: TIssueMap, - groupedIssueCount: Record | undefined - ) => { - const currentIssues: TSubGroupedIssues = {}; - if (!subGroupBy || !groupBy|| !groupedIssueCount) return currentIssues; - - this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group) => { - const groupByIssues: TGroupedIssues = {}; - this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { - groupByIssues[group] = { issueIds: [], issueCount: groupedIssueCount[group] }; - }); - currentIssues[sub_group] = groupByIssues; - }); - - const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); - - for (const issue in projectIssues) { - const currentIssue = projectIssues[issue]; - let subGroupArray = []; - let groupArray = []; - if ((subGroupBy === "state_detail.group" || groupBy === "state_detail.group") && currentIssue?.state_id) { - const state_group = (this.rootIssueStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None"; - - subGroupArray = [state_group]; - groupArray = [state_group]; - } else { - const subGroupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]); - const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); - - subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : ["None"]; - groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : ["None"]; - } - - for (const subGroup of subGroupArray) { - for (const group of groupArray) { - if (subGroup && group && currentIssues?.[subGroup]?.[group]) currentIssues[subGroup][group].issueIds.push(currentIssue.id); - else if (subGroup && group && currentIssues[subGroup]) currentIssues[subGroup][group].issueIds = [currentIssue.id]; - else if (subGroup && group) - currentIssues[subGroup] = { [group]: { issueIds: [currentIssue.id], issueCount: groupedIssueCount[group] } }; + //if update is delete, remove it at a particular path + if (issueUpdate.action === EIssueGroupedAction.DELETE) { + update(this, ["groupedIssueIds", ...issueUpdate.path], (issueIds: string[] = []) => { + return pull(issueIds, issue.id); + }); + this.updateIssueCount(issueUpdate.path, -1); } } + }); + } + + updateIssueCount(path: string[], increment: number) { + const [groupId, subGroupId] = path; + + if (subGroupId) { + const subGroupIssueCount = get(this.groupedIssueCount, [subGroupId]); + + set(this.groupedIssueCount, [subGroupId], subGroupIssueCount + increment); } - return currentIssues; + if (groupId) { + const groupIssueCount = get(this.groupedIssueCount, [groupId]); + + set(this.groupedIssueCount, [groupId], groupIssueCount + increment); + } + + if (groupId !== ALL_ISSUES) { + const totalIssueCount = get(this.groupedIssueCount, [ALL_ISSUES]); + + set(this.groupedIssueCount, [ALL_ISSUES], totalIssueCount + increment); + } + } + + getUpdateDetails = ( + issue?: Partial, + issueBeforeUpdate?: Partial, + action?: EIssueGroupedAction + ): { path: string[]; action: EIssueGroupedAction }[] => { + if (!this.issueGroupKey || !issue) return action ? [{ path: [ALL_ISSUES], action }] : []; + + const groupActionsArray = this.getDifference( + this.getArrayStringArray(issue[this.issueGroupKey], this.groupBy), + this.getArrayStringArray(issueBeforeUpdate?.[this.issueGroupKey], this.groupBy) + ); + + if (!this.issueSubGroupKey) + return this.getGroupIssueKeyActions( + groupActionsArray[EIssueGroupedAction.ADD], + groupActionsArray[EIssueGroupedAction.DELETE] + ); + + const subGroupActionsArray = this.getDifference( + this.getArrayStringArray(issue[this.issueSubGroupKey], this.subGroupBy), + this.getArrayStringArray(issueBeforeUpdate?.[this.issueSubGroupKey], this.subGroupBy) + ); + + return this.getSubGroupIssueKeyActions( + groupActionsArray, + subGroupActionsArray, + this.getArrayStringArray(issueBeforeUpdate?.[this.issueGroupKey] ?? issue[this.issueGroupKey], this.groupBy), + this.getArrayStringArray(issue[this.issueGroupKey], this.groupBy), + this.getArrayStringArray( + issueBeforeUpdate?.[this.issueSubGroupKey] ?? issue[this.issueSubGroupKey], + this.subGroupBy + ), + this.getArrayStringArray(issue[this.issueSubGroupKey], this.subGroupBy) + ); }; - unGroupedIssues = (orderBy: TIssueOrderByOptions, issues: TIssueMap, count: number | undefined) => { - const issueIds = this.issuesSortWithOrderBy(issues, orderBy).map((issue) => issue.id); + getArrayStringArray = ( + value: string | string[] | undefined | null, + groupByKey: TIssueGroupByOptions | undefined + ): string[] => { + if (!value) return []; + if (Array.isArray(value)) return value; - return { "All Issues": { issueIds, issueCount: count || issueIds.length } }; + if (groupByKey === "state_detail.group") { + return [this.rootIssueStore.rootStore.state.stateMap?.[value]?.group]; + } + + return [value]; + }; + + getSubGroupIssueKeyActions = ( + groupActionsArray: { + [EIssueGroupedAction.ADD]: string[]; + [EIssueGroupedAction.DELETE]: string[]; + }, + subGroupActionsArray: { + [EIssueGroupedAction.ADD]: string[]; + [EIssueGroupedAction.DELETE]: string[]; + }, + previousIssueGroupProperties: string[], + currentIssueGroupProperties: string[], + previousIssueSubGroupProperties: string[], + currentIssueSubGroupProperties: string[] + ): { path: string[]; action: EIssueGroupedAction }[] => { + const issueKeyActions = []; + + for (const addKey of groupActionsArray[EIssueGroupedAction.ADD]) { + for (const subGroupProperty of currentIssueSubGroupProperties) { + issueKeyActions.push({ path: [addKey, subGroupProperty], action: EIssueGroupedAction.ADD }); + } + } + + for (const deleteKey of groupActionsArray[EIssueGroupedAction.DELETE]) { + for (const subGroupProperty of previousIssueSubGroupProperties) { + issueKeyActions.push({ + path: [deleteKey, subGroupProperty], + action: EIssueGroupedAction.DELETE, + }); + } + } + + for (const addKey of subGroupActionsArray[EIssueGroupedAction.ADD]) { + for (const groupProperty of currentIssueGroupProperties) { + issueKeyActions.push({ path: [groupProperty, addKey], action: EIssueGroupedAction.ADD }); + } + } + + for (const deleteKey of subGroupActionsArray[EIssueGroupedAction.DELETE]) { + for (const groupProperty of previousIssueGroupProperties) { + issueKeyActions.push({ + path: [groupProperty, deleteKey], + action: EIssueGroupedAction.DELETE, + }); + } + } + + return issueKeyActions; + }; + + getGroupIssueKeyActions = ( + addArray: string[], + deleteArray: string[] + ): { path: string[]; action: EIssueGroupedAction }[] => { + const issueKeyActions = []; + + for (const addKey of addArray) { + issueKeyActions.push({ path: [addKey], action: EIssueGroupedAction.ADD }); + } + + for (const deleteKey of deleteArray) { + issueKeyActions.push({ path: [deleteKey], action: EIssueGroupedAction.DELETE }); + } + + return issueKeyActions; + }; + + getDifference = ( + current: string[], + previous: string[] + ): { [EIssueGroupedAction.ADD]: string[]; [EIssueGroupedAction.DELETE]: string[] } => { + const ADD = []; + const DELETE = []; + for (const currentValue of current) { + if (previous.includes(currentValue)) continue; + ADD.push(currentValue); + } + + for (const previousValue of previous) { + if (current.includes(previousValue)) continue; + DELETE.push(previousValue); + } + + return { [EIssueGroupedAction.ADD]: ADD, [EIssueGroupedAction.DELETE]: DELETE }; }; issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => { @@ -560,10 +661,10 @@ export class BaseIssuesStore implements IBaseIssuesStore { } /** - * This Method is mainly used to filter out empty values in the begining + * This Method is mainly used to filter out empty values in the beginning * @param key key of the value that is to be checked if empty * @param object any object in which the key's value is to be checked - * @returns 1 if emoty, 0 if not empty + * @returns 1 if empty, 0 if not empty */ getSortOrderToFilterEmptyValues(key: string, object: any) { const value = object?.[key]; @@ -573,142 +674,180 @@ export class BaseIssuesStore implements IBaseIssuesStore { return 0; } - issuesSortWithOrderBy = (issueObject: TIssueMap, key: Partial): TIssue[] => { - let array = values(issueObject); - array = orderBy(array, "created_at", ["asc"]); + getIssueIds(issues: TIssue[]) { + return issues.map((issue) => issue?.id); + } + + issuesSortWithOrderBy = (issueIds: string[], key: TIssueOrderByOptions | undefined): string[] => { + const issues = this.rootIssueStore.issues.getIssuesByIds(issueIds, this.isArchived ? "archived" : "un-archived"); + const array = orderBy(issues, "created_at", ["asc"]); switch (key) { case "sort_order": - return orderBy(array, "sort_order"); + return this.getIssueIds(orderBy(array, "sort_order")); case "state__name": - return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"])); + return this.getIssueIds( + orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"])) + ); case "-state__name": - return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"]); + return this.getIssueIds( + orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"]) + ); // dates case "created_at": - return orderBy(array, "created_at"); + return this.getIssueIds(orderBy(array, "created_at")); case "-created_at": - return orderBy(array, "created_at", ["desc"]); + return this.getIssueIds(orderBy(array, "created_at", ["desc"])); case "updated_at": - return orderBy(array, "updated_at"); + return this.getIssueIds(orderBy(array, "updated_at")); case "-updated_at": - return orderBy(array, "updated_at", ["desc"]); + return this.getIssueIds(orderBy(array, "updated_at", ["desc"])); case "start_date": - return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"]); //preferring sorting based on empty values to always keep the empty values below + return this.getIssueIds( + orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"]) + ); //preferring sorting based on empty values to always keep the empty values below case "-start_date": - return orderBy( - array, - [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"], //preferring sorting based on empty values to always keep the empty values below - ["asc", "desc"] + return this.getIssueIds( + orderBy( + array, + [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"], //preferring sorting based on empty values to always keep the empty values below + ["asc", "desc"] + ) ); case "target_date": - return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"]); //preferring sorting based on empty values to always keep the empty values below + return this.getIssueIds( + orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"]) + ); //preferring sorting based on empty values to always keep the empty values below case "-target_date": - return orderBy( - array, - [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"], //preferring sorting based on empty values to always keep the empty values below - ["asc", "desc"] + return this.getIssueIds( + orderBy( + array, + [this.getSortOrderToFilterEmptyValues.bind(null, "target_date"), "target_date"], //preferring sorting based on empty values to always keep the empty values below + ["asc", "desc"] + ) ); // custom case "priority": { const sortArray = ISSUE_PRIORITIES.map((i) => i.key); - return orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue.priority), ["desc"]); + return this.getIssueIds( + orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue.priority), ["desc"]) + ); } case "-priority": { const sortArray = ISSUE_PRIORITIES.map((i) => i.key); - return orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue.priority)); + return this.getIssueIds(orderBy(array, (currentIssue: TIssue) => indexOf(sortArray, currentIssue.priority))); } // number case "attachment_count": - return orderBy(array, "attachment_count"); + return this.getIssueIds(orderBy(array, "attachment_count")); case "-attachment_count": - return orderBy(array, "attachment_count", ["desc"]); + return this.getIssueIds(orderBy(array, "attachment_count", ["desc"])); case "estimate_point": - return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"]); //preferring sorting based on empty values to always keep the empty values below + return this.getIssueIds( + orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"]) + ); //preferring sorting based on empty values to always keep the empty values below case "-estimate_point": - return orderBy( - array, - [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below - ["asc", "desc"] + return this.getIssueIds( + orderBy( + array, + [this.getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below + ["asc", "desc"] + ) ); case "link_count": - return orderBy(array, "link_count"); + return this.getIssueIds(orderBy(array, "link_count")); case "-link_count": - return orderBy(array, "link_count", ["desc"]); + return this.getIssueIds(orderBy(array, "link_count", ["desc"])); case "sub_issues_count": - return orderBy(array, "sub_issues_count"); + return this.getIssueIds(orderBy(array, "sub_issues_count")); case "-sub_issues_count": - return orderBy(array, "sub_issues_count", ["desc"]); + return this.getIssueIds(orderBy(array, "sub_issues_count", ["desc"])); // Array case "labels__name": - return orderBy(array, [ - this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below - (issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "asc"), - ]); - case "-labels__name": - return orderBy( - array, - [ + return this.getIssueIds( + orderBy(array, [ this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below - (issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "desc"), - ], - ["asc", "desc"] + (issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "asc"), + ]) + ); + case "-labels__name": + return this.getIssueIds( + orderBy( + array, + [ + this.getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below + (issue) => this.populateIssueDataForSorting("label_ids", issue["label_ids"], "desc"), + ], + ["asc", "desc"] + ) ); case "modules__name": - return orderBy(array, [ - this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below - (issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "asc"), - ]); - case "-modules__name": - return orderBy( - array, - [ + return this.getIssueIds( + orderBy(array, [ this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below - (issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "desc"), - ], - ["asc", "desc"] + (issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "asc"), + ]) + ); + case "-modules__name": + return this.getIssueIds( + orderBy( + array, + [ + this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below + (issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "desc"), + ], + ["asc", "desc"] + ) ); case "cycle__name": - return orderBy(array, [ - this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below - (issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "asc"), - ]); - case "-cycle__name": - return orderBy( - array, - [ + return this.getIssueIds( + orderBy(array, [ this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below - (issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "desc"), - ], - ["asc", "desc"] + (issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "asc"), + ]) + ); + case "-cycle__name": + return this.getIssueIds( + orderBy( + array, + [ + this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below + (issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "desc"), + ], + ["asc", "desc"] + ) ); case "assignees__first_name": - return orderBy(array, [ - this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below - (issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "asc"), - ]); - case "-assignees__first_name": - return orderBy( - array, - [ + return this.getIssueIds( + orderBy(array, [ this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below - (issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "desc"), - ], - ["asc", "desc"] + (issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "asc"), + ]) + ); + case "-assignees__first_name": + return this.getIssueIds( + orderBy( + array, + [ + this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below + (issue) => this.populateIssueDataForSorting("assignee_ids", issue["assignee_ids"], "desc"), + ], + ["asc", "desc"] + ) ); default: - return array; + return this.getIssueIds(array); } }; @@ -723,37 +862,134 @@ export class BaseIssuesStore implements IBaseIssuesStore { else return [value || "None"]; } + storePreviousPaginationValues = ( + issuesResponse: TIssuesResponse, + options?: IssuePaginationOptions, + groupId?: string, + subGroupId?: string + ) => { + if (options) this.paginationOptions = options; + + this.setPaginationData( + issuesResponse.prev_cursor, + issuesResponse.next_cursor, + issuesResponse.next_page_results, + groupId, + subGroupId + ); + }; + processIssueResponse(issueResponse: TIssuesResponse): { issueList: TIssue[]; - groupedIssueCount: Record; + groupedIssues: TIssues; + groupedIssueCount: TGroupedIssueCount; } { const issueResult = issueResponse?.results; if (!issueResult) return { issueList: [], + groupedIssues: {}, groupedIssueCount: {}, }; if (Array.isArray(issueResult)) { return { issueList: issueResult, - groupedIssueCount: { "All Issues": issueResponse.count }, + groupedIssues: { + [ALL_ISSUES]: issueResult.map((issue) => issue.id), + }, + groupedIssueCount: { + [ALL_ISSUES]: issueResponse.total_count, + }, }; } const issueList: TIssue[] = []; - const groupedIssueCount: Record = {}; + const groupedIssues: TGroupedIssues | TSubGroupedIssues = {}; + const groupedIssueCount: TGroupedIssueCount = {}; for (const groupId in issueResult) { - const groupIssueResult = issueResult[groupId]; + const groupIssuesObject = issueResult[groupId]; + const groupIssueResult = groupIssuesObject?.results; if (!groupIssueResult) continue; - issueList.push(...groupIssueResult.results); - groupedIssueCount[groupId] = groupIssueResult.total_results; + set(groupedIssueCount, [groupId], groupIssuesObject.total_results); + + if (Array.isArray(groupIssueResult)) { + issueList.push(...groupIssueResult); + set( + groupedIssues, + [groupId], + groupIssueResult.map((issue) => issue.id) + ); + continue; + } + + for (const subGroupId in groupIssueResult) { + const subGroupIssuesObject = groupIssueResult[subGroupId]; + const subGroupIssueResult = subGroupIssuesObject?.results; + + if (!subGroupIssueResult) continue; + + set(groupedIssueCount, [subGroupId], subGroupIssuesObject.total_results); + + if (Array.isArray(subGroupIssueResult)) { + issueList.push(...subGroupIssueResult); + set( + groupedIssues, + [groupId, subGroupId], + subGroupIssueResult.map((issue) => issue.id) + ); + + continue; + } + } } - return { issueList, groupedIssueCount }; + return { issueList, groupedIssues, groupedIssueCount }; } + + setPaginationData( + prevCursor: string, + nextCursor: string, + nextPageResults: boolean, + groupId?: string, + subGroupId?: string + ) { + const cursorObject = { + prevCursor, + nextCursor, + nextPageResults, + }; + + if (groupId && subGroupId) { + set(this.issuePaginationData, [subGroupId], cursorObject); + return; + } + + if (groupId) { + set(this.issuePaginationData, [groupId], cursorObject); + return; + } + + set(this.issuePaginationData, [ALL_ISSUES], cursorObject); + } + + getPaginationData = computedFn((groupId: string | undefined): TPaginationData | undefined => { + if (groupId) { + return get(this.issuePaginationData, [groupId]); + } + + return get(this.issuePaginationData, [ALL_ISSUES]); + }); + + getGroupIssueCount = computedFn((groupId: string | undefined): number | undefined => { + if (groupId) { + return get(this.groupedIssueCount, [groupId]); + } + + return get(this.groupedIssueCount, [ALL_ISSUES]); + }); } diff --git a/web/store/issue/helpers/issue-filter-helper.store.ts b/web/store/issue/helpers/issue-filter-helper.store.ts index 1af7ae5b5..7ed50a5c2 100644 --- a/web/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/store/issue/helpers/issue-filter-helper.store.ts @@ -6,12 +6,18 @@ import { IIssueFilterOptions, IIssueFilters, IIssueFiltersResponse, + IssuePaginationOptions, TIssueKanbanFilters, TIssueParams, TStaticViewTypes, } from "@plane/types"; // constants -import { EIssueFilterType, EIssuesStoreType, IssueGroupByOptions } from "constants/issue"; +import { + EIssueFilterType, + EIssuesStoreType, + EIssueGroupByToServerOptions, + EServerGroupByToFilterOptions, +} from "constants/issue"; // lib import { storage } from "lib/local-storage"; @@ -89,7 +95,10 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { project: filters?.project || undefined, subscriber: filters?.subscriber || undefined, // 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, type: displayFilters?.type || undefined, sub_issue: displayFilters?.sub_issue ?? true, @@ -209,7 +218,6 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { cycle: displayProperties?.cycle ?? true, }); - handleIssuesLocalFilters = { fetchFiltersFromStorage: () => { const _filters = storage.get("issue_local_filters"); @@ -272,4 +280,64 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { 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> | 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> = { + ...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; + } } diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index 635c75b24..78a141edb 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -18,7 +18,7 @@ export type IIssueStore = { removeIssue(issueId: string): void; // helper methods getIssueById(issueId: string): undefined | TIssue; - getIssuesByIds(issueIds: string[], type: "archived" | "un-archived"): undefined | Record; // 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 { @@ -112,15 +112,16 @@ export class IssueStore implements IIssueStore { * @returns {Record | undefined} */ getIssuesByIds = computedFn((issueIds: string[], type: "archived" | "un-archived") => { - if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return undefined; - const filteredIssues: { [key: string]: TIssue } = {}; - Object.values(this.issuesMap).forEach((issue) => { + if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return []; + const filteredIssues: TIssue[] = []; + Object.values(issueIds).forEach((issueId) => { // if type is archived then check archived_at is not null // if type is un-archived then check archived_at is null - if ((type === "archived" && issue.archived_at) || (type === "un-archived" && !issue.archived_at)) { - if (issueIds.includes(issue.id)) filteredIssues[issue.id] = issue; + const issue = this.issuesMap[issueId]; + if ((issue && type === "archived" && issue.archived_at) || (type === "un-archived" && !issue?.archived_at)) { + filteredIssues.push(issue); } }); - return isEmpty(filteredIssues) ? undefined : filteredIssues; + return filteredIssues; }); } diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index 8dfb5cb25..4d9819676 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -28,7 +28,9 @@ export interface IModuleIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor: string | undefined + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; @@ -97,21 +99,19 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul return filteredRouteParams; } - getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { - const filterParams = this.appliedFilters; + getFilterParams = computedFn( + ( + options: IssuePaginationOptions, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.appliedFilters; - const paginationOptions: Partial> = { - ...filterParams, - cursor: cursor ? cursor : `${options.perPageCount}:0:0`, - per_page: options.perPageCount.toString(), - }; - - if (options.groupedBy) { - paginationOptions.group_by = options.groupedBy; + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; } - - return paginationOptions; - }); + ); fetchFilters = async (workspaceSlug: string, projectId: string, moduleId: string) => { try { diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index 56338b525..d471795fe 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -28,7 +28,13 @@ export interface IModuleIssues extends IBaseIssuesStore { loadType: TLoader, moduleId: string ) => Promise; - fetchNextIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + groupId?: string, + subGroupId?: string + ) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial, moduleId: string) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; @@ -111,7 +117,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { 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); this.onfetchIssues(response, options); @@ -122,15 +128,27 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { } }; - fetchNextIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => { - if (!this.paginationOptions || !this.next_page_results) return; + fetchNextIssues = async ( + workspaceSlug: string, + projectId: string, + moduleId: string, + groupId?: string, + subGroupId?: string + ) => { + const cursorObject = this.getPaginationData(subGroupId ?? groupId); + if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; try { 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); - this.onfetchNexIssues(response); + this.onfetchNexIssues(response, groupId, subGroupId); return response; } catch (error) { this.loader = undefined; @@ -177,11 +195,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { if (fetchAddedIssues) await this.rootIssueStore.issues.getIssues(workspaceSlug, projectId, issueIds); runInAction(() => { - this.moduleId === moduleId && - update(this, "issues", (moduleIssueIds = []) => { - if (!moduleIssueIds) return [...issueIds]; - else return uniq(concat(moduleIssueIds, issueIds)); - }); + this.moduleId === moduleId && issueIds.forEach((issueId) => this.addIssueToList(issueId)); }); issueIds.forEach((issueId) => { @@ -207,10 +221,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { ); runInAction(() => { - this.moduleId === moduleId && - issueIds.forEach((issueId) => { - this.issues && pull(this.issues, issueId); - }); + this.moduleId === moduleId && issueIds.forEach((issueId) => this.removeIssueFromList(issueId)); }); runInAction(() => { @@ -238,11 +249,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { runInAction(() => { moduleIds.forEach((moduleId) => { - this.moduleId === moduleId && - update(this, "issues", (moduleIssueIds = []) => { - if (moduleIssueIds.includes(issueId)) return moduleIssueIds; - else return uniq(concat(moduleIssueIds, [issueId])); - }); + this.moduleId === moduleId && this.addIssueToList(issueId); }); update(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => uniq(concat(issueModuleIds, moduleIds)) @@ -259,11 +266,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { try { runInAction(() => { moduleIds.forEach((moduleId) => { - this.moduleId === moduleId && - update(this, "issues", (moduleIssueIds = []) => { - if (moduleIssueIds.includes(issueId)) return pull(moduleIssueIds, issueId); - else return uniq(concat(moduleIssueIds, [issueId])); - }); + this.moduleId === moduleId && this.removeIssueFromList(issueId); update(this.rootIssueStore.issues.issuesMap, [issueId, "module_ids"], (issueModuleIds = []) => pull(issueModuleIds, moduleId) ); @@ -286,7 +289,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { try { 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 = []) => pull(issueModuleIds, moduleId) ); diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index 991611853..4006001c7 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -30,7 +30,9 @@ export interface IProfileIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor: string | undefined + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, userId: string) => Promise; @@ -99,21 +101,19 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf return filteredRouteParams; } - getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { - const filterParams = this.appliedFilters; + getFilterParams = computedFn( + ( + options: IssuePaginationOptions, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.appliedFilters; - const paginationOptions: Partial> = { - ...filterParams, - cursor: cursor ? cursor : `${options.perPageCount}:0:0`, - per_page: options.perPageCount.toString(), - }; - - if (options.groupedBy) { - paginationOptions.group_by = options.groupedBy; + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; } - - return paginationOptions; - }); + ); fetchFilters = async (workspaceSlug: string, userId: string) => { try { diff --git a/web/store/issue/profile/issue.store.ts b/web/store/issue/profile/issue.store.ts index 8a43caed2..8dc984096 100644 --- a/web/store/issue/profile/issue.store.ts +++ b/web/store/issue/profile/issue.store.ts @@ -27,7 +27,12 @@ export interface IProfileIssues extends IBaseIssuesStore { userId: string, loadType: TLoader ) => Promise; - fetchNextIssues: (workspaceSlug: string, userId: string) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + userId: string, + groupId?: string, + subGroupId?: string + ) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; @@ -95,7 +100,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues { this.setViewId(view); - let params = this.issueFilterStore?.getFilterParams(options, undefined); + let params = this.issueFilterStore?.getFilterParams(options, undefined, undefined, undefined); params = { ...params, assignees: undefined, @@ -116,12 +121,18 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues { } }; - fetchNextIssues = async (workspaceSlug: string, userId: string) => { - if (!this.paginationOptions || !this.currentView || !this.next_page_results) return; + fetchNextIssues = async (workspaceSlug: string, userId: string, groupId?: string, subGroupId?: string) => { + const cursorObject = this.getPaginationData(subGroupId ?? groupId); + if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; try { 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, assignees: undefined, @@ -134,7 +145,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues { const response = await this.userService.getUserProfileIssues(workspaceSlug, userId, params); - this.onfetchNexIssues(response); + this.onfetchNexIssues(response, groupId, subGroupId); return response; } catch (error) { this.loader = undefined; diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index 6a5dad963..8c37d4dc9 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -28,7 +28,9 @@ export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor: string | undefined + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise; @@ -95,21 +97,19 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I return filteredRouteParams; } - getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { - const filterParams = this.appliedFilters; + getFilterParams = computedFn( + ( + options: IssuePaginationOptions, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.appliedFilters; - const paginationOptions: Partial> = { - ...filterParams, - cursor: cursor ? cursor : `${options.perPageCount}:0:0`, - per_page: options.perPageCount.toString(), - }; - - if (options.groupedBy) { - paginationOptions.group_by = options.groupedBy; + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; } - - return paginationOptions; - }); + ); fetchFilters = async (workspaceSlug: string, projectId: string, viewId: string) => { try { diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index 9c3ea1d50..d1cd34672 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -21,7 +21,12 @@ export interface IProjectViewIssues extends IBaseIssuesStore { projectId: string, loadType: TLoader ) => Promise; - fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + projectId: string, + groupId?: string, + subGroupId?: string + ) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; @@ -62,7 +67,7 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs this.loader = loadType; }); 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); this.onfetchIssues(response, options); @@ -73,15 +78,21 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs } }; - fetchNextIssues = async (workspaceSlug: string, projectId: string) => { - if (!this.paginationOptions || !this.next_page_results) return; + fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => { + const cursorObject = this.getPaginationData(subGroupId ?? groupId); + if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; try { 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); - this.onfetchNexIssues(response); + this.onfetchNexIssues(response, groupId, subGroupId); return response; } catch (error) { this.loader = undefined; diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index 8d286125e..1afbc3ace 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -20,7 +20,7 @@ import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue- // types import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType, IssueGroupByOptions } from "constants/issue"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { computedFn } from "mobx-utils"; // services @@ -28,7 +28,9 @@ export interface IProjectIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor: string | undefined + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string) => Promise; @@ -94,25 +96,18 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj return filteredRouteParams; } - getFilterParams = computedFn((options: IssuePaginationOptions, cursor: string | undefined) => { - const filterParams = this.appliedFilters; - - const paginationOptions: Partial> = { - ...filterParams, - cursor: cursor ? cursor : `${options.perPageCount}:0:0`, - per_page: options.perPageCount.toString(), - }; - - if (options.groupedBy) { - paginationOptions.group_by = options.groupedBy; + getFilterParams = computedFn( + ( + options: IssuePaginationOptions, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.appliedFilters; + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; } - - if (options.after && options.before) { - paginationOptions["target_date"] = `${options.after};after,${options.before};before`; - } - - return paginationOptions; - }); + ); fetchFilters = async (workspaceSlug: string, projectId: string) => { 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, { display_filters: _filters.displayFilters, diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index a7a853784..0271e3297 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -22,7 +22,12 @@ export interface IProjectIssues extends IBaseIssuesStore { projectId: string, loadType: TLoader ) => Promise; - fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + projectId: string, + groupId?: string, + subGroupId?: string + ) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; @@ -65,7 +70,7 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues { this.loader = loadType; }); 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); this.onfetchIssues(response, options); @@ -76,15 +81,21 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues { } }; - fetchNextIssues = async (workspaceSlug: string, projectId: string) => { - if (!this.paginationOptions || !this.next_page_results) return; + fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => { + const cursorObject = this.getPaginationData(subGroupId ?? groupId); + if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; try { 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); - this.onfetchNexIssues(response); + this.onfetchNexIssues(response, groupId, subGroupId); return response; } catch (error) { this.loader = undefined; diff --git a/web/store/issue/workspace/filter.store.ts b/web/store/issue/workspace/filter.store.ts index 1791c7cfd..59cf1b43f 100644 --- a/web/store/issue/workspace/filter.store.ts +++ b/web/store/issue/workspace/filter.store.ts @@ -43,7 +43,9 @@ export interface IWorkspaceIssuesFilter extends IBaseIssueFilterStore { getFilterParams: ( viewId: string, options: IssuePaginationOptions, - cursor?: string + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined ) => Partial>; } @@ -102,21 +104,20 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo return filteredRouteParams; }; - getFilterParams = computedFn((viewId: string, options: IssuePaginationOptions, cursor: string | undefined) => { - const filterParams = this.getAppliedFilters(viewId); + getFilterParams = computedFn( + ( + viewId: string, + options: IssuePaginationOptions, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.getAppliedFilters(viewId); - const paginationOptions: Partial> = { - ...filterParams, - cursor: cursor ? cursor : `${options.perPageCount}:0:0`, - per_page: options.perPageCount.toString(), - }; - - if (options.groupedBy) { - paginationOptions.group_by = options.groupedBy; + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; } - - return paginationOptions; - }); + ); get issueFilters() { 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)) this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, { diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index 31db275f1..317993d1f 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -1,7 +1,7 @@ import { action, makeObservable, runInAction } from "mobx"; // base class 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 // types import { IIssueRootStore } from "../root.store"; @@ -23,7 +23,12 @@ export interface IWorkspaceIssues extends IBaseIssuesStore { viewId: string, loadType: TLoader ) => Promise; - fetchNextIssues: (workspaceSlug: string, viewId: string) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + viewId: string, + groupId?: string, + subGroupId?: string + ) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; @@ -64,7 +69,7 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues this.loader = loadType; }); 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); this.onfetchIssues(response, options); @@ -75,15 +80,22 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues } }; - fetchNextIssues = async (workspaceSlug: string, viewId: string) => { - if (!this.paginationOptions) return; + fetchNextIssues = async (workspaceSlug: string, viewId: string, groupId?: string, subGroupId?: string) => { + const cursorObject = this.getPaginationData(subGroupId ?? groupId); + if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return; try { 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); - this.onfetchNexIssues(response); + this.onfetchNexIssues(response, groupId, subGroupId); return response; } catch (error) { this.loader = undefined;