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 TIssueGroup = { issueIds: string[]; issueCount: number };
|
||||
export type TGroupedIssues = {
|
||||
[group_id: string]: { issueIds: string[]; issueCount: number };
|
||||
[group_id: string]: TIssueGroup;
|
||||
};
|
||||
|
||||
export type TSubGroupedIssues = {
|
||||
[sub_grouped_id: string]: TGroupedIssues;
|
||||
};
|
||||
export type TUnGroupedIssues = {
|
||||
"All Issues": { issueIds: string[]; issueCount: number };
|
||||
"All Issues": TIssueGroup;
|
||||
};
|
||||
|
||||
export type TIssues = TGroupedIssues | TUnGroupedIssues;
|
||||
|
70
packages/types/src/view-props.d.ts
vendored
70
packages/types/src/view-props.d.ts
vendored
@ -1,9 +1,4 @@
|
||||
export type TIssueLayouts =
|
||||
| "list"
|
||||
| "kanban"
|
||||
| "calendar"
|
||||
| "spreadsheet"
|
||||
| "gantt_chart";
|
||||
import { EIssueLayoutTypes } from "constants/issue";
|
||||
|
||||
export type TIssueGroupByOptions =
|
||||
| "state"
|
||||
@ -15,6 +10,7 @@ export type TIssueGroupByOptions =
|
||||
| "assignees"
|
||||
| "cycle"
|
||||
| "module"
|
||||
| "target_date"
|
||||
| null;
|
||||
|
||||
export type TIssueOrderByOptions =
|
||||
@ -50,9 +46,9 @@ export type TIssueOrderByOptions =
|
||||
|
||||
export type TIssueTypeFilters = "active" | "backlog" | null;
|
||||
|
||||
export type TIssueExtraOptions = "show_empty_groups" | "sub_issue";
|
||||
export type TIssueExtraOptions = "show_empty_groups" | "sub_issue";
|
||||
|
||||
export type TIssueParams =
|
||||
export type TIssueParams =
|
||||
| "priority"
|
||||
| "state_group"
|
||||
| "state"
|
||||
@ -75,9 +71,9 @@ export type TIssueTypeFilters = "active" | "backlog" | null;
|
||||
| "cursor"
|
||||
| "per_page";
|
||||
|
||||
export type TCalendarLayouts = "month" | "week";
|
||||
export type TCalendarLayouts = "month" | "week";
|
||||
|
||||
export interface IIssueFilterOptions {
|
||||
export interface IIssueFilterOptions {
|
||||
assignees?: string[] | null;
|
||||
mentions?: string[] | null;
|
||||
created_by?: string[] | null;
|
||||
@ -91,9 +87,9 @@ export type TIssueTypeFilters = "active" | "backlog" | null;
|
||||
state_group?: string[] | null;
|
||||
subscriber?: string[] | null;
|
||||
target_date?: string[] | null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IIssueDisplayFilterOptions {
|
||||
export interface IIssueDisplayFilterOptions {
|
||||
calendar?: {
|
||||
show_weekends?: boolean;
|
||||
layout?: TCalendarLayouts;
|
||||
@ -105,8 +101,8 @@ export type TIssueTypeFilters = "active" | "backlog" | null;
|
||||
show_empty_groups?: boolean;
|
||||
sub_issue?: boolean;
|
||||
type?: TIssueTypeFilters;
|
||||
}
|
||||
export interface IIssueDisplayProperties {
|
||||
}
|
||||
export interface IIssueDisplayProperties {
|
||||
assignee?: boolean;
|
||||
start_date?: boolean;
|
||||
due_date?: boolean;
|
||||
@ -122,27 +118,27 @@ export type TIssueTypeFilters = "active" | "backlog" | null;
|
||||
updated_on?: boolean;
|
||||
modules?: boolean;
|
||||
cycle?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export type TIssueKanbanFilters = {
|
||||
export type TIssueKanbanFilters = {
|
||||
group_by: string[];
|
||||
sub_group_by: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export interface IIssueFilters {
|
||||
export interface IIssueFilters {
|
||||
filters: IIssueFilterOptions | undefined;
|
||||
displayFilters: IIssueDisplayFilterOptions | undefined;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
kanbanFilters: TIssueKanbanFilters | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IIssueFiltersResponse {
|
||||
export interface IIssueFiltersResponse {
|
||||
filters: IIssueFilterOptions;
|
||||
display_filters: IIssueDisplayFilterOptions;
|
||||
display_properties: IIssueDisplayProperties;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkspaceIssueFilterOptions {
|
||||
export interface IWorkspaceIssueFilterOptions {
|
||||
assignees?: string[] | null;
|
||||
created_by?: string[] | null;
|
||||
labels?: string[] | null;
|
||||
@ -152,16 +148,16 @@ export type TIssueTypeFilters = "active" | "backlog" | null;
|
||||
start_date?: string[] | null;
|
||||
target_date?: string[] | null;
|
||||
project?: string[] | null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkspaceGlobalViewDisplayFilterOptions {
|
||||
export interface IWorkspaceGlobalViewDisplayFilterOptions {
|
||||
order_by?: string | undefined;
|
||||
type?: "active" | "backlog" | null;
|
||||
sub_issue?: boolean;
|
||||
layout?: TIssueViewOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkspaceViewIssuesParams {
|
||||
export interface IWorkspaceViewIssuesParams {
|
||||
assignees?: string | undefined;
|
||||
created_by?: string | undefined;
|
||||
labels?: string | undefined;
|
||||
@ -175,28 +171,28 @@ export type TIssueTypeFilters = "active" | "backlog" | null;
|
||||
order_by?: string | undefined;
|
||||
type?: "active" | "backlog" | undefined;
|
||||
sub_issue?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IProjectViewProps {
|
||||
export interface IProjectViewProps {
|
||||
display_filters: IIssueDisplayFilterOptions | undefined;
|
||||
filters: IIssueFilterOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkspaceViewProps {
|
||||
export interface IWorkspaceViewProps {
|
||||
filters: IIssueFilterOptions;
|
||||
display_filters: IIssueDisplayFilterOptions | undefined;
|
||||
display_properties: IIssueDisplayProperties;
|
||||
}
|
||||
export interface IWorkspaceGlobalViewProps {
|
||||
}
|
||||
export interface IWorkspaceGlobalViewProps {
|
||||
filters: IWorkspaceIssueFilterOptions;
|
||||
display_filters: IWorkspaceIssueDisplayFilterOptions | undefined;
|
||||
display_properties: IIssueDisplayProperties;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IssuePaginationOptions {
|
||||
export interface IssuePaginationOptions {
|
||||
canGroup: boolean;
|
||||
perPageCount: number;
|
||||
greaterThanDate?: Date;
|
||||
lessThanDate?: Date;
|
||||
before?: string;
|
||||
after?: string;
|
||||
groupedBy?: TIssueGroupByOptions;
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,15 @@ import { CustomMenu } from "@plane/ui";
|
||||
// constants
|
||||
import { ProjectAnalyticsModal } from "components/analytics";
|
||||
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 { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
export const CycleMobileHeader = () => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@ -30,7 +36,7 @@ export const CycleMobileHeader = () => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
|
@ -10,7 +10,12 @@ import { BreadcrumbLink } from "components/common";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
import { CycleMobileHeader } from "components/cycles/cycle-mobile-header";
|
||||
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 { cn } from "helpers/common.helper";
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
@ -31,7 +36,7 @@ import useLocalStorage from "hooks/use-local-storage";
|
||||
// icons
|
||||
// helpers
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import { ProjectLogo } from "components/project";
|
||||
// constants
|
||||
|
||||
@ -95,7 +100,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
};
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId);
|
||||
},
|
||||
@ -233,7 +238,13 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-2 ">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -10,7 +10,12 @@ import { BreadcrumbLink } from "components/common";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
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 { cn } from "helpers/common.helper";
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
@ -32,7 +37,7 @@ import useLocalStorage from "hooks/use-local-storage";
|
||||
// icons
|
||||
// helpers
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import { ProjectLogo } from "components/project";
|
||||
// constants
|
||||
|
||||
@ -96,7 +101,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!projectId) return;
|
||||
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="hidden md:flex gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -9,9 +9,14 @@ import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-ham
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
// ui
|
||||
// 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 { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import { ProjectLogo } from "components/project";
|
||||
|
||||
export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
@ -51,7 +56,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
);
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
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">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban"]}
|
||||
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -9,7 +9,12 @@ import { BreadcrumbLink } from "components/common";
|
||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
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 {
|
||||
useApplication,
|
||||
@ -24,7 +29,7 @@ import { useIssues } from "hooks/store/use-issues";
|
||||
// components
|
||||
// ui
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import { ProjectLogo } from "components/project";
|
||||
// constants
|
||||
// helper
|
||||
@ -75,7 +80,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
);
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
@ -177,7 +182,13 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
<div className="items-center gap-2 hidden md:flex">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -13,7 +13,12 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
|
||||
// helpers
|
||||
// types
|
||||
// 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 { truncateText } from "helpers/string.helper";
|
||||
import {
|
||||
@ -27,7 +32,7 @@ import {
|
||||
useProjectView,
|
||||
useUser,
|
||||
} from "hooks/store";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import { ProjectLogo } from "components/project";
|
||||
|
||||
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
@ -56,7 +61,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
@ -195,7 +200,13 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
layouts={[
|
||||
EIssueLayoutTypes.LIST,
|
||||
EIssueLayoutTypes.KANBAN,
|
||||
EIssueLayoutTypes.CALENDAR,
|
||||
EIssueLayoutTypes.SPREADSHEET,
|
||||
EIssueLayoutTypes.GANTT,
|
||||
]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useCallback } from "react";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { TGroupedIssues } from "@plane/types";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { CalendarChart } from "components/issues";
|
||||
// hooks
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
import { useCalendarView, useIssues, useUser } from "hooks/store";
|
||||
import { useIssuesActions } from "hooks/use-issues-actions";
|
||||
// ui
|
||||
// types
|
||||
import { TGroupedIssues } from "@plane/types";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType, IssueGroupByOptions } from "constants/issue";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { handleDragDrop } from "./utils";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
||||
|
||||
type CalendarStoreType =
|
||||
| EIssuesStoreType.PROJECT
|
||||
@ -42,8 +44,18 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { issues, issuesFilter, issueMap } = useIssues(storeType);
|
||||
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } =
|
||||
useIssuesActions(storeType);
|
||||
const {
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
removeIssueFromView,
|
||||
archiveIssue,
|
||||
restoreIssue,
|
||||
updateFilters,
|
||||
} = useIssuesActions(storeType);
|
||||
|
||||
const issueCalendarView = useCalendarView();
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
@ -51,6 +63,27 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
|
||||
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) => {
|
||||
if (!result) return;
|
||||
|
||||
@ -79,8 +112,12 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadMoreIssues = useCallback(() => {
|
||||
fetchNextIssues();
|
||||
}, [fetchNextIssues]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.CALENDAR}>
|
||||
<div className="h-full w-full overflow-hidden bg-custom-background-100 pt-4">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
@ -89,6 +126,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
layout={displayFilters?.calendar?.layout}
|
||||
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
||||
issueCalendarView={issueCalendarView}
|
||||
quickActions={(issue, customActionButton) => (
|
||||
<QuickActions
|
||||
customActionButton={customActionButton}
|
||||
@ -103,6 +141,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
/>
|
||||
)}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
addIssuesToView={addIssuesToView}
|
||||
quickAddCallback={issues.quickAddIssue}
|
||||
viewId={viewId}
|
||||
@ -111,6 +150,6 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
/>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</>
|
||||
</IssueLayoutHOC>
|
||||
);
|
||||
});
|
||||
|
@ -24,6 +24,7 @@ import { ICycleIssuesFilter } from "store/issue/cycle";
|
||||
import { IModuleIssuesFilter } from "store/issue/module";
|
||||
import { IProjectIssuesFilter } from "store/issue/project";
|
||||
import { IProjectViewIssuesFilter } from "store/issue/project-views";
|
||||
import { ICalendarStore } from "store/issue/issue_calendar_view.store";
|
||||
|
||||
type Props = {
|
||||
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
|
||||
@ -31,6 +32,8 @@ type Props = {
|
||||
groupedIssueIds: TGroupedIssues;
|
||||
layout: "month" | "week" | undefined;
|
||||
showWeekends: boolean;
|
||||
issueCalendarView: ICalendarStore;
|
||||
loadMoreIssues: () => void;
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
@ -55,6 +58,8 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
groupedIssueIds,
|
||||
layout,
|
||||
showWeekends,
|
||||
issueCalendarView,
|
||||
loadMoreIssues,
|
||||
quickActions,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
@ -66,7 +71,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
issues: { viewFlags },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const issueCalendarView = useCalendarView();
|
||||
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
@ -102,6 +107,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
week={week}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
@ -119,6 +125,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
week={issueCalendarView.allDaysOfActiveWeek}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
|
@ -19,6 +19,7 @@ type Props = {
|
||||
date: ICalendarDate;
|
||||
issues: TIssueMap | undefined;
|
||||
groupedIssueIds: TGroupedIssues;
|
||||
loadMoreIssues: () => void;
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
@ -39,6 +40,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
date,
|
||||
issues,
|
||||
groupedIssueIds,
|
||||
loadMoreIssues,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
@ -47,14 +49,13 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
viewId,
|
||||
readOnly = false,
|
||||
} = props;
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||
|
||||
const formattedDatePayload = renderFormattedPayloadDate(date.date);
|
||||
if (!formattedDatePayload) return 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();
|
||||
|
||||
@ -100,9 +101,8 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
>
|
||||
<CalendarIssueBlocks
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
issueIdList={issueIdList?.issueIds ?? []}
|
||||
quickActions={quickActions}
|
||||
showAllIssues={showAllIssues}
|
||||
isDragDisabled={readOnly}
|
||||
/>
|
||||
|
||||
@ -117,19 +117,18 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
onOpen={() => setShowAllIssues(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalIssues > 4 && (
|
||||
{totalIssues > (issueIdList?.issueIds?.length ?? 0) && (
|
||||
<div className="flex items-center px-2.5 py-1">
|
||||
<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"
|
||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||
onClick={loadMoreIssues}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
@ -16,12 +16,11 @@ type Props = {
|
||||
issues: TIssueMap | undefined;
|
||||
issueIdList: string[] | null;
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
showAllIssues?: boolean;
|
||||
isDragDisabled?: boolean;
|
||||
};
|
||||
|
||||
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
|
||||
const {
|
||||
router: { workspaceSlug, projectId },
|
||||
@ -57,7 +56,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => {
|
||||
{issueIdList?.map((issueId, index) => {
|
||||
if (!issues?.[issueId]) return null;
|
||||
|
||||
const issue = issues?.[issueId];
|
||||
|
@ -17,6 +17,7 @@ type Props = {
|
||||
groupedIssueIds: TGroupedIssues;
|
||||
week: ICalendarWeek | undefined;
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
loadMoreIssues: () => void;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
quickAddCallback?: (
|
||||
@ -36,6 +37,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
issues,
|
||||
groupedIssueIds,
|
||||
week,
|
||||
loadMoreIssues,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
@ -66,6 +68,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
date={date}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
quickActions={quickActions}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
|
@ -9,33 +9,30 @@ import { ExistingIssuesListModal } from "components/core";
|
||||
// components
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// types
|
||||
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
import size from "lodash/size";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
projectId: string | undefined;
|
||||
cycleId: string | undefined;
|
||||
activeLayout: TIssueLayouts | undefined;
|
||||
handleClearAllFilters: () => void;
|
||||
isEmptyFilters?: boolean;
|
||||
};
|
||||
|
||||
export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
|
||||
export const CycleEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
// states
|
||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||
// store hooks
|
||||
const { getCycleById } = useCycle();
|
||||
const { issues } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
@ -43,7 +40,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const issueIds = data.map((i) => i.id);
|
||||
|
||||
await issues
|
||||
.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds)
|
||||
.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds)
|
||||
.then(() =>
|
||||
setToast({
|
||||
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 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
|
||||
? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES
|
||||
: isEmptyFilters
|
||||
@ -71,10 +91,10 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const emptyStateSize = isEmptyFilters ? "lg" : "sm";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
isOpen={cycleIssuesListModal}
|
||||
handleClose={() => setCycleIssuesListModal(false)}
|
||||
searchParams={{ cycle: true }}
|
||||
@ -100,6 +120,6 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
/>
|
||||
</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 { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import size from "lodash/size";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useIssues } from "hooks/store";
|
||||
// ui
|
||||
@ -9,31 +11,27 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// types
|
||||
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
projectId: string | undefined;
|
||||
moduleId: string | undefined;
|
||||
activeLayout: TIssueLayouts | undefined;
|
||||
handleClearAllFilters: () => void;
|
||||
isEmptyFilters?: boolean;
|
||||
};
|
||||
|
||||
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
|
||||
export const ModuleEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
// states
|
||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.MODULE);
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
|
||||
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 additionalPath = activeLayout ?? "list";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
isOpen={moduleIssuesListModal}
|
||||
handleClose={() => setModuleIssuesListModal(false)}
|
||||
searchParams={{ module: moduleId != undefined ? moduleId.toString() : "" }}
|
||||
@ -84,6 +106,6 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -14,6 +14,7 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<EmptyState
|
||||
title="View issues will appear here"
|
||||
@ -29,5 +30,6 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -3,14 +3,13 @@ import React from "react";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { ISSUE_LAYOUTS } from "constants/issue";
|
||||
import { TIssueLayouts } from "@plane/types";
|
||||
// constants
|
||||
import { EIssueLayoutTypes, ISSUE_LAYOUTS } from "constants/issue";
|
||||
|
||||
type Props = {
|
||||
layouts: TIssueLayouts[];
|
||||
onChange: (layout: TIssueLayouts) => void;
|
||||
selectedLayout: TIssueLayouts | undefined;
|
||||
layouts: EIssueLayoutTypes[];
|
||||
onChange: (layout: EIssueLayoutTypes) => void;
|
||||
selectedLayout: EIssueLayoutTypes | undefined;
|
||||
};
|
||||
|
||||
export const LayoutSelection: React.FC<Props> = (props) => {
|
||||
|
@ -13,7 +13,8 @@ import { useIssuesActions } from "hooks/use-issues-actions";
|
||||
// types
|
||||
import { TIssue, TUnGroupedIssues } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
|
||||
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
||||
|
||||
type GanttStoreType =
|
||||
| EIssuesStoreType.PROJECT
|
||||
@ -57,7 +58,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.GANTT}>
|
||||
<div className="h-full w-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
@ -80,6 +81,6 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
showAllBlocks
|
||||
/>
|
||||
</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 { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { DeleteIssueModal } from "components/issues";
|
||||
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 { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
import { useIssuesActions } from "hooks/use-issues-actions";
|
||||
@ -18,6 +19,8 @@ import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
import { handleDragDrop } from "./utils";
|
||||
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
export type KanbanStoreType =
|
||||
| EIssuesStoreType.PROJECT
|
||||
@ -61,10 +64,31 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
} = useUser();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { issueMap, issuesFilter, issues } = useIssues(storeType);
|
||||
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } =
|
||||
useIssuesActions(storeType);
|
||||
const {
|
||||
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 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: [] };
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.KANBAN}>
|
||||
<DeleteIssueModal
|
||||
dataId={dragState.draggedIssueId}
|
||||
isOpen={deleteIssueModal}
|
||||
@ -215,12 +239,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
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
|
||||
className="vertical-scrollbar horizontal-scrollbar scrollbar-lg relative flex h-full w-full overflow-auto bg-custom-background-90"
|
||||
ref={scrollableContainerRef}
|
||||
@ -253,7 +271,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
<div className="h-max w-max">
|
||||
<KanBanView
|
||||
issuesMap={issueMap}
|
||||
issueIds={issueIds}
|
||||
issueIds={issueIds!}
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
@ -271,11 +289,12 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
addIssuesToView={addIssuesToView}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
loadMoreIssues={debouncedFetchMoreIssues}
|
||||
/>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</IssueLayoutHOC>
|
||||
);
|
||||
});
|
||||
|
@ -43,6 +43,7 @@ export interface IGroupByKanBan {
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
kanbanFilters: TIssueKanbanFilters;
|
||||
handleKanbanFilters: any;
|
||||
loadMoreIssues: (() => void) | undefined;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
@ -75,6 +76,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
handleKanbanFilters,
|
||||
enableQuickIssueCreate,
|
||||
quickAddCallback,
|
||||
loadMoreIssues,
|
||||
viewId,
|
||||
disableIssueCreation,
|
||||
storeType,
|
||||
@ -105,7 +107,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
|
||||
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;
|
||||
|
||||
@ -141,7 +143,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
column_id={_list.id}
|
||||
icon={_list.icon}
|
||||
title={_list.name}
|
||||
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
||||
count={(issueIds as TGroupedIssues)?.[_list.id]?.issueCount || 0}
|
||||
issuePayload={_list.payload}
|
||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
||||
storeType={storeType}
|
||||
@ -173,6 +175,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
groupByVisibilityToggle={groupByVisibilityToggle}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -184,7 +187,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
|
||||
export interface IKanBan {
|
||||
issuesMap: IIssueMap;
|
||||
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
|
||||
issueIds: TGroupedIssues | TSubGroupedIssues;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
sub_group_by: string | null;
|
||||
group_by: string | null;
|
||||
@ -193,6 +196,7 @@ export interface IKanBan {
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
kanbanFilters: TIssueKanbanFilters;
|
||||
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||
loadMoreIssues: (() => void) | undefined;
|
||||
showEmptyGroup: boolean;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
quickAddCallback?: (
|
||||
@ -222,6 +226,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
quickActions,
|
||||
kanbanFilters,
|
||||
handleKanbanFilters,
|
||||
loadMoreIssues,
|
||||
enableQuickIssueCreate,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
@ -249,6 +254,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
quickActions={quickActions}
|
||||
kanbanFilters={kanbanFilters}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MutableRefObject } from "react";
|
||||
import { MutableRefObject, useRef } from "react";
|
||||
import { Droppable } from "@hello-pangea/dnd";
|
||||
// hooks
|
||||
import { useProjectState } from "hooks/store";
|
||||
@ -13,6 +13,8 @@ import {
|
||||
TUnGroupedIssues,
|
||||
} from "@plane/types";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||
import { KanbanIssueBlockLoader } from "components/ui/loader";
|
||||
import { useIntersectionObserver } from "hooks/use-intersection-observer";
|
||||
|
||||
interface IKanbanGroup {
|
||||
groupId: string;
|
||||
@ -33,6 +35,7 @@ interface IKanbanGroup {
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
loadMoreIssues: (() => void) | undefined;
|
||||
viewId?: string;
|
||||
disableIssueCreation?: boolean;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
@ -55,6 +58,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
updateIssue,
|
||||
quickActions,
|
||||
canEditProperties,
|
||||
loadMoreIssues,
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
quickAddCallback,
|
||||
@ -65,6 +69,10 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
// hooks
|
||||
const projectState = useProjectState();
|
||||
|
||||
const intersectionRef = useRef<HTMLSpanElement | null>(null);
|
||||
|
||||
useIntersectionObserver(scrollableContainerRef, intersectionRef, loadMoreIssues, `50% 0% 50% 0%`);
|
||||
|
||||
const prePopulateQuickAddData = (
|
||||
groupByKey: string | null,
|
||||
subGroupByKey: string | null,
|
||||
@ -131,7 +139,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
columnId={groupId}
|
||||
issuesMap={issuesMap}
|
||||
peekIssueId={peekIssueId}
|
||||
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []}
|
||||
issueIds={(issueIds as TGroupedIssues)?.[groupId]?.issueIds || []}
|
||||
displayProperties={displayProperties}
|
||||
isDragDisabled={isDragDisabled}
|
||||
updateIssue={updateIssue}
|
||||
@ -143,6 +151,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
|
||||
{provided.placeholder}
|
||||
|
||||
{loadMoreIssues && <KanbanIssueBlockLoader ref={intersectionRef} />}
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
|
||||
<KanBanQuickAddIssueForm
|
||||
|
@ -22,7 +22,7 @@ import { KanbanStoreType } from "./base-kanban-root";
|
||||
// constants
|
||||
|
||||
interface ISubGroupSwimlaneHeader {
|
||||
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
|
||||
issueIds: TGroupedIssues | TSubGroupedIssues;
|
||||
sub_group_by: string | null;
|
||||
group_by: string | null;
|
||||
list: IGroupByColumn[];
|
||||
@ -34,7 +34,7 @@ interface ISubGroupSwimlaneHeader {
|
||||
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
|
||||
let headerCount = 0;
|
||||
Object.keys(issueIds).map((groupState) => {
|
||||
headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.length || 0);
|
||||
headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.issueCount || 0);
|
||||
});
|
||||
return headerCount;
|
||||
};
|
||||
@ -93,6 +93,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||
) => Promise<TIssue | undefined>;
|
||||
viewId?: string;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
loadMoreIssues: (() => void) | undefined;
|
||||
}
|
||||
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
const {
|
||||
@ -107,6 +108,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
displayProperties,
|
||||
kanbanFilters,
|
||||
handleKanbanFilters,
|
||||
loadMoreIssues,
|
||||
showEmptyGroup,
|
||||
enableQuickIssueCreate,
|
||||
canEditProperties,
|
||||
@ -122,7 +124,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
const subGroupedIds = issueIds as TSubGroupedIssues;
|
||||
subGroupedIds?.[column_id] &&
|
||||
Object.keys(subGroupedIds?.[column_id])?.forEach((_list: any) => {
|
||||
issueCount += subGroupedIds?.[column_id]?.[_list]?.length || 0;
|
||||
issueCount += subGroupedIds?.[column_id]?.[_list]?.issueCount || 0;
|
||||
});
|
||||
return issueCount;
|
||||
};
|
||||
@ -131,7 +133,9 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
<div className="relative h-max min-h-full w-full">
|
||||
{list &&
|
||||
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 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">
|
||||
@ -169,18 +173,20 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
storeType={storeType}
|
||||
loadMoreIssues={isLastSubGroup ? loadMoreIssues : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export interface IKanBanSwimLanes {
|
||||
issuesMap: IIssueMap;
|
||||
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
|
||||
issueIds: TGroupedIssues | TSubGroupedIssues;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
sub_group_by: string | null;
|
||||
group_by: string | null;
|
||||
@ -188,6 +194,7 @@ export interface IKanBanSwimLanes {
|
||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
kanbanFilters: TIssueKanbanFilters;
|
||||
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||
loadMoreIssues: (() => void) | undefined;
|
||||
showEmptyGroup: boolean;
|
||||
isDragStarted?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
@ -217,6 +224,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
quickActions,
|
||||
kanbanFilters,
|
||||
handleKanbanFilters,
|
||||
loadMoreIssues,
|
||||
showEmptyGroup,
|
||||
isDragStarted,
|
||||
disableIssueCreation,
|
||||
@ -282,6 +290,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
quickActions={quickActions}
|
||||
kanbanFilters={kanbanFilters}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// types
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
|
||||
import { TIssue } from "@plane/types";
|
||||
import { TGroupedIssues, TIssue, TUnGroupedIssues } from "@plane/types";
|
||||
// components
|
||||
import { List } from "./default";
|
||||
import { IQuickActionProps } from "./list-view-types";
|
||||
import { useIssuesActions } from "hooks/use-issues-actions";
|
||||
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
||||
import useSWR from "swr";
|
||||
// constants
|
||||
// hooks
|
||||
|
||||
@ -40,7 +42,8 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
} = props;
|
||||
|
||||
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
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
@ -48,9 +51,14 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
|
||||
const { issueMap } = useIssues();
|
||||
|
||||
useSWR(`ISSUE_LIST_LAYOUT_${storeType}`, () => fetchIssues("init-loader", { canGroup: true, perPageCount: 100 }), {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
});
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const issueIds = issues?.groupedIssueIds || [];
|
||||
const issueIds = issues?.groupedIssueIds as TGroupedIssues | undefined;
|
||||
|
||||
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
|
||||
const canEditProperties = useCallback(
|
||||
@ -85,7 +93,12 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
|
||||
);
|
||||
|
||||
const loadMoreIssues = useCallback(() => {
|
||||
fetchNextIssues();
|
||||
}, [fetchNextIssues]);
|
||||
|
||||
return (
|
||||
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.LIST}>
|
||||
<div className={`relative h-full w-full bg-custom-background-90`}>
|
||||
<List
|
||||
issuesMap={issueMap}
|
||||
@ -93,7 +106,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
group_by={group_by}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
issueIds={issueIds}
|
||||
issueIds={issueIds!}
|
||||
shouldLoadMore={issues.next_page_results}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
viewId={viewId}
|
||||
quickAddCallback={issues?.quickAddIssue}
|
||||
@ -105,5 +120,6 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
/>
|
||||
</div>
|
||||
</IssueLayoutHOC>
|
||||
);
|
||||
});
|
||||
|
@ -28,7 +28,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
|
||||
key={`${issueId}`}
|
||||
defaultHeight="3rem"
|
||||
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}
|
||||
>
|
||||
<IssueBlock
|
||||
|
@ -13,13 +13,17 @@ import {
|
||||
TIssueMap,
|
||||
TUnGroupedIssues,
|
||||
IGroupByColumn,
|
||||
TIssueGroup,
|
||||
} from "@plane/types";
|
||||
import { getGroupByColumns } from "../utils";
|
||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import { ListLoaderItemRow } from "components/ui";
|
||||
import { useIntersectionObserver } from "hooks/use-intersection-observer";
|
||||
|
||||
export interface IGroupByList {
|
||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||
issueIds: TGroupedIssues;
|
||||
issuesMap: TIssueMap;
|
||||
group_by: string | null;
|
||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||
@ -39,6 +43,8 @@ export interface IGroupByList {
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
viewId?: string;
|
||||
isCompletedCycle?: boolean;
|
||||
shouldLoadMore: boolean;
|
||||
loadMoreIssues: () => void;
|
||||
}
|
||||
|
||||
const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
@ -57,7 +63,9 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
disableIssueCreation,
|
||||
storeType,
|
||||
addIssuesToView,
|
||||
shouldLoadMore,
|
||||
isCompletedCycle = false,
|
||||
loadMoreIssues,
|
||||
} = props;
|
||||
// store hooks
|
||||
const member = useMember();
|
||||
@ -67,8 +75,11 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
const cycle = useCycle();
|
||||
const projectModule = useModule();
|
||||
|
||||
const intersectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useIntersectionObserver(containerRef, intersectionRef, loadMoreIssues, `50% 0% 50% 0%`);
|
||||
|
||||
const groups = getGroupByColumns(
|
||||
group_by as GroupByColumnTypes,
|
||||
project,
|
||||
@ -112,14 +123,12 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
return preloadedData;
|
||||
};
|
||||
|
||||
const validateEmptyIssueGroups = (issues: TIssue[]) => {
|
||||
const issuesCount = issues?.length || 0;
|
||||
const validateEmptyIssueGroups = (issues: TIssueGroup) => {
|
||||
const issuesCount = issues?.issueCount || 0;
|
||||
if (!showEmptyGroup && issuesCount <= 0) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const is_list = group_by === null ? true : false;
|
||||
|
||||
const isGroupByCreatedBy = group_by === "created_by";
|
||||
|
||||
return (
|
||||
@ -129,15 +138,18 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
>
|
||||
{groups &&
|
||||
groups.length > 0 &&
|
||||
groups.map(
|
||||
(_list: IGroupByColumn) =>
|
||||
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
|
||||
groups.map((_list: IGroupByColumn) => {
|
||||
const issueGroup = issueIds?.[_list.id] as TIssueGroup;
|
||||
|
||||
return (
|
||||
issueGroup &&
|
||||
validateEmptyIssueGroups(issueGroup) && (
|
||||
<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">
|
||||
<HeaderGroupByCard
|
||||
icon={_list.icon}
|
||||
title={_list.name || ""}
|
||||
count={is_list ? issueIds?.length || 0 : issueIds?.[_list.id]?.length || 0}
|
||||
count={issueGroup.issueCount}
|
||||
issuePayload={_list.payload}
|
||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle}
|
||||
storeType={storeType}
|
||||
@ -147,7 +159,7 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
|
||||
{issueIds && (
|
||||
<IssueBlocksList
|
||||
issueIds={is_list ? issueIds || 0 : issueIds?.[_list.id] || 0}
|
||||
issueIds={issueGroup.issueIds}
|
||||
issuesMap={issuesMap}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
@ -156,6 +168,21 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
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 && (
|
||||
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
|
||||
@ -168,13 +195,14 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IList {
|
||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||
issueIds: TGroupedIssues | TUnGroupedIssues;
|
||||
issuesMap: TIssueMap;
|
||||
group_by: string | null;
|
||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||
@ -183,6 +211,7 @@ export interface IList {
|
||||
showEmptyGroup: boolean;
|
||||
enableIssueQuickAdd: boolean;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
shouldLoadMore: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@ -193,6 +222,7 @@ export interface IList {
|
||||
disableIssueCreation?: boolean;
|
||||
storeType: EIssuesStoreType;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
loadMoreIssues: () => void;
|
||||
isCompletedCycle?: boolean;
|
||||
}
|
||||
|
||||
@ -212,15 +242,19 @@ export const List: React.FC<IList> = (props) => {
|
||||
disableIssueCreation,
|
||||
storeType,
|
||||
addIssuesToView,
|
||||
shouldLoadMore,
|
||||
loadMoreIssues,
|
||||
isCompletedCycle = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<GroupByList
|
||||
issueIds={issueIds as TUnGroupedIssues}
|
||||
issueIds={issueIds}
|
||||
issuesMap={issuesMap}
|
||||
group_by={group_by}
|
||||
shouldLoadMore={shouldLoadMore}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
|
@ -156,7 +156,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<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>
|
||||
|
@ -30,21 +30,16 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const {
|
||||
issuesFilter: { filters, fetchFilters, updateFilters },
|
||||
issues: { loader, groupedIssueIds, fetchIssues },
|
||||
issues: { loader, issueCount: totalIssueCount, groupedIssueIds, fetchIssues, fetchNextIssues },
|
||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||
const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL);
|
||||
|
||||
const { dataViewId, issueIds } = groupedIssueIds;
|
||||
const {
|
||||
membership: { currentWorkspaceAllProjectsRole },
|
||||
} = useUser();
|
||||
const { fetchAllGlobalViews } = useGlobalView();
|
||||
const { workspaceProjectIds } = useProject();
|
||||
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
|
||||
|
||||
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(
|
||||
workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null,
|
||||
async () => {
|
||||
@ -92,7 +91,15 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
if (workspaceSlug && globalViewId) {
|
||||
await fetchAllGlobalViews(workspaceSlug.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();
|
||||
}
|
||||
},
|
||||
@ -136,30 +143,34 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)}
|
||||
handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)}
|
||||
portalElement={portalElement}
|
||||
readOnly={!canEditProperties(issue.project_id)}
|
||||
readOnly={!canEditProperties(issue.project_id ?? undefined)}
|
||||
/>
|
||||
),
|
||||
[canEditProperties, removeIssue, updateIssue, archiveIssue]
|
||||
);
|
||||
|
||||
if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) {
|
||||
if (loader === "init-loader" || !globalViewId || !groupedIssueIds) {
|
||||
return <SpreadsheetLayoutLoader />;
|
||||
}
|
||||
|
||||
const {
|
||||
"All Issues": { issueIds, issueCount },
|
||||
} = groupedIssueIds;
|
||||
|
||||
const emptyStateType =
|
||||
(workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS;
|
||||
(workspaceProjectIds ?? []).length > 0 ? `workspace-${globalViewId}` : EmptyStateType.WORKSPACE_NO_PROJECTS;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="relative h-full w-full flex flex-col">
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
|
||||
{issueIds.length === 0 ? (
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} />
|
||||
{!totalIssueCount ? (
|
||||
<EmptyState
|
||||
type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS}
|
||||
size="sm"
|
||||
primaryButtonOnClick={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentView !== "custom-view" && currentView !== "subscribed"
|
||||
? globalViewId !== "custom-view" && globalViewId !== "subscribed"
|
||||
? () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
@ -177,11 +188,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issueIds={issueIds}
|
||||
issueIds={Array.isArray(issueIds) ? issueIds : []}
|
||||
quickActions={renderQuickActions}
|
||||
updateIssue={updateIssue}
|
||||
canEditProperties={canEditProperties}
|
||||
viewId={globalViewId}
|
||||
viewId={globalViewId.toString()}
|
||||
onEndOfListTrigger={fetchNextPages}
|
||||
/>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
|
@ -4,13 +4,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// mobx store
|
||||
// components
|
||||
import {
|
||||
ArchivedIssueListLayout,
|
||||
ArchivedIssueAppliedFiltersRoot,
|
||||
ProjectArchivedEmptyState,
|
||||
IssuePeekOverview,
|
||||
} from "components/issues";
|
||||
import { ListLayoutLoader } from "components/ui";
|
||||
import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, IssuePeekOverview } from "components/issues";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
// ui
|
||||
import { useIssues } from "hooks/store";
|
||||
@ -27,37 +21,19 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||
await issues?.fetchIssues(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||
);
|
||||
}
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <ListLayoutLoader />;
|
||||
}
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<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">
|
||||
<ArchivedIssueListLayout />
|
||||
</div>
|
||||
<IssuePeekOverview is_archived />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import size from "lodash/size";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
@ -10,19 +9,32 @@ import { TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||
import {
|
||||
CycleAppliedFiltersRoot,
|
||||
CycleCalendarLayout,
|
||||
CycleEmptyState,
|
||||
CycleGanttLayout,
|
||||
CycleKanBanLayout,
|
||||
CycleListLayout,
|
||||
CycleSpreadsheetLayout,
|
||||
IssuePeekOverview,
|
||||
} from "components/issues";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
|
||||
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(() => {
|
||||
const router = useRouter();
|
||||
@ -40,12 +52,6 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && cycleId) {
|
||||
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 }
|
||||
@ -56,37 +62,8 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
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 (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
||||
@ -99,36 +76,11 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
)}
|
||||
<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">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
<CycleIssueLayout activeLayout={activeLayout} />
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -4,17 +4,25 @@ import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { IssuePeekOverview } from "components/issues/peek-overview";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
|
||||
import { useIssues } from "hooks/store";
|
||||
// components
|
||||
import { ProjectDraftEmptyState } from "../empty-states";
|
||||
import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue";
|
||||
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
|
||||
import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
|
||||
// ui
|
||||
// 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(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
@ -27,11 +35,6 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||
await issues?.fetchIssues(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||
);
|
||||
}
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
@ -41,29 +44,14 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<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">
|
||||
{activeLayout === "list" ? (
|
||||
<DraftIssueListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<DraftKanBanLayout />
|
||||
) : null}
|
||||
<DraftIssueLayout activeLayout={activeLayout} />
|
||||
{/* issue peek overview */}
|
||||
<IssuePeekOverview is_draft />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { Fragment } from "react";
|
||||
import size from "lodash/size";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
@ -9,18 +8,32 @@ import {
|
||||
IssuePeekOverview,
|
||||
ModuleAppliedFiltersRoot,
|
||||
ModuleCalendarLayout,
|
||||
ModuleEmptyState,
|
||||
ModuleGanttLayout,
|
||||
ModuleKanBanLayout,
|
||||
ModuleListLayout,
|
||||
ModuleSpreadsheetLayout,
|
||||
} from "components/issues";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
|
||||
import { useIssues } from "hooks/store";
|
||||
// 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(() => {
|
||||
// router
|
||||
@ -36,84 +49,23 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && moduleId) {
|
||||
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 }
|
||||
);
|
||||
|
||||
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 <></>;
|
||||
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<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">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ModuleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ModuleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ModuleSpreadsheetLayout />
|
||||
) : null}
|
||||
<ModuleIssueLayout activeLayout={activeLayout} />
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
@ -12,16 +12,31 @@ import {
|
||||
KanBanLayout,
|
||||
ProjectAppliedFiltersRoot,
|
||||
ProjectSpreadsheetLayout,
|
||||
ProjectEmptyState,
|
||||
IssuePeekOverview,
|
||||
} from "components/issues";
|
||||
// hooks
|
||||
// helpers
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
|
||||
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(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
@ -34,11 +49,6 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||
await issues?.fetchIssues(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||
);
|
||||
}
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
@ -48,18 +58,9 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ProjectAppliedFiltersRoot />
|
||||
|
||||
{issues?.groupedIssueIds?.length === 0 ? (
|
||||
<ProjectEmptyState />
|
||||
) : (
|
||||
<Fragment>
|
||||
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
||||
{/* mutation loader */}
|
||||
{issues?.loader === "mutation" && (
|
||||
@ -67,23 +68,11 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
||||
<Spinner className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectSpreadsheetLayout />
|
||||
) : null}
|
||||
<ProjectIssueLayout activeLayout={activeLayout} />
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from "react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
@ -8,18 +8,33 @@ import {
|
||||
IssuePeekOverview,
|
||||
ProjectViewAppliedFiltersRoot,
|
||||
ProjectViewCalendarLayout,
|
||||
ProjectViewEmptyState,
|
||||
ProjectViewGanttLayout,
|
||||
ProjectViewKanBanLayout,
|
||||
ProjectViewListLayout,
|
||||
ProjectViewSpreadsheetLayout,
|
||||
} from "components/issues";
|
||||
import { ActiveLoader } from "components/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
|
||||
import { useIssues } from "hooks/store";
|
||||
// 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(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
@ -32,12 +47,6 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && viewId) {
|
||||
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 }
|
||||
@ -47,38 +56,15 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||
|
||||
if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) {
|
||||
return <>{activeLayout && <ActiveLoader layout={activeLayout} />}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<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">
|
||||
{activeLayout === "list" ? (
|
||||
<ProjectViewListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProjectViewKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ProjectViewCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ProjectViewGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectViewSpreadsheetLayout />
|
||||
) : null}
|
||||
<ProjectViewIssueLayout activeLayout={activeLayout} />
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// hooks
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
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 { IQuickActionProps } from "../list/list-view-types";
|
||||
import { SpreadsheetView } from "./spreadsheet-view";
|
||||
import useSWR from "swr";
|
||||
import { IssueLayoutHOC } from "../issue-layout-HOC";
|
||||
|
||||
export type SpreadsheetStoreType =
|
||||
| EIssuesStoreType.PROJECT
|
||||
@ -36,13 +38,30 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { issues, issuesFilter } = useIssues(storeType);
|
||||
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } =
|
||||
useIssuesActions(storeType);
|
||||
const {
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
removeIssueFromView,
|
||||
archiveIssue,
|
||||
restoreIssue,
|
||||
updateFilters,
|
||||
} = useIssuesActions(storeType);
|
||||
// derived values
|
||||
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
|
||||
// user role validation
|
||||
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(
|
||||
(projectId: string | undefined) => {
|
||||
const isEditingAllowedBasedOnProject =
|
||||
@ -53,7 +72,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
|
||||
);
|
||||
|
||||
const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues;
|
||||
const issueIds = issues.groupedIssueIds?.["All Issues"]?.issueIds ?? [];
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
@ -83,7 +102,10 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
|
||||
);
|
||||
|
||||
if (!Array.isArray(issueIds)) return null;
|
||||
|
||||
return (
|
||||
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.SPREADSHEET}>
|
||||
<SpreadsheetView
|
||||
displayProperties={issuesFilter.issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issuesFilter.issueFilters?.displayFilters ?? {}}
|
||||
@ -96,6 +118,8 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
viewId={viewId}
|
||||
enableQuickCreateIssue={enableQuickAdd}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
||||
onEndOfListTrigger={fetchNextIssues}
|
||||
/>
|
||||
</IssueLayoutHOC>
|
||||
);
|
||||
});
|
||||
|
@ -14,7 +14,9 @@ type Props = {
|
||||
issueDetail: TIssue;
|
||||
disableUserActions: boolean;
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -29,7 +29,9 @@ interface Props {
|
||||
portalElement?: HTMLDivElement | null
|
||||
) => React.ReactNode;
|
||||
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>;
|
||||
nestingLevel: number;
|
||||
issueId: string;
|
||||
@ -115,7 +117,9 @@ interface IssueRowDetailsProps {
|
||||
portalElement?: HTMLDivElement | null
|
||||
) => React.ReactNode;
|
||||
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>;
|
||||
nestingLevel: number;
|
||||
issueId: string;
|
||||
@ -163,7 +167,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
|
||||
const handleToggleExpand = () => {
|
||||
setExpanded((prevState) => {
|
||||
if (!prevState && workspaceSlug && issueDetail)
|
||||
if (!prevState && workspaceSlug && issueDetail && issueDetail.project_id)
|
||||
subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issueDetail.project_id, issueDetail.id);
|
||||
return !prevState;
|
||||
});
|
||||
@ -182,7 +186,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
);
|
||||
if (!issueDetail) return null;
|
||||
|
||||
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
||||
const disableUserActions = !canEditProperties(issueDetail.project_id ?? undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -6,6 +6,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@pl
|
||||
//components
|
||||
import { SpreadsheetIssueRow } from "./issue-row";
|
||||
import { SpreadsheetHeader } from "./spreadsheet-header";
|
||||
import { useIntersectionObserver } from "hooks/use-intersection-observer";
|
||||
|
||||
type Props = {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
@ -18,10 +19,13 @@ type Props = {
|
||||
customActionButton?: React.ReactElement,
|
||||
portalElement?: HTMLDivElement | null
|
||||
) => 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;
|
||||
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
||||
containerRef: MutableRefObject<HTMLTableElement | null>;
|
||||
onEndOfListTrigger: () => void;
|
||||
};
|
||||
|
||||
export const SpreadsheetTable = observer((props: Props) => {
|
||||
@ -36,10 +40,12 @@ export const SpreadsheetTable = observer((props: Props) => {
|
||||
updateIssue,
|
||||
canEditProperties,
|
||||
containerRef,
|
||||
onEndOfListTrigger,
|
||||
} = props;
|
||||
|
||||
// states
|
||||
const isScrolled = useRef(false);
|
||||
const intersectionRef = useRef<HTMLTableSectionElement | null>(null);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (!containerRef.current) return;
|
||||
@ -74,6 +80,28 @@ export const SpreadsheetTable = observer((props: Props) => {
|
||||
};
|
||||
}, [handleScroll, containerRef]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (intersectionRef.current) {
|
||||
// const observer = new IntersectionObserver(
|
||||
// (entries) => {
|
||||
// if (entries[0].isIntersecting) onEndOfListTrigger();
|
||||
// },
|
||||
// {
|
||||
// root: containerRef?.current,
|
||||
// rootMargin: `50% 0% 50% 0%`,
|
||||
// }
|
||||
// );
|
||||
// observer.observe(intersectionRef.current);
|
||||
// return () => {
|
||||
// if (intersectionRef.current) {
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// observer.unobserve(intersectionRef.current);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
// }, [intersectionRef, containerRef]);
|
||||
useIntersectionObserver(containerRef, intersectionRef, onEndOfListTrigger, `50% 0% 50% 0%`);
|
||||
|
||||
const handleKeyBoardNavigation = useTableKeyboardNavigation();
|
||||
|
||||
return (
|
||||
@ -102,6 +130,7 @@ export const SpreadsheetTable = observer((props: Props) => {
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot ref={intersectionRef}>Loading...</tfoot>
|
||||
</table>
|
||||
);
|
||||
});
|
||||
|
@ -19,7 +19,9 @@ type Props = {
|
||||
customActionButton?: React.ReactElement,
|
||||
portalElement?: HTMLDivElement | null
|
||||
) => 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;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
@ -29,6 +31,7 @@ type Props = {
|
||||
) => Promise<TIssue | undefined>;
|
||||
viewId?: string;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
onEndOfListTrigger: () => void;
|
||||
enableQuickCreateIssue?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
};
|
||||
@ -46,6 +49,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
canEditProperties,
|
||||
enableQuickCreateIssue,
|
||||
disableIssueCreation,
|
||||
onEndOfListTrigger,
|
||||
} = props;
|
||||
// refs
|
||||
const containerRef = useRef<HTMLTableElement | null>(null);
|
||||
@ -77,6 +81,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
updateIssue={updateIssue}
|
||||
canEditProperties={canEditProperties}
|
||||
containerRef={containerRef}
|
||||
onEndOfListTrigger={onEndOfListTrigger}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-custom-border-100">
|
||||
|
@ -47,7 +47,7 @@ export const getGroupByColumns = (
|
||||
case "created_by":
|
||||
return getCreatedByColumns(member) as any;
|
||||
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
|
||||
// constants
|
||||
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
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store";
|
||||
// layouts
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "./issue-layouts";
|
||||
|
||||
export const IssuesMobileHeader = () => {
|
||||
@ -38,7 +44,7 @@ export const IssuesMobileHeader = () => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
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 { ProjectAnalyticsModal } from "components/analytics";
|
||||
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 { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
export const ModuleMobileHeader = () => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@ -34,7 +40,7 @@ export const ModuleMobileHeader = () => {
|
||||
} = useMember();
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
|
||||
},
|
||||
|
@ -4,10 +4,15 @@ import { useRouter } from "next/router";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "components/issues";
|
||||
// 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";
|
||||
// constants
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
export const ProfileIssuesFilter = observer(() => {
|
||||
// router
|
||||
@ -25,7 +30,7 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
@ -94,7 +99,7 @@ export const ProfileIssuesFilter = observer(() => {
|
||||
return (
|
||||
<div className="relative flex items-center justify-end gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban"]}
|
||||
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
@ -28,7 +28,7 @@ export const CalendarLayoutLoader = () => (
|
||||
<span className="h-7 w-20 bg-custom-background-80 rounded" />
|
||||
</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) => (
|
||||
<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[] }) => (
|
||||
<div className="flex gap-5 px-3.5 py-1.5 overflow-x-auto">
|
||||
{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 item-center gap-1.5">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded" />
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded" />
|
||||
<div className="flex item-center">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded animate-pulse" />
|
||||
</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>
|
||||
{Array.from({ length: cardsInColumn }, (_, cardIndex) => (
|
||||
<span key={cardIndex} className="h-28 w-80 bg-custom-background-80 rounded" />
|
||||
<KanbanIssueBlockLoader key={cardIndex} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { forwardRef } from "react";
|
||||
import { getRandomInt, getRandomLength } from "../utils";
|
||||
|
||||
const ListItemRow = () => (
|
||||
<div className="flex items-center justify-between h-11 p-3 border-b border-custom-border-200">
|
||||
export const ListLoaderItemRow = forwardRef<HTMLDivElement>((props, ref) => (
|
||||
<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">
|
||||
<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`} />
|
||||
@ -18,7 +19,7 @@ const ListItemRow = () => (
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
));
|
||||
|
||||
const ListSection = ({ itemCount }: { itemCount: number }) => (
|
||||
<div className="flex flex-shrink-0 flex-col">
|
||||
@ -30,7 +31,7 @@ const ListSection = ({ itemCount }: { itemCount: number }) => (
|
||||
</div>
|
||||
<div className="relative h-full w-full">
|
||||
{[...Array(itemCount)].map((_, index) => (
|
||||
<ListItemRow key={index} />
|
||||
<ListLoaderItemRow key={index} />
|
||||
))}
|
||||
</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 getRandomLength = (lengthArray: string[]) => {
|
||||
const randomIndex = Math.floor(Math.random() * lengthArray.length);
|
||||
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,
|
||||
TIssueExtraOptions,
|
||||
TIssueGroupByOptions,
|
||||
TIssueLayouts,
|
||||
TIssueOrderByOptions,
|
||||
TIssuePriorities,
|
||||
TIssueTypeFilters,
|
||||
@ -24,6 +23,14 @@ export enum EIssuesStoreType {
|
||||
DEFAULT = "DEFAULT",
|
||||
}
|
||||
|
||||
export enum EIssueLayoutTypes {
|
||||
LIST = "list",
|
||||
KANBAN = "kanban",
|
||||
CALENDAR = "calendar",
|
||||
GANTT = "gantt_chart",
|
||||
SPREADSHEET = "spreadsheet",
|
||||
}
|
||||
|
||||
export type TCreateModalStoreTypes =
|
||||
| EIssuesStoreType.PROJECT
|
||||
| EIssuesStoreType.PROJECT_VIEW
|
||||
@ -115,15 +122,15 @@ export const ISSUE_EXTRA_OPTIONS: {
|
||||
];
|
||||
|
||||
export const ISSUE_LAYOUTS: {
|
||||
key: TIssueLayouts;
|
||||
key: EIssueLayoutTypes;
|
||||
title: string;
|
||||
icon: any;
|
||||
}[] = [
|
||||
{ key: "list", title: "List Layout", icon: List },
|
||||
{ key: "kanban", title: "Kanban Layout", icon: Kanban },
|
||||
{ key: "calendar", title: "Calendar Layout", icon: Calendar },
|
||||
{ key: "spreadsheet", title: "Spreadsheet Layout", icon: Sheet },
|
||||
{ key: "gantt_chart", title: "Gantt Chart Layout", icon: GanttChartSquare },
|
||||
{ key: EIssueLayoutTypes.LIST, title: "List Layout", icon: List },
|
||||
{ key: EIssueLayoutTypes.KANBAN, title: "Kanban Layout", icon: Kanban },
|
||||
{ key: EIssueLayoutTypes.CALENDAR, title: "Calendar Layout", icon: Calendar },
|
||||
{ key: EIssueLayoutTypes.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
|
||||
{ key: EIssueLayoutTypes.GANTT, title: "Gantt Chart Layout", icon: GanttChartSquare },
|
||||
];
|
||||
|
||||
export interface ILayoutDisplayFiltersOptions {
|
||||
|
@ -4,17 +4,10 @@ import { v4 as uuidv4 } from "uuid";
|
||||
// types
|
||||
import { IGanttBlock } from "components/gantt-chart";
|
||||
// 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 { orderArrayBy } from "helpers/array.helper";
|
||||
import {
|
||||
TIssue,
|
||||
TIssueGroupByOptions,
|
||||
TIssueLayouts,
|
||||
TIssueOrderByOptions,
|
||||
TIssueParams,
|
||||
TStateGroups,
|
||||
} from "@plane/types";
|
||||
import { TIssue, TIssueGroupByOptions, TIssueOrderByOptions, TIssueParams, TStateGroups } from "@plane/types";
|
||||
|
||||
type THandleIssuesMutation = (
|
||||
formData: Partial<TIssue>,
|
||||
@ -89,7 +82,7 @@ export const handleIssuesMutation: THandleIssuesMutation = (
|
||||
};
|
||||
|
||||
export const handleIssueQueryParamsByLayout = (
|
||||
layout: TIssueLayouts | undefined,
|
||||
layout: EIssueLayoutTypes | undefined,
|
||||
viewType: "my_issues" | "issues" | "profile_issues" | "archived_issues" | "draft_issues"
|
||||
): TIssueParams[] | null => {
|
||||
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
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
cursor?: string
|
||||
cursor: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
// action
|
||||
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.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>;
|
||||
|
||||
restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
|
||||
quickAddIssue: undefined;
|
||||
}
|
||||
|
||||
export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
|
||||
@ -44,6 +46,9 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
|
||||
restoreIssue: action,
|
||||
});
|
||||
// filter store
|
||||
@ -61,7 +66,7 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
|
||||
this.loader = loadType;
|
||||
});
|
||||
this.clear();
|
||||
const params = this.issueFilterStore?.getFilterParams(options);
|
||||
const params = this.issueFilterStore?.getFilterParams(options, undefined);
|
||||
const response = await this.issueArchiveService.getArchivedIssues(workspaceSlug, projectId, params);
|
||||
|
||||
this.onfetchIssues(response, options);
|
||||
@ -73,11 +78,11 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
|
||||
};
|
||||
|
||||
fetchNextIssues = async (workspaceSlug: string, projectId: string) => {
|
||||
if (!this.paginationOptions) return;
|
||||
if (!this.paginationOptions || !this.next_page_results) return;
|
||||
try {
|
||||
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);
|
||||
|
||||
this.onfetchNexIssues(response);
|
||||
@ -113,4 +118,6 @@ export class ArchivedIssues extends BaseIssuesStore implements IArchivedIssues {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
quickAddIssue = undefined;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
cursor?: string
|
||||
cursor: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
// action
|
||||
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(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
|
@ -80,11 +80,15 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
||||
cycleId: observable.ref,
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
|
||||
addIssueToCycle: action,
|
||||
removeIssueFromCycle: action,
|
||||
transferIssuesFromCycle: action,
|
||||
fetchActiveCycleIssues: action,
|
||||
|
||||
quickAddIssue: action,
|
||||
});
|
||||
// service
|
||||
this.cycleService = new CycleService();
|
||||
@ -107,7 +111,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
||||
|
||||
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);
|
||||
|
||||
this.onfetchIssues(response, options);
|
||||
@ -119,11 +123,11 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
||||
};
|
||||
|
||||
fetchNextIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||
if (!this.paginationOptions) return;
|
||||
if (!this.paginationOptions || !this.next_page_results) return;
|
||||
try {
|
||||
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);
|
||||
|
||||
this.onfetchNexIssues(response);
|
||||
@ -241,4 +245,6 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
quickAddIssue = this.issueQuickAdd;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export interface IDraftIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
cursor?: string
|
||||
cursor: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
@ -107,6 +107,10 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
|
||||
paginationOptions.group_by = options.groupedBy;
|
||||
}
|
||||
|
||||
if (options.after && options.before) {
|
||||
paginationOptions["target_date"] = `${options.after};after,${options.before};before`;
|
||||
}
|
||||
|
||||
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.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>;
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
|
||||
quickAddIssue: undefined;
|
||||
}
|
||||
|
||||
export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
|
||||
@ -43,9 +45,8 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
@ -62,7 +63,7 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
|
||||
this.loader = loadType;
|
||||
});
|
||||
this.clear();
|
||||
const params = this.issueFilterStore?.getFilterParams(options);
|
||||
const params = this.issueFilterStore?.getFilterParams(options, undefined);
|
||||
const response = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params);
|
||||
|
||||
this.onfetchIssues(response, options);
|
||||
@ -74,11 +75,11 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
|
||||
};
|
||||
|
||||
fetchNextIssues = async (workspaceSlug: string, projectId: string) => {
|
||||
if (!this.paginationOptions) return;
|
||||
if (!this.paginationOptions || !this.next_page_results) return;
|
||||
try {
|
||||
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);
|
||||
|
||||
this.onfetchNexIssues(response);
|
||||
@ -100,4 +101,6 @@ export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
|
||||
|
||||
createIssue = this.createDraftIssue;
|
||||
updateIssue = this.updateDraftIssue;
|
||||
|
||||
quickAddIssue = undefined;
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import { IBaseIssueFilterStore } from "./issue-filter-helper.store";
|
||||
// constants
|
||||
import { ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { EIssueLayoutTypes, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
@ -44,6 +44,9 @@ export interface IBaseIssuesStore {
|
||||
issueCount: number | undefined;
|
||||
pageCount: number | undefined;
|
||||
|
||||
next_page_results: boolean;
|
||||
prev_page_results: boolean;
|
||||
|
||||
groupedIssueCount: Record<string, number> | undefined;
|
||||
|
||||
// computed
|
||||
@ -96,6 +99,9 @@ export class BaseIssuesStore implements IBaseIssuesStore {
|
||||
issueCount: number | undefined = undefined;
|
||||
pageCount: number | undefined = undefined;
|
||||
|
||||
next_page_results: boolean = true;
|
||||
prev_page_results: boolean = false;
|
||||
|
||||
paginationOptions: IssuePaginationOptions | undefined = undefined;
|
||||
|
||||
isArchived: boolean;
|
||||
@ -119,6 +125,8 @@ export class BaseIssuesStore implements IBaseIssuesStore {
|
||||
prevCursor: observable.ref,
|
||||
issueCount: observable.ref,
|
||||
pageCount: observable.ref,
|
||||
next_page_results: observable.ref,
|
||||
prev_page_results: observable.ref,
|
||||
|
||||
paginationOptions: observable,
|
||||
// computed
|
||||
@ -134,7 +142,6 @@ export class BaseIssuesStore implements IBaseIssuesStore {
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
archiveIssue: action,
|
||||
quickAddIssue: action,
|
||||
removeBulkIssues: action,
|
||||
});
|
||||
this.rootIssueStore = _rootStore;
|
||||
@ -155,6 +162,9 @@ export class BaseIssuesStore implements IBaseIssuesStore {
|
||||
|
||||
this.issueCount = issuesResponse.count;
|
||||
this.pageCount = issuesResponse.total_pages;
|
||||
|
||||
this.next_page_results = issuesResponse.next_page_results;
|
||||
this.prev_page_results = issuesResponse.prev_page_results;
|
||||
};
|
||||
|
||||
get groupedIssueIds() {
|
||||
@ -172,22 +182,22 @@ export class BaseIssuesStore implements IBaseIssuesStore {
|
||||
this.issues,
|
||||
this.isArchived ? "archived" : "un-archived"
|
||||
);
|
||||
if (!currentIssues) return {};
|
||||
if (!currentIssues) return { "All Issues": { issueIds: [], issueCount: 0 } };
|
||||
|
||||
let groupedIssues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = {};
|
||||
|
||||
if (layout === "list" && orderBy) {
|
||||
if (layout === EIssueLayoutTypes.LIST && orderBy) {
|
||||
if (groupBy) groupedIssues = this.groupedIssues(groupBy, orderBy, currentIssues, this.groupedIssueCount);
|
||||
else groupedIssues = this.unGroupedIssues(orderBy, currentIssues, this.issueCount);
|
||||
} else if (layout === "kanban" && groupBy && orderBy) {
|
||||
} else if (layout === EIssueLayoutTypes.KANBAN && groupBy && orderBy) {
|
||||
if (subGroupBy)
|
||||
groupedIssues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, currentIssues, this.groupedIssueCount);
|
||||
else groupedIssues = this.groupedIssues(groupBy, orderBy, currentIssues, this.groupedIssueCount);
|
||||
} else if (layout === "calendar")
|
||||
} else if (layout === EIssueLayoutTypes.CALENDAR)
|
||||
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);
|
||||
else if (layout === "gantt_chart")
|
||||
else if (layout === EIssueLayoutTypes.GANTT)
|
||||
groupedIssues = this.unGroupedIssues(orderBy ?? "sort_order", currentIssues, this.issueCount);
|
||||
|
||||
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 = [];
|
||||
try {
|
||||
this.addIssue(data);
|
||||
@ -400,8 +410,8 @@ export class BaseIssuesStore implements IBaseIssuesStore {
|
||||
}
|
||||
|
||||
for (const group of groupArray) {
|
||||
if (group && currentIssues[group]) currentIssues[group].issueIds.push(currentIssue.id);
|
||||
else if (group) currentIssues[group].issueIds = [currentIssue.id];
|
||||
if (!currentIssues[group]) currentIssues[group] = { issueIds: [], issueCount: groupedIssueCount[group] };
|
||||
if (group) currentIssues[group].issueIds.push(currentIssue.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,19 +209,6 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||
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 = {
|
||||
fetchFiltersFromStorage: () => {
|
||||
|
@ -5,6 +5,7 @@ import { ICalendarPayload, ICalendarWeek } from "components/issues";
|
||||
import { generateCalendarData } from "helpers/calendar.helper";
|
||||
// types
|
||||
import { getWeekNumberOfDate } from "helpers/date-time.helper";
|
||||
import { computedFn } from "mobx-utils";
|
||||
|
||||
export interface ICalendarStore {
|
||||
calendarFilters: {
|
||||
@ -25,6 +26,7 @@ export interface ICalendarStore {
|
||||
| undefined;
|
||||
activeWeekNumber: number;
|
||||
allDaysOfActiveWeek: ICalendarWeek | undefined;
|
||||
getStartAndEndDate: (layout: "week" | "month") => { startDate: string; endDate: string } | undefined;
|
||||
}
|
||||
|
||||
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 }>) => {
|
||||
this.updateCalendarPayload(filters.activeMonthDate || filters.activeWeekDate || new Date());
|
||||
|
||||
|
@ -28,7 +28,7 @@ export interface IModuleIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
cursor?: string
|
||||
cursor: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
// action
|
||||
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(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
|
@ -79,12 +79,16 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
|
||||
moduleId: observable.ref,
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
|
||||
addIssuesToModule: action,
|
||||
removeIssuesFromModule: action,
|
||||
addModulesToIssue: action,
|
||||
removeModulesFromIssue: action,
|
||||
removeIssueFromModule: action,
|
||||
|
||||
quickAddIssue: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
@ -107,7 +111,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
|
||||
|
||||
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);
|
||||
|
||||
this.onfetchIssues(response, options);
|
||||
@ -119,11 +123,11 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
|
||||
};
|
||||
|
||||
fetchNextIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => {
|
||||
if (!this.paginationOptions) return;
|
||||
if (!this.paginationOptions || !this.next_page_results) return;
|
||||
try {
|
||||
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);
|
||||
|
||||
this.onfetchNexIssues(response);
|
||||
@ -295,4 +299,6 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
quickAddIssue = this.issueQuickAdd;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export interface IProfileIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
cursor?: string
|
||||
cursor: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
// action
|
||||
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.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>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
|
||||
quickAddIssue: undefined;
|
||||
}
|
||||
|
||||
export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
|
||||
@ -51,6 +53,8 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
|
||||
// action
|
||||
setViewId: action.bound,
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
@ -91,7 +95,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
|
||||
|
||||
this.setViewId(view);
|
||||
|
||||
let params = this.issueFilterStore?.getFilterParams(options);
|
||||
let params = this.issueFilterStore?.getFilterParams(options, undefined);
|
||||
params = {
|
||||
...params,
|
||||
assignees: undefined,
|
||||
@ -113,7 +117,7 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
|
||||
};
|
||||
|
||||
fetchNextIssues = async (workspaceSlug: string, userId: string) => {
|
||||
if (!this.paginationOptions || !this.currentView) return;
|
||||
if (!this.paginationOptions || !this.currentView || !this.next_page_results) return;
|
||||
try {
|
||||
this.loader = "pagination";
|
||||
|
||||
@ -142,4 +146,6 @@ export class ProfileIssues extends BaseIssuesStore implements IProfileIssues {
|
||||
if (!this.paginationOptions || !this.currentView) return;
|
||||
return await this.fetchIssues(workspaceSlug, userId, loadType, this.paginationOptions, this.currentView);
|
||||
};
|
||||
|
||||
quickAddIssue = undefined;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
cursor?: string
|
||||
cursor: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
// action
|
||||
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, {
|
||||
display_filters: _filters.displayFilters,
|
||||
|
@ -44,6 +44,8 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
});
|
||||
//filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
@ -60,7 +62,7 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
|
||||
this.loader = loadType;
|
||||
});
|
||||
this.clear();
|
||||
const params = this.issueFilterStore?.getFilterParams(options);
|
||||
const params = this.issueFilterStore?.getFilterParams(options, undefined);
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
|
||||
|
||||
this.onfetchIssues(response, options);
|
||||
@ -72,11 +74,11 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
|
||||
};
|
||||
|
||||
fetchNextIssues = async (workspaceSlug: string, projectId: string) => {
|
||||
if (!this.paginationOptions) return;
|
||||
if (!this.paginationOptions || !this.next_page_results) return;
|
||||
try {
|
||||
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);
|
||||
|
||||
this.onfetchNexIssues(response);
|
||||
@ -91,4 +93,6 @@ export class ProjectViewIssues extends BaseIssuesStore implements IProjectViewIs
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions);
|
||||
};
|
||||
|
||||
quickAddIssue = this.issueQuickAdd;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export interface IProjectIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
cursor?: string
|
||||
cursor: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
@ -107,6 +107,10 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
|
||||
paginationOptions.group_by = options.groupedBy;
|
||||
}
|
||||
|
||||
if (options.after && options.before) {
|
||||
paginationOptions["target_date"] = `${options.after};after,${options.before};before`;
|
||||
}
|
||||
|
||||
return paginationOptions;
|
||||
});
|
||||
|
||||
@ -217,7 +221,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
|
||||
});
|
||||
});
|
||||
|
||||
if (this.requiresServerUpdate(updatedDisplayFilters))
|
||||
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
|
||||
|
||||
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, {
|
||||
|
@ -47,6 +47,8 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
|
||||
quickAddIssue: action,
|
||||
});
|
||||
// filter store
|
||||
this.issueFilterStore = issueFilterStore;
|
||||
@ -63,7 +65,7 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||
this.loader = loadType;
|
||||
});
|
||||
this.clear();
|
||||
const params = this.issueFilterStore?.getFilterParams(options);
|
||||
const params = this.issueFilterStore?.getFilterParams(options, undefined);
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params);
|
||||
|
||||
this.onfetchIssues(response, options);
|
||||
@ -75,11 +77,11 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||
};
|
||||
|
||||
fetchNextIssues = async (workspaceSlug: string, projectId: string) => {
|
||||
if (!this.paginationOptions) return;
|
||||
if (!this.paginationOptions || !this.next_page_results) return;
|
||||
try {
|
||||
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);
|
||||
|
||||
this.onfetchNexIssues(response);
|
||||
@ -98,4 +100,6 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||
if (!this.paginationOptions) return;
|
||||
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");
|
||||
|
||||
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
|
||||
|
@ -24,9 +24,12 @@ export interface IWorkspaceIssues extends IBaseIssuesStore {
|
||||
loadType: TLoader
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (workspaceSlug: string, viewId: string) => Promise<TIssuesResponse | undefined>;
|
||||
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
|
||||
quickAddIssue: undefined;
|
||||
}
|
||||
|
||||
export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues {
|
||||
@ -46,6 +49,8 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
|
||||
makeObservable(this, {
|
||||
// action
|
||||
fetchIssues: action,
|
||||
fetchNextIssues: action,
|
||||
fetchIssuesWithExistingPagination: action,
|
||||
});
|
||||
// services
|
||||
this.workspaceService = new WorkspaceService();
|
||||
@ -90,4 +95,6 @@ export class WorkspaceIssues extends BaseIssuesStore implements IWorkspaceIssues
|
||||
if (!this.paginationOptions) return;
|
||||
return await this.fetchIssues(workspaceSlug, viewId, loadType, this.paginationOptions);
|
||||
};
|
||||
|
||||
quickAddIssue = undefined;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user