implement pagination for spreadsheet, list, kanban and calendar

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

View File

@ -12,15 +12,16 @@ export * from "./activity/base";
export type TLoader = "init-loader" | "mutation" | "pagination" | undefined;
export type 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;

View File

@ -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,153 +46,153 @@ 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 =
| "priority"
| "state_group"
| "state"
| "assignees"
| "mentions"
| "created_by"
| "subscriber"
| "labels"
| "cycle"
| "module"
| "start_date"
| "target_date"
| "project"
| "group_by"
| "sub_group_by"
| "order_by"
| "type"
| "sub_issue"
| "show_empty_groups"
| "cursor"
| "per_page";
export type TIssueParams =
| "priority"
| "state_group"
| "state"
| "assignees"
| "mentions"
| "created_by"
| "subscriber"
| "labels"
| "cycle"
| "module"
| "start_date"
| "target_date"
| "project"
| "group_by"
| "sub_group_by"
| "order_by"
| "type"
| "sub_issue"
| "show_empty_groups"
| "cursor"
| "per_page";
export type TCalendarLayouts = "month" | "week";
export type TCalendarLayouts = "month" | "week";
export interface IIssueFilterOptions {
assignees?: string[] | null;
mentions?: string[] | null;
created_by?: string[] | null;
labels?: string[] | null;
priority?: string[] | null;
cycle?: string[] | null;
module?: string[] | null;
project?: string[] | null;
start_date?: string[] | null;
state?: string[] | null;
state_group?: string[] | null;
subscriber?: string[] | null;
target_date?: string[] | null;
}
export interface IIssueFilterOptions {
assignees?: string[] | null;
mentions?: string[] | null;
created_by?: string[] | null;
labels?: string[] | null;
priority?: string[] | null;
cycle?: string[] | null;
module?: string[] | null;
project?: string[] | null;
start_date?: string[] | null;
state?: string[] | null;
state_group?: string[] | null;
subscriber?: string[] | null;
target_date?: string[] | null;
}
export interface IIssueDisplayFilterOptions {
calendar?: {
show_weekends?: boolean;
layout?: TCalendarLayouts;
};
group_by?: TIssueGroupByOptions;
sub_group_by?: TIssueGroupByOptions;
layout?: TIssueLayouts;
order_by?: TIssueOrderByOptions;
show_empty_groups?: boolean;
sub_issue?: boolean;
type?: TIssueTypeFilters;
}
export interface IIssueDisplayProperties {
assignee?: boolean;
start_date?: boolean;
due_date?: boolean;
labels?: boolean;
key?: boolean;
priority?: boolean;
state?: boolean;
sub_issue_count?: boolean;
link?: boolean;
attachment_count?: boolean;
estimate?: boolean;
created_on?: boolean;
updated_on?: boolean;
modules?: boolean;
cycle?: boolean;
}
export type TIssueKanbanFilters = {
group_by: string[];
sub_group_by: string[];
export interface IIssueDisplayFilterOptions {
calendar?: {
show_weekends?: boolean;
layout?: TCalendarLayouts;
};
group_by?: TIssueGroupByOptions;
sub_group_by?: TIssueGroupByOptions;
layout?: TIssueLayouts;
order_by?: TIssueOrderByOptions;
show_empty_groups?: boolean;
sub_issue?: boolean;
type?: TIssueTypeFilters;
}
export interface IIssueDisplayProperties {
assignee?: boolean;
start_date?: boolean;
due_date?: boolean;
labels?: boolean;
key?: boolean;
priority?: boolean;
state?: boolean;
sub_issue_count?: boolean;
link?: boolean;
attachment_count?: boolean;
estimate?: boolean;
created_on?: boolean;
updated_on?: boolean;
modules?: boolean;
cycle?: boolean;
}
export interface IIssueFilters {
filters: IIssueFilterOptions | undefined;
displayFilters: IIssueDisplayFilterOptions | undefined;
displayProperties: IIssueDisplayProperties | undefined;
kanbanFilters: TIssueKanbanFilters | undefined;
}
export type TIssueKanbanFilters = {
group_by: string[];
sub_group_by: string[];
};
export interface IIssueFiltersResponse {
filters: IIssueFilterOptions;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
}
export interface IIssueFilters {
filters: IIssueFilterOptions | undefined;
displayFilters: IIssueDisplayFilterOptions | undefined;
displayProperties: IIssueDisplayProperties | undefined;
kanbanFilters: TIssueKanbanFilters | undefined;
}
export interface IWorkspaceIssueFilterOptions {
assignees?: string[] | null;
created_by?: string[] | null;
labels?: string[] | null;
priority?: string[] | null;
state_group?: string[] | null;
subscriber?: string[] | null;
start_date?: string[] | null;
target_date?: string[] | null;
project?: string[] | null;
}
export interface IIssueFiltersResponse {
filters: IIssueFilterOptions;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
}
export interface IWorkspaceGlobalViewDisplayFilterOptions {
order_by?: string | undefined;
type?: "active" | "backlog" | null;
sub_issue?: boolean;
layout?: TIssueViewOptions;
}
export interface IWorkspaceIssueFilterOptions {
assignees?: string[] | null;
created_by?: string[] | null;
labels?: string[] | null;
priority?: string[] | null;
state_group?: string[] | null;
subscriber?: string[] | null;
start_date?: string[] | null;
target_date?: string[] | null;
project?: string[] | null;
}
export interface IWorkspaceViewIssuesParams {
assignees?: string | undefined;
created_by?: string | undefined;
labels?: string | undefined;
priority?: string | undefined;
start_date?: string | undefined;
state?: string | undefined;
state_group?: string | undefined;
subscriber?: string | undefined;
target_date?: string | undefined;
project?: string | undefined;
order_by?: string | undefined;
type?: "active" | "backlog" | undefined;
sub_issue?: boolean;
}
export interface IWorkspaceGlobalViewDisplayFilterOptions {
order_by?: string | undefined;
type?: "active" | "backlog" | null;
sub_issue?: boolean;
layout?: TIssueViewOptions;
}
export interface IProjectViewProps {
display_filters: IIssueDisplayFilterOptions | undefined;
filters: IIssueFilterOptions;
}
export interface IWorkspaceViewIssuesParams {
assignees?: string | undefined;
created_by?: string | undefined;
labels?: string | undefined;
priority?: string | undefined;
start_date?: string | undefined;
state?: string | undefined;
state_group?: string | undefined;
subscriber?: string | undefined;
target_date?: string | undefined;
project?: string | undefined;
order_by?: string | undefined;
type?: "active" | "backlog" | undefined;
sub_issue?: boolean;
}
export interface IWorkspaceViewProps {
filters: IIssueFilterOptions;
display_filters: IIssueDisplayFilterOptions | undefined;
display_properties: IIssueDisplayProperties;
}
export interface IWorkspaceGlobalViewProps {
filters: IWorkspaceIssueFilterOptions;
display_filters: IWorkspaceIssueDisplayFilterOptions | undefined;
display_properties: IIssueDisplayProperties;
}
export interface IProjectViewProps {
display_filters: IIssueDisplayFilterOptions | undefined;
filters: IIssueFilterOptions;
}
export interface IssuePaginationOptions {
canGroup: boolean;
perPageCount: number;
greaterThanDate?: Date;
lessThanDate?: Date;
groupedBy?: TIssueGroupByOptions;
}
export interface IWorkspaceViewProps {
filters: IIssueFilterOptions;
display_filters: IIssueDisplayFilterOptions | undefined;
display_properties: IIssueDisplayProperties;
}
export interface IWorkspaceGlobalViewProps {
filters: IWorkspaceIssueFilterOptions;
display_filters: IWorkspaceIssueDisplayFilterOptions | undefined;
display_properties: IIssueDisplayProperties;
}
export interface IssuePaginationOptions {
canGroup: boolean;
perPageCount: number;
before?: string;
after?: string;
groupedBy?: TIssueGroupByOptions;
}

View File

@ -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(),

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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>
);
});

