implement pagination for spreadsheet, list, kanban and calendar

This commit is contained in:
rahulramesha 2024-03-13 12:26:10 +05:30
parent 3b3f04b7e7
commit cf470d715a
71 changed files with 1148 additions and 825 deletions

View File

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

View File

@ -1,9 +1,4 @@
export type TIssueLayouts = import { EIssueLayoutTypes } from "constants/issue";
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt_chart";
export type TIssueGroupByOptions = export type TIssueGroupByOptions =
| "state" | "state"
@ -15,6 +10,7 @@ export type TIssueGroupByOptions =
| "assignees" | "assignees"
| "cycle" | "cycle"
| "module" | "module"
| "target_date"
| null; | null;
export type TIssueOrderByOptions = export type TIssueOrderByOptions =
@ -196,7 +192,7 @@ export type TIssueTypeFilters = "active" | "backlog" | null;
export interface IssuePaginationOptions { export interface IssuePaginationOptions {
canGroup: boolean; canGroup: boolean;
perPageCount: number; perPageCount: number;
greaterThanDate?: Date; before?: string;
lessThanDate?: Date; after?: string;
groupedBy?: TIssueGroupByOptions; groupedBy?: TIssueGroupByOptions;
} }

View File

@ -8,9 +8,15 @@ import { CustomMenu } from "@plane/ui";
// constants // constants
import { ProjectAnalyticsModal } from "components/analytics"; import { ProjectAnalyticsModal } from "components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
ISSUE_LAYOUTS,
} from "constants/issue";
import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store"; import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
export const CycleMobileHeader = () => { export const CycleMobileHeader = () => {
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
@ -30,7 +36,7 @@ export const CycleMobileHeader = () => {
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
updateFilters( updateFilters(
workspaceSlug.toString(), workspaceSlug.toString(),

View File

@ -10,7 +10,12 @@ import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; import { CycleMobileHeader } from "components/cycles/cycle-mobile-header";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
@ -31,7 +36,7 @@ import useLocalStorage from "hooks/use-local-storage";
// icons // icons
// helpers // helpers
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import { ProjectLogo } from "components/project"; import { ProjectLogo } from "components/project";
// constants // constants
@ -95,7 +100,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
}; };
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId);
}, },
@ -233,7 +238,13 @@ export const CycleIssuesHeader: React.FC = observer(() => {
</div> </div>
<div className="hidden md:flex items-center gap-2 "> <div className="hidden md:flex items-center gap-2 ">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />

View File

@ -10,7 +10,12 @@ import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { ModuleMobileHeader } from "components/modules/module-mobile-header"; import { ModuleMobileHeader } from "components/modules/module-mobile-header";
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import {
EIssuesStoreType,
EIssueFilterType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
EIssueLayoutTypes,
} from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
@ -32,7 +37,7 @@ import useLocalStorage from "hooks/use-local-storage";
// icons // icons
// helpers // helpers
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import { ProjectLogo } from "components/project"; import { ProjectLogo } from "components/project";
// constants // constants
@ -96,7 +101,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!projectId) return; if (!projectId) return;
updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
}, },
@ -235,7 +240,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="hidden md:flex gap-2"> <div className="hidden md:flex gap-2">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />

View File

@ -9,9 +9,14 @@ import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-ham
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
// ui // ui
// helper // helper
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "constants/issue";
import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import { ProjectLogo } from "components/project"; import { ProjectLogo } from "components/project";
export const ProjectDraftIssueHeader: FC = observer(() => { export const ProjectDraftIssueHeader: FC = observer(() => {
@ -51,7 +56,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
); );
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
}, },
@ -124,7 +129,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban"]} layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />

View File

@ -9,7 +9,12 @@ import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; import { IssuesMobileHeader } from "components/issues/issues-mobile-header";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { import {
useApplication, useApplication,
@ -24,7 +29,7 @@ import { useIssues } from "hooks/store/use-issues";
// components // components
// ui // ui
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import { ProjectLogo } from "components/project"; import { ProjectLogo } from "components/project";
// constants // constants
// helper // helper
@ -75,7 +80,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
); );
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
}, },
@ -177,7 +182,13 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</div> </div>
<div className="items-center gap-2 hidden md:flex"> <div className="items-center gap-2 hidden md:flex">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />

View File

@ -13,7 +13,12 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
// helpers // helpers
// types // types
// constants // constants
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import {
EIssuesStoreType,
EIssueFilterType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
EIssueLayoutTypes,
} from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { import {
@ -27,7 +32,7 @@ import {
useProjectView, useProjectView,
useUser, useUser,
} from "hooks/store"; } from "hooks/store";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import { ProjectLogo } from "components/project"; import { ProjectLogo } from "components/project";
export const ProjectViewIssuesHeader: React.FC = observer(() => { export const ProjectViewIssuesHeader: React.FC = observer(() => {
@ -56,7 +61,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId || !viewId) return; if (!workspaceSlug || !projectId || !viewId) return;
updateFilters( updateFilters(
workspaceSlug.toString(), workspaceSlug.toString(),
@ -195,7 +200,13 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]} layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />

View File

@ -1,20 +1,22 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { DragDropContext, DropResult } from "@hello-pangea/dnd"; import { DragDropContext, DropResult } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { TGroupedIssues } from "@plane/types";
import useSWR from "swr";
// components // components
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
import { CalendarChart } from "components/issues"; import { CalendarChart } from "components/issues";
// hooks // hooks
import { useIssues, useUser } from "hooks/store"; import { useCalendarView, useIssues, useUser } from "hooks/store";
import { useIssuesActions } from "hooks/use-issues-actions"; import { useIssuesActions } from "hooks/use-issues-actions";
// ui // ui
// types // types
import { TGroupedIssues } from "@plane/types"; import { EIssueLayoutTypes, EIssuesStoreType, IssueGroupByOptions } from "constants/issue";
import { EIssuesStoreType } from "constants/issue";
import { IQuickActionProps } from "../list/list-view-types"; import { IQuickActionProps } from "../list/list-view-types";
import { handleDragDrop } from "./utils"; import { handleDragDrop } from "./utils";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { IssueLayoutHOC } from "../issue-layout-HOC";
type CalendarStoreType = type CalendarStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
@ -42,8 +44,18 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { issues, issuesFilter, issueMap } = useIssues(storeType); const { issues, issuesFilter, issueMap } = useIssues(storeType);
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = const {
useIssuesActions(storeType); fetchIssues,
fetchNextIssues,
updateIssue,
removeIssue,
removeIssueFromView,
archiveIssue,
restoreIssue,
updateFilters,
} = useIssuesActions(storeType);
const issueCalendarView = useCalendarView();
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
@ -51,6 +63,27 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues; const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues;
const layout = displayFilters?.calendar?.layout ?? "month";
const { startDate, endDate } = issueCalendarView.getStartAndEndDate(layout) ?? {};
useSWR(
startDate && endDate && layout ? `ISSUE_CALENDAR_LAYOUT_${storeType}_${startDate}_${endDate}_${layout}` : null,
startDate && endDate
? () =>
fetchIssues("init-loader", {
canGroup: true,
perPageCount: layout === "month" ? 4 : 30,
before: endDate,
after: startDate,
groupedBy: IssueGroupByOptions["target_date"],
})
: null,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
const onDragEnd = async (result: DropResult) => { const onDragEnd = async (result: DropResult) => {
if (!result) return; if (!result) return;
@ -79,8 +112,12 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
} }
}; };
const loadMoreIssues = useCallback(() => {
fetchNextIssues();
}, [fetchNextIssues]);
return ( return (
<> <IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.CALENDAR}>
<div className="h-full w-full overflow-hidden bg-custom-background-100 pt-4"> <div className="h-full w-full overflow-hidden bg-custom-background-100 pt-4">
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<CalendarChart <CalendarChart
@ -89,6 +126,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
layout={displayFilters?.calendar?.layout} layout={displayFilters?.calendar?.layout}
showWeekends={displayFilters?.calendar?.show_weekends ?? false} showWeekends={displayFilters?.calendar?.show_weekends ?? false}
issueCalendarView={issueCalendarView}
quickActions={(issue, customActionButton) => ( quickActions={(issue, customActionButton) => (
<QuickActions <QuickActions
customActionButton={customActionButton} customActionButton={customActionButton}
@ -103,6 +141,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
readOnly={!isEditingAllowed || isCompletedCycle} readOnly={!isEditingAllowed || isCompletedCycle}
/> />
)} )}
loadMoreIssues={loadMoreIssues}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
quickAddCallback={issues.quickAddIssue} quickAddCallback={issues.quickAddIssue}
viewId={viewId} viewId={viewId}
@ -111,6 +150,6 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
/> />
</DragDropContext> </DragDropContext>
</div> </div>
</> </IssueLayoutHOC>
); );
}); });

View File

@ -24,6 +24,7 @@ import { ICycleIssuesFilter } from "store/issue/cycle";
import { IModuleIssuesFilter } from "store/issue/module"; import { IModuleIssuesFilter } from "store/issue/module";
import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectIssuesFilter } from "store/issue/project";
import { IProjectViewIssuesFilter } from "store/issue/project-views"; import { IProjectViewIssuesFilter } from "store/issue/project-views";
import { ICalendarStore } from "store/issue/issue_calendar_view.store";
type Props = { type Props = {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
@ -31,6 +32,8 @@ type Props = {
groupedIssueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
layout: "month" | "week" | undefined; layout: "month" | "week" | undefined;
showWeekends: boolean; showWeekends: boolean;
issueCalendarView: ICalendarStore;
loadMoreIssues: () => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
@ -55,6 +58,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
groupedIssueIds, groupedIssueIds,
layout, layout,
showWeekends, showWeekends,
issueCalendarView,
loadMoreIssues,
quickActions, quickActions,
quickAddCallback, quickAddCallback,
addIssuesToView, addIssuesToView,
@ -66,7 +71,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
const { const {
issues: { viewFlags }, issues: { viewFlags },
} = useIssues(EIssuesStoreType.PROJECT); } = useIssues(EIssuesStoreType.PROJECT);
const issueCalendarView = useCalendarView();
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
@ -102,6 +107,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
week={week} week={week}
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
loadMoreIssues={loadMoreIssues}
enableQuickIssueCreate enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
quickActions={quickActions} quickActions={quickActions}
@ -119,6 +125,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
week={issueCalendarView.allDaysOfActiveWeek} week={issueCalendarView.allDaysOfActiveWeek}
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
loadMoreIssues={loadMoreIssues}
enableQuickIssueCreate enableQuickIssueCreate
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
quickActions={quickActions} quickActions={quickActions}

View File

@ -19,6 +19,7 @@ type Props = {
date: ICalendarDate; date: ICalendarDate;
issues: TIssueMap | undefined; issues: TIssueMap | undefined;
groupedIssueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
loadMoreIssues: () => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
@ -39,6 +40,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
date, date,
issues, issues,
groupedIssueIds, groupedIssueIds,
loadMoreIssues,
quickActions, quickActions,
enableQuickIssueCreate, enableQuickIssueCreate,
disableIssueCreation, disableIssueCreation,
@ -47,14 +49,13 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
viewId, viewId,
readOnly = false, readOnly = false,
} = props; } = props;
const [showAllIssues, setShowAllIssues] = useState(false);
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
const formattedDatePayload = renderFormattedPayloadDate(date.date); const formattedDatePayload = renderFormattedPayloadDate(date.date);
if (!formattedDatePayload) return null; if (!formattedDatePayload) return null;
const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null; const issueIdList = groupedIssueIds ? groupedIssueIds[formattedDatePayload] : null;
const totalIssues = issueIdList?.length ?? 0; const totalIssues = issueIdList?.issueCount ?? 0;
const isToday = date.date.toDateString() === new Date().toDateString(); const isToday = date.date.toDateString() === new Date().toDateString();
@ -100,9 +101,8 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
> >
<CalendarIssueBlocks <CalendarIssueBlocks
issues={issues} issues={issues}
issueIdList={issueIdList} issueIdList={issueIdList?.issueIds ?? []}
quickActions={quickActions} quickActions={quickActions}
showAllIssues={showAllIssues}
isDragDisabled={readOnly} isDragDisabled={readOnly}
/> />
@ -117,19 +117,18 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
viewId={viewId} viewId={viewId}
onOpen={() => setShowAllIssues(true)}
/> />
</div> </div>
)} )}
{totalIssues > 4 && ( {totalIssues > (issueIdList?.issueIds?.length ?? 0) && (
<div className="flex items-center px-2.5 py-1"> <div className="flex items-center px-2.5 py-1">
<button <button
type="button" type="button"
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300" className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
onClick={() => setShowAllIssues((prevData) => !prevData)} onClick={loadMoreIssues}
> >
{showAllIssues ? "Hide" : totalIssues - 4 + " more"} Load More
</button> </button>
</div> </div>
)} )}

View File

