mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
implement pagination for spreadsheet, list, kanban and calendar
This commit is contained in:
parent
3b3f04b7e7
commit
cf470d715a
5
packages/types/src/issues/base.d.ts
vendored
5
packages/types/src/issues/base.d.ts
vendored
@ -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;
|
||||||
|
12
packages/types/src/view-props.d.ts
vendored
12
packages/types/src/view-props.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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];
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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";
|
|
33
web/components/issues/issue-layouts/empty-states/index.tsx
Normal file
33
web/components/issues/issue-layouts/empty-states/index.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
51
web/components/issues/issue-layouts/issue-layout-HOC.tsx
Normal file
51
web/components/issues/issue-layouts/issue-layout-HOC.tsx
Normal 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}</>;
|
||||||
|
});
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
@ -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 ↓
|
||||||
|
</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}
|
||||||
|
@ -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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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">
|
||||||
|
@ -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 }];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 });
|
||||||
},
|
},
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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" />
|
||||||
))}
|
))}
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
@ -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>
|
||||||
|
@ -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 />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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[] = [];
|
||||||
|
41
web/hooks/use-intersection-observer.ts
Normal file
41
web/hooks/use-intersection-observer.ts
Normal 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]);
|
||||||
|
};
|
@ -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, {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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, {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: () => {
|
||||||
|
@ -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());
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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, {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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, {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user