diff --git a/apps/api/plane/app/views/page/base.py b/apps/api/plane/app/views/page/base.py
index d3ad49b5fda..ec2f277c29a 100644
--- a/apps/api/plane/app/views/page/base.py
+++ b/apps/api/plane/app/views/page/base.py
@@ -42,6 +42,7 @@
UserRecentVisit,
)
from plane.utils.error_codes import ERROR_CODES
+from plane.utils.page_filters import page_filters
# Local imports
from ..base import BaseAPIView, BaseViewSet
@@ -96,9 +97,7 @@ def get_queryset(self):
.select_related("workspace")
.select_related("owned_by")
.annotate(is_favorite=Exists(subquery))
- .order_by(self.request.GET.get("order_by", "-created_at"))
.prefetch_related("labels")
- .order_by("-is_favorite", "-created_at")
.annotate(
project=Exists(
ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=self.kwargs.get("project_id"))
@@ -285,8 +284,9 @@ def access(self, request, slug, project_id, page_id):
return Response(status=status.HTTP_204_NO_CONTENT)
def list(self, request, slug, project_id):
- queryset = self.get_queryset()
project = Project.objects.get(pk=project_id)
+ queryset = self.get_queryset()
+
if (
ProjectMember.objects.filter(
workspace__slug=slug,
@@ -298,8 +298,34 @@ def list(self, request, slug, project_id):
and not project.guest_view_all_features
):
queryset = queryset.filter(owned_by=request.user)
- pages = PageSerializer(queryset, many=True).data
- return Response(pages, status=status.HTTP_200_OK)
+
+ # Filter by page type (public, private, archived)
+ page_type = request.GET.get("type", "public")
+ if page_type == "private":
+ queryset = queryset.filter(access=1, archived_at__isnull=True)
+ elif page_type == "archived":
+ queryset = queryset.filter(archived_at__isnull=False)
+ elif page_type == "public":
+ queryset = queryset.filter(access=0, archived_at__isnull=True)
+
+ # Apply additional filters from query params
+ filters = page_filters(request.query_params, "GET")
+ queryset = queryset.filter(**filters)
+
+ order_by_param = request.GET.get("order_by", "-created_at")
+ ALLOWED_ORDER_FIELDS = {"created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"}
+
+ if order_by_param not in ALLOWED_ORDER_FIELDS:
+ order_by_param = "-created_at"
+ queryset = queryset.order_by(order_by_param)
+
+ return self.paginate(
+ request=request,
+ queryset=queryset,
+ on_results=lambda pages: PageSerializer(pages, many=True).data,
+ default_per_page=20,
+ max_per_page=100,
+ )
def archive(self, request, slug, project_id, page_id):
page = Page.objects.get(
diff --git a/apps/api/plane/utils/page_filters.py b/apps/api/plane/utils/page_filters.py
new file mode 100644
index 00000000000..2642389ca36
--- /dev/null
+++ b/apps/api/plane/utils/page_filters.py
@@ -0,0 +1,156 @@
+# Python imports
+import re
+import uuid
+from datetime import timedelta
+
+# Django imports
+from django.utils import timezone
+
+
+# The date from pattern
+pattern = re.compile(r"\d+_(weeks|months)$")
+
+
+# check the valid uuids
+def filter_valid_uuids(uuid_list):
+ valid_uuids = []
+ for uuid_str in uuid_list:
+ try:
+ uuid_obj = uuid.UUID(uuid_str)
+ valid_uuids.append(uuid_obj)
+ except ValueError:
+ # ignore the invalid uuids
+ pass
+ return valid_uuids
+
+
+# Get the 2_weeks, 3_months
+def string_date_filter(page_filter, duration, subsequent, term, date_filter, offset):
+ now = timezone.now().date()
+ if term == "months":
+ if subsequent == "after":
+ if offset == "fromnow":
+ page_filter[f"{date_filter}__gte"] = now + timedelta(days=duration * 30)
+ else:
+ page_filter[f"{date_filter}__gte"] = now - timedelta(days=duration * 30)
+ else:
+ if offset == "fromnow":
+ page_filter[f"{date_filter}__lte"] = now + timedelta(days=duration * 30)
+ else:
+ page_filter[f"{date_filter}__lte"] = now - timedelta(days=duration * 30)
+ if term == "weeks":
+ if subsequent == "after":
+ if offset == "fromnow":
+ page_filter[f"{date_filter}__gte"] = now + timedelta(weeks=duration)
+ else:
+ page_filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration)
+ else:
+ if offset == "fromnow":
+ page_filter[f"{date_filter}__lte"] = now + timedelta(weeks=duration)
+ else:
+ page_filter[f"{date_filter}__lte"] = now - timedelta(weeks=duration)
+
+
+def date_filter(page_filter, date_term, queries):
+ """
+ Handle all date filters
+ """
+ for query in queries:
+ date_query = query.split(";")
+ if date_query:
+ if len(date_query) >= 2:
+ match = pattern.match(date_query[0])
+ if match:
+ if len(date_query) == 3:
+ digit, term = date_query[0].split("_")
+ string_date_filter(
+ page_filter=page_filter,
+ duration=int(digit),
+ subsequent=date_query[1],
+ term=term,
+ date_filter=date_term,
+ offset=date_query[2],
+ )
+ else:
+ if "after" in date_query:
+ page_filter[f"{date_term}__gte"] = date_query[0]
+ else:
+ page_filter[f"{date_term}__lte"] = date_query[0]
+ else:
+ page_filter[f"{date_term}__contains"] = date_query[0]
+ return page_filter
+
+
+def filter_created_by(params, page_filter, method, prefix=""):
+ if method == "GET":
+ created_bys = [item for item in params.get("created_by").split(",") if item != "null"]
+ if "None" in created_bys:
+ page_filter[f"{prefix}owned_by__isnull"] = True
+ created_bys = filter_valid_uuids(created_bys)
+ if len(created_bys) and "" not in created_bys:
+ page_filter[f"{prefix}owned_by__in"] = created_bys
+ else:
+ if params.get("created_by", None) and len(params.get("created_by")) and params.get("created_by") != "null":
+ page_filter[f"{prefix}owned_by__in"] = params.get("created_by")
+ return page_filter
+
+
+def filter_created_at(params, page_filter, method, prefix=""):
+ if method == "GET":
+ created_ats = params.get("created_at").split(",")
+ if len(created_ats) and "" not in created_ats:
+ date_filter(
+ page_filter=page_filter,
+ date_term=f"{prefix}created_at__date",
+ queries=created_ats,
+ )
+ else:
+ if params.get("created_at", None) and len(params.get("created_at")):
+ date_filter(
+ page_filter=page_filter,
+ date_term=f"{prefix}created_at__date",
+ queries=params.get("created_at", []),
+ )
+ return page_filter
+
+
+def filter_favorites(params, page_filter, method, prefix=""):
+ if method == "GET":
+ favorites = params.get("favorites", "false").lower()
+ if favorites == "true":
+ page_filter[f"{prefix}is_favorite"] = True
+ else:
+ if params.get("favorites", False):
+ page_filter[f"{prefix}is_favorite"] = True
+
+ return page_filter
+
+
+def filter_search(params, page_filter, method, prefix=""):
+ if method == "GET":
+ search = params.get("search", None)
+ if search:
+ page_filter[f"{prefix}name__icontains"] = search
+ else:
+ if params.get("search", None):
+ page_filter[f"{prefix}name__icontains"] = params.get("search")
+
+ return page_filter
+
+
+def page_filters(query_params, method, prefix=""):
+ page_filter = {}
+
+ PAGE_FILTER_MAP = {
+ "created_by": filter_created_by,
+ "created_at": filter_created_at,
+ "favorites": filter_favorites,
+ "search": filter_search,
+ }
+
+ for key, value in PAGE_FILTER_MAP.items():
+ if key in query_params:
+ func = value
+ func(query_params, page_filter, method, prefix)
+
+ return page_filter
diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx
index 59f609dbfbb..6ae73282ed4 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx
@@ -1,5 +1,6 @@
// component
import { Outlet } from "react-router";
+import { useSearchParams } from "next/navigation";
import useSWR from "swr";
import { AppHeader } from "@/components/core/app-header";
import { ContentWrapper } from "@/components/core/content-wrapper";
@@ -8,12 +9,22 @@ import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
// local components
import type { Route } from "./+types/layout";
import { PageDetailsHeader } from "./header";
+import type { TPageNavigationTabs } from "@plane/types";
+
+const getPageType = (pageType?: string | null): TPageNavigationTabs => {
+ if (pageType === "private") return "private";
+ if (pageType === "archived") return "archived";
+ return "public";
+};
export default function ProjectPageDetailsLayout({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
const { fetchPagesList } = usePageStore(EPageStoreType.PROJECT);
+ const searchParams = useSearchParams();
+ const type = searchParams.get("type");
+ const pageType = getPageType(type);
// fetching pages list
- useSWR(`PROJECT_PAGES_${projectId}`, () => fetchPagesList(workspaceSlug, projectId));
+ useSWR(`PROJECT_PAGES_${projectId}`, () => fetchPagesList(workspaceSlug, projectId, pageType));
return (
<>
} />
diff --git a/apps/web/core/components/pages/list/loader.tsx b/apps/web/core/components/pages/list/loader.tsx
new file mode 100644
index 00000000000..6c504d9b3ed
--- /dev/null
+++ b/apps/web/core/components/pages/list/loader.tsx
@@ -0,0 +1,15 @@
+import { Loader } from "@plane/ui";
+
+export function PageListLoader() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/core/components/pages/list/root.tsx b/apps/web/core/components/pages/list/root.tsx
index 0d477745fef..8a36546fda9 100644
--- a/apps/web/core/components/pages/list/root.tsx
+++ b/apps/web/core/components/pages/list/root.tsx
@@ -1,13 +1,18 @@
+import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
+import { useParams } from "next/navigation";
// types
import type { TPageNavigationTabs } from "@plane/types";
// components
import { ListLayout } from "@/components/core/list";
+// hooks
+import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
// plane web hooks
import type { EPageStoreType } from "@/plane-web/hooks/store";
import { usePageStore } from "@/plane-web/hooks/store";
// local imports
import { PageListBlock } from "./block";
+import { PageListLoader } from "./loader";
type TPagesListRoot = {
pageType: TPageNavigationTabs;
@@ -16,17 +21,58 @@ type TPagesListRoot = {
export const PagesListRoot = observer(function PagesListRoot(props: TPagesListRoot) {
const { pageType, storeType } = props;
+ // params
+ const { workspaceSlug, projectId } = useParams();
// store hooks
- const { getCurrentProjectFilteredPageIdsByTab } = usePageStore(storeType);
+ const { getCurrentProjectFilteredPageIdsByTab, fetchPagesList, getPaginationInfo, getPaginationLoader } =
+ usePageStore(storeType);
+
+ // pagination hooks
+ const paginationInfo = getPaginationInfo(pageType);
+ const paginationLoader = getPaginationLoader(pageType);
+ const hasNextPage = paginationInfo.hasNextPage;
+ const isFetchingNextPage = paginationLoader === "pagination";
+
+ // state for intersection observer element
+ const [intersectionElement, setIntersectionElement] = useState(null);
+ // ref for container - we'll wrap ListLayout
+ const containerRef = useRef(null);
+
// derived values
const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType);
+ // Function to fetch next page
+ const fetchNextPage = useCallback(() => {
+ if (!workspaceSlug || !projectId || !hasNextPage || isFetchingNextPage) {
+ return;
+ }
+ // Use fetchPagesList with cursor from pagination info
+ void fetchPagesList(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ pageType,
+ paginationInfo.nextCursor ?? undefined
+ );
+ }, [workspaceSlug, projectId, hasNextPage, isFetchingNextPage, fetchPagesList, pageType, paginationInfo.nextCursor]);
+
+ // Set up intersection observer to trigger loading more pages
+ useIntersectionObserver(
+ containerRef,
+ isFetchingNextPage ? null : intersectionElement,
+ fetchNextPage,
+ `100% 0% 100% 0%`
+ );
+
if (!filteredPageIds) return <>>;
+
return (
-
- {filteredPageIds.map((pageId) => (
-
- ))}
-
+
+
+ {filteredPageIds.map((pageId) => (
+
+ ))}
+ {hasNextPage && }
+
+
);
});
diff --git a/apps/web/core/components/pages/pages-list-main-content.tsx b/apps/web/core/components/pages/pages-list-main-content.tsx
index 58e239140b9..8396e53c78d 100644
--- a/apps/web/core/components/pages/pages-list-main-content.tsx
+++ b/apps/web/core/components/pages/pages-list-main-content.tsx
@@ -156,6 +156,11 @@ export const PagesListMainContent = observer(function PagesListMainContent(props
/>
);
}
+
+ if (loader === "mutation-loader" && (!filteredPageIds || filteredPageIds.length === 0)) {
+ return ;
+ }
+
// if no pages match the filter criteria
if (filteredPageIds?.length === 0)
return (
diff --git a/apps/web/core/components/pages/pages-list-view.tsx b/apps/web/core/components/pages/pages-list-view.tsx
index 15f114f46ec..86e3e25ddac 100644
--- a/apps/web/core/components/pages/pages-list-view.tsx
+++ b/apps/web/core/components/pages/pages-list-view.tsx
@@ -19,11 +19,20 @@ type TPageView = {
export const PagesListView = observer(function PagesListView(props: TPageView) {
const { children, pageType, projectId, storeType, workspaceSlug } = props;
// store hooks
- const { isAnyPageAvailable, fetchPagesList } = usePageStore(storeType);
- // fetching pages list
+ const { isAnyPageAvailable, fetchPagesList, filters } = usePageStore(storeType);
+
+ // fetching pages list - include filters and sorting in SWR key to refetch when they change
useSWR(
- workspaceSlug && projectId && pageType ? `PROJECT_PAGES_${projectId}` : null,
- workspaceSlug && projectId && pageType ? () => fetchPagesList(workspaceSlug, projectId, pageType) : null
+ workspaceSlug && projectId && pageType
+ ? `PROJECT_PAGES_${projectId}_${pageType}_${filters.searchQuery || ""}_${filters.sortKey || ""}_${
+ filters.sortBy || ""
+ }_${JSON.stringify(filters.filters || {})}`
+ : null,
+ workspaceSlug && projectId && pageType ? () => fetchPagesList(workspaceSlug, projectId, pageType) : null,
+ {
+ revalidateOnFocus: true,
+ revalidateIfStale: true,
+ }
);
// pages loader
diff --git a/apps/web/core/services/page/project-page.service.ts b/apps/web/core/services/page/project-page.service.ts
index 16a67038428..7443add803c 100644
--- a/apps/web/core/services/page/project-page.service.ts
+++ b/apps/web/core/services/page/project-page.service.ts
@@ -1,11 +1,16 @@
// types
import { API_BASE_URL } from "@plane/constants";
-import type { TDocumentPayload, TPage } from "@plane/types";
+import type { TDocumentPayload, TPage, TPaginationInfo } from "@plane/types";
// helpers
// services
import { APIService } from "@/services/api.service";
import { FileUploadService } from "@/services/file-upload.service";
+// Paginated response type for pages
+export type TPagePaginatedResponse = TPaginationInfo & {
+ results: TPage[];
+};
+
export class ProjectPageService extends APIService {
private fileUploadService: FileUploadService;
@@ -15,8 +20,14 @@ export class ProjectPageService extends APIService {
this.fileUploadService = new FileUploadService();
}
- async fetchAll(workspaceSlug: string, projectId: string): Promise {
- return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`)
+ async fetchAll(
+ workspaceSlug: string,
+ projectId: string,
+ queries?: Record
+ ): Promise {
+ return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, {
+ params: queries,
+ })
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
diff --git a/apps/web/core/store/pages/project-page.store.ts b/apps/web/core/store/pages/project-page.store.ts
index 040fea59a7d..095ce3be027 100644
--- a/apps/web/core/store/pages/project-page.store.ts
+++ b/apps/web/core/store/pages/project-page.store.ts
@@ -18,9 +18,27 @@ import type { TProjectPage } from "./project-page";
import { ProjectPage } from "./project-page";
type TLoader = "init-loader" | "mutation-loader" | undefined;
+type TPaginationLoader = "pagination" | undefined;
type TError = { title: string; description: string };
+// Pagination info for each page type
+export type TPagePaginationInfo = {
+ nextCursor: string | null;
+ hasNextPage: boolean;
+ totalResults: number;
+};
+
+// Default pagination info
+const DEFAULT_PAGINATION_INFO: TPagePaginationInfo = {
+ nextCursor: null,
+ hasNextPage: false, // Don't assume there's a next page until API tells us
+ totalResults: 0,
+};
+
+// Per page count for pagination
+const PAGES_PER_PAGE = 20;
+
export const ROLE_PERMISSIONS_TO_CREATE_PAGE = [
EUserPermissions.ADMIN,
EUserPermissions.MEMBER,
@@ -34,9 +52,17 @@ export interface IProjectPageStore {
data: Record; // pageId => Page
error: TError | undefined;
filters: TPageFilters;
+ // filtered page type arrays
+ filteredPublicPageIds: string[];
+ filteredPrivatePageIds: string[];
+ filteredArchivedPageIds: string[];
+ // pagination info per page type
+ paginationInfo: Record;
+ paginationLoader: Record;
// computed
isAnyPageAvailable: boolean;
canCurrentUserCreatePage: boolean;
+ hasActiveFilters: boolean;
// helper actions
getCurrentProjectPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined;
getCurrentProjectPageIds: (projectId: string) => string[];
@@ -44,11 +70,14 @@ export interface IProjectPageStore {
getPageById: (pageId: string) => TProjectPage | undefined;
updateFilters: (filterKey: T, filterValue: TPageFilters[T]) => void;
clearAllFilters: () => void;
+ getPaginationInfo: (pageType: TPageNavigationTabs) => TPagePaginationInfo;
+ getPaginationLoader: (pageType: TPageNavigationTabs) => TPaginationLoader;
// actions
fetchPagesList: (
workspaceSlug: string,
projectId: string,
- pageType?: TPageNavigationTabs
+ pageType: TPageNavigationTabs,
+ cursor?: string
) => Promise;
fetchPageDetails: (
workspaceSlug: string,
@@ -71,6 +100,21 @@ export class ProjectPageStore implements IProjectPageStore {
sortKey: "updated_at",
sortBy: "desc",
};
+ // filtered page type arrays
+ filteredPublicPageIds: string[] = [];
+ filteredPrivatePageIds: string[] = [];
+ filteredArchivedPageIds: string[] = [];
+ // pagination info per page type
+ paginationInfo: Record = {
+ public: { ...DEFAULT_PAGINATION_INFO },
+ private: { ...DEFAULT_PAGINATION_INFO },
+ archived: { ...DEFAULT_PAGINATION_INFO },
+ };
+ paginationLoader: Record = {
+ public: undefined,
+ private: undefined,
+ archived: undefined,
+ };
// service
service: ProjectPageService;
rootStore: CoreRootStore;
@@ -82,9 +126,16 @@ export class ProjectPageStore implements IProjectPageStore {
data: observable,
error: observable,
filters: observable,
+ filteredPublicPageIds: observable,
+ filteredPrivatePageIds: observable,
+ filteredArchivedPageIds: observable,
+ // pagination info
+ paginationInfo: observable,
+ paginationLoader: observable,
// computed
isAnyPageAvailable: computed,
canCurrentUserCreatePage: computed,
+ hasActiveFilters: computed,
// helper actions
updateFilters: action,
clearAllFilters: action,
@@ -128,6 +179,33 @@ export class ProjectPageStore implements IProjectPageStore {
return !!currentUserProjectRole && ROLE_PERMISSIONS_TO_CREATE_PAGE.includes(currentUserProjectRole);
}
+ /**
+ * @description check if there are any active filters applied
+ */
+ get hasActiveFilters() {
+ // Check if search query is active
+ if (this.filters.searchQuery && this.filters.searchQuery.trim().length > 0) {
+ return true;
+ }
+
+ // Check if any filters are applied
+ if (this.filters.filters) {
+ const filterValues = Object.values(this.filters.filters);
+ // Check if any filter has a non-empty value
+ return filterValues.some((value) => {
+ if (Array.isArray(value)) {
+ return value.length > 0;
+ }
+ if (typeof value === "boolean") {
+ return value === true;
+ }
+ return value !== null && value !== undefined;
+ });
+ }
+
+ return false;
+ }
+
/**
* @description get the current project page ids based on the pageType
* @param {TPageNavigationTabs} pageType
@@ -162,19 +240,17 @@ export class ProjectPageStore implements IProjectPageStore {
const { projectId } = this.store.router;
if (!projectId) return undefined;
- // helps to filter pages based on the pageType
- const pagesByType = filterPagesByPageType(pageType, Object.values(this?.data || {}));
- let filteredPages = pagesByType.filter(
- (p) =>
- p.project_ids?.includes(projectId) &&
- getPageName(p.name).toLowerCase().includes(this.filters.searchQuery.toLowerCase()) &&
- shouldFilterPage(p, this.filters.filters)
- );
- filteredPages = orderPages(filteredPages, this.filters.sortKey, this.filters.sortBy);
-
- const pages = (filteredPages.map((page) => page.id) as string[]) || undefined;
-
- return pages ?? undefined;
+ // Return filtered page IDs from the filtered arrays
+ switch (pageType) {
+ case "public":
+ return this.filteredPublicPageIds;
+ case "private":
+ return this.filteredPrivatePageIds;
+ case "archived":
+ return this.filteredArchivedPageIds;
+ default:
+ return [];
+ }
});
/**
@@ -198,26 +274,148 @@ export class ProjectPageStore implements IProjectPageStore {
});
/**
- * @description fetch all the pages
+ * @description get pagination info for a page type
+ * @param {TPageNavigationTabs} pageType
*/
- fetchPagesList = async (workspaceSlug: string, projectId: string, pageType?: TPageNavigationTabs) => {
+ getPaginationInfo = computedFn((pageType: TPageNavigationTabs): TPagePaginationInfo => {
+ return this.paginationInfo[pageType] ?? DEFAULT_PAGINATION_INFO;
+ });
+
+ /**
+ * @description get pagination loader for a page type
+ * @param {TPageNavigationTabs} pageType
+ */
+ getPaginationLoader = computedFn((pageType: TPageNavigationTabs): TPaginationLoader => {
+ return this.paginationLoader[pageType];
+ });
+
+ /**
+ * @description fetch all the pages with pagination support
+ * @param {string} workspaceSlug
+ * @param {string} projectId
+ * @param {TPageNavigationTabs} pageType - The type of pages to fetch
+ * @param {string} [cursor] - Optional cursor for pagination. If not provided, fetches first page
+ */
+ fetchPagesList = async (workspaceSlug: string, projectId: string, pageType: TPageNavigationTabs, cursor?: string) => {
try {
if (!workspaceSlug || !projectId) return undefined;
- const currentPageIds = pageType ? this.getCurrentProjectPageIdsByTab(pageType) : undefined;
+ const pageNavigationTab = pageType as TPageNavigationTabs;
+ const isFirstPage = !cursor;
+
+ // For next page fetches, validate pagination state
+ if (!isFirstPage) {
+ const paginationInfo = this.paginationInfo[pageNavigationTab];
+ // Don't fetch if there's no next page
+ if (!paginationInfo?.hasNextPage || !paginationInfo?.nextCursor) {
+ return undefined;
+ }
+
+ // Don't fetch if already loading
+ const loader = this.paginationLoader[pageNavigationTab];
+ if (loader === "pagination") {
+ return undefined;
+ }
+ }
+
+ // Build query parameters
+ const queries: Record = {
+ per_page: PAGES_PER_PAGE,
+ type: pageType, // Send page type to backend for filtering
+ };
+
+ // Add search query if provided
+ if (this.filters.searchQuery) {
+ queries.search = this.filters.searchQuery;
+ }
+
+ // Add sorting parameters
+ // Convert sortKey and sortBy to Django order_by format
+ if (this.filters.sortKey && this.filters.sortBy) {
+ const orderByField = this.filters.sortBy === "desc" ? `-${this.filters.sortKey}` : this.filters.sortKey;
+ queries.order_by = orderByField;
+ }
+
+ // Add cursor if provided
+ if (!isFirstPage) {
+ const nextCursor = this.paginationInfo[pageNavigationTab].nextCursor;
+ if (nextCursor) {
+ queries.cursor = nextCursor;
+ }
+ }
+
+ // Add filter parameters from store filters
+ const storeFilters = this.filters.filters || {};
+ if (storeFilters.favorites) {
+ queries.favorites = "true";
+ }
+ if (storeFilters.created_by && Array.isArray(storeFilters.created_by) && storeFilters.created_by.length > 0) {
+ queries.created_by = storeFilters.created_by.join(",");
+ }
+ if (storeFilters.created_at && Array.isArray(storeFilters.created_at) && storeFilters.created_at.length > 0) {
+ queries.created_at = storeFilters.created_at.join(",");
+ }
+
+ // Set appropriate loader and clear store on first page
runInAction(() => {
- this.loader = currentPageIds && currentPageIds.length > 0 ? `mutation-loader` : `init-loader`;
+ if (isFirstPage) {
+ const currentPageIds = this.getCurrentProjectPageIdsByTab(pageType);
+ this.loader = currentPageIds && currentPageIds.length > 0 ? `mutation-loader` : `init-loader`;
+
+ // Reset pagination info when fetching first page
+ this.paginationInfo[pageNavigationTab] = { ...DEFAULT_PAGINATION_INFO };
+
+ // Clear filtered arrays immediately
+ switch (pageNavigationTab) {
+ case "public":
+ this.filteredPublicPageIds = [];
+ break;
+ case "private":
+ this.filteredPrivatePageIds = [];
+ break;
+ case "archived":
+ this.filteredArchivedPageIds = [];
+ break;
+ }
+ } else {
+ // Set pagination loader
+ this.paginationLoader[pageNavigationTab] = "pagination";
+ }
this.error = undefined;
});
- const pages = await this.service.fetchAll(workspaceSlug, projectId);
+ const response = await this.service.fetchAll(workspaceSlug, projectId, queries);
+
+ // Parse response
+ let pages: TPage[] = [];
+ let paginationData: TPagePaginationInfo = {
+ nextCursor: null,
+ hasNextPage: false,
+ totalResults: 0,
+ };
+
+ if (Array.isArray(response)) {
+ pages = response;
+ paginationData.totalResults = response.length;
+ } else {
+ pages = response.results || [];
+ paginationData = {
+ nextCursor: response.next_cursor ?? null,
+ hasNextPage: response.next_page_results ?? false,
+ totalResults: response.total_results ?? 0,
+ };
+ }
+
runInAction(() => {
+ // Get page IDs from response
+ const responsePageIds = pages.map((p) => p.id).filter((id): id is string => id !== undefined);
+
+ // Add/update pages in store
for (const page of pages) {
if (page?.id) {
const existingPage = this.getPageById(page.id);
if (existingPage) {
// If page already exists, update all fields except name
-
const { name, ...otherFields } = page;
existingPage.mutateProperties(otherFields, false);
} else {
@@ -226,16 +424,54 @@ export class ProjectPageStore implements IProjectPageStore {
}
}
}
- this.loader = undefined;
+
+ // Populate filtered arrays
+ switch (pageNavigationTab) {
+ case "public":
+ this.filteredPublicPageIds = isFirstPage
+ ? responsePageIds
+ : Array.from(new Set([...this.filteredPublicPageIds, ...responsePageIds]));
+ break;
+ case "private":
+ this.filteredPrivatePageIds = isFirstPage
+ ? responsePageIds
+ : Array.from(new Set([...this.filteredPrivatePageIds, ...responsePageIds]));
+ break;
+ case "archived":
+ this.filteredArchivedPageIds = isFirstPage
+ ? responsePageIds
+ : Array.from(new Set([...this.filteredArchivedPageIds, ...responsePageIds]));
+ break;
+ }
+
+ // Update pagination info
+ this.paginationInfo[pageNavigationTab] = paginationData;
+
+ // Clear appropriate loader
+ if (isFirstPage) {
+ this.loader = undefined;
+ } else {
+ this.paginationLoader[pageNavigationTab] = undefined;
+ }
});
return pages;
} catch (error) {
runInAction(() => {
- this.loader = undefined;
+ const pageNavigationTab = pageType as TPageNavigationTabs;
+ const isFirstPage = !cursor;
+
+ if (isFirstPage) {
+ this.loader = undefined;
+ } else {
+ this.paginationLoader[pageNavigationTab] = undefined;
+ }
+
this.error = {
title: "Failed",
- description: "Failed to fetch the pages, Please try again later.",
+ description: isFirstPage
+ ? "Failed to fetch the pages, Please try again later."
+ : "Failed to fetch more pages, Please try again later.",
};
});
throw error;