@ -16,12 +16,11 @@ type Props = {
issues: TIssueMap | undefined; issues: TIssueMap | undefined;
issueIdList: string[] | null; issueIdList: string[] | null;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
showAllIssues?: boolean;
isDragDisabled?: boolean; isDragDisabled?: boolean;
}; };
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => { export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { issues, issueIdList, quickActions, showAllIssues = false, isDragDisabled = false } = props; const { issues, issueIdList, quickActions, isDragDisabled = false } = props;
// hooks // hooks
const { const {
router: { workspaceSlug, projectId }, router: { workspaceSlug, projectId },
@ -57,7 +56,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
return ( return (
<> <>
{issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => { {issueIdList?.map((issueId, index) => {
if (!issues?.[issueId]) return null; if (!issues?.[issueId]) return null;
const issue = issues?.[issueId]; const issue = issues?.[issueId];

View File

@ -17,6 +17,7 @@ type Props = {
groupedIssueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
week: ICalendarWeek | undefined; week: ICalendarWeek | undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
loadMoreIssues: () => void;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
quickAddCallback?: ( quickAddCallback?: (
@ -36,6 +37,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
issues, issues,
groupedIssueIds, groupedIssueIds,
week, week,
loadMoreIssues,
quickActions, quickActions,
enableQuickIssueCreate, enableQuickIssueCreate,
disableIssueCreation, disableIssueCreation,
@ -66,6 +68,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
date={date} date={date}
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds} groupedIssueIds={groupedIssueIds}
loadMoreIssues={loadMoreIssues}
quickActions={quickActions} quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}

View File

@ -9,33 +9,30 @@ import { ExistingIssuesListModal } from "components/core";
// components // components
import { EmptyState } from "components/empty-state"; import { EmptyState } from "components/empty-state";
// types // types
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; import { IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
// constants // constants
import { EIssuesStoreType } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { EmptyStateType } from "constants/empty-state"; import { EmptyStateType } from "constants/empty-state";
import size from "lodash/size";
import { useRouter } from "next/router";
type Props = { export const CycleEmptyState: React.FC = observer(() => {
workspaceSlug: string | undefined; // router
projectId: string | undefined; const router = useRouter();
cycleId: string | undefined; const { workspaceSlug, projectId, cycleId } = router.query;
activeLayout: TIssueLayouts | undefined;
handleClearAllFilters: () => void;
isEmptyFilters?: boolean;
};
export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
// states // states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
// store hooks // store hooks
const { getCycleById } = useCycle(); const { getCycleById } = useCycle();
const { issues } = useIssues(EIssuesStoreType.CYCLE); const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
const { const {
commandPalette: { toggleCreateIssueModal }, commandPalette: { toggleCreateIssueModal },
} = useApplication(); } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
@ -43,7 +40,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
const issueIds = data.map((i) => i.id); const issueIds = data.map((i) => i.id);
await issues await issues
.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds) .addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds)
.then(() => .then(() =>
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
@ -59,9 +56,32 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
}) })
); );
}; };
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {}); const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !cycleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});
issuesFilter.updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{
...newFilters,
},
cycleId.toString()
);
};
const isEmptyFilters = issueFilterCount > 0;
const emptyStateType = isCompletedCycleSnapshotAvailable const emptyStateType = isCompletedCycleSnapshotAvailable
? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES ? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES
: isEmptyFilters : isEmptyFilters
@ -71,10 +91,10 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
const emptyStateSize = isEmptyFilters ? "lg" : "sm"; const emptyStateSize = isEmptyFilters ? "lg" : "sm";
return ( return (
<> <div className="relative h-full w-full overflow-y-auto">
<ExistingIssuesListModal <ExistingIssuesListModal
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug?.toString()}
projectId={projectId} projectId={projectId?.toString()}
isOpen={cycleIssuesListModal} isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)} handleClose={() => setCycleIssuesListModal(false)}
searchParams={{ cycle: true }} searchParams={{ cycle: true }}
@ -100,6 +120,6 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
} }
/> />
</div> </div>
</> </div>
); );
}); });

View File

@ -1,7 +0,0 @@
export * from "./cycle";
export * from "./global-view";
export * from "./module";
export * from "./project-view";
export * from "./project-issues";
export * from "./draft-issues";
export * from "./archived-issues";

View File

@ -0,0 +1,33 @@
import { EIssuesStoreType } from "constants/issue";
import { ProjectEmptyState } from "./project-issues";
import { ProjectViewEmptyState } from "./project-view";
import { ProjectArchivedEmptyState } from "./archived-issues";
import { CycleEmptyState } from "./cycle";
import { ModuleEmptyState } from "./module";
import { ProjectDraftEmptyState } from "./draft-issues";
import { GlobalViewEmptyState } from "./global-view";
interface Props {
storeType: EIssuesStoreType;
}
export const IssueLayoutEmptyState = (props: Props) => {
switch (props.storeType) {
case EIssuesStoreType.PROJECT:
return <ProjectEmptyState />;
case EIssuesStoreType.PROJECT_VIEW:
return <ProjectViewEmptyState />;
case EIssuesStoreType.ARCHIVED:
return <ProjectArchivedEmptyState />;
case EIssuesStoreType.CYCLE:
return <CycleEmptyState />;
case EIssuesStoreType.MODULE:
return <ModuleEmptyState />;
case EIssuesStoreType.DRAFT:
return <ProjectDraftEmptyState />;
case EIssuesStoreType.GLOBAL:
return <GlobalViewEmptyState />;
default:
return null;
}
};

View File

@ -1,5 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import size from "lodash/size";
// hooks // hooks
import { useApplication, useEventTracker, useIssues } from "hooks/store"; import { useApplication, useEventTracker, useIssues } from "hooks/store";
// ui // ui
@ -9,31 +11,27 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
import { ExistingIssuesListModal } from "components/core"; import { ExistingIssuesListModal } from "components/core";
import { EmptyState } from "components/empty-state"; import { EmptyState } from "components/empty-state";
// types // types
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; import { IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
// constants // constants
import { EIssuesStoreType } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { EmptyStateType } from "constants/empty-state"; import { EmptyStateType } from "constants/empty-state";
type Props = { export const ModuleEmptyState: React.FC = observer(() => {
workspaceSlug: string | undefined; // router
projectId: string | undefined; const router = useRouter();
moduleId: string | undefined; const { workspaceSlug, projectId, moduleId } = router.query;
activeLayout: TIssueLayouts | undefined;
handleClearAllFilters: () => void;
isEmptyFilters?: boolean;
};
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
// states // states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
// store hooks // store hooks
const { issues } = useIssues(EIssuesStoreType.MODULE); const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const { const {
commandPalette: { toggleCreateIssueModal }, commandPalette: { toggleCreateIssueModal },
} = useApplication(); } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !moduleId) return; if (!workspaceSlug || !projectId || !moduleId) return;
@ -56,14 +54,38 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
); );
}; };
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !moduleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});
issuesFilter.updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{
...newFilters,
},
moduleId.toString()
);
};
const isEmptyFilters = issueFilterCount > 0;
const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES; const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES;
const additionalPath = activeLayout ?? "list"; const additionalPath = activeLayout ?? "list";
return ( return (
<> <div className="relative h-full w-full overflow-y-auto">
<ExistingIssuesListModal <ExistingIssuesListModal
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug?.toString()}
projectId={projectId} projectId={projectId?.toString()}
isOpen={moduleIssuesListModal} isOpen={moduleIssuesListModal}
handleClose={() => setModuleIssuesListModal(false)} handleClose={() => setModuleIssuesListModal(false)}
searchParams={{ module: moduleId != undefined ? moduleId.toString() : "" }} searchParams={{ module: moduleId != undefined ? moduleId.toString() : "" }}
@ -84,6 +106,6 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)}
/> />
</div> </div>
</> </div>
); );
}); });

View File

@ -14,6 +14,7 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
return ( return (
<div className="relative h-full w-full overflow-y-auto">
<div className="grid h-full w-full place-items-center"> <div className="grid h-full w-full place-items-center">
<EmptyState <EmptyState
title="View issues will appear here" title="View issues will appear here"
@ -29,5 +30,6 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
}} }}
/> />
</div> </div>
</div>
); );
}); });

View File

@ -3,14 +3,13 @@ import React from "react";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types // types
import { ISSUE_LAYOUTS } from "constants/issue";
import { TIssueLayouts } from "@plane/types";
// constants // constants
import { EIssueLayoutTypes, ISSUE_LAYOUTS } from "constants/issue";
type Props = { type Props = {
layouts: TIssueLayouts[]; layouts: EIssueLayoutTypes[];
onChange: (layout: TIssueLayouts) => void; onChange: (layout: EIssueLayoutTypes) => void;
selectedLayout: TIssueLayouts | undefined; selectedLayout: EIssueLayoutTypes | undefined;
}; };
export const LayoutSelection: React.FC<Props> = (props) => { export const LayoutSelection: React.FC<Props> = (props) => {

View File

@ -13,7 +13,8 @@ import { useIssuesActions } from "hooks/use-issues-actions";
// types // types
import { TIssue, TUnGroupedIssues } from "@plane/types"; import { TIssue, TUnGroupedIssues } from "@plane/types";
// constants // constants
import { EIssuesStoreType } from "constants/issue"; import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { IssueLayoutHOC } from "../issue-layout-HOC";
type GanttStoreType = type GanttStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
@ -57,7 +58,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
return ( return (
<> <IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.GANTT}>
<div className="h-full w-full"> <div className="h-full w-full">
<GanttChartRoot <GanttChartRoot
border={false} border={false}
@ -80,6 +81,6 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
showAllBlocks showAllBlocks
/> />
</div> </div>
</> </IssueLayoutHOC>
); );
}); });

View File

@ -0,0 +1,51 @@
import {
CalendarLayoutLoader,
GanttLayoutLoader,
KanbanLayoutLoader,
ListLayoutLoader,
SpreadsheetLayoutLoader,
} from "components/ui";
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { useIssues } from "hooks/store";
import { observer } from "mobx-react";
import { IssueLayoutEmptyState } from "./empty-states";
const ActiveLoader = (props: { layout: EIssueLayoutTypes }) => {
const { layout } = props;
switch (layout) {
case EIssueLayoutTypes.LIST:
return <ListLayoutLoader />;
case EIssueLayoutTypes.KANBAN:
return <KanbanLayoutLoader />;
case EIssueLayoutTypes.SPREADSHEET:
return <SpreadsheetLayoutLoader />;
case EIssueLayoutTypes.CALENDAR:
return <CalendarLayoutLoader />;
case EIssueLayoutTypes.GANTT:
return <GanttLayoutLoader />;
default:
return null;
}
};
interface Props {
children: string | JSX.Element | JSX.Element[];
storeType: EIssuesStoreType;
layout: EIssueLayoutTypes;
}
export const IssueLayoutHOC = observer((props: Props) => {
const { storeType, layout } = props;
const { issues } = useIssues(storeType);
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <ActiveLoader layout={layout} />;
}
if (issues.issueCount === 0) {
return <IssueLayoutEmptyState storeType={storeType} />;
}
return <>{props.children}</>;
});

View File

@ -2,11 +2,12 @@ import { FC, useCallback, useRef, useState } from "react";
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd"; import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
// hooks // hooks
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
import { DeleteIssueModal } from "components/issues"; import { DeleteIssueModal } from "components/issues";
import { ISSUE_DELETED } from "constants/event-tracker"; import { ISSUE_DELETED } from "constants/event-tracker";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { useEventTracker, useIssues, useUser } from "hooks/store"; import { useEventTracker, useIssues, useUser } from "hooks/store";
import { useIssuesActions } from "hooks/use-issues-actions"; import { useIssuesActions } from "hooks/use-issues-actions";
@ -18,6 +19,8 @@ import { IQuickActionProps } from "../list/list-view-types";
import { KanBan } from "./default"; import { KanBan } from "./default";
import { KanBanSwimLanes } from "./swimlanes"; import { KanBanSwimLanes } from "./swimlanes";
import { handleDragDrop } from "./utils"; import { handleDragDrop } from "./utils";
import { IssueLayoutHOC } from "../issue-layout-HOC";
import debounce from "lodash/debounce";
export type KanbanStoreType = export type KanbanStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
@ -61,10 +64,31 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
} = useUser(); } = useUser();
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const { issueMap, issuesFilter, issues } = useIssues(storeType); const { issueMap, issuesFilter, issues } = useIssues(storeType);
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = const {
useIssuesActions(storeType); fetchIssues,
fetchNextIssues,
updateIssue,
removeIssue,
removeIssueFromView,
archiveIssue,
restoreIssue,
updateFilters,
} = useIssuesActions(storeType);
const issueIds = issues?.groupedIssueIds || []; useSWR(`ISSUE_KANBAN_LAYOUT_${storeType}`, () => fetchIssues("init-loader", { canGroup: true, perPageCount: 30 }), {
revalidateOnFocus: false,
revalidateOnReconnect: false,
});
const fetchMoreIssues = useCallback(() => {
if (issues.loader !== "pagination") {
fetchNextIssues();
}
}, [fetchNextIssues]);
const debouncedFetchMoreIssues = debounce(() => fetchMoreIssues(), 300, { leading: true, trailing: false });
const issueIds = issues?.groupedIssueIds;
const displayFilters = issuesFilter?.issueFilters?.displayFilters; const displayFilters = issuesFilter?.issueFilters?.displayFilters;
const displayProperties = issuesFilter?.issueFilters?.displayProperties; const displayProperties = issuesFilter?.issueFilters?.displayProperties;
@ -207,7 +231,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] }; const kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] };
return ( return (
<> <IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.KANBAN}>
<DeleteIssueModal <DeleteIssueModal
dataId={dragState.draggedIssueId} dataId={dragState.draggedIssueId}
isOpen={deleteIssueModal} isOpen={deleteIssueModal}
@ -215,12 +239,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
onSubmit={handleDeleteIssue} onSubmit={handleDeleteIssue}
/> />
{showLoader && issues?.loader === "init-loader" && (
<div className="fixed right-2 top-16 z-30 flex h-10 w-10 items-center justify-center rounded bg-custom-background-80 shadow-custom-shadow-sm">
<Spinner className="h-5 w-5" />
</div>
)}
<div <div
className="vertical-scrollbar horizontal-scrollbar scrollbar-lg relative flex h-full w-full overflow-auto bg-custom-background-90" className="vertical-scrollbar horizontal-scrollbar scrollbar-lg relative flex h-full w-full overflow-auto bg-custom-background-90"
ref={scrollableContainerRef} ref={scrollableContainerRef}
@ -253,7 +271,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
<div className="h-max w-max"> <div className="h-max w-max">
<KanBanView <KanBanView
issuesMap={issueMap} issuesMap={issueMap}
issueIds={issueIds} issueIds={issueIds!}
displayProperties={displayProperties} displayProperties={displayProperties}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
@ -271,11 +289,12 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
loadMoreIssues={debouncedFetchMoreIssues}
/> />
</div> </div>
</DragDropContext> </DragDropContext>
</div> </div>
</div> </div>
</> </IssueLayoutHOC>
); );
}); });

