From cf470d715ab90a6c0a99101d5be90535f16c3dfe Mon Sep 17 00:00:00 2001 From: rahulramesha Date: Wed, 13 Mar 2024 12:26:10 +0530 Subject: [PATCH] implement pagination for spreadsheet, list, kanban and calendar --- packages/types/src/issues/base.d.ts | 5 +- packages/types/src/view-props.d.ts | 282 +++++++++--------- web/components/cycles/cycle-mobile-header.tsx | 12 +- web/components/headers/cycle-issues.tsx | 19 +- web/components/headers/module-issues.tsx | 19 +- .../headers/project-draft-issues.tsx | 13 +- web/components/headers/project-issues.tsx | 19 +- .../headers/project-view-issues.tsx | 19 +- .../calendar/base-calendar-root.tsx | 55 +++- .../issue-layouts/calendar/calendar.tsx | 9 +- .../issue-layouts/calendar/day-tile.tsx | 15 +- .../issue-layouts/calendar/issue-blocks.tsx | 5 +- .../issue-layouts/calendar/week-days.tsx | 3 + .../issue-layouts/empty-states/cycle.tsx | 58 ++-- .../issue-layouts/empty-states/index.ts | 7 - .../issue-layouts/empty-states/index.tsx | 33 ++ .../issue-layouts/empty-states/module.tsx | 58 ++-- .../empty-states/project-view.tsx | 30 +- .../filters/header/layout-selection.tsx | 9 +- .../issue-layouts/gantt/base-gantt-root.tsx | 7 +- .../issues/issue-layouts/issue-layout-HOC.tsx | 51 ++++ .../issue-layouts/kanban/base-kanban-root.tsx | 47 ++- .../issues/issue-layouts/kanban/default.tsx | 12 +- .../issue-layouts/kanban/kanban-group.tsx | 14 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 99 +++--- .../issue-layouts/list/base-list-root.tsx | 62 ++-- .../issues/issue-layouts/list/blocks-list.tsx | 2 +- .../issues/issue-layouts/list/default.tsx | 60 +++- .../list/quick-add-issue-form.tsx | 2 +- .../roots/all-issue-layout-root.tsx | 42 ++- .../roots/archived-issue-layout-root.tsx | 34 +-- .../issue-layouts/roots/cycle-layout-root.tsx | 96 ++---- .../roots/draft-issue-layout-root.tsx | 44 +-- .../roots/module-layout-root.tsx | 96 ++---- .../roots/project-layout-root.tsx | 69 ++--- .../roots/project-view-layout-root.tsx | 62 ++-- .../spreadsheet/base-spreadsheet-root.tsx | 58 ++-- .../spreadsheet/issue-column.tsx | 4 +- .../issue-layouts/spreadsheet/issue-row.tsx | 12 +- .../spreadsheet/spreadsheet-table.tsx | 31 +- .../spreadsheet/spreadsheet-view.tsx | 7 +- web/components/issues/issue-layouts/utils.tsx | 2 +- .../issues/issues-mobile-header.tsx | 12 +- .../modules/module-mobile-header.tsx | 12 +- .../profile/profile-issues-filter.tsx | 13 +- .../loader/layouts/calendar-layout-loader.tsx | 2 +- .../loader/layouts/kanban-layout-loader.tsx | 18 +- .../ui/loader/layouts/list-layout-loader.tsx | 9 +- web/components/ui/loader/utils.tsx | 29 -- web/constants/issue.ts | 21 +- web/helpers/issue.helper.ts | 13 +- web/hooks/use-intersection-observer.ts | 41 +++ web/store/issue/archived/filter.store.ts | 5 +- web/store/issue/archived/issue.store.ts | 13 +- web/store/issue/cycle/filter.store.ts | 15 +- web/store/issue/cycle/issue.store.ts | 12 +- web/store/issue/draft/filter.store.ts | 9 +- web/store/issue/draft/issue.store.ts | 15 +- web/store/issue/helpers/base-issues.store.ts | 32 +- .../helpers/issue-filter-helper.store.ts | 13 - web/store/issue/issue_calendar_view.store.ts | 18 ++ web/store/issue/module/filter.store.ts | 15 +- web/store/issue/module/issue.store.ts | 12 +- web/store/issue/profile/filter.store.ts | 5 +- web/store/issue/profile/issue.store.ts | 10 +- web/store/issue/project-views/filter.store.ts | 9 +- web/store/issue/project-views/issue.store.ts | 10 +- web/store/issue/project/filter.store.ts | 9 +- web/store/issue/project/issue.store.ts | 10 +- web/store/issue/workspace/filter.store.ts | 1 - web/store/issue/workspace/issue.store.ts | 7 + 71 files changed, 1148 insertions(+), 825 deletions(-) delete mode 100644 web/components/issues/issue-layouts/empty-states/index.ts create mode 100644 web/components/issues/issue-layouts/empty-states/index.tsx create mode 100644 web/components/issues/issue-layouts/issue-layout-HOC.tsx create mode 100644 web/hooks/use-intersection-observer.ts diff --git a/packages/types/src/issues/base.d.ts b/packages/types/src/issues/base.d.ts index a0c7f5f79..81ee13a57 100644 --- a/packages/types/src/issues/base.d.ts +++ b/packages/types/src/issues/base.d.ts @@ -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; diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index a1c858bee..f151aac00 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -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; +} diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/components/cycles/cycle-mobile-header.tsx index add78943c..9689430b4 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/components/cycles/cycle-mobile-header.tsx @@ -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(), diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 6f9c61545..e92019d20 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -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(() => {
handleLayoutChange(layout)} selectedLayout={activeLayout} /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 9505d7145..d905aa187 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -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(() => {
handleLayoutChange(layout)} selectedLayout={activeLayout} /> diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 789c3f60f..4859409d5 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -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(() => {
handleLayoutChange(layout)} selectedLayout={activeLayout} /> diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 9739e7832..be278adaf 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -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(() => {
handleLayoutChange(layout)} selectedLayout={activeLayout} /> diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index ab3959716..0f3194fa1 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -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(() => {
handleLayoutChange(layout)} selectedLayout={activeLayout} /> diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 8d2b56d2a..a3706e113 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -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 ( - <> +
{ groupedIssueIds={groupedIssueIds} layout={displayFilters?.calendar?.layout} showWeekends={displayFilters?.calendar?.show_weekends ?? false} + issueCalendarView={issueCalendarView} quickActions={(issue, customActionButton) => ( { readOnly={!isEditingAllowed || isCompletedCycle} /> )} + loadMoreIssues={loadMoreIssues} addIssuesToView={addIssuesToView} quickAddCallback={issues.quickAddIssue} viewId={viewId} @@ -111,6 +150,6 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { />
- +
); }); diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index efd785d3e..9494ff721 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -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 = observer((props) => { groupedIssueIds, layout, showWeekends, + issueCalendarView, + loadMoreIssues, quickActions, quickAddCallback, addIssuesToView, @@ -66,7 +71,7 @@ export const CalendarChart: React.FC = observer((props) => { const { issues: { viewFlags }, } = useIssues(EIssuesStoreType.PROJECT); - const issueCalendarView = useCalendarView(); + const { membership: { currentProjectRole }, } = useUser(); @@ -102,6 +107,7 @@ export const CalendarChart: React.FC = 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 = observer((props) => { week={issueCalendarView.allDaysOfActiveWeek} issues={issues} groupedIssueIds={groupedIssueIds} + loadMoreIssues={loadMoreIssues} enableQuickIssueCreate disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 8ac1e460c..f8feb00ca 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -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 = observer((props) => { date, issues, groupedIssueIds, + loadMoreIssues, quickActions, enableQuickIssueCreate, disableIssueCreation, @@ -47,14 +49,13 @@ export const CalendarDayTile: React.FC = 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 = observer((props) => { > @@ -117,19 +117,18 @@ export const CalendarDayTile: React.FC = observer((props) => { quickAddCallback={quickAddCallback} addIssuesToView={addIssuesToView} viewId={viewId} - onOpen={() => setShowAllIssues(true)} />
)} - {totalIssues > 4 && ( + {totalIssues > (issueIdList?.issueIds?.length ?? 0) && (
)} diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index 595cc963c..11aa13b8c 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -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 = 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 = 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]; diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index ec1d12e59..644efe8d5 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -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 = observer((props) => { issues, groupedIssueIds, week, + loadMoreIssues, quickActions, enableQuickIssueCreate, disableIssueCreation, @@ -66,6 +68,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { date={date} issues={issues} groupedIssueIds={groupedIssueIds} + loadMoreIssues={loadMoreIssues} quickActions={quickActions} enableQuickIssueCreate={enableQuickIssueCreate} disableIssueCreation={disableIssueCreation} diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 1a49794c6..d6d6f64c2 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -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 = 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 = 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 = 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 = observer((props) => { const emptyStateSize = isEmptyFilters ? "lg" : "sm"; return ( - <> +
setCycleIssuesListModal(false)} searchParams={{ cycle: true }} @@ -100,6 +120,6 @@ export const CycleEmptyState: React.FC = observer((props) => { } />
- +
); }); diff --git a/web/components/issues/issue-layouts/empty-states/index.ts b/web/components/issues/issue-layouts/empty-states/index.ts deleted file mode 100644 index 1320076e7..000000000 --- a/web/components/issues/issue-layouts/empty-states/index.ts +++ /dev/null @@ -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"; diff --git a/web/components/issues/issue-layouts/empty-states/index.tsx b/web/components/issues/issue-layouts/empty-states/index.tsx new file mode 100644 index 000000000..e705d5252 --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/index.tsx @@ -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 ; + case EIssuesStoreType.PROJECT_VIEW: + return ; + case EIssuesStoreType.ARCHIVED: + return ; + case EIssuesStoreType.CYCLE: + return ; + case EIssuesStoreType.MODULE: + return ; + case EIssuesStoreType.DRAFT: + return ; + case EIssuesStoreType.GLOBAL: + return ; + default: + return null; + } +}; diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index d8e3516cd..34e27466a 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -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 = 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 = 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 ( - <> +
setModuleIssuesListModal(false)} searchParams={{ module: moduleId != undefined ? moduleId.toString() : "" }} @@ -84,6 +106,6 @@ export const ModuleEmptyState: React.FC = observer((props) => { secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} />
- +
); }); diff --git a/web/components/issues/issue-layouts/empty-states/project-view.tsx b/web/components/issues/issue-layouts/empty-states/project-view.tsx index fd98011fa..83b4db8a0 100644 --- a/web/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-view.tsx @@ -14,20 +14,22 @@ export const ProjectViewEmptyState: React.FC = observer(() => { const { setTrackElement } = useEventTracker(); return ( -
- , - onClick: () => { - setTrackElement("View issue empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW); - }, - }} - /> +
+
+ , + onClick: () => { + setTrackElement("View issue empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW); + }, + }} + /> +
); }); diff --git a/web/components/issues/issue-layouts/filters/header/layout-selection.tsx b/web/components/issues/issue-layouts/filters/header/layout-selection.tsx index a69ead577..ba741ed32 100644 --- a/web/components/issues/issue-layouts/filters/header/layout-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/layout-selection.tsx @@ -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) => { diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 11f52db80..becda7096 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -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 = observer((props: IBaseGan const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return ( - <> +
= observer((props: IBaseGan showAllBlocks />
- +
); }); diff --git a/web/components/issues/issue-layouts/issue-layout-HOC.tsx b/web/components/issues/issue-layouts/issue-layout-HOC.tsx new file mode 100644 index 000000000..ae9582ccf --- /dev/null +++ b/web/components/issues/issue-layouts/issue-layout-HOC.tsx @@ -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 ; + case EIssueLayoutTypes.KANBAN: + return ; + case EIssueLayoutTypes.SPREADSHEET: + return ; + case EIssueLayoutTypes.CALENDAR: + return ; + case EIssueLayoutTypes.GANTT: + return ; + 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 ; + } + + if (issues.issueCount === 0) { + return ; + } + + return <>{props.children}; +}); diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index e90823c5b..7212ebd14 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -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 = 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 = observer((props: IBas const kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] }; return ( - <> + = observer((props: IBas onSubmit={handleDeleteIssue} /> - {showLoader && issues?.loader === "init-loader" && ( -
- -
- )} -
= observer((props: IBas
= observer((props: IBas addIssuesToView={addIssuesToView} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} + loadMoreIssues={debouncedFetchMoreIssues} />
- + ); }); diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 394f5ef18..19283ef63 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -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 = observer((props) => { handleKanbanFilters, enableQuickIssueCreate, quickAddCallback, + loadMoreIssues, viewId, disableIssueCreation, storeType, @@ -105,7 +107,7 @@ const GroupByKanBan: React.FC = 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 = 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 = observer((props) => { groupByVisibilityToggle={groupByVisibilityToggle} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} + loadMoreIssues={loadMoreIssues} /> )}
@@ -184,7 +187,7 @@ const GroupByKanBan: React.FC = 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 = observer((props) => { quickActions, kanbanFilters, handleKanbanFilters, + loadMoreIssues, enableQuickIssueCreate, quickAddCallback, viewId, @@ -249,6 +254,7 @@ export const KanBan: React.FC = observer((props) => { quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} + loadMoreIssues={loadMoreIssues} enableQuickIssueCreate={enableQuickIssueCreate} quickAddCallback={quickAddCallback} viewId={viewId} diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index 48e92feba..933b222d7 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -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; + 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(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 && } + {enableQuickIssueCreate && !disableIssueCreation && (
{ 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; viewId?: string; scrollableContainerRef?: MutableRefObject; + loadMoreIssues: (() => void) | undefined; } const SubGroupSwimlane: React.FC = observer((props) => { const { @@ -107,6 +108,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { displayProperties, kanbanFilters, handleKanbanFilters, + loadMoreIssues, showEmptyGroup, enableQuickIssueCreate, canEditProperties, @@ -122,7 +124,7 @@ const SubGroupSwimlane: React.FC = 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 = observer((props) => {
{list && list.length > 0 && - list.map((_list: any) => ( -
-
-
- + list.map((_list: any, index: number) => { + const isLastSubGroup = index === list.length - 1; + return ( +
+
+
+ +
+
-
-
- {!kanbanFilters?.sub_group_by.includes(_list.id) && ( -
- -
- )} -
- ))} + {!kanbanFilters?.sub_group_by.includes(_list.id) && ( +
+ +
+ )} +
+ ); + })}
); }); 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 = observer((props) => { quickActions, kanbanFilters, handleKanbanFilters, + loadMoreIssues, showEmptyGroup, isDragStarted, disableIssueCreation, @@ -282,6 +290,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} + loadMoreIssues={loadMoreIssues} showEmptyGroup={showEmptyGroup} isDragStarted={isDragStarted} disableIssueCreation={disableIssueCreation} diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index ae198f1ae..dcfd63000 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -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 ( -
- -
+ +
+ +
+
); }); diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 2296e7b68..a5e0ff53f 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -28,7 +28,7 @@ export const IssueBlocksList: FC = (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} > ) => Promise) | undefined; @@ -39,6 +43,8 @@ export interface IGroupByList { addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; + shouldLoadMore: boolean; + loadMoreIssues: () => void; } const GroupByList: React.FC = (props) => { @@ -57,7 +63,9 @@ const GroupByList: React.FC = (props) => { disableIssueCreation, storeType, addIssuesToView, + shouldLoadMore, isCompletedCycle = false, + loadMoreIssues, } = props; // store hooks const member = useMember(); @@ -67,8 +75,11 @@ const GroupByList: React.FC = (props) => { const cycle = useCycle(); const projectModule = useModule(); + const intersectionRef = useRef(null); const containerRef = useRef(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 = (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 = (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) && (
= (props) => { {issueIds && ( = (props) => { containerRef={containerRef} /> )} + {/* && + issueGroup.issueIds?.length <= issueGroup.issueCount */} + {shouldLoadMore && + (group_by ? ( +
+ Load more ↓ +
+ ) : ( + + ))} {enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && !isCompletedCycle && (
@@ -168,13 +195,14 @@ const GroupByList: React.FC = (props) => { )}
) - )} + ); + })}
); }; export interface IList { - issueIds: TGroupedIssues | TUnGroupedIssues | any; + issueIds: TGroupedIssues | TUnGroupedIssues; issuesMap: TIssueMap; group_by: string | null; updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | 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; + loadMoreIssues: () => void; isCompletedCycle?: boolean; } @@ -212,15 +242,19 @@ export const List: React.FC = (props) => { disableIssueCreation, storeType, addIssuesToView, + shouldLoadMore, + loadMoreIssues, isCompletedCycle = false, } = props; return (
= observer((props onClick={() => setIsOpen(true)} > - New Issue + New Issue
)}
diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index bcba7152e..265e15df8 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -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 ; } + 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 (
- - {issueIds.length === 0 ? ( + + {!totalIssueCount ? ( 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 */} diff --git a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx index ae8ca400a..e692f2988 100644 --- a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx @@ -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 ; - } - if (!workspaceSlug || !projectId) return <>; return (
- - {issues?.groupedIssueIds?.length === 0 ? ( -
- -
- ) : ( - -
- -
- -
- )} +
+ +
+
); }); diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index ce0a9943e..59174b78f 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -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 ; + case EIssueLayoutTypes.KANBAN: + return ; + case EIssueLayoutTypes.CALENDAR: + return ; + case EIssueLayoutTypes.GANTT: + return ; + case EIssueLayoutTypes.SPREADSHEET: + return ; + 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 && }; - } - return ( <> setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> @@ -99,36 +76,11 @@ export const CycleLayoutRoot: React.FC = observer(() => { )} - {issues?.groupedIssueIds?.length === 0 ? ( -
- 0} - /> -
- ) : ( - -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
- {/* peek overview */} - -
- )} +
+ +
+ {/* peek overview */} +
); diff --git a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx index 1a1602ad1..19d84b1b9 100644 --- a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx @@ -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 ; + case EIssueLayoutTypes.KANBAN: + return ; + 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 && }; - } - return (
- - {issues?.groupedIssueIds?.length === 0 ? ( -
- -
- ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : null} - {/* issue peek overview */} - -
- )} +
+ + {/* issue peek overview */} + +
); }); diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 268a2c60c..647e74a27 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -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 ; + case EIssueLayoutTypes.KANBAN: + return ; + case EIssueLayoutTypes.CALENDAR: + return ; + case EIssueLayoutTypes.GANTT: + return ; + case EIssueLayoutTypes.SPREADSHEET: + return ; + 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 && }; - } - return (
- - {issues?.groupedIssueIds?.length === 0 ? ( -
- 0} - /> -
- ) : ( - -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
- {/* peek overview */} - -
- )} +
+ +
+ {/* peek overview */} +
); }); diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index a57d73b2c..f3ee870b1 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -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 ; + case EIssueLayoutTypes.KANBAN: + return ; + case EIssueLayoutTypes.CALENDAR: + return ; + case EIssueLayoutTypes.GANTT: + return ; + case EIssueLayoutTypes.SPREADSHEET: + return ; + 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 && }; - } - return (
- - {issues?.groupedIssueIds?.length === 0 ? ( - - ) : ( - -
- {/* mutation loader */} - {issues?.loader === "mutation" && ( -
- -
- )} - {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} +
+ {/* mutation loader */} + {issues?.loader === "mutation" && ( +
+
+ )} + +
- {/* peek overview */} - - - )} + {/* peek overview */} +
); }); diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index d15e65865..052b77b4e 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -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 ; + case EIssueLayoutTypes.KANBAN: + return ; + case EIssueLayoutTypes.CALENDAR: + return ; + case EIssueLayoutTypes.GANTT: + return ; + case EIssueLayoutTypes.SPREADSHEET: + return ; + 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 && }; - } - return (
+
+ +
- {issues?.groupedIssueIds?.length === 0 ? ( -
- -
- ) : ( - -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
- - {/* peek overview */} - -
- )} + {/* peek overview */} +
); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 653cc28f2..6c2a85213 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -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) => { @@ -83,19 +102,24 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); + if (!Array.isArray(issueIds)) return null; + return ( - + + + ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx index 825c2b31c..fd6614cec 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx @@ -14,7 +14,9 @@ type Props = { issueDetail: TIssue; disableUserActions: boolean; property: keyof IIssueDisplayProperties; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | undefined | null, issueId: string, data: Partial) => Promise) + | undefined; isEstimateEnabled: boolean; }; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 8a8ce29f4..71c11379c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -29,7 +29,9 @@ interface Props { portalElement?: HTMLDivElement | null ) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | undefined | null, issueId: string, data: Partial) => Promise) + | undefined; portalElement: React.MutableRefObject; 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) => Promise) | undefined; + updateIssue: + | ((projectId: string | undefined | null, issueId: string, data: Partial) => Promise) + | undefined; portalElement: React.MutableRefObject; 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 ( <> diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx index 896d5a4dd..52de26d6a 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx @@ -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) => Promise) | undefined; + updateIssue: + | ((projectId: string | undefined | null, issueId: string, data: Partial) => Promise) + | undefined; canEditProperties: (projectId: string | undefined) => boolean; portalElement: React.MutableRefObject; containerRef: MutableRefObject; + 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(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) => { /> ))} + Loading... ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index ed243d312..a2873776b 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -19,7 +19,9 @@ type Props = { customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null ) => React.ReactNode; - updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | undefined | null, issueId: string, data: Partial) => Promise) + | undefined; openIssuesListModal?: (() => void) | null; quickAddCallback?: ( workspaceSlug: string, @@ -29,6 +31,7 @@ type Props = { ) => Promise; viewId?: string; canEditProperties: (projectId: string | undefined) => boolean; + onEndOfListTrigger: () => void; enableQuickCreateIssue?: boolean; disableIssueCreation?: boolean; }; @@ -46,6 +49,7 @@ export const SpreadsheetView: React.FC = observer((props) => { canEditProperties, enableQuickCreateIssue, disableIssueCreation, + onEndOfListTrigger, } = props; // refs const containerRef = useRef(null); @@ -77,6 +81,7 @@ export const SpreadsheetView: React.FC = observer((props) => { updateIssue={updateIssue} canEditProperties={canEditProperties} containerRef={containerRef} + onEndOfListTrigger={onEndOfListTrigger} />
diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index ffe979a56..293fc5f43 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -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 }]; } }; diff --git a/web/components/issues/issues-mobile-header.tsx b/web/components/issues/issues-mobile-header.tsx index e0263b8c8..1a9c79af9 100644 --- a/web/components/issues/issues-mobile-header.tsx +++ b/web/components/issues/issues-mobile-header.tsx @@ -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 }); }, diff --git a/web/components/modules/module-mobile-header.tsx b/web/components/modules/module-mobile-header.tsx index 4763639ed..7e63be058 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/components/modules/module-mobile-header.tsx @@ -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); }, diff --git a/web/components/profile/profile-issues-filter.tsx b/web/components/profile/profile-issues-filter.tsx index 491c00f3a..d6b4162dd 100644 --- a/web/components/profile/profile-issues-filter.tsx +++ b/web/components/profile/profile-issues-filter.tsx @@ -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 (
handleLayoutChange(layout)} selectedLayout={activeLayout} /> diff --git a/web/components/ui/loader/layouts/calendar-layout-loader.tsx b/web/components/ui/loader/layouts/calendar-layout-loader.tsx index 6bb75d3c6..db7bc4066 100644 --- a/web/components/ui/loader/layouts/calendar-layout-loader.tsx +++ b/web/components/ui/loader/layouts/calendar-layout-loader.tsx @@ -28,7 +28,7 @@ export const CalendarLayoutLoader = () => (
- + {[...Array(5)].map((_, index) => ( ))} diff --git a/web/components/ui/loader/layouts/kanban-layout-loader.tsx b/web/components/ui/loader/layouts/kanban-layout-loader.tsx index b949c1b73..f7419d772 100644 --- a/web/components/ui/loader/layouts/kanban-layout-loader.tsx +++ b/web/components/ui/loader/layouts/kanban-layout-loader.tsx @@ -1,16 +1,22 @@ +import { forwardRef } from "react"; + +export const KanbanIssueBlockLoader = forwardRef((props, ref) => ( + +)); + export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { cardsInEachColumn?: number[] }) => (
{cardsInEachColumn.map((cardsInColumn, columnIndex) => ( -
+
-
- - +
+ +
- +
{Array.from({ length: cardsInColumn }, (_, cardIndex) => ( - + ))}
))} diff --git a/web/components/ui/loader/layouts/list-layout-loader.tsx b/web/components/ui/loader/layouts/list-layout-loader.tsx index 9b861cc97..2936f334e 100644 --- a/web/components/ui/loader/layouts/list-layout-loader.tsx +++ b/web/components/ui/loader/layouts/list-layout-loader.tsx @@ -1,7 +1,8 @@ +import { forwardRef } from "react"; import { getRandomInt, getRandomLength } from "../utils"; -const ListItemRow = () => ( -
+export const ListLoaderItemRow = forwardRef((props, ref) => ( +
@@ -18,7 +19,7 @@ const ListItemRow = () => ( ))}
-); +)); const ListSection = ({ itemCount }: { itemCount: number }) => (
@@ -30,7 +31,7 @@ const ListSection = ({ itemCount }: { itemCount: number }) => (
{[...Array(itemCount)].map((_, index) => ( - + ))}
diff --git a/web/components/ui/loader/utils.tsx b/web/components/ui/loader/utils.tsx index 312df038e..3637626ed 100644 --- a/web/components/ui/loader/utils.tsx +++ b/web/components/ui/loader/utils.tsx @@ -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) => { - const { layout } = props; - switch (layout) { - case "list": - return ; - case "kanban": - return ; - case "spreadsheet": - return ; - case "calendar": - return ; - case "gantt_chart": - return ; - default: - return ; - } -}; diff --git a/web/constants/issue.ts b/web/constants/issue.ts index d474d2a49..21bc8fb6a 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -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 { diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 3e6689151..7c8ba8554 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -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, @@ -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[] = []; diff --git a/web/hooks/use-intersection-observer.ts b/web/hooks/use-intersection-observer.ts new file mode 100644 index 000000000..aec2ab5ba --- /dev/null +++ b/web/hooks/use-intersection-observer.ts @@ -0,0 +1,41 @@ +import { RefObject, useState, useEffect } from "react"; + +export type UseIntersectionObserverProps = { + containerRef: RefObject | undefined; + elementRef: RefObject; + callback: () => void; + rootMargin?: string; +}; + +export const useIntersectionObserver = ( + containerRef: RefObject | undefined, + elementRef: RefObject, + 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]); +}; diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index 1d169bb19..461477531 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -28,7 +28,7 @@ export interface IArchivedIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor?: string + cursor: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string) => Promise; @@ -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, diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index c3c753236..96bb4040e 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -26,6 +26,8 @@ export interface IArchivedIssues extends IBaseIssuesStore { fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise; restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + + 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; } diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index 27785616e..753413a27 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -28,7 +28,7 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor?: string + cursor: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; @@ -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, diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 3a961c9e8..43e3fed35 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -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; } diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index bde6b3f76..fb92aa3e9 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -28,7 +28,7 @@ export interface IDraftIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor?: string + cursor: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string) => Promise; @@ -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, diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index e7d4b26c0..e4f7330ee 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -27,6 +27,8 @@ export interface IDraftIssues extends IBaseIssuesStore { fetchNextIssues: (workspaceSlug: string, projectId: string) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + + 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; } diff --git a/web/store/issue/helpers/base-issues.store.ts b/web/store/issue/helpers/base-issues.store.ts index 350e69c21..0f97a8e0e 100644 --- a/web/store/issue/helpers/base-issues.store.ts +++ b/web/store/issue/helpers/base-issues.store.ts @@ -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 | 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); } } diff --git a/web/store/issue/helpers/issue-filter-helper.store.ts b/web/store/issue/helpers/issue-filter-helper.store.ts index 41222eeed..1af7ae5b5 100644 --- a/web/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/store/issue/helpers/issue-filter-helper.store.ts @@ -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: () => { diff --git a/web/store/issue/issue_calendar_view.store.ts b/web/store/issue/issue_calendar_view.store.ts index 98181d730..ee2b79811 100644 --- a/web/store/issue/issue_calendar_view.store.ts +++ b/web/store/issue/issue_calendar_view.store.ts @@ -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()); diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index a3359d16e..8dfb5cb25 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -28,7 +28,7 @@ export interface IModuleIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor?: string + cursor: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; @@ -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, diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index 156fe486b..56338b525 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -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; } diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index 11b1dbd9d..991611853 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -30,7 +30,7 @@ export interface IProfileIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor?: string + cursor: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, userId: string) => Promise; @@ -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, diff --git a/web/store/issue/profile/issue.store.ts b/web/store/issue/profile/issue.store.ts index 7d4ba0f21..8a43caed2 100644 --- a/web/store/issue/profile/issue.store.ts +++ b/web/store/issue/profile/issue.store.ts @@ -32,6 +32,8 @@ export interface IProfileIssues extends IBaseIssuesStore { createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + + 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; } diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index 9cfffde1e..6a5dad963 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -28,7 +28,7 @@ export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor?: string + cursor: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise; @@ -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, diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index 701d0c3d4..9c3ea1d50 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -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; } diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index 78cec2c8c..8d286125e 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -28,7 +28,7 @@ export interface IProjectIssuesFilter extends IBaseIssueFilterStore { //helper actions getFilterParams: ( options: IssuePaginationOptions, - cursor?: string + cursor: string | undefined ) => Partial>; // action fetchFilters: (workspaceSlug: string, projectId: string) => Promise; @@ -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, diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index 42f9f20e4..a7a853784 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -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; } diff --git a/web/store/issue/workspace/filter.store.ts b/web/store/issue/workspace/filter.store.ts index b1870dcd6..1791c7cfd 100644 --- a/web/store/issue/workspace/filter.store.ts +++ b/web/store/issue/workspace/filter.store.ts @@ -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)) diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index d1c8917de..31db275f1 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -24,9 +24,12 @@ export interface IWorkspaceIssues extends IBaseIssuesStore { loadType: TLoader ) => Promise; fetchNextIssues: (workspaceSlug: string, viewId: string) => Promise; + createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + + 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; }