View File

@ -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}

View File

@ -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>
)}

View File

@ -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];

View File

@ -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}

View File

@ -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>
);
});

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import { useState } from "react";
import { 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>
);
});

View File

@ -14,20 +14,22 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
const { setTrackElement } = useEventTracker();
return (
<div className="grid h-full w-full place-items-center">
<EmptyState
title="View issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("View issue empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
}}
/>
<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"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("View issue empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
}}
/>
</div>
</div>
);
});

View File

@ -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) => {

View File

@ -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>
);
});

View File

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

View File

@ -2,11 +2,12 @@ import { FC, useCallback, useRef, useState } from "react";
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
import { 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>
);
});

View File

@ -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}

View File

@ -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

View File

@ -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,56 +133,60 @@ 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) => (
<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">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
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">
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
storeType={storeType}
/>
</div>
)}
</div>
))}
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
<div className="relative">
<KanBan
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={_list.id}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
showEmptyGroup={showEmptyGroup}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
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}

View File

@ -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,25 +93,33 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
);
const loadMoreIssues = useCallback(() => {
fetchNextIssues();
}, [fetchNextIssues]);
return (
<div className={`relative h-full w-full bg-custom-background-90`}>
<List
issuesMap={issueMap}
displayProperties={displayProperties}
group_by={group_by}
updateIssue={updateIssue}
quickActions={renderQuickActions}
issueIds={issueIds}
showEmptyGroup={showEmptyGroup}
viewId={viewId}
quickAddCallback={issues?.quickAddIssue}
enableIssueQuickAdd={!!enableQuickAdd}
canEditProperties={canEditProperties}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
storeType={storeType}
addIssuesToView={addIssuesToView}
isCompletedCycle={isCompletedCycle}
/>
</div>
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.LIST}>
<div className={`relative h-full w-full bg-custom-background-90`}>
<List
issuesMap={issueMap}
displayProperties={displayProperties}
group_by={group_by}
updateIssue={updateIssue}
quickActions={renderQuickActions}
issueIds={issueIds!}
shouldLoadMore={issues.next_page_results}
loadMoreIssues={loadMoreIssues}
showEmptyGroup={showEmptyGroup}
viewId={viewId}
quickAddCallback={issues?.quickAddIssue}
enableIssueQuickAdd={!!enableQuickAdd}
canEditProperties={canEditProperties}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
storeType={storeType}
addIssuesToView={addIssuesToView}
isCompletedCycle={isCompletedCycle}
/>
</div>
</IssueLayoutHOC>
);
});