View File

@ -43,6 +43,7 @@ export interface IGroupByKanBan {
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: any; handleKanbanFilters: any;
loadMoreIssues: (() => void) | undefined;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
@ -75,6 +76,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
handleKanbanFilters, handleKanbanFilters,
enableQuickIssueCreate, enableQuickIssueCreate,
quickAddCallback, quickAddCallback,
loadMoreIssues,
viewId, viewId,
disableIssueCreation, disableIssueCreation,
storeType, storeType,
@ -105,7 +107,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
if (!list) return null; if (!list) return null;
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.issueCount > 0);
const groupList = showEmptyGroup ? list : groupWithIssues; const groupList = showEmptyGroup ? list : groupWithIssues;
@ -141,7 +143,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
column_id={_list.id} column_id={_list.id}
icon={_list.icon} icon={_list.icon}
title={_list.name} title={_list.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0} count={(issueIds as TGroupedIssues)?.[_list.id]?.issueCount || 0}
issuePayload={_list.payload} issuePayload={_list.payload}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy} disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
storeType={storeType} storeType={storeType}
@ -173,6 +175,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
groupByVisibilityToggle={groupByVisibilityToggle} groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
loadMoreIssues={loadMoreIssues}
/> />
)} )}
</div> </div>
@ -184,7 +187,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
export interface IKanBan { export interface IKanBan {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; issueIds: TGroupedIssues | TSubGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
@ -193,6 +196,7 @@ export interface IKanBan {
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
loadMoreIssues: (() => void) | undefined;
showEmptyGroup: boolean; showEmptyGroup: boolean;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: ( quickAddCallback?: (
@ -222,6 +226,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
quickActions, quickActions,
kanbanFilters, kanbanFilters,
handleKanbanFilters, handleKanbanFilters,
loadMoreIssues,
enableQuickIssueCreate, enableQuickIssueCreate,
quickAddCallback, quickAddCallback,
viewId, viewId,
@ -249,6 +254,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
quickActions={quickActions} quickActions={quickActions}
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}
loadMoreIssues={loadMoreIssues}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
viewId={viewId} viewId={viewId}

View File

@ -1,4 +1,4 @@
import { MutableRefObject } from "react"; import { MutableRefObject, useRef } from "react";
import { Droppable } from "@hello-pangea/dnd"; import { Droppable } from "@hello-pangea/dnd";
// hooks // hooks
import { useProjectState } from "hooks/store"; import { useProjectState } from "hooks/store";
@ -13,6 +13,8 @@ import {
TUnGroupedIssues, TUnGroupedIssues,
} from "@plane/types"; } from "@plane/types";
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
import { KanbanIssueBlockLoader } from "components/ui/loader";
import { useIntersectionObserver } from "hooks/use-intersection-observer";
interface IKanbanGroup { interface IKanbanGroup {
groupId: string; groupId: string;
@ -33,6 +35,7 @@ interface IKanbanGroup {
data: TIssue, data: TIssue,
viewId?: string viewId?: string
) => Promise<TIssue | undefined>; ) => Promise<TIssue | undefined>;
loadMoreIssues: (() => void) | undefined;
viewId?: string; viewId?: string;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
@ -55,6 +58,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
updateIssue, updateIssue,
quickActions, quickActions,
canEditProperties, canEditProperties,
loadMoreIssues,
enableQuickIssueCreate, enableQuickIssueCreate,
disableIssueCreation, disableIssueCreation,
quickAddCallback, quickAddCallback,
@ -65,6 +69,10 @@ export const KanbanGroup = (props: IKanbanGroup) => {
// hooks // hooks
const projectState = useProjectState(); const projectState = useProjectState();
const intersectionRef = useRef<HTMLSpanElement | null>(null);
useIntersectionObserver(scrollableContainerRef, intersectionRef, loadMoreIssues, `50% 0% 50% 0%`);
const prePopulateQuickAddData = ( const prePopulateQuickAddData = (
groupByKey: string | null, groupByKey: string | null,
subGroupByKey: string | null, subGroupByKey: string | null,
@ -131,7 +139,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
columnId={groupId} columnId={groupId}
issuesMap={issuesMap} issuesMap={issuesMap}
peekIssueId={peekIssueId} peekIssueId={peekIssueId}
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []} issueIds={(issueIds as TGroupedIssues)?.[groupId]?.issueIds || []}
displayProperties={displayProperties} displayProperties={displayProperties}
isDragDisabled={isDragDisabled} isDragDisabled={isDragDisabled}
updateIssue={updateIssue} updateIssue={updateIssue}
@ -143,6 +151,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
{provided.placeholder} {provided.placeholder}
{loadMoreIssues && <KanbanIssueBlockLoader ref={intersectionRef} />}
{enableQuickIssueCreate && !disableIssueCreation && ( {enableQuickIssueCreate && !disableIssueCreation && (
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0"> <div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
<KanBanQuickAddIssueForm <KanBanQuickAddIssueForm

View File

@ -22,7 +22,7 @@ import { KanbanStoreType } from "./base-kanban-root";
// constants // constants
interface ISubGroupSwimlaneHeader { interface ISubGroupSwimlaneHeader {
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; issueIds: TGroupedIssues | TSubGroupedIssues;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
list: IGroupByColumn[]; list: IGroupByColumn[];
@ -34,7 +34,7 @@ interface ISubGroupSwimlaneHeader {
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
let headerCount = 0; let headerCount = 0;
Object.keys(issueIds).map((groupState) => { Object.keys(issueIds).map((groupState) => {
headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.length || 0); headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.issueCount || 0);
}); });
return headerCount; return headerCount;
}; };
@ -93,6 +93,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
) => Promise<TIssue | undefined>; ) => Promise<TIssue | undefined>;
viewId?: string; viewId?: string;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
loadMoreIssues: (() => void) | undefined;
} }
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => { const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const { const {
@ -107,6 +108,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
displayProperties, displayProperties,
kanbanFilters, kanbanFilters,
handleKanbanFilters, handleKanbanFilters,
loadMoreIssues,
showEmptyGroup, showEmptyGroup,
enableQuickIssueCreate, enableQuickIssueCreate,
canEditProperties, canEditProperties,
@ -122,7 +124,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const subGroupedIds = issueIds as TSubGroupedIssues; const subGroupedIds = issueIds as TSubGroupedIssues;
subGroupedIds?.[column_id] && subGroupedIds?.[column_id] &&
Object.keys(subGroupedIds?.[column_id])?.forEach((_list: any) => { Object.keys(subGroupedIds?.[column_id])?.forEach((_list: any) => {
issueCount += subGroupedIds?.[column_id]?.[_list]?.length || 0; issueCount += subGroupedIds?.[column_id]?.[_list]?.issueCount || 0;
}); });
return issueCount; return issueCount;
}; };
@ -131,7 +133,9 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
<div className="relative h-max min-h-full w-full"> <div className="relative h-max min-h-full w-full">
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: any) => ( list.map((_list: any, index: number) => {
const isLastSubGroup = index === list.length - 1;
return (
<div key={_list.id} className="flex flex-shrink-0 flex-col"> <div key={_list.id} className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1"> <div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2"> <div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
@ -169,18 +173,20 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
storeType={storeType} storeType={storeType}
loadMoreIssues={isLastSubGroup ? loadMoreIssues : undefined}
/> />
</div> </div>
)} )}
</div> </div>
))} );
})}
</div> </div>
); );
}); });
export interface IKanBanSwimLanes { export interface IKanBanSwimLanes {
issuesMap: IIssueMap; issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; issueIds: TGroupedIssues | TSubGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
@ -188,6 +194,7 @@ export interface IKanBanSwimLanes {
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
loadMoreIssues: (() => void) | undefined;
showEmptyGroup: boolean; showEmptyGroup: boolean;
isDragStarted?: boolean; isDragStarted?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
@ -217,6 +224,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
quickActions, quickActions,
kanbanFilters, kanbanFilters,
handleKanbanFilters, handleKanbanFilters,
loadMoreIssues,
showEmptyGroup, showEmptyGroup,
isDragStarted, isDragStarted,
disableIssueCreation, disableIssueCreation,
@ -282,6 +290,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
quickActions={quickActions} quickActions={quickActions}
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}
loadMoreIssues={loadMoreIssues}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}

View File

@ -1,15 +1,17 @@
import { FC, useCallback } from "react"; import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types // types
import { EIssuesStoreType } from "constants/issue"; import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { useIssues, useUser } from "hooks/store"; import { useIssues, useUser } from "hooks/store";
import { TIssue } from "@plane/types"; import { TGroupedIssues, TIssue, TUnGroupedIssues } from "@plane/types";
// components // components
import { List } from "./default"; import { List } from "./default";
import { IQuickActionProps } from "./list-view-types"; import { IQuickActionProps } from "./list-view-types";
import { useIssuesActions } from "hooks/use-issues-actions"; import { useIssuesActions } from "hooks/use-issues-actions";
import { IssueLayoutHOC } from "../issue-layout-HOC";
import useSWR from "swr";
// constants // constants
// hooks // hooks
@ -40,7 +42,8 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
} = props; } = props;
const { issuesFilter, issues } = useIssues(storeType); const { issuesFilter, issues } = useIssues(storeType);
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType); const { fetchIssues, fetchNextIssues, updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } =
useIssuesActions(storeType);
// mobx store // mobx store
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
@ -48,9 +51,14 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
const { issueMap } = useIssues(); const { issueMap } = useIssues();
useSWR(`ISSUE_LIST_LAYOUT_${storeType}`, () => fetchIssues("init-loader", { canGroup: true, perPageCount: 100 }), {
revalidateOnFocus: false,
revalidateOnReconnect: false,
});
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const issueIds = issues?.groupedIssueIds || []; const issueIds = issues?.groupedIssueIds as TGroupedIssues | undefined;
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
const canEditProperties = useCallback( const canEditProperties = useCallback(
@ -85,7 +93,12 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
); );
const loadMoreIssues = useCallback(() => {
fetchNextIssues();
}, [fetchNextIssues]);
return ( return (
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.LIST}>
<div className={`relative h-full w-full bg-custom-background-90`}> <div className={`relative h-full w-full bg-custom-background-90`}>
<List <List
issuesMap={issueMap} issuesMap={issueMap}
@ -93,7 +106,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
group_by={group_by} group_by={group_by}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={renderQuickActions} quickActions={renderQuickActions}
issueIds={issueIds} issueIds={issueIds!}
shouldLoadMore={issues.next_page_results}
loadMoreIssues={loadMoreIssues}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
viewId={viewId} viewId={viewId}
quickAddCallback={issues?.quickAddIssue} quickAddCallback={issues?.quickAddIssue}
@ -105,5 +120,6 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
isCompletedCycle={isCompletedCycle} isCompletedCycle={isCompletedCycle}
/> />
</div> </div>
</IssueLayoutHOC>
); );
}); });

View File

@ -28,7 +28,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
key={`${issueId}`} key={`${issueId}`}
defaultHeight="3rem" defaultHeight="3rem"
root={containerRef} root={containerRef}
classNames={"relative border border-transparent border-b-custom-border-200 last:border-b-transparent"} classNames={"relative border border-transparent border-b-custom-border-200"}
changingReference={issueIds} changingReference={issueIds}
> >
<IssueBlock <IssueBlock

View File

