feat(client): migrate pagination from offset to cursor-based

This commit is contained in:
Philipinho
2026-01-23 21:58:07 +00:00
parent a40b90d0d9
commit 20aa40a9fd
24 changed files with 211 additions and 164 deletions
@@ -2,17 +2,17 @@ import { Button, Group } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export interface PagePaginationProps { export interface PagePaginationProps {
currentPage: number;
hasPrevPage: boolean; hasPrevPage: boolean;
hasNextPage: boolean; hasNextPage: boolean;
onPageChange: (newPage: number) => void; onPrev: () => void;
onNext: () => void;
} }
export default function Paginate({ export default function Paginate({
currentPage,
hasPrevPage, hasPrevPage,
hasNextPage, hasNextPage,
onPageChange, onPrev,
onNext,
}: PagePaginationProps) { }: PagePaginationProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -25,7 +25,7 @@ export default function Paginate({
<Button <Button
variant="default" variant="default"
size="compact-sm" size="compact-sm"
onClick={() => onPageChange(currentPage - 1)} onClick={onPrev}
disabled={!hasPrevPage} disabled={!hasPrevPage}
> >
{t("Prev")} {t("Prev")}
@@ -34,7 +34,7 @@ export default function Paginate({
<Button <Button
variant="default" variant="default"
size="compact-sm" size="compact-sm"
onClick={() => onPageChange(currentPage + 1)} onClick={onNext}
disabled={!hasNextPage} disabled={!hasNextPage}
> >
{t("Next")} {t("Next")}
@@ -13,7 +13,7 @@ import { getShares } from "@/features/share/services/share-service.ts";
import { getApiKeys } from "@/ee/api-key"; import { getApiKeys } from "@/ee/api-key";
export const prefetchWorkspaceMembers = () => { export const prefetchWorkspaceMembers = () => {
const params = { limit: 100, page: 1, query: "" } as QueryParams; const params: QueryParams = { limit: 100, query: "" };
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["workspaceMembers", params], queryKey: ["workspaceMembers", params],
queryFn: () => getWorkspaceMembers(params), queryFn: () => getWorkspaceMembers(params),
@@ -22,15 +22,15 @@ export const prefetchWorkspaceMembers = () => {
export const prefetchSpaces = () => { export const prefetchSpaces = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["spaces", { page: 1 }], queryKey: ["spaces", {}],
queryFn: () => getSpaces({ page: 1 }), queryFn: () => getSpaces({}),
}); });
}; };
export const prefetchGroups = () => { export const prefetchGroups = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["groups", { page: 1 }], queryKey: ["groups", {}],
queryFn: () => getGroups({ page: 1 }), queryFn: () => getGroups({}),
}); });
}; };
@@ -62,21 +62,21 @@ export const prefetchSsoProviders = () => {
export const prefetchShares = () => { export const prefetchShares = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["share-list", { page: 1 }], queryKey: ["share-list", {}],
queryFn: () => getShares({ page: 1, limit: 100 }), queryFn: () => getShares({}),
}); });
}; };
export const prefetchApiKeys = () => { export const prefetchApiKeys = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["api-key-list", { page: 1 }], queryKey: ["api-key-list", {}],
queryFn: () => getApiKeys({ page: 1 }), queryFn: () => getApiKeys({}),
}); });
}; };
export const prefetchApiKeyManagement = () => { export const prefetchApiKeyManagement = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["api-key-list", { page: 1 }], queryKey: ["api-key-list", { adminView: true }],
queryFn: () => getApiKeys({ page: 1, adminView: true }), queryFn: () => getApiKeys({ adminView: true }),
}); });
}; };
@@ -10,19 +10,19 @@ import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-moda
import { UpdateApiKeyModal } from "@/ee/api-key/components/update-api-key-modal"; import { UpdateApiKeyModal } from "@/ee/api-key/components/update-api-key-modal";
import { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal"; import { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal";
import Paginate from "@/components/common/paginate"; import Paginate from "@/components/common/paginate";
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts"; import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts";
import { IApiKey } from "@/ee/api-key"; import { IApiKey } from "@/ee/api-key";
export default function UserApiKeys() { export default function UserApiKeys() {
const { t } = useTranslation(); const { t } = useTranslation();
const { page, setPage } = usePaginateAndSearch(); const { cursor, goNext, goPrev } = useCursorPaginate();
const [createModalOpened, setCreateModalOpened] = useState(false); const [createModalOpened, setCreateModalOpened] = useState(false);
const [createdApiKey, setCreatedApiKey] = useState<IApiKey | null>(null); const [createdApiKey, setCreatedApiKey] = useState<IApiKey | null>(null);
const [updateModalOpened, setUpdateModalOpened] = useState(false); const [updateModalOpened, setUpdateModalOpened] = useState(false);
const [revokeModalOpened, setRevokeModalOpened] = useState(false); const [revokeModalOpened, setRevokeModalOpened] = useState(false);
const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null); const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null);
const { data, isLoading } = useGetApiKeysQuery({ page }); const { data, isLoading } = useGetApiKeysQuery({ cursor });
const handleCreateSuccess = (response: IApiKey) => { const handleCreateSuccess = (response: IApiKey) => {
setCreatedApiKey(response); setCreatedApiKey(response);
@@ -65,10 +65,10 @@ export default function UserApiKeys() {
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
@@ -10,20 +10,20 @@ import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-moda
import { UpdateApiKeyModal } from "@/ee/api-key/components/update-api-key-modal"; import { UpdateApiKeyModal } from "@/ee/api-key/components/update-api-key-modal";
import { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal"; import { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal";
import Paginate from "@/components/common/paginate"; import Paginate from "@/components/common/paginate";
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts"; import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts";
import { IApiKey } from "@/ee/api-key"; import { IApiKey } from "@/ee/api-key";
import useUserRole from '@/hooks/use-user-role.tsx'; import useUserRole from '@/hooks/use-user-role.tsx';
export default function WorkspaceApiKeys() { export default function WorkspaceApiKeys() {
const { t } = useTranslation(); const { t } = useTranslation();
const { page, setPage } = usePaginateAndSearch(); const { cursor, goNext, goPrev } = useCursorPaginate();
const [createModalOpened, setCreateModalOpened] = useState(false); const [createModalOpened, setCreateModalOpened] = useState(false);
const [createdApiKey, setCreatedApiKey] = useState<IApiKey | null>(null); const [createdApiKey, setCreatedApiKey] = useState<IApiKey | null>(null);
const [updateModalOpened, setUpdateModalOpened] = useState(false); const [updateModalOpened, setUpdateModalOpened] = useState(false);
const [revokeModalOpened, setRevokeModalOpened] = useState(false); const [revokeModalOpened, setRevokeModalOpened] = useState(false);
const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null); const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null);
const { data, isLoading } = useGetApiKeysQuery({ page, adminView: true }); const { data, isLoading } = useGetApiKeysQuery({ cursor, adminView: true });
const { isAdmin } = useUserRole(); const { isAdmin } = useUserRole();
if (!isAdmin) { if (!isAdmin) {
@@ -76,10 +76,10 @@ export default function WorkspaceApiKeys() {
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
@@ -43,7 +43,7 @@ export default function SsoProviderList() {
return null; return null;
} }
if (data?.length === 0) { if (data?.items.length === 0) {
return <Text c="dimmed">{t("No SSO providers found.")}</Text>; return <Text c="dimmed">{t("No SSO providers found.")}</Text>;
} }
@@ -81,7 +81,7 @@ export default function SsoProviderList() {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{data {data?.items
.sort((a, b) => { .sort((a, b) => {
const enabledDiff = Number(b.isEnabled) - Number(a.isEnabled); const enabledDiff = Number(b.isEnabled) - Number(a.isEnabled);
if (enabledDiff !== 0) return enabledDiff; if (enabledDiff !== 0) return enabledDiff;
@@ -104,7 +104,11 @@ export default function SsoProviderList() {
</Group> </Group>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Badge color={"gray"} variant="light" style={{ whiteSpace: "nowrap" }}> <Badge
color={"gray"}
variant="light"
style={{ whiteSpace: "nowrap" }}
>
{provider.type.toUpperCase()} {provider.type.toUpperCase()}
</Badge> </Badge>
</Table.Td> </Table.Td>
@@ -134,41 +138,41 @@ export default function SsoProviderList() {
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Group gap="xs" wrap="nowrap"> <Group gap="xs" wrap="nowrap">
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
color="gray" color="gray"
onClick={() => handleEdit(provider)} onClick={() => handleEdit(provider)}
> >
<IconPencil size={16} /> <IconPencil size={16} />
</ActionIcon> </ActionIcon>
<Menu <Menu
transitionProps={{ transition: "pop" }} transitionProps={{ transition: "pop" }}
withArrow withArrow
position="bottom-end" position="bottom-end"
withinPortal withinPortal
> >
<Menu.Target> <Menu.Target>
<ActionIcon variant="subtle" color="gray"> <ActionIcon variant="subtle" color="gray">
<IconDots size={16} /> <IconDots size={16} />
</ActionIcon> </ActionIcon>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item <Menu.Item
onClick={() => handleEdit(provider)} onClick={() => handleEdit(provider)}
leftSection={<IconPencil size={16} />} leftSection={<IconPencil size={16} />}
> >
{t("Edit")} {t("Edit")}
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
onClick={() => openDeleteModal(provider.id)} onClick={() => openDeleteModal(provider.id)}
leftSection={<IconTrash size={16} />} leftSection={<IconTrash size={16} />}
color="red" color="red"
disabled={provider.type === SSO_PROVIDER.GOOGLE} disabled={provider.type === SSO_PROVIDER.GOOGLE}
> >
{t("Delete")} {t("Delete")}
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
</Group> </Group>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
@@ -13,8 +13,9 @@ import {
} from "@/ee/security/services/security-service.ts"; } from "@/ee/security/services/security-service.ts";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { IAuthProvider } from "@/ee/security/types/security.types.ts"; import { IAuthProvider } from "@/ee/security/types/security.types.ts";
import { IPagination } from "@/lib/types.ts";
export function useGetSsoProviders(): UseQueryResult<IAuthProvider[], Error> { export function useGetSsoProviders(): UseQueryResult<IPagination<IAuthProvider>, Error> {
return useQuery({ return useQuery({
queryKey: ["sso-providers"], queryKey: ["sso-providers"],
queryFn: () => getSsoProviders(), queryFn: () => getSsoProviders(),
@@ -1,5 +1,6 @@
import api from "@/lib/api-client.ts"; import api from "@/lib/api-client.ts";
import { IAuthProvider } from "@/ee/security/types/security.types.ts"; import { IAuthProvider } from "@/ee/security/types/security.types.ts";
import { IPagination } from "@/lib/types.ts";
export async function getSsoProviderById(data: { export async function getSsoProviderById(data: {
providerId: string; providerId: string;
@@ -8,8 +9,8 @@ export async function getSsoProviderById(data: {
return req.data; return req.data;
} }
export async function getSsoProviders(): Promise<IAuthProvider[]> { export async function getSsoProviders(): Promise<IPagination<IAuthProvider>> {
const req = await api.post<IAuthProvider[]>("/sso/providers"); const req = await api.post<IPagination<IAuthProvider>>("/sso/providers");
return req.data; return req.data;
} }
@@ -1,26 +1,25 @@
import { Table, Group, Text, Anchor } from "@mantine/core"; import { Table, Group, Text, Anchor } from "@mantine/core";
import { useGetGroupsQuery } from "@/features/group/queries/group-query"; import { useGetGroupsQuery } from "@/features/group/queries/group-query";
import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx"; import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { formatMemberCount } from "@/lib"; import { formatMemberCount } from "@/lib";
import { IGroup } from "@/features/group/types/group.types.ts"; import { IGroup } from "@/features/group/types/group.types.ts";
import Paginate from "@/components/common/paginate.tsx"; import Paginate from "@/components/common/paginate.tsx";
import { queryClient } from "@/main.tsx"; import { queryClient } from "@/main.tsx";
import { getSpaces } from "@/features/space/services/space-service.ts";
import { getGroupMembers } from "@/features/group/services/group-service.ts"; import { getGroupMembers } from "@/features/group/services/group-service.ts";
import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx"; import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
export default function GroupList() { export default function GroupList() {
const { t } = useTranslation(); const { t } = useTranslation();
const [page, setPage] = useState(1); const { cursor, goNext, goPrev } = useCursorPaginate();
const { data, isLoading } = useGetGroupsQuery({ page }); const { data, isLoading } = useGetGroupsQuery({ cursor });
const prefetchGroupMembers = (groupId: string) => { const prefetchGroupMembers = (groupId: string) => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["groupMembers", groupId, { page: 1 }], queryKey: ["groupMembers", groupId, {}],
queryFn: () => getGroupMembers(groupId, { page: 1 }), queryFn: () => getGroupMembers(groupId, {}),
}); });
}; };
@@ -85,10 +84,10 @@ export default function GroupList() {
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
</> </>
@@ -4,7 +4,7 @@ import {
useRemoveGroupMemberMutation, useRemoveGroupMemberMutation,
} from "@/features/group/queries/group-query"; } from "@/features/group/queries/group-query";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import React, { useState } from "react"; import React from "react";
import { IconDots } from "@tabler/icons-react"; import { IconDots } from "@tabler/icons-react";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
@@ -12,12 +12,13 @@ import useUserRole from "@/hooks/use-user-role.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IUser } from "@/features/user/types/user.types.ts"; import { IUser } from "@/features/user/types/user.types.ts";
import Paginate from "@/components/common/paginate.tsx"; import Paginate from "@/components/common/paginate.tsx";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
export default function GroupMembersList() { export default function GroupMembersList() {
const { t } = useTranslation(); const { t } = useTranslation();
const { groupId } = useParams(); const { groupId } = useParams();
const [page, setPage] = useState(1); const { cursor, goNext, goPrev } = useCursorPaginate();
const { data, isLoading } = useGroupMembersQuery(groupId, { page }); const { data, isLoading } = useGroupMembersQuery(groupId, { cursor });
const removeGroupMember = useRemoveGroupMemberMutation(); const removeGroupMember = useRemoveGroupMemberMutation();
const { isAdmin } = useUserRole(); const { isAdmin } = useUserRole();
@@ -107,10 +108,10 @@ export default function GroupMembersList() {
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
</> </>
@@ -253,12 +253,10 @@ export function useGetSidebarPagesQuery(
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: ["sidebar-pages", data], queryKey: ["sidebar-pages", data],
enabled: !!data?.pageId || !!data?.spaceId, enabled: !!data?.pageId || !!data?.spaceId,
queryFn: ({ pageParam }) => getSidebarPages({ ...data, page: pageParam }), queryFn: ({ pageParam }) => getSidebarPages({ ...data, cursor: pageParam }),
initialPageParam: 1, initialPageParam: undefined,
getPreviousPageParam: (firstPage) =>
firstPage.meta.hasPrevPage ? firstPage.meta.page - 1 : undefined,
getNextPageParam: (lastPage) => getNextPageParam: (lastPage) =>
lastPage.meta.hasNextPage ? lastPage.meta.page + 1 : undefined, lastPage.meta?.nextCursor ?? undefined,
}); });
} }
@@ -266,13 +264,11 @@ export function useGetRootSidebarPagesQuery(data: SidebarPagesParams) {
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: ["root-sidebar-pages", data.spaceId], queryKey: ["root-sidebar-pages", data.spaceId],
queryFn: async ({ pageParam }) => { queryFn: async ({ pageParam }) => {
return getSidebarPages({ spaceId: data.spaceId, page: pageParam }); return getSidebarPages({ spaceId: data.spaceId, cursor: pageParam });
}, },
initialPageParam: 1, initialPageParam: undefined,
getPreviousPageParam: (firstPage) =>
firstPage.meta.hasPrevPage ? firstPage.meta.page - 1 : undefined,
getNextPageParam: (lastPage) => getNextPageParam: (lastPage) =>
lastPage.meta.hasNextPage ? lastPage.meta.page + 1 : undefined, lastPage.meta?.nextCursor ?? undefined,
}); });
} }
@@ -72,22 +72,19 @@ export async function getSidebarPages(
export async function getAllSidebarPages( export async function getAllSidebarPages(
params: SidebarPagesParams, params: SidebarPagesParams,
): Promise<InfiniteData<IPagination<IPage>, unknown>> { ): Promise<InfiniteData<IPagination<IPage>, unknown>> {
let page = 1; let cursor: string | undefined = undefined;
let hasNextPage = false;
const pages: IPagination<IPage>[] = []; const pages: IPagination<IPage>[] = [];
const pageParams: number[] = []; const pageParams: (string | undefined)[] = [];
do { do {
const req = await api.post("/pages/sidebar-pages", { ...params, page: page }); const req = await api.post("/pages/sidebar-pages", { ...params, cursor });
const data: IPagination<IPage> = req.data; const data: IPagination<IPage> = req.data;
pages.push(data); pages.push(data);
pageParams.push(page); pageParams.push(cursor);
hasNextPage = data.meta.hasNextPage; cursor = data.meta.nextCursor ?? undefined;
} while (cursor);
page += 1;
} while (hasNextPage);
return { return {
pageParams, pageParams,
@@ -30,15 +30,15 @@ import { useState } from "react";
import TrashPageContentModal from "@/features/page/trash/components/trash-page-content-modal"; import TrashPageContentModal from "@/features/page/trash/components/trash-page-content-modal";
import { UserInfo } from "@/components/common/user-info.tsx"; import { UserInfo } from "@/components/common/user-info.tsx";
import Paginate from "@/components/common/paginate.tsx"; import Paginate from "@/components/common/paginate.tsx";
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
export default function Trash() { export default function Trash() {
const { t } = useTranslation(); const { t } = useTranslation();
const { spaceSlug } = useParams(); const { spaceSlug } = useParams();
const { page, setPage } = usePaginateAndSearch(); const { cursor, goNext, goPrev } = useCursorPaginate();
const { data: space } = useGetSpaceBySlugQuery(spaceSlug); const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
const { data: deletedPages, isLoading } = useDeletedPagesQuery(space?.id, { const { data: deletedPages, isLoading } = useDeletedPagesQuery(space?.id, {
page, limit: 50 cursor, limit: 50
}); });
const restorePageMutation = useRestorePageMutation(); const restorePageMutation = useRestorePageMutation();
const deletePageMutation = useDeletePageMutation(); const deletePageMutation = useDeletePageMutation();
@@ -206,10 +206,10 @@ export default function Trash() {
{deletedPages && deletedPages.items.length > 0 && ( {deletedPages && deletedPages.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={deletedPages.meta?.hasPrevPage}
hasPrevPage={deletedPages.meta.hasPrevPage} hasNextPage={deletedPages.meta?.hasNextPage}
hasNextPage={deletedPages.meta.hasNextPage} onNext={() => goNext(deletedPages.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
</Stack> </Stack>
@@ -62,7 +62,7 @@ export interface ICopyPageToSpace {
export interface SidebarPagesParams { export interface SidebarPagesParams {
spaceId?: string; spaceId?: string;
pageId?: string; pageId?: string;
page?: number; // pagination cursor?: string;
} }
export interface IPageInput { export interface IPageInput {
@@ -1,8 +1,9 @@
import { Table, Group, Text, Anchor } from "@mantine/core"; import { Table, Group, Text, Anchor } from "@mantine/core";
import React, { useState } from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Paginate from "@/components/common/paginate.tsx"; import Paginate from "@/components/common/paginate.tsx";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import { useGetSharesQuery } from "@/features/share/queries/share-query.ts"; import { useGetSharesQuery } from "@/features/share/queries/share-query.ts";
import { ISharedItem } from "@/features/share/types/share.types.ts"; import { ISharedItem } from "@/features/share/types/share.types.ts";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -14,8 +15,8 @@ import classes from "./share.module.css";
export default function ShareList() { export default function ShareList() {
const { t } = useTranslation(); const { t } = useTranslation();
const [page, setPage] = useState(1); const { cursor, goNext, goPrev } = useCursorPaginate();
const { data, isLoading } = useGetSharesQuery({ page }); const { data, isLoading } = useGetSharesQuery({ cursor });
return ( return (
<> <>
@@ -86,10 +87,10 @@ export default function ShareList() {
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
</> </>
@@ -33,7 +33,7 @@ export function useGetSharesQuery(
params?: QueryParams, params?: QueryParams,
): UseQueryResult<IPagination<ISharedItem>, Error> { ): UseQueryResult<IPagination<ISharedItem>, Error> {
return useQuery({ return useQuery({
queryKey: ["share-list"], queryKey: ["share-list", params],
queryFn: () => getShares(params), queryFn: () => getShares(params),
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
}); });
@@ -1,5 +1,6 @@
import { Group, Table, Text } from "@mantine/core"; import { Group, Table, Text } from "@mantine/core";
import React, { useState } from "react"; import React, { useState } from "react";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts"; import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx"; import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
@@ -12,8 +13,8 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
export default function SpaceList() { export default function SpaceList() {
const { t } = useTranslation(); const { t } = useTranslation();
const [page, setPage] = useState(1); const { cursor, goNext, goPrev } = useCursorPaginate();
const { data, isLoading } = useGetSpacesQuery({ page }); const { data, isLoading } = useGetSpacesQuery({ cursor });
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null); const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
@@ -72,10 +73,10 @@ export default function SpaceList() {
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
@@ -41,9 +41,9 @@ export default function SpaceMembersList({
readOnly, readOnly,
}: SpaceMembersProps) { }: SpaceMembersProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { search, page, setPage, handleSearch } = usePaginateAndSearch(); const { search, cursor, goNext, goPrev, handleSearch } = usePaginateAndSearch();
const { data, isLoading } = useSpaceMembersQuery(spaceId, { const { data, isLoading } = useSpaceMembersQuery(spaceId, {
page, cursor,
limit: 100, limit: 100,
query: search, query: search,
}); });
@@ -206,10 +206,10 @@ export default function SpaceMembersList({
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
</> </>
@@ -28,19 +28,19 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
interface AllSpacesListProps { interface AllSpacesListProps {
spaces: any[]; spaces: any[];
onSearch: (query: string) => void; onSearch: (query: string) => void;
page: number;
hasPrevPage?: boolean; hasPrevPage?: boolean;
hasNextPage?: boolean; hasNextPage?: boolean;
onPageChange: (page: number) => void; onNext: () => void;
onPrev: () => void;
} }
export default function AllSpacesList({ export default function AllSpacesList({
spaces, spaces,
onSearch, onSearch,
page,
hasPrevPage, hasPrevPage,
hasNextPage, hasNextPage,
onPageChange, onNext,
onPrev,
}: AllSpacesListProps) { }: AllSpacesListProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [settingsOpened, { open: openSettings, close: closeSettings }] = const [settingsOpened, { open: openSettings, close: closeSettings }] =
@@ -145,10 +145,10 @@ export default function AllSpacesList({
{spaces.length > 0 && ( {spaces.length > 0 && (
<Paginate <Paginate
currentPage={page}
hasPrevPage={hasPrevPage} hasPrevPage={hasPrevPage}
hasNextPage={hasNextPage} hasNextPage={hasNextPage}
onPageChange={onPageChange} onNext={onNext}
onPrev={onPrev}
/> />
)} )}
@@ -1,6 +1,6 @@
import { Group, Table, Avatar, Text, Alert } from "@mantine/core"; import { Group, Table, Avatar, Text, Alert } from "@mantine/core";
import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts"; import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts";
import React, { useState } from "react"; import React from "react";
import { getUserRoleLabel } from "@/features/workspace/types/user-role-data.ts"; import { getUserRoleLabel } from "@/features/workspace/types/user-role-data.ts";
import InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx"; import InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx";
import { IconInfoCircle } from "@tabler/icons-react"; import { IconInfoCircle } from "@tabler/icons-react";
@@ -8,12 +8,13 @@ import { timeAgo } from "@/lib/time.ts";
import useUserRole from "@/hooks/use-user-role.tsx"; import useUserRole from "@/hooks/use-user-role.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Paginate from "@/components/common/paginate.tsx"; import Paginate from "@/components/common/paginate.tsx";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
export default function WorkspaceInvitesTable() { export default function WorkspaceInvitesTable() {
const { t } = useTranslation(); const { t } = useTranslation();
const [page, setPage] = useState(1); const { cursor, goNext, goPrev } = useCursorPaginate();
const { data, isLoading } = useWorkspaceInvitationsQuery({ const { data, isLoading } = useWorkspaceInvitationsQuery({
page, cursor,
limit: 100, limit: 100,
}); });
const { isAdmin } = useUserRole(); const { isAdmin } = useUserRole();
@@ -65,10 +66,10 @@ export default function WorkspaceInvitesTable() {
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
</> </>
@@ -21,9 +21,9 @@ import MemberActionMenu from "@/features/workspace/components/members/components
export default function WorkspaceMembersTable() { export default function WorkspaceMembersTable() {
const { t } = useTranslation(); const { t } = useTranslation();
const { search, page, setPage, handleSearch } = usePaginateAndSearch(); const { search, cursor, goNext, goPrev, handleSearch } = usePaginateAndSearch();
const { data, isLoading } = useWorkspaceMembersQuery({ const { data, isLoading } = useWorkspaceMembersQuery({
page, cursor,
limit: 100, limit: 100,
query: search, query: search,
}); });
@@ -111,10 +111,10 @@ export default function WorkspaceMembersTable() {
{data?.items.length > 0 && ( {data?.items.length > 0 && (
<Paginate <Paginate
currentPage={page} hasPrevPage={data?.meta?.hasPrevPage}
hasPrevPage={data?.meta.hasPrevPage} hasNextPage={data?.meta?.hasNextPage}
hasNextPage={data?.meta.hasNextPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPageChange={setPage} onPrev={goPrev}
/> />
)} )}
</> </>
@@ -0,0 +1,28 @@
import { useState, useCallback } from "react";
export function useCursorPaginate() {
const [cursor, setCursor] = useState<string | undefined>(undefined);
const [cursorStack, setCursorStack] = useState<(string | undefined)[]>([]);
const goNext = useCallback((nextCursor: string | null | undefined) => {
if (nextCursor) {
setCursorStack((prev) => [...prev, cursor]);
setCursor(nextCursor);
}
}, [cursor]);
const goPrev = useCallback(() => {
setCursorStack((prev) => {
const next = prev.slice(0, -1);
setCursor(prev[prev.length - 1]);
return next;
});
}, []);
const resetCursor = useCallback(() => {
setCursor(undefined);
setCursorStack([]);
}, []);
return { cursor, goNext, goPrev, resetCursor };
}
@@ -2,16 +2,33 @@ import { useState, useRef, useCallback } from "react";
export function usePaginateAndSearch(initialQuery: string = "") { export function usePaginateAndSearch(initialQuery: string = "") {
const [search, setSearch] = useState(initialQuery); const [search, setSearch] = useState(initialQuery);
const [page, setPage] = useState(1); const [cursor, setCursor] = useState<string | undefined>(undefined);
const [cursorStack, setCursorStack] = useState<(string | undefined)[]>([]);
const prevSearchRef = useRef(search); const prevSearchRef = useRef(search);
const handleSearch = useCallback((newQuery: string) => { const handleSearch = useCallback((newQuery: string) => {
if (prevSearchRef.current !== newQuery) { if (prevSearchRef.current !== newQuery) {
prevSearchRef.current = newQuery; prevSearchRef.current = newQuery;
setSearch(newQuery); setSearch(newQuery);
setPage(1); setCursor(undefined);
setCursorStack([]);
} }
}, []); }, []);
return { search, page, setPage, handleSearch }; const goNext = useCallback((nextCursor: string | null | undefined) => {
if (nextCursor) {
setCursorStack((prev) => [...prev, cursor]);
setCursor(nextCursor);
}
}, [cursor]);
const goPrev = useCallback(() => {
setCursorStack((prev) => {
const next = prev.slice(0, -1);
setCursor(prev[prev.length - 1]);
return next;
});
}, []);
return { search, cursor, goNext, goPrev, handleSearch };
} }
+2 -2
View File
@@ -1,6 +1,6 @@
export interface QueryParams { export interface QueryParams {
query?: string; query?: string;
page?: number; cursor?: string;
limit?: number; limit?: number;
adminView?: boolean; adminView?: boolean;
} }
@@ -29,9 +29,9 @@ export interface ApiResponse<T> {
export type IPaginationMeta = { export type IPaginationMeta = {
limit: number; limit: number;
page: number;
hasNextPage: boolean; hasNextPage: boolean;
hasPrevPage: boolean; hasPrevPage: boolean;
nextCursor: string | null;
}; };
export type IPagination<T> = { export type IPagination<T> = {
items: T[]; items: T[];
+4 -4
View File
@@ -11,10 +11,10 @@ import useUserRole from "@/hooks/use-user-role";
export default function Spaces() { export default function Spaces() {
const { t } = useTranslation(); const { t } = useTranslation();
const { isAdmin } = useUserRole(); const { isAdmin } = useUserRole();
const { search, page, setPage, handleSearch } = usePaginateAndSearch(); const { search, cursor, goNext, goPrev, handleSearch } = usePaginateAndSearch();
const { data, isLoading } = useGetSpacesQuery({ const { data, isLoading } = useGetSpacesQuery({
page, cursor,
limit: 30, limit: 30,
query: search, query: search,
}); });
@@ -41,10 +41,10 @@ export default function Spaces() {
<AllSpacesList <AllSpacesList
spaces={data?.items || []} spaces={data?.items || []}
onSearch={handleSearch} onSearch={handleSearch}
page={page}
hasPrevPage={data?.meta?.hasPrevPage} hasPrevPage={data?.meta?.hasPrevPage}
hasNextPage={data?.meta?.hasNextPage} hasNextPage={data?.meta?.hasNextPage}
onPageChange={setPage} onNext={() => goNext(data?.meta?.nextCursor)}
onPrev={goPrev}
/> />
</Box> </Box>
</Container> </Container>