View File

@ -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

View File

@ -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 &darr;
</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}

View File

@ -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>

View File

@ -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 />

View File

@ -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 className="relative h-full w-full overflow-auto">
<ArchivedIssueListLayout />
</div>
<IssuePeekOverview is_archived />
</div>
);
});

View File

@ -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}
</div>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
<div className="h-full w-full overflow-auto">
<CycleIssueLayout activeLayout={activeLayout} />
</div>
{/* peek overview */}
<IssuePeekOverview />
</div>
</>
);

View File

@ -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}
{/* issue peek overview */}
<IssuePeekOverview is_draft />
</div>
)}
<div className="relative h-full w-full overflow-auto">
<DraftIssueLayout activeLayout={activeLayout} />
{/* issue peek overview */}
<IssuePeekOverview is_draft />
</div>
</div>
);
});

View File

@ -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}
</div>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
<div className="h-full w-full overflow-auto">
<ModuleIssueLayout activeLayout={activeLayout} />
</div>
{/* peek overview */}
<IssuePeekOverview />
</div>
);
});

View File

@ -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,42 +58,21 @@ 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" && (
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
<Spinner className="w-4 h-4" />
</div>
)}
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
{/* mutation loader */}
{issues?.loader === "mutation" && (
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
<Spinner className="w-4 h-4" />
</div>
)}
<ProjectIssueLayout activeLayout={activeLayout} />
</div>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
{/* peek overview */}
<IssuePeekOverview />
</div>
);
});

View File