@ -13,13 +13,17 @@ import {
TIssueMap, TIssueMap,
TUnGroupedIssues, TUnGroupedIssues,
IGroupByColumn, IGroupByColumn,
TIssueGroup,
} from "@plane/types"; } from "@plane/types";
import { getGroupByColumns } from "../utils"; import { getGroupByColumns } from "../utils";
import { HeaderGroupByCard } from "./headers/group-by-card"; import { HeaderGroupByCard } from "./headers/group-by-card";
import { EIssuesStoreType } from "constants/issue"; import { EIssuesStoreType } from "constants/issue";
import { ArrowDown } from "lucide-react";
import { ListLoaderItemRow } from "components/ui";
import { useIntersectionObserver } from "hooks/use-intersection-observer";
export interface IGroupByList { export interface IGroupByList {
issueIds: TGroupedIssues | TUnGroupedIssues | any; issueIds: TGroupedIssues;
issuesMap: TIssueMap; issuesMap: TIssueMap;
group_by: string | null; group_by: string | null;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
@ -39,6 +43,8 @@ export interface IGroupByList {
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
viewId?: string; viewId?: string;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
shouldLoadMore: boolean;
loadMoreIssues: () => void;
} }
const GroupByList: React.FC<IGroupByList> = (props) => { const GroupByList: React.FC<IGroupByList> = (props) => {
@ -57,7 +63,9 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
disableIssueCreation, disableIssueCreation,
storeType, storeType,
addIssuesToView, addIssuesToView,
shouldLoadMore,
isCompletedCycle = false, isCompletedCycle = false,
loadMoreIssues,
} = props; } = props;
// store hooks // store hooks
const member = useMember(); const member = useMember();
@ -67,8 +75,11 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
const cycle = useCycle(); const cycle = useCycle();
const projectModule = useModule(); const projectModule = useModule();
const intersectionRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
useIntersectionObserver(containerRef, intersectionRef, loadMoreIssues, `50% 0% 50% 0%`);
const groups = getGroupByColumns( const groups = getGroupByColumns(
group_by as GroupByColumnTypes, group_by as GroupByColumnTypes,
project, project,
@ -112,14 +123,12 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
return preloadedData; return preloadedData;
}; };
const validateEmptyIssueGroups = (issues: TIssue[]) => { const validateEmptyIssueGroups = (issues: TIssueGroup) => {
const issuesCount = issues?.length || 0; const issuesCount = issues?.issueCount || 0;
if (!showEmptyGroup && issuesCount <= 0) return false; if (!showEmptyGroup && issuesCount <= 0) return false;
return true; return true;
}; };
const is_list = group_by === null ? true : false;
const isGroupByCreatedBy = group_by === "created_by"; const isGroupByCreatedBy = group_by === "created_by";
return ( return (
@ -129,15 +138,18 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
> >
{groups && {groups &&
groups.length > 0 && groups.length > 0 &&
groups.map( groups.map((_list: IGroupByColumn) => {
(_list: IGroupByColumn) => const issueGroup = issueIds?.[_list.id] as TIssueGroup;
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
return (
issueGroup &&
validateEmptyIssueGroups(issueGroup) && (
<div key={_list.id} className={`flex flex-shrink-0 flex-col`}> <div key={_list.id} className={`flex flex-shrink-0 flex-col`}>
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1"> <div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
<HeaderGroupByCard <HeaderGroupByCard
icon={_list.icon} icon={_list.icon}
title={_list.name || ""} title={_list.name || ""}
count={is_list ? issueIds?.length || 0 : issueIds?.[_list.id]?.length || 0} count={issueGroup.issueCount}
issuePayload={_list.payload} issuePayload={_list.payload}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle} disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle}
storeType={storeType} storeType={storeType}
@ -147,7 +159,7 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
{issueIds && ( {issueIds && (
<IssueBlocksList <IssueBlocksList
issueIds={is_list ? issueIds || 0 : issueIds?.[_list.id] || 0} issueIds={issueGroup.issueIds}
issuesMap={issuesMap} issuesMap={issuesMap}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={quickActions} quickActions={quickActions}
@ -156,6 +168,21 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
containerRef={containerRef} containerRef={containerRef}
/> />
)} )}
{/* &&
issueGroup.issueIds?.length <= issueGroup.issueCount */}
{shouldLoadMore &&
(group_by ? (
<div
className={
"h-11 relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm text-custom-primary-100 hover:underline cursor-pointer"
}
onClick={loadMoreIssues}
>
Load more &darr;
</div>
) : (
<ListLoaderItemRow ref={intersectionRef} />
))}
{enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && !isCompletedCycle && ( {enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && !isCompletedCycle && (
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0"> <div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
@ -168,13 +195,14 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
)} )}
</div> </div>
) )
)} );
})}
</div> </div>
); );
}; };
export interface IList { export interface IList {
issueIds: TGroupedIssues | TUnGroupedIssues | any; issueIds: TGroupedIssues | TUnGroupedIssues;
issuesMap: TIssueMap; issuesMap: TIssueMap;
group_by: string | null; group_by: string | null;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
@ -183,6 +211,7 @@ export interface IList {
showEmptyGroup: boolean; showEmptyGroup: boolean;
enableIssueQuickAdd: boolean; enableIssueQuickAdd: boolean;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
shouldLoadMore: boolean;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -193,6 +222,7 @@ export interface IList {
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
storeType: EIssuesStoreType; storeType: EIssuesStoreType;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
loadMoreIssues: () => void;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
} }
@ -212,15 +242,19 @@ export const List: React.FC<IList> = (props) => {
disableIssueCreation, disableIssueCreation,
storeType, storeType,
addIssuesToView, addIssuesToView,
shouldLoadMore,
loadMoreIssues,
isCompletedCycle = false, isCompletedCycle = false,
} = props; } = props;
return ( return (
<div className="relative h-full w-full"> <div className="relative h-full w-full">
<GroupByList <GroupByList
issueIds={issueIds as TUnGroupedIssues} issueIds={issueIds}
issuesMap={issuesMap} issuesMap={issuesMap}
group_by={group_by} group_by={group_by}
shouldLoadMore={shouldLoadMore}
loadMoreIssues={loadMoreIssues}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}

View File

@ -156,7 +156,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
> >
<PlusIcon className="h-3.5 w-3.5 stroke-2" /> <PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span> <span className="text-sm font-medium loader">New Issue</span>
</div> </div>
)} )}
</div> </div>

View File

@ -30,21 +30,16 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const { const {
issuesFilter: { filters, fetchFilters, updateFilters }, issuesFilter: { filters, fetchFilters, updateFilters },
issues: { loader, groupedIssueIds, fetchIssues }, issues: { loader, issueCount: totalIssueCount, groupedIssueIds, fetchIssues, fetchNextIssues },
} = useIssues(EIssuesStoreType.GLOBAL); } = useIssues(EIssuesStoreType.GLOBAL);
const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL);
const { dataViewId, issueIds } = groupedIssueIds;
const { const {
membership: { currentWorkspaceAllProjectsRole }, membership: { currentWorkspaceAllProjectsRole },
} = useUser(); } = useUser();
const { fetchAllGlobalViews } = useGlobalView(); const { fetchAllGlobalViews } = useGlobalView();
const { workspaceProjectIds } = useProject(); const { workspaceProjectIds } = useProject();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId);
const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view";
// filter init from the query params // filter init from the query params
const routerFilterParams = () => { const routerFilterParams = () => {
@ -76,6 +71,10 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
} }
}; };
const fetchNextPages = useCallback(() => {
if (workspaceSlug && globalViewId) fetchNextIssues(workspaceSlug.toString(), globalViewId.toString());
}, [fetchNextIssues, workspaceSlug, globalViewId]);
useSWR( useSWR(
workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null, workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null,
async () => { async () => {
@ -92,7 +91,15 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
if (workspaceSlug && globalViewId) { if (workspaceSlug && globalViewId) {
await fetchAllGlobalViews(workspaceSlug.toString()); await fetchAllGlobalViews(workspaceSlug.toString());
await fetchFilters(workspaceSlug.toString(), globalViewId.toString()); await fetchFilters(workspaceSlug.toString(), globalViewId.toString());
await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader"); await fetchIssues(
workspaceSlug.toString(),
globalViewId.toString(),
groupedIssueIds ? "mutation" : "init-loader",
{
canGroup: false,
perPageCount: 100,
}
);
routerFilterParams(); routerFilterParams();
} }
}, },
@ -136,30 +143,34 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)}
handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)}
portalElement={portalElement} portalElement={portalElement}
readOnly={!canEditProperties(issue.project_id)} readOnly={!canEditProperties(issue.project_id ?? undefined)}
/> />
), ),
[canEditProperties, removeIssue, updateIssue, archiveIssue] [canEditProperties, removeIssue, updateIssue, archiveIssue]
); );
if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { if (loader === "init-loader" || !globalViewId || !groupedIssueIds) {
return <SpreadsheetLayoutLoader />; return <SpreadsheetLayoutLoader />;
} }
const {
"All Issues": { issueIds, issueCount },
} = groupedIssueIds;
const emptyStateType = const emptyStateType =
(workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS; (workspaceProjectIds ?? []).length > 0 ? `workspace-${globalViewId}` : EmptyStateType.WORKSPACE_NO_PROJECTS;
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<div className="relative h-full w-full flex flex-col"> <div className="relative h-full w-full flex flex-col">
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} /> <GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} />
{issueIds.length === 0 ? ( {!totalIssueCount ? (
<EmptyState <EmptyState
type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS}
size="sm" size="sm"
primaryButtonOnClick={ primaryButtonOnClick={
(workspaceProjectIds ?? []).length > 0 (workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed" ? globalViewId !== "custom-view" && globalViewId !== "subscribed"
? () => { ? () => {
setTrackElement("All issues empty state"); setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
@ -177,11 +188,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
displayProperties={issueFilters?.displayProperties ?? {}} displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate} handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={issueIds} issueIds={Array.isArray(issueIds) ? issueIds : []}
quickActions={renderQuickActions} quickActions={renderQuickActions}
updateIssue={updateIssue} updateIssue={updateIssue}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
viewId={globalViewId} viewId={globalViewId.toString()}
onEndOfListTrigger={fetchNextPages}
/> />
{/* peek overview */} {/* peek overview */}
<IssuePeekOverview /> <IssuePeekOverview />

View File

@ -4,13 +4,7 @@ import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx store // mobx store
// components // components
import { import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, IssuePeekOverview } from "components/issues";
ArchivedIssueListLayout,
ArchivedIssueAppliedFiltersRoot,
ProjectArchivedEmptyState,
IssuePeekOverview,
} from "components/issues";
import { ListLayoutLoader } from "components/ui";
import { EIssuesStoreType } from "constants/issue"; import { EIssuesStoreType } from "constants/issue";
// ui // ui
import { useIssues } from "hooks/store"; import { useIssues } from "hooks/store";
@ -27,37 +21,19 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
async () => { async () => {
if (workspaceSlug && projectId) { if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
} }
}, },
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
); );
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <ListLayoutLoader />;
}
if (!workspaceSlug || !projectId) return <></>; if (!workspaceSlug || !projectId) return <></>;
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<ArchivedIssueAppliedFiltersRoot /> <ArchivedIssueAppliedFiltersRoot />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ProjectArchivedEmptyState />
</div>
) : (
<Fragment>
<div className="relative h-full w-full overflow-auto"> <div className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout /> <ArchivedIssueListLayout />
</div> </div>
<IssuePeekOverview is_archived /> <IssuePeekOverview is_archived />
</Fragment>
)}
</div> </div>
); );
}); });

View File

@ -1,6 +1,5 @@
import React, { Fragment, useState } from "react"; import React, { useState } from "react";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import size from "lodash/size";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -10,19 +9,32 @@ import { TransferIssues, TransferIssuesModal } from "components/cycles";
import { import {
CycleAppliedFiltersRoot, CycleAppliedFiltersRoot,
CycleCalendarLayout, CycleCalendarLayout,
CycleEmptyState,
CycleGanttLayout, CycleGanttLayout,
CycleKanBanLayout, CycleKanBanLayout,
CycleListLayout, CycleListLayout,
CycleSpreadsheetLayout, CycleSpreadsheetLayout,
IssuePeekOverview, IssuePeekOverview,
} from "components/issues"; } from "components/issues";
import { ActiveLoader } from "components/ui";
// constants // constants
import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { useCycle, useIssues } from "hooks/store"; import { useCycle, useIssues } from "hooks/store";
// types
import { IIssueFilterOptions } from "@plane/types"; const CycleIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined }) => {
switch (props.activeLayout) {
case EIssueLayoutTypes.LIST:
return <CycleListLayout />;
case EIssueLayoutTypes.KANBAN:
return <CycleKanBanLayout />;
case EIssueLayoutTypes.CALENDAR:
return <CycleCalendarLayout />;
case EIssueLayoutTypes.GANTT:
return <CycleGanttLayout />;
case EIssueLayoutTypes.SPREADSHEET:
return <CycleSpreadsheetLayout />;
default:
return null;
}
};
export const CycleLayoutRoot: React.FC = observer(() => { export const CycleLayoutRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
@ -40,12 +52,6 @@ export const CycleLayoutRoot: React.FC = observer(() => {
async () => { async () => {
if (workspaceSlug && projectId && cycleId) { if (workspaceSlug && projectId && cycleId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader",
cycleId.toString()
);
} }
}, },
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
@ -56,37 +62,8 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft"; const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
const userFilters = issuesFilter?.issueFilters?.filters;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !cycleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = [];
});
issuesFilter.updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{
...newFilters,
},
cycleId.toString()
);
};
if (!workspaceSlug || !projectId || !cycleId) return <></>; if (!workspaceSlug || !projectId || !cycleId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
}
return ( return (
<> <>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> <TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
@ -99,36 +76,11 @@ export const CycleLayoutRoot: React.FC = observer(() => {
)} )}
<CycleAppliedFiltersRoot /> <CycleAppliedFiltersRoot />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<CycleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleId.toString()}
activeLayout={activeLayout}
handleClearAllFilters={handleClearAllFilters}
isEmptyFilters={issueFilterCount > 0}
/>
</div>
) : (
<Fragment>
<div className="h-full w-full overflow-auto"> <div className="h-full w-full overflow-auto">
{activeLayout === "list" ? ( <CycleIssueLayout activeLayout={activeLayout} />
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div> </div>
{/* peek overview */} {/* peek overview */}
<IssuePeekOverview /> <IssuePeekOverview />
</Fragment>
)}
</div> </div>
</> </>
); );

View File

@ -4,17 +4,25 @@ import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import { IssuePeekOverview } from "components/issues/peek-overview"; import { IssuePeekOverview } from "components/issues/peek-overview";
import { ActiveLoader } from "components/ui"; import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { EIssuesStoreType } from "constants/issue";
import { useIssues } from "hooks/store"; import { useIssues } from "hooks/store";
// components // components
import { ProjectDraftEmptyState } from "../empty-states";
import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue"; import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue";
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
import { DraftIssueListLayout } from "../list/roots/draft-issue-root"; import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
// ui // ui
// constants // constants
const DraftIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined }) => {
switch (props.activeLayout) {
case EIssueLayoutTypes.LIST:
return <DraftIssueListLayout />;
case EIssueLayoutTypes.KANBAN:
return <DraftKanBanLayout />;
default:
return null;
}
};
export const DraftIssueLayoutRoot: React.FC = observer(() => { export const DraftIssueLayoutRoot: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
@ -27,11 +35,6 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
async () => { async () => {
if (workspaceSlug && projectId) { if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
} }
}, },
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
@ -41,29 +44,14 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
if (!workspaceSlug || !projectId) return <></>; if (!workspaceSlug || !projectId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<DraftIssueAppliedFiltersRoot /> <DraftIssueAppliedFiltersRoot />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ProjectDraftEmptyState />
</div>
) : (
<div className="relative h-full w-full overflow-auto"> <div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? ( <DraftIssueLayout activeLayout={activeLayout} />
<DraftIssueListLayout />
) : activeLayout === "kanban" ? (
<DraftKanBanLayout />
) : null}
{/* issue peek overview */} {/* issue peek overview */}
<IssuePeekOverview is_draft /> <IssuePeekOverview is_draft />
</div> </div>
)}
</div> </div>
); );
}); });

View File

@ -1,5 +1,4 @@
import React, { Fragment } from "react"; import React from "react";
import size from "lodash/size";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -9,18 +8,32 @@ import {
IssuePeekOverview, IssuePeekOverview,
ModuleAppliedFiltersRoot, ModuleAppliedFiltersRoot,
ModuleCalendarLayout, ModuleCalendarLayout,
ModuleEmptyState,
ModuleGanttLayout, ModuleGanttLayout,
ModuleKanBanLayout, ModuleKanBanLayout,
ModuleListLayout, ModuleListLayout,
ModuleSpreadsheetLayout, ModuleSpreadsheetLayout,
} from "components/issues"; } from "components/issues";
import { ActiveLoader } from "components/ui";
// constants // constants
import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { useIssues } from "hooks/store"; import { useIssues } from "hooks/store";
// types // types
import { IIssueFilterOptions } from "@plane/types";
const ModuleIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined }) => {
switch (props.activeLayout) {
case EIssueLayoutTypes.LIST:
return <ModuleListLayout />;
case EIssueLayoutTypes.KANBAN:
return <ModuleKanBanLayout />;
case EIssueLayoutTypes.CALENDAR:
return <ModuleCalendarLayout />;
case EIssueLayoutTypes.GANTT:
return <ModuleGanttLayout />;
case EIssueLayoutTypes.SPREADSHEET:
return <ModuleSpreadsheetLayout />;
default:
return null;
}
};
export const ModuleLayoutRoot: React.FC = observer(() => { export const ModuleLayoutRoot: React.FC = observer(() => {
// router // router
@ -36,84 +49,23 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
async () => { async () => {
if (workspaceSlug && projectId && moduleId) { if (workspaceSlug && projectId && moduleId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader",
moduleId.toString()
);
} }
}, },
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
); );
const userFilters = issuesFilter?.issueFilters?.filters;
const issueFilterCount = size(
Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
)
);
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !moduleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = [];
});
issuesFilter.updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{
...newFilters,
},
moduleId.toString()
);
};
if (!workspaceSlug || !projectId || !moduleId) return <></>; if (!workspaceSlug || !projectId || !moduleId) return <></>;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<ModuleAppliedFiltersRoot /> <ModuleAppliedFiltersRoot />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ModuleEmptyState
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId.toString()}
activeLayout={activeLayout}
handleClearAllFilters={handleClearAllFilters}
isEmptyFilters={issueFilterCount > 0}
/>
</div>
) : (
<Fragment>
<div className="h-full w-full overflow-auto"> <div className="h-full w-full overflow-auto">
{activeLayout === "list" ? ( <ModuleIssueLayout activeLayout={activeLayout} />
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div> </div>
{/* peek overview */} {/* peek overview */}
<IssuePeekOverview /> <IssuePeekOverview />
</Fragment>
)}
</div> </div>
); );
}); });

View File

@ -1,4 +1,4 @@
import { FC, Fragment } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -12,16 +12,31 @@ import {
KanBanLayout, KanBanLayout,
ProjectAppliedFiltersRoot, ProjectAppliedFiltersRoot,
ProjectSpreadsheetLayout, ProjectSpreadsheetLayout,
ProjectEmptyState,
IssuePeekOverview, IssuePeekOverview,
} from "components/issues"; } from "components/issues";
// hooks // hooks
// helpers // helpers
import { ActiveLoader } from "components/ui";
// constants // constants
import { EIssuesStoreType } from "constants/issue"; import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { useIssues } from "hooks/store"; import { useIssues } from "hooks/store";
const ProjectIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined }) => {
switch (props.activeLayout) {
case EIssueLayoutTypes.LIST:
return <ListLayout />;
case EIssueLayoutTypes.KANBAN:
return <KanBanLayout />;
case EIssueLayoutTypes.CALENDAR:
return <CalendarLayout />;
case EIssueLayoutTypes.GANTT:
return <GanttLayout />;
case EIssueLayoutTypes.SPREADSHEET:
return <ProjectSpreadsheetLayout />;
default:
return null;
}
};
export const ProjectLayoutRoot: FC = observer(() => { export const ProjectLayoutRoot: FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
@ -34,11 +49,6 @@ export const ProjectLayoutRoot: FC = observer(() => {
async () => { async () => {
if (workspaceSlug && projectId) { if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader"
);
} }
}, },
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
@ -48,18 +58,9 @@ export const ProjectLayoutRoot: FC = observer(() => {
if (!workspaceSlug || !projectId) return <></>; if (!workspaceSlug || !projectId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectAppliedFiltersRoot /> <ProjectAppliedFiltersRoot />
{issues?.groupedIssueIds?.length === 0 ? (
<ProjectEmptyState />
) : (
<Fragment>
<div className="relative h-full w-full overflow-auto bg-custom-background-90"> <div className="relative h-full w-full overflow-auto bg-custom-background-90">
{/* mutation loader */} {/* mutation loader */}
{issues?.loader === "mutation" && ( {issues?.loader === "mutation" && (
@ -67,23 +68,11 @@ export const ProjectLayoutRoot: FC = observer(() => {
<Spinner className="w-4 h-4" /> <Spinner className="w-4 h-4" />
</div> </div>
)} )}
{activeLayout === "list" ? ( <ProjectIssueLayout activeLayout={activeLayout} />
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div> </div>
{/* peek overview */} {/* peek overview */}
<IssuePeekOverview /> <IssuePeekOverview />
</Fragment>
)}
</div> </div>
); );
}); });

View File

@ -1,4 +1,4 @@
import React, { Fragment } from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -8,18 +8,33 @@ import {
IssuePeekOverview, IssuePeekOverview,
ProjectViewAppliedFiltersRoot, ProjectViewAppliedFiltersRoot,
ProjectViewCalendarLayout, ProjectViewCalendarLayout,
ProjectViewEmptyState,
ProjectViewGanttLayout, ProjectViewGanttLayout,
ProjectViewKanBanLayout, ProjectViewKanBanLayout,
ProjectViewListLayout, ProjectViewListLayout,
ProjectViewSpreadsheetLayout, ProjectViewSpreadsheetLayout,
} from "components/issues"; } from "components/issues";
import { ActiveLoader } from "components/ui";
// constants // constants
import { EIssuesStoreType } from "constants/issue"; import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { useIssues } from "hooks/store"; import { useIssues } from "hooks/store";
// types // types
const ProjectViewIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined }) => {
switch (props.activeLayout) {
case EIssueLayoutTypes.LIST:
return <ProjectViewListLayout />;
case EIssueLayoutTypes.KANBAN:
return <ProjectViewKanBanLayout />;
case EIssueLayoutTypes.CALENDAR:
return <ProjectViewCalendarLayout />;
case EIssueLayoutTypes.GANTT:
return <ProjectViewGanttLayout />;
case EIssueLayoutTypes.SPREADSHEET:
return <ProjectViewSpreadsheetLayout />;
default:
return null;
}
};
export const ProjectViewLayoutRoot: React.FC = observer(() => { export const ProjectViewLayoutRoot: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
@ -32,12 +47,6 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
async () => { async () => {
if (workspaceSlug && projectId && viewId) { if (workspaceSlug && projectId && viewId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString()); await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString());
await issues?.fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
issues?.groupedIssueIds ? "mutation" : "init-loader",
viewId.toString()
);
} }
}, },
{ revalidateIfStale: false, revalidateOnFocus: false } { revalidateIfStale: false, revalidateOnFocus: false }
@ -47,38 +56,15 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
if (!workspaceSlug || !projectId || !viewId) return <></>; if (!workspaceSlug || !projectId || !viewId) return <></>;
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
}
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectViewAppliedFiltersRoot /> <ProjectViewAppliedFiltersRoot />
{issues?.groupedIssueIds?.length === 0 ? (
<div className="relative h-full w-full overflow-y-auto">
<ProjectViewEmptyState />
</div>
) : (
<Fragment>
<div className="relative h-full w-full overflow-auto"> <div className="relative h-full w-full overflow-auto">
{activeLayout === "list" ? ( <ProjectViewIssueLayout activeLayout={activeLayout} />
<ProjectViewListLayout />
) : activeLayout === "kanban" ? (
<ProjectViewKanBanLayout />
) : activeLayout === "calendar" ? (
<ProjectViewCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ProjectViewGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectViewSpreadsheetLayout />
) : null}
</div> </div>
{/* peek overview */} {/* peek overview */}
<IssuePeekOverview /> <IssuePeekOverview />
</Fragment>
)}
</div> </div>
); );
}); });

View File

@ -2,7 +2,7 @@ import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { useIssues, useUser } from "hooks/store"; import { useIssues, useUser } from "hooks/store";
import { useIssuesActions } from "hooks/use-issues-actions"; import { useIssuesActions } from "hooks/use-issues-actions";
@ -12,6 +12,8 @@ import { useIssuesActions } from "hooks/use-issues-actions";
import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types";
import { IQuickActionProps } from "../list/list-view-types"; import { IQuickActionProps } from "../list/list-view-types";
import { SpreadsheetView } from "./spreadsheet-view"; import { SpreadsheetView } from "./spreadsheet-view";
import useSWR from "swr";
import { IssueLayoutHOC } from "../issue-layout-HOC";
export type SpreadsheetStoreType = export type SpreadsheetStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
@ -36,13 +38,30 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { issues, issuesFilter } = useIssues(storeType); const { issues, issuesFilter } = useIssues(storeType);
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = const {
useIssuesActions(storeType); fetchIssues,
fetchNextIssues,
updateIssue,
removeIssue,
removeIssueFromView,
archiveIssue,
restoreIssue,
updateFilters,
} = useIssuesActions(storeType);
// derived values // derived values
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
// user role validation // user role validation
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
useSWR(
`ISSUE_SPREADSHEET_LAYOUT_${storeType}`,
() => fetchIssues("init-loader", { canGroup: false, perPageCount: 100 }),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
const canEditProperties = useCallback( const canEditProperties = useCallback(
(projectId: string | undefined) => { (projectId: string | undefined) => {
const isEditingAllowedBasedOnProject = const isEditingAllowedBasedOnProject =
@ -53,7 +72,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
); );
const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; const issueIds = issues.groupedIssueIds?.["All Issues"]?.issueIds ?? [];
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
@ -83,7 +102,10 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
); );
if (!Array.isArray(issueIds)) return null;
return ( return (
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.SPREADSHEET}>
<SpreadsheetView <SpreadsheetView
displayProperties={issuesFilter.issueFilters?.displayProperties ?? {}} displayProperties={issuesFilter.issueFilters?.displayProperties ?? {}}
displayFilters={issuesFilter.issueFilters?.displayFilters ?? {}} displayFilters={issuesFilter.issueFilters?.displayFilters ?? {}}
@ -96,6 +118,8 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
viewId={viewId} viewId={viewId}
enableQuickCreateIssue={enableQuickAdd} enableQuickCreateIssue={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
onEndOfListTrigger={fetchNextIssues}
/> />
</IssueLayoutHOC>
); );
}); });