@ -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 />
<div className="relative h-full w-full overflow-auto">
<ProjectViewIssueLayout activeLayout={activeLayout} />
</div>
{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}
</div>
{/* peek overview */}
<IssuePeekOverview />
</Fragment>
)}
{/* peek overview */}
<IssuePeekOverview />
</div>
);
});

View File

@ -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,19 +102,24 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
[isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue]
);
if (!Array.isArray(issueIds)) return null;
return (
<SpreadsheetView
displayProperties={issuesFilter.issueFilters?.displayProperties ?? {}}
displayFilters={issuesFilter.issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={issueIds}
quickActions={renderQuickActions}
updateIssue={updateIssue}
canEditProperties={canEditProperties}
quickAddCallback={issues.quickAddIssue}
viewId={viewId}
enableQuickCreateIssue={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
/>
<IssueLayoutHOC storeType={storeType} layout={EIssueLayoutTypes.SPREADSHEET}>
<SpreadsheetView
displayProperties={issuesFilter.issueFilters?.displayProperties ?? {}}
displayFilters={issuesFilter.issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issueIds={issueIds}
quickActions={renderQuickActions}
updateIssue={updateIssue}
canEditProperties={canEditProperties}
quickAddCallback={issues.quickAddIssue}
viewId={viewId}
enableQuickCreateIssue={enableQuickAdd}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
onEndOfListTrigger={fetchNextIssues}
/>
</IssueLayoutHOC>
);
});

View File

@ -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;
};

View File

@ -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 (
<>

View File

@ -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>
);
});

View File

@ -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">

View File

@ -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 }];
}
};

View File

@ -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 });
},

View File

@ -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);
},

View File

@ -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}
/>

View File

@ -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" />
))}

View File

@ -1,16 +1,22 @@
import { forwardRef } from "react";
export const KanbanIssueBlockLoader = forwardRef<HTMLSpanElement>((props, ref) => (
<span ref={ref} className="block h-28 m-1.5 animate-pulse bg-custom-background-80 rounded" />
));
export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { cardsInEachColumn?: number[] }) => (
<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>
))}

View File

@ -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>

View File

@ -1,35 +1,6 @@
import {
CalendarLayoutLoader,
GanttLayoutLoader,
KanbanLayoutLoader,
ListLayoutLoader,
SpreadsheetLayoutLoader,
} from "./layouts";
export const getRandomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
export const 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 />;
}
};

View File

@ -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 {

View File

@ -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[] = [];

View File

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

View File

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

View File

@ -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;
}

View File

@ -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,13 +222,12 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
});
});
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.cycleIssues.fetchIssuesWithExistingPagination(
workspaceSlug,
projectId,
"mutation",
cycleId
);
this.rootIssueStore.cycleIssues.fetchIssuesWithExistingPagination(
workspaceSlug,
projectId,
"mutation",
cycleId
);
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
display_filters: _filters.displayFilters,

View File

@ -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;
}

View File

@ -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,8 +209,7 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
});
});
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.draftIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
this.rootIssueStore.draftIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, undefined, {
display_filters: _filters.displayFilters,

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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: () => {

View File

@ -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());

View File

@ -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,13 +221,12 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul
});
});
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.moduleIssues.fetchIssuesWithExistingPagination(
workspaceSlug,
projectId,
"mutation",
moduleId
);
this.rootIssueStore.moduleIssues.fetchIssuesWithExistingPagination(
workspaceSlug,
projectId,
"mutation",
moduleId
);
await this.issueFilterService.patchModuleIssueFilters(workspaceSlug, projectId, moduleId, {
display_filters: _filters.displayFilters,

View File

@ -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;
}

View File

@ -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,8 +212,7 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf
});
});
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, userId, "mutation");
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, userId, "mutation");
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, userId, undefined, {
display_filters: _filters.displayFilters,

View File

@ -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;
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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,8 +221,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
});
});
if (this.requiresServerUpdate(updatedDisplayFilters))
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
this.rootIssueStore.projectIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, {
display_filters: _filters.displayFilters,

View File

@ -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;
}

View File

@ -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))

View File

@ -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;
}