View File

@ -14,7 +14,9 @@ type Props = {
issueDetail: TIssue; issueDetail: TIssue;
disableUserActions: boolean; disableUserActions: boolean;
property: keyof IIssueDisplayProperties; property: keyof IIssueDisplayProperties;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
isEstimateEnabled: boolean; isEstimateEnabled: boolean;
}; };

View File

@ -29,7 +29,9 @@ interface Props {
portalElement?: HTMLDivElement | null portalElement?: HTMLDivElement | null
) => React.ReactNode; ) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
portalElement: React.MutableRefObject<HTMLDivElement | null>; portalElement: React.MutableRefObject<HTMLDivElement | null>;
nestingLevel: number; nestingLevel: number;
issueId: string; issueId: string;
@ -115,7 +117,9 @@ interface IssueRowDetailsProps {
portalElement?: HTMLDivElement | null portalElement?: HTMLDivElement | null
) => React.ReactNode; ) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
portalElement: React.MutableRefObject<HTMLDivElement | null>; portalElement: React.MutableRefObject<HTMLDivElement | null>;
nestingLevel: number; nestingLevel: number;
issueId: string; issueId: string;
@ -163,7 +167,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
const handleToggleExpand = () => { const handleToggleExpand = () => {
setExpanded((prevState) => { setExpanded((prevState) => {
if (!prevState && workspaceSlug && issueDetail) if (!prevState && workspaceSlug && issueDetail && issueDetail.project_id)
subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issueDetail.project_id, issueDetail.id); subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issueDetail.project_id, issueDetail.id);
return !prevState; return !prevState;
}); });
@ -182,7 +186,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
); );
if (!issueDetail) return null; if (!issueDetail) return null;
const disableUserActions = !canEditProperties(issueDetail.project_id); const disableUserActions = !canEditProperties(issueDetail.project_id ?? undefined);
return ( return (
<> <>

View File

@ -6,6 +6,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@pl
//components //components
import { SpreadsheetIssueRow } from "./issue-row"; import { SpreadsheetIssueRow } from "./issue-row";
import { SpreadsheetHeader } from "./spreadsheet-header"; import { SpreadsheetHeader } from "./spreadsheet-header";
import { useIntersectionObserver } from "hooks/use-intersection-observer";
type Props = { type Props = {
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
@ -18,10 +19,13 @@ type Props = {
customActionButton?: React.ReactElement, customActionButton?: React.ReactElement,
portalElement?: HTMLDivElement | null portalElement?: HTMLDivElement | null
) => React.ReactNode; ) => React.ReactNode;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
portalElement: React.MutableRefObject<HTMLDivElement | null>; portalElement: React.MutableRefObject<HTMLDivElement | null>;
containerRef: MutableRefObject<HTMLTableElement | null>; containerRef: MutableRefObject<HTMLTableElement | null>;
onEndOfListTrigger: () => void;
}; };
export const SpreadsheetTable = observer((props: Props) => { export const SpreadsheetTable = observer((props: Props) => {
@ -36,10 +40,12 @@ export const SpreadsheetTable = observer((props: Props) => {
updateIssue, updateIssue,
canEditProperties, canEditProperties,
containerRef, containerRef,
onEndOfListTrigger,
} = props; } = props;
// states // states
const isScrolled = useRef(false); const isScrolled = useRef(false);
const intersectionRef = useRef<HTMLTableSectionElement | null>(null);
const handleScroll = useCallback(() => { const handleScroll = useCallback(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
@ -74,6 +80,28 @@ export const SpreadsheetTable = observer((props: Props) => {
}; };
}, [handleScroll, containerRef]); }, [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%`);
const handleKeyBoardNavigation = useTableKeyboardNavigation(); const handleKeyBoardNavigation = useTableKeyboardNavigation();
return ( return (
@ -102,6 +130,7 @@ export const SpreadsheetTable = observer((props: Props) => {
/> />
))} ))}
</tbody> </tbody>
<tfoot ref={intersectionRef}>Loading...</tfoot>
</table> </table>
); );
}); });

View File

@ -19,7 +19,9 @@ type Props = {
customActionButton?: React.ReactElement, customActionButton?: React.ReactElement,
portalElement?: HTMLDivElement | null portalElement?: HTMLDivElement | null
) => React.ReactNode; ) => React.ReactNode;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue:
| ((projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => Promise<void>)
| undefined;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
quickAddCallback?: ( quickAddCallback?: (
workspaceSlug: string, workspaceSlug: string,
@ -29,6 +31,7 @@ type Props = {
) => Promise<TIssue | undefined>; ) => Promise<TIssue | undefined>;
viewId?: string; viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
onEndOfListTrigger: () => void;
enableQuickCreateIssue?: boolean; enableQuickCreateIssue?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
}; };
@ -46,6 +49,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
canEditProperties, canEditProperties,
enableQuickCreateIssue, enableQuickCreateIssue,
disableIssueCreation, disableIssueCreation,
onEndOfListTrigger,
} = props; } = props;
// refs // refs
const containerRef = useRef<HTMLTableElement | null>(null); const containerRef = useRef<HTMLTableElement | null>(null);
@ -77,6 +81,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
updateIssue={updateIssue} updateIssue={updateIssue}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
containerRef={containerRef} containerRef={containerRef}
onEndOfListTrigger={onEndOfListTrigger}
/> />
</div> </div>
<div className="border-t border-custom-border-100"> <div className="border-t border-custom-border-100">

View File

@ -47,7 +47,7 @@ export const getGroupByColumns = (
case "created_by": case "created_by":
return getCreatedByColumns(member) as any; return getCreatedByColumns(member) as any;
default: default:
if (includeNone) return [{ id: `null`, name: `All Issues`, payload: {}, icon: undefined }]; if (includeNone) return [{ id: `All Issues`, name: `All Issues`, payload: {}, icon: undefined }];
} }
}; };

View File

@ -6,11 +6,17 @@ import { CustomMenu } from "@plane/ui";
// icons // icons
// constants // constants
import { ProjectAnalyticsModal } from "components/analytics"; import { ProjectAnalyticsModal } from "components/analytics";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
ISSUE_LAYOUTS,
} from "constants/issue";
// hooks // hooks
import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store";
// layouts // layouts
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "./issue-layouts"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "./issue-layouts";
export const IssuesMobileHeader = () => { export const IssuesMobileHeader = () => {
@ -38,7 +44,7 @@ export const IssuesMobileHeader = () => {
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
}, },

View File

@ -4,9 +4,15 @@ import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
import { ProjectAnalyticsModal } from "components/analytics"; import { ProjectAnalyticsModal } from "components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
ISSUE_LAYOUTS,
} from "constants/issue";
import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store"; import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
export const ModuleMobileHeader = () => { export const ModuleMobileHeader = () => {
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
@ -34,7 +40,7 @@ export const ModuleMobileHeader = () => {
} = useMember(); } = useMember();
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
}, },

View File

@ -4,10 +4,15 @@ import { useRouter } from "next/router";
// components // components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "components/issues";
// hooks // hooks
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import {
EIssuesStoreType,
EIssueFilterType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
EIssueLayoutTypes,
} from "constants/issue";
import { useIssues, useLabel } from "hooks/store"; import { useIssues, useLabel } from "hooks/store";
// constants // constants
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
export const ProfileIssuesFilter = observer(() => { export const ProfileIssuesFilter = observer(() => {
// router // router
@ -25,7 +30,7 @@ export const ProfileIssuesFilter = observer(() => {
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !userId) return; if (!workspaceSlug || !userId) return;
updateFilters( updateFilters(
workspaceSlug.toString(), workspaceSlug.toString(),
@ -94,7 +99,7 @@ export const ProfileIssuesFilter = observer(() => {
return ( return (
<div className="relative flex items-center justify-end gap-2"> <div className="relative flex items-center justify-end gap-2">
<LayoutSelection <LayoutSelection
layouts={["list", "kanban"]} layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN]}
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />

View File

@ -28,7 +28,7 @@ export const CalendarLayoutLoader = () => (
<span className="h-7 w-20 bg-custom-background-80 rounded" /> <span className="h-7 w-20 bg-custom-background-80 rounded" />
</div> </div>
</div> </div>
<span className="relative grid divide-x-[0.5px] divide-custom-border-200 text-sm font-medium grid-cols-5 pr-[1rem]"> <span className="relative grid divide-x-[0.5px] divide-custom-border-200 text-sm font-medium grid-cols-5">
{[...Array(5)].map((_, index) => ( {[...Array(5)].map((_, index) => (
<span key={index} className="h-11 w-full bg-custom-background-80" /> <span key={index} className="h-11 w-full bg-custom-background-80" />
))} ))}

View File

@ -1,16 +1,22 @@
import { forwardRef } from "react";
export const KanbanIssueBlockLoader = forwardRef<HTMLSpanElement>((props, ref) => (
<span ref={ref} className="block h-28 m-1.5 animate-pulse bg-custom-background-80 rounded" />
));
export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { cardsInEachColumn?: number[] }) => ( export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { cardsInEachColumn?: number[] }) => (
<div className="flex gap-5 px-3.5 py-1.5 overflow-x-auto"> <div className="flex gap-5 px-3.5 py-1.5 overflow-x-auto">
{cardsInEachColumn.map((cardsInColumn, columnIndex) => ( {cardsInEachColumn.map((cardsInColumn, columnIndex) => (
<div key={columnIndex} className="flex flex-col gap-3 animate-pulse"> <div key={columnIndex} className="flex flex-col gap-3">
<div className="flex items-center justify-between h-9 w-80"> <div className="flex items-center justify-between h-9 w-80">
<div className="flex item-center gap-1.5"> <div className="flex item-center">
<span className="h-6 w-6 bg-custom-background-80 rounded" /> <span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
<span className="h-6 w-24 bg-custom-background-80 rounded" /> <span className="h-6 w-24 bg-custom-background-80 rounded animate-pulse" />
</div> </div>
<span className="h-6 w-6 bg-custom-background-80 rounded" /> <span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
</div> </div>
{Array.from({ length: cardsInColumn }, (_, cardIndex) => ( {Array.from({ length: cardsInColumn }, (_, cardIndex) => (
<span key={cardIndex} className="h-28 w-80 bg-custom-background-80 rounded" /> <KanbanIssueBlockLoader key={cardIndex} />
))} ))}
</div> </div>
))} ))}

View File

@ -1,7 +1,8 @@
import { forwardRef } from "react";
import { getRandomInt, getRandomLength } from "../utils"; import { getRandomInt, getRandomLength } from "../utils";
const ListItemRow = () => ( export const ListLoaderItemRow = forwardRef<HTMLDivElement>((props, ref) => (
<div className="flex items-center justify-between h-11 p-3 border-b border-custom-border-200"> <div ref={ref} className="flex items-center justify-between h-11 p-3 border-b border-custom-border-200">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="h-5 w-10 bg-custom-background-80 rounded" /> <span className="h-5 w-10 bg-custom-background-80 rounded" />
<span className={`h-5 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} /> <span className={`h-5 w-${getRandomLength(["32", "52", "72"])} bg-custom-background-80 rounded`} />
@ -18,7 +19,7 @@ const ListItemRow = () => (
))} ))}
</div> </div>
</div> </div>
); ));
const ListSection = ({ itemCount }: { itemCount: number }) => ( const ListSection = ({ itemCount }: { itemCount: number }) => (
<div className="flex flex-shrink-0 flex-col"> <div className="flex flex-shrink-0 flex-col">
@ -30,7 +31,7 @@ const ListSection = ({ itemCount }: { itemCount: number }) => (
</div> </div>
<div className="relative h-full w-full"> <div className="relative h-full w-full">
{[...Array(itemCount)].map((_, index) => ( {[...Array(itemCount)].map((_, index) => (
<ListItemRow key={index} /> <ListLoaderItemRow key={index} />
))} ))}
</div> </div>
</div> </div>

View File

@ -1,35 +1,6 @@
import {
CalendarLayoutLoader,
GanttLayoutLoader,
KanbanLayoutLoader,
ListLayoutLoader,
SpreadsheetLayoutLoader,
} from "./layouts";
export const getRandomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; export const getRandomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
export const getRandomLength = (lengthArray: string[]) => { export const getRandomLength = (lengthArray: string[]) => {
const randomIndex = Math.floor(Math.random() * lengthArray.length); const randomIndex = Math.floor(Math.random() * lengthArray.length);
return `${lengthArray[randomIndex]}`; return `${lengthArray[randomIndex]}`;
}; };
interface Props {
layout: string;
}
export const ActiveLoader: React.FC<Props> = (props) => {
const { layout } = props;
switch (layout) {
case "list":
return <ListLayoutLoader />;
case "kanban":
return <KanbanLayoutLoader />;
case "spreadsheet":
return <SpreadsheetLayoutLoader />;
case "calendar":
return <CalendarLayoutLoader />;
case "gantt_chart":
return <GanttLayoutLoader />;
default:
return <KanbanLayoutLoader />;
}
};

View File

@ -6,7 +6,6 @@ import {
IIssueDisplayProperties, IIssueDisplayProperties,
TIssueExtraOptions, TIssueExtraOptions,
TIssueGroupByOptions, TIssueGroupByOptions,
TIssueLayouts,
TIssueOrderByOptions, TIssueOrderByOptions,
TIssuePriorities, TIssuePriorities,
TIssueTypeFilters, TIssueTypeFilters,
@ -24,6 +23,14 @@ export enum EIssuesStoreType {
DEFAULT = "DEFAULT", DEFAULT = "DEFAULT",
} }
export enum EIssueLayoutTypes {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
GANTT = "gantt_chart",
SPREADSHEET = "spreadsheet",
}
export type TCreateModalStoreTypes = export type TCreateModalStoreTypes =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
| EIssuesStoreType.PROJECT_VIEW | EIssuesStoreType.PROJECT_VIEW
@ -115,15 +122,15 @@ export const ISSUE_EXTRA_OPTIONS: {
]; ];
export const ISSUE_LAYOUTS: { export const ISSUE_LAYOUTS: {
key: TIssueLayouts; key: EIssueLayoutTypes;
title: string; title: string;
icon: any; icon: any;
}[] = [ }[] = [
{ key: "list", title: "List Layout", icon: List }, { key: EIssueLayoutTypes.LIST, title: "List Layout", icon: List },
{ key: "kanban", title: "Kanban Layout", icon: Kanban }, { key: EIssueLayoutTypes.KANBAN, title: "Kanban Layout", icon: Kanban },
{ key: "calendar", title: "Calendar Layout", icon: Calendar }, { key: EIssueLayoutTypes.CALENDAR, title: "Calendar Layout", icon: Calendar },
{ key: "spreadsheet", title: "Spreadsheet Layout", icon: Sheet }, { key: EIssueLayoutTypes.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
{ key: "gantt_chart", title: "Gantt Chart Layout", icon: GanttChartSquare }, { key: EIssueLayoutTypes.GANTT, title: "Gantt Chart Layout", icon: GanttChartSquare },
]; ];
export interface ILayoutDisplayFiltersOptions { export interface ILayoutDisplayFiltersOptions {

View File

@ -4,17 +4,10 @@ import { v4 as uuidv4 } from "uuid";
// types // types
import { IGanttBlock } from "components/gantt-chart"; import { IGanttBlock } from "components/gantt-chart";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { STATE_GROUPS } from "constants/state"; import { STATE_GROUPS } from "constants/state";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
import { import { TIssue, TIssueGroupByOptions, TIssueOrderByOptions, TIssueParams, TStateGroups } from "@plane/types";
TIssue,
TIssueGroupByOptions,
TIssueLayouts,
TIssueOrderByOptions,
TIssueParams,
TStateGroups,
} from "@plane/types";
type THandleIssuesMutation = ( type THandleIssuesMutation = (
formData: Partial<TIssue>, formData: Partial<TIssue>,
@ -89,7 +82,7 @@ export const handleIssuesMutation: THandleIssuesMutation = (
}; };
export const handleIssueQueryParamsByLayout = ( export const handleIssueQueryParamsByLayout = (
layout: TIssueLayouts | undefined, layout: EIssueLayoutTypes | undefined,
viewType: "my_issues" | "issues" | "profile_issues" | "archived_issues" | "draft_issues" viewType: "my_issues" | "issues" | "profile_issues" | "archived_issues" | "draft_issues"
): TIssueParams[] | null => { ): TIssueParams[] | null => {
const queryParams: TIssueParams[] = []; const queryParams: TIssueParams[] = [];

View File

@ -0,0 +1,41 @@
import { RefObject, useState, useEffect } from "react";
export type UseIntersectionObserverProps = {
containerRef: RefObject<HTMLDivElement | null> | undefined;
elementRef: RefObject<HTMLDivElement>;
callback: () => void;
rootMargin?: string;
};
export const useIntersectionObserver = (
containerRef: RefObject<HTMLDivElement | null> | undefined,
elementRef: RefObject<HTMLElement | null>,
callback: (() => void) | undefined,
rootMargin?: string
) => {
useEffect(() => {
if (elementRef.current) {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
callback && callback();
}
},
{
root: containerRef?.current,
rootMargin,
}
);
observer.observe(elementRef.current);
return () => {
if (elementRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
observer.unobserve(elementRef.current);
}
};
}
// When i am passing callback as a dependency, it is causing infinite loop,
// Please make sure you fix this eslint lint disable error with caution
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef, containerRef, rootMargin, callback]);
};

View File

@ -28,7 +28,7 @@ export interface IArchivedIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor?: string cursor: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
@ -210,7 +210,6 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
}); });
}); });
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.archivedIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation"); this.rootIssueStore.archivedIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, { this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, {

View File

@ -26,6 +26,8 @@ export interface IArchivedIssues extends IBaseIssuesStore {
fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise<TIssuesResponse | undefined>;
restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: undefined;
} }
export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues { export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
@ -44,6 +46,9 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
makeObservable(this, { makeObservable(this, {
// action // action
fetchIssues: action, fetchIssues: action,
fetchNextIssues: action,
fetchIssuesWithExistingPagination: action,
restoreIssue: action, restoreIssue: action,
}); });
// filter store // filter store
@ -61,7 +66,7 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(options); const params = this.issueFilterStore?.getFilterParams(options, undefined);
const response = await this.issueArchiveService.getArchivedIssues(workspaceSlug, projectId, params); const response = await this.issueArchiveService.getArchivedIssues(workspaceSlug, projectId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -73,11 +78,11 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string) => {
if (!this.paginationOptions) return; if (!this.paginationOptions || !this.next_page_results) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions); const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response);
@ -113,4 +118,6 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
throw error; throw error;
} }
}; };
quickAddIssue = undefined;
} }

View File

@ -28,7 +28,7 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor?: string cursor: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
@ -222,7 +222,6 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
}); });
}); });
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.cycleIssues.fetchIssuesWithExistingPagination( this.rootIssueStore.cycleIssues.fetchIssuesWithExistingPagination(
workspaceSlug, workspaceSlug,
projectId, projectId,

View File

@ -80,11 +80,15 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
cycleId: observable.ref, cycleId: observable.ref,
// action // action
fetchIssues: action, fetchIssues: action,
fetchNextIssues: action,
fetchIssuesWithExistingPagination: action,
addIssueToCycle: action, addIssueToCycle: action,
removeIssueFromCycle: action, removeIssueFromCycle: action,
transferIssuesFromCycle: action, transferIssuesFromCycle: action,
fetchActiveCycleIssues: action, fetchActiveCycleIssues: action,
quickAddIssue: action,
}); });
// service // service
this.cycleService = new CycleService(); this.cycleService = new CycleService();
@ -107,7 +111,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
this.cycleId = cycleId; this.cycleId = cycleId;
const params = this.issueFilterStore?.getFilterParams(options); const params = this.issueFilterStore?.getFilterParams(options, undefined);
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params); const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -119,11 +123,11 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => {
if (!this.paginationOptions) return; if (!this.paginationOptions || !this.next_page_results) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions); const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor);
const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params); const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response);
@ -241,4 +245,6 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
throw error; throw error;
} }
}; };
quickAddIssue = this.issueQuickAdd;
} }

View File

@ -28,7 +28,7 @@ export interface IDraftIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor?: string cursor: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
@ -107,6 +107,10 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
paginationOptions.group_by = options.groupedBy; paginationOptions.group_by = options.groupedBy;
} }
if (options.after && options.before) {
paginationOptions["target_date"] = `${options.after};after,${options.before};before`;
}
return paginationOptions; return paginationOptions;
}); });
@ -205,7 +209,6 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
}); });
}); });
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.draftIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation"); this.rootIssueStore.draftIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, undefined, { this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, undefined, {

View File

@ -27,6 +27,8 @@ export interface IDraftIssues extends IBaseIssuesStore {
fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
quickAddIssue: undefined;
} }
export class DraftIssues extends BaseIssuesStore implements IDraftIssues { export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
@ -43,9 +45,8 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
makeObservable(this, { makeObservable(this, {
// action // action
fetchIssues: action, fetchIssues: action,
createIssue: action, fetchNextIssues: action,
updateIssue: action, fetchIssuesWithExistingPagination: action,
removeIssue: action,
}); });
// filter store // filter store
this.issueFilterStore = issueFilterStore; this.issueFilterStore = issueFilterStore;
@ -62,7 +63,7 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(options); const params = this.issueFilterStore?.getFilterParams(options, undefined);
const response = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params); const response = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -74,11 +75,11 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string) => {
if (!this.paginationOptions) return; if (!this.paginationOptions || !this.next_page_results) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions); const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response);
@ -100,4 +101,6 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
createIssue = this.createDraftIssue; createIssue = this.createDraftIssue;
updateIssue = this.updateDraftIssue; updateIssue = this.updateDraftIssue;
quickAddIssue = undefined;
} }

View File

@ -24,7 +24,7 @@ import {
import { IIssueRootStore } from "../root.store"; import { IIssueRootStore } from "../root.store";
import { IBaseIssueFilterStore } from "./issue-filter-helper.store"; import { IBaseIssueFilterStore } from "./issue-filter-helper.store";
// constants // constants
import { ISSUE_PRIORITIES } from "constants/issue"; import { EIssueLayoutTypes, ISSUE_PRIORITIES } from "constants/issue";
import { STATE_GROUPS } from "constants/state"; import { STATE_GROUPS } from "constants/state";
// helpers // helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper";
@ -44,6 +44,9 @@ export interface IBaseIssuesStore {
issueCount: number | undefined; issueCount: number | undefined;
pageCount: number | undefined; pageCount: number | undefined;
next_page_results: boolean;
prev_page_results: boolean;
groupedIssueCount: Record<string, number> | undefined; groupedIssueCount: Record<string, number> | undefined;
// computed // computed
@ -96,6 +99,9 @@ export class BaseIssuesStore implements IBaseIssuesStore {
issueCount: number | undefined = undefined; issueCount: number | undefined = undefined;
pageCount: number | undefined = undefined; pageCount: number | undefined = undefined;
next_page_results: boolean = true;
prev_page_results: boolean = false;
paginationOptions: IssuePaginationOptions | undefined = undefined; paginationOptions: IssuePaginationOptions | undefined = undefined;
isArchived: boolean; isArchived: boolean;
@ -119,6 +125,8 @@ export class BaseIssuesStore implements IBaseIssuesStore {
prevCursor: observable.ref, prevCursor: observable.ref,
issueCount: observable.ref, issueCount: observable.ref,
pageCount: observable.ref, pageCount: observable.ref,
next_page_results: observable.ref,
prev_page_results: observable.ref,
paginationOptions: observable, paginationOptions: observable,
// computed // computed
@ -134,7 +142,6 @@ export class BaseIssuesStore implements IBaseIssuesStore {
updateIssue: action, updateIssue: action,
removeIssue: action, removeIssue: action,
archiveIssue: action, archiveIssue: action,
quickAddIssue: action,
removeBulkIssues: action, removeBulkIssues: action,
}); });
this.rootIssueStore = _rootStore; this.rootIssueStore = _rootStore;
@ -155,6 +162,9 @@ export class BaseIssuesStore implements IBaseIssuesStore {
this.issueCount = issuesResponse.count; this.issueCount = issuesResponse.count;
this.pageCount = issuesResponse.total_pages; this.pageCount = issuesResponse.total_pages;
this.next_page_results = issuesResponse.next_page_results;
this.prev_page_results = issuesResponse.prev_page_results;
}; };
get groupedIssueIds() { get groupedIssueIds() {
@ -172,22 +182,22 @@ export class BaseIssuesStore implements IBaseIssuesStore {
this.issues, this.issues,
this.isArchived ? "archived" : "un-archived" this.isArchived ? "archived" : "un-archived"
); );
if (!currentIssues) return {}; if (!currentIssues) return { "All Issues": { issueIds: [], issueCount: 0 } };
let groupedIssues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = {}; let groupedIssues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = {};
if (layout === "list" && orderBy) { if (layout === EIssueLayoutTypes.LIST && orderBy) {
if (groupBy) groupedIssues = this.groupedIssues(groupBy, orderBy, currentIssues, this.groupedIssueCount); if (groupBy) groupedIssues = this.groupedIssues(groupBy, orderBy, currentIssues, this.groupedIssueCount);
else groupedIssues = this.unGroupedIssues(orderBy, currentIssues, this.issueCount); else groupedIssues = this.unGroupedIssues(orderBy, currentIssues, this.issueCount);
} else if (layout === "kanban" && groupBy && orderBy) { } else if (layout === EIssueLayoutTypes.KANBAN && groupBy && orderBy) {
if (subGroupBy) if (subGroupBy)
groupedIssues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, currentIssues, this.groupedIssueCount); groupedIssues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, currentIssues, this.groupedIssueCount);
else groupedIssues = this.groupedIssues(groupBy, orderBy, currentIssues, this.groupedIssueCount); else groupedIssues = this.groupedIssues(groupBy, orderBy, currentIssues, this.groupedIssueCount);
} else if (layout === "calendar") } else if (layout === EIssueLayoutTypes.CALENDAR)
groupedIssues = this.groupedIssues("target_date", "target_date", currentIssues, this.groupedIssueCount, true); groupedIssues = this.groupedIssues("target_date", "target_date", currentIssues, this.groupedIssueCount, true);
else if (layout === "spreadsheet") else if (layout === EIssueLayoutTypes.SPREADSHEET)
groupedIssues = this.unGroupedIssues(orderBy ?? "-created_at", currentIssues, this.issueCount); groupedIssues = this.unGroupedIssues(orderBy ?? "-created_at", currentIssues, this.issueCount);
else if (layout === "gantt_chart") else if (layout === EIssueLayoutTypes.GANTT)
groupedIssues = this.unGroupedIssues(orderBy ?? "sort_order", currentIssues, this.issueCount); groupedIssues = this.unGroupedIssues(orderBy ?? "sort_order", currentIssues, this.issueCount);
return groupedIssues; return groupedIssues;
@ -308,7 +318,7 @@ export class BaseIssuesStore implements IBaseIssuesStore {
} }
} }
async quickAddIssue(workspaceSlug: string, projectId: string, data: TIssue) { async issueQuickAdd(workspaceSlug: string, projectId: string, data: TIssue) {
if (!this.issues) this.issues = []; if (!this.issues) this.issues = [];
try { try {
this.addIssue(data); this.addIssue(data);
@ -400,8 +410,8 @@ export class BaseIssuesStore implements IBaseIssuesStore {
} }
for (const group of groupArray) { for (const group of groupArray) {
if (group && currentIssues[group]) currentIssues[group].issueIds.push(currentIssue.id); if (!currentIssues[group]) currentIssues[group] = { issueIds: [], issueCount: groupedIssueCount[group] };
else if (group) currentIssues[group].issueIds = [currentIssue.id]; if (group) currentIssues[group].issueIds.push(currentIssue.id);
} }
} }

View File

@ -209,19 +209,6 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
cycle: displayProperties?.cycle ?? true, cycle: displayProperties?.cycle ?? true,
}); });
/**
* This Method returns true if the display properties changed requires a server side update
* @param displayFilters
* @returns
*/
requiresServerUpdate = (displayFilters: IIssueDisplayFilterOptions) => {
const SERVER_DISPLAY_FILTERS = ["sub_issue", "type"];
const displayFilterKeys = Object.keys(displayFilters);
return SERVER_DISPLAY_FILTERS.some((serverDisplayfilter: string) =>
displayFilterKeys.includes(serverDisplayfilter)
);
};
handleIssuesLocalFilters = { handleIssuesLocalFilters = {
fetchFiltersFromStorage: () => { fetchFiltersFromStorage: () => {

View File

@ -5,6 +5,7 @@ import { ICalendarPayload, ICalendarWeek } from "components/issues";
import { generateCalendarData } from "helpers/calendar.helper"; import { generateCalendarData } from "helpers/calendar.helper";
// types // types
import { getWeekNumberOfDate } from "helpers/date-time.helper"; import { getWeekNumberOfDate } from "helpers/date-time.helper";
import { computedFn } from "mobx-utils";
export interface ICalendarStore { export interface ICalendarStore {
calendarFilters: { calendarFilters: {
@ -25,6 +26,7 @@ export interface ICalendarStore {
| undefined; | undefined;
activeWeekNumber: number; activeWeekNumber: number;
allDaysOfActiveWeek: ICalendarWeek | undefined; allDaysOfActiveWeek: ICalendarWeek | undefined;
getStartAndEndDate: (layout: "week" | "month") => { startDate: string; endDate: string } | undefined;
} }
export class CalendarStore implements ICalendarStore { export class CalendarStore implements ICalendarStore {
@ -82,6 +84,22 @@ export class CalendarStore implements ICalendarStore {
]; ];
} }
getStartAndEndDate = computedFn((layout: "week" | "month") => {
switch (layout) {
case "week":
if (!this.allDaysOfActiveWeek) return;
const dates = Object.keys(this.allDaysOfActiveWeek);
return { startDate: dates[0], endDate: dates[dates.length - 1] };
case "month":
if (!this.allWeeksOfActiveMonth) return;
const weeks = Object.keys(this.allWeeksOfActiveMonth);
const firstWeekDates = Object.keys(this.allWeeksOfActiveMonth[weeks[0]]);
const lastWeekDates = Object.keys(this.allWeeksOfActiveMonth[weeks[weeks.length - 1]]);
return { startDate: firstWeekDates[0], endDate: lastWeekDates[lastWeekDates.length - 1] };
}
});
updateCalendarFilters = (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => { updateCalendarFilters = (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => {
this.updateCalendarPayload(filters.activeMonthDate || filters.activeWeekDate || new Date()); this.updateCalendarPayload(filters.activeMonthDate || filters.activeWeekDate || new Date());

View File

@ -28,7 +28,7 @@ export interface IModuleIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor?: string cursor: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<void>;
@ -221,7 +221,6 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul
}); });
}); });
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.moduleIssues.fetchIssuesWithExistingPagination( this.rootIssueStore.moduleIssues.fetchIssuesWithExistingPagination(
workspaceSlug, workspaceSlug,
projectId, projectId,

View File

@ -79,12 +79,16 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
moduleId: observable.ref, moduleId: observable.ref,
// action // action
fetchIssues: action, fetchIssues: action,
fetchNextIssues: action,
fetchIssuesWithExistingPagination: action,
addIssuesToModule: action, addIssuesToModule: action,
removeIssuesFromModule: action, removeIssuesFromModule: action,
addModulesToIssue: action, addModulesToIssue: action,
removeModulesFromIssue: action, removeModulesFromIssue: action,
removeIssueFromModule: action, removeIssueFromModule: action,
quickAddIssue: action,
}); });
// filter store // filter store
this.issueFilterStore = issueFilterStore; this.issueFilterStore = issueFilterStore;
@ -107,7 +111,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
this.moduleId = moduleId; this.moduleId = moduleId;
const params = this.issueFilterStore?.getFilterParams(options); const params = this.issueFilterStore?.getFilterParams(options, undefined);
const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params); const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -119,11 +123,11 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => {
if (!this.paginationOptions) return; if (!this.paginationOptions || !this.next_page_results) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions); const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor);
const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params); const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response);
@ -295,4 +299,6 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
throw error; throw error;
} }
}; };
quickAddIssue = this.issueQuickAdd;
} }

View File

@ -30,7 +30,7 @@ export interface IProfileIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor?: string cursor: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, userId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, userId: string) => Promise<void>;
@ -212,7 +212,6 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf
}); });
}); });
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, userId, "mutation"); this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, userId, "mutation");
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, userId, undefined, { this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, userId, undefined, {

View File

@ -32,6 +32,8 @@ export interface IProfileIssues extends IBaseIssuesStore {
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: undefined;
} }
export class ProfileIssues extends BaseIssuesStore implements IProfileIssues { export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
@ -51,6 +53,8 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
// action // action
setViewId: action.bound, setViewId: action.bound,
fetchIssues: action, fetchIssues: action,
fetchNextIssues: action,
fetchIssuesWithExistingPagination: action,
}); });
// filter store // filter store
this.issueFilterStore = issueFilterStore; this.issueFilterStore = issueFilterStore;
@ -91,7 +95,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
this.setViewId(view); this.setViewId(view);
let params = this.issueFilterStore?.getFilterParams(options); let params = this.issueFilterStore?.getFilterParams(options, undefined);
params = { params = {
...params, ...params,
assignees: undefined, assignees: undefined,
@ -113,7 +117,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
}; };
fetchNextIssues = async (workspaceSlug: string, userId: string) => { fetchNextIssues = async (workspaceSlug: string, userId: string) => {
if (!this.paginationOptions || !this.currentView) return; if (!this.paginationOptions || !this.currentView || !this.next_page_results) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
@ -142,4 +146,6 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
if (!this.paginationOptions || !this.currentView) return; if (!this.paginationOptions || !this.currentView) return;
return await this.fetchIssues(workspaceSlug, userId, loadType, this.paginationOptions, this.currentView); return await this.fetchIssues(workspaceSlug, userId, loadType, this.paginationOptions, this.currentView);
}; };
quickAddIssue = undefined;
} }

View File

@ -28,7 +28,7 @@ export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor?: string cursor: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise<void>;
@ -216,12 +216,7 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
}); });
}); });
if (this.requiresServerUpdate(updatedDisplayFilters)) this.rootIssueStore.projectViewIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
this.rootIssueStore.projectViewIssues.fetchIssuesWithExistingPagination(
workspaceSlug,
projectId,
"mutation"
);
await this.issueFilterService.patchView(workspaceSlug, projectId, viewId, { await this.issueFilterService.patchView(workspaceSlug, projectId, viewId, {
display_filters: _filters.displayFilters, display_filters: _filters.displayFilters,

View File

@ -44,6 +44,8 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
makeObservable(this, { makeObservable(this, {
// action // action
fetchIssues: action, fetchIssues: action,
fetchNextIssues: action,
fetchIssuesWithExistingPagination: action,
}); });
//filter store //filter store
this.issueFilterStore = issueFilterStore; this.issueFilterStore = issueFilterStore;
@ -60,7 +62,7 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(options); const params = this.issueFilterStore?.getFilterParams(options, undefined);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -72,11 +74,11 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string) => {
if (!this.paginationOptions) return; if (!this.paginationOptions || !this.next_page_results) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions); const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response);
@ -91,4 +93,6 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
if (!this.paginationOptions) return; if (!this.paginationOptions) return;
return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions); return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions);
}; };
quickAddIssue = this.issueQuickAdd;
} }

View File

@ -28,7 +28,7 @@ export interface IProjectIssuesFilter extends IBaseIssueFilterStore {
//helper actions //helper actions
getFilterParams: ( getFilterParams: (
options: IssuePaginationOptions, options: IssuePaginationOptions,
cursor?: string cursor: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>; ) => Partial<Record<TIssueParams, string | boolean>>;
// action // action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>; fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
@ -107,6 +107,10 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
paginationOptions.group_by = options.groupedBy; paginationOptions.group_by = options.groupedBy;
} }
if (options.after && options.before) {
paginationOptions["target_date"] = `${options.after};after,${options.before};before`;
}
return paginationOptions; return paginationOptions;
}); });
@ -217,7 +221,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
}); });
}); });
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation"); this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, { await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, {

View File

@ -47,6 +47,8 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
fetchIssues: action, fetchIssues: action,
fetchNextIssues: action, fetchNextIssues: action,
fetchIssuesWithExistingPagination: action, fetchIssuesWithExistingPagination: action,
quickAddIssue: action,
}); });
// filter store // filter store
this.issueFilterStore = issueFilterStore; this.issueFilterStore = issueFilterStore;
@ -63,7 +65,7 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
this.loader = loadType; this.loader = loadType;
}); });
this.clear(); this.clear();
const params = this.issueFilterStore?.getFilterParams(options); const params = this.issueFilterStore?.getFilterParams(options, undefined);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchIssues(response, options); this.onfetchIssues(response, options);
@ -75,11 +77,11 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
}; };
fetchNextIssues = async (workspaceSlug: string, projectId: string) => { fetchNextIssues = async (workspaceSlug: string, projectId: string) => {
if (!this.paginationOptions) return; if (!this.paginationOptions || !this.next_page_results) return;
try { try {
this.loader = "pagination"; this.loader = "pagination";
const params = this.issueFilterStore?.getFilterParams(this.paginationOptions); const params = this.issueFilterStore?.getFilterParams(this.paginationOptions, this.nextCursor);
const response = await this.issueService.getIssues(workspaceSlug, projectId, params); const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
this.onfetchNexIssues(response); this.onfetchNexIssues(response);
@ -98,4 +100,6 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
if (!this.paginationOptions) return; if (!this.paginationOptions) return;
return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions); return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions);
}; };
quickAddIssue = this.issueQuickAdd;
} }

View File

@ -237,7 +237,6 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
}); });
}); });
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation"); this.rootIssueStore.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation");
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))

View File

@ -24,9 +24,12 @@ export interface IWorkspaceIssues extends IBaseIssuesStore {
loadType: TLoader loadType: TLoader
) => Promise<TIssuesResponse | undefined>; ) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (workspaceSlug: string, viewId: string) => Promise<TIssuesResponse | undefined>; fetchNextIssues: (workspaceSlug: string, viewId: string) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>; createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: undefined;
} }
export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues { export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues {
@ -46,6 +49,8 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
makeObservable(this, { makeObservable(this, {
// action // action
fetchIssues: action, fetchIssues: action,
fetchNextIssues: action,
fetchIssuesWithExistingPagination: action,
}); });
// services // services
this.workspaceService = new WorkspaceService(); this.workspaceService = new WorkspaceService();
@ -90,4 +95,6 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
if (!this.paginationOptions) return; if (!this.paginationOptions) return;
return await this.fetchIssues(workspaceSlug, viewId, loadType, this.paginationOptions); return await this.fetchIssues(workspaceSlug, viewId, loadType, this.paginationOptions);
}; };
quickAddIssue = undefined;
} }