mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat: switch to cursor pagination (#1884)
* add cursor pagination function * support custom order modifier * refactor returned object * feat(db): migrate paginated endpoints to cursor-based pagination * sync * support hasPrevPage boolean * feat(client): migrate pagination from offset to cursor-based * support beforeCursor/prevCursor * wrap search results in items array for API consistency
This commit is contained in:
@@ -2,17 +2,17 @@ import { Button, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface PagePaginationProps {
|
||||
currentPage: number;
|
||||
hasPrevPage: boolean;
|
||||
hasNextPage: boolean;
|
||||
onPageChange: (newPage: number) => void;
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
export default function Paginate({
|
||||
currentPage,
|
||||
hasPrevPage,
|
||||
hasNextPage,
|
||||
onPageChange,
|
||||
onPrev,
|
||||
onNext,
|
||||
}: PagePaginationProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function Paginate({
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
onClick={onPrev}
|
||||
disabled={!hasPrevPage}
|
||||
>
|
||||
{t("Prev")}
|
||||
@@ -34,7 +34,7 @@ export default function Paginate({
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
onClick={onNext}
|
||||
disabled={!hasNextPage}
|
||||
>
|
||||
{t("Next")}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getShares } from "@/features/share/services/share-service.ts";
|
||||
import { getApiKeys } from "@/ee/api-key";
|
||||
|
||||
export const prefetchWorkspaceMembers = () => {
|
||||
const params = { limit: 100, page: 1, query: "" } as QueryParams;
|
||||
const params: QueryParams = { limit: 100, query: "" };
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["workspaceMembers", params],
|
||||
queryFn: () => getWorkspaceMembers(params),
|
||||
@@ -22,15 +22,15 @@ export const prefetchWorkspaceMembers = () => {
|
||||
|
||||
export const prefetchSpaces = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["spaces", { page: 1 }],
|
||||
queryFn: () => getSpaces({ page: 1 }),
|
||||
queryKey: ["spaces", {}],
|
||||
queryFn: () => getSpaces({}),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchGroups = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["groups", { page: 1 }],
|
||||
queryFn: () => getGroups({ page: 1 }),
|
||||
queryKey: ["groups", {}],
|
||||
queryFn: () => getGroups({}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -62,21 +62,21 @@ export const prefetchSsoProviders = () => {
|
||||
|
||||
export const prefetchShares = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["share-list", { page: 1 }],
|
||||
queryFn: () => getShares({ page: 1, limit: 100 }),
|
||||
queryKey: ["share-list", {}],
|
||||
queryFn: () => getShares({}),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchApiKeys = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["api-key-list", { page: 1 }],
|
||||
queryFn: () => getApiKeys({ page: 1 }),
|
||||
queryKey: ["api-key-list", {}],
|
||||
queryFn: () => getApiKeys({}),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchApiKeyManagement = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["api-key-list", { page: 1 }],
|
||||
queryFn: () => getApiKeys({ page: 1, adminView: true }),
|
||||
queryKey: ["api-key-list", { 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 { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal";
|
||||
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 { IApiKey } from "@/ee/api-key";
|
||||
|
||||
export default function UserApiKeys() {
|
||||
const { t } = useTranslation();
|
||||
const { page, setPage } = usePaginateAndSearch();
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const [createModalOpened, setCreateModalOpened] = useState(false);
|
||||
const [createdApiKey, setCreatedApiKey] = useState<IApiKey | null>(null);
|
||||
const [updateModalOpened, setUpdateModalOpened] = useState(false);
|
||||
const [revokeModalOpened, setRevokeModalOpened] = useState(false);
|
||||
const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null);
|
||||
const { data, isLoading } = useGetApiKeysQuery({ page });
|
||||
const { data, isLoading } = useGetApiKeysQuery({ cursor });
|
||||
|
||||
const handleCreateSuccess = (response: IApiKey) => {
|
||||
setCreatedApiKey(response);
|
||||
@@ -65,10 +65,10 @@ export default function UserApiKeys() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
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 { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal";
|
||||
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 { IApiKey } from "@/ee/api-key";
|
||||
import useUserRole from '@/hooks/use-user-role.tsx';
|
||||
|
||||
export default function WorkspaceApiKeys() {
|
||||
const { t } = useTranslation();
|
||||
const { page, setPage } = usePaginateAndSearch();
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const [createModalOpened, setCreateModalOpened] = useState(false);
|
||||
const [createdApiKey, setCreatedApiKey] = useState<IApiKey | null>(null);
|
||||
const [updateModalOpened, setUpdateModalOpened] = useState(false);
|
||||
const [revokeModalOpened, setRevokeModalOpened] = useState(false);
|
||||
const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null);
|
||||
const { data, isLoading } = useGetApiKeysQuery({ page, adminView: true });
|
||||
const { data, isLoading } = useGetApiKeysQuery({ cursor, adminView: true });
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
if (!isAdmin) {
|
||||
@@ -76,10 +76,10 @@ export default function WorkspaceApiKeys() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function SsoProviderList() {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data?.length === 0) {
|
||||
if (data?.items.length === 0) {
|
||||
return <Text c="dimmed">{t("No SSO providers found.")}</Text>;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function SsoProviderList() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data
|
||||
{data?.items
|
||||
.sort((a, b) => {
|
||||
const enabledDiff = Number(b.isEnabled) - Number(a.isEnabled);
|
||||
if (enabledDiff !== 0) return enabledDiff;
|
||||
@@ -104,7 +104,11 @@ export default function SsoProviderList() {
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={"gray"} variant="light" style={{ whiteSpace: "nowrap" }}>
|
||||
<Badge
|
||||
color={"gray"}
|
||||
variant="light"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{provider.type.toUpperCase()}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
@@ -134,41 +138,41 @@ export default function SsoProviderList() {
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => handleEdit(provider)}
|
||||
>
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
<Menu
|
||||
transitionProps={{ transition: "pop" }}
|
||||
withArrow
|
||||
position="bottom-end"
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => handleEdit(provider)}
|
||||
leftSection={<IconPencil size={16} />}
|
||||
>
|
||||
{t("Edit")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => openDeleteModal(provider.id)}
|
||||
leftSection={<IconTrash size={16} />}
|
||||
color="red"
|
||||
disabled={provider.type === SSO_PROVIDER.GOOGLE}
|
||||
>
|
||||
{t("Delete")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => handleEdit(provider)}
|
||||
>
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
<Menu
|
||||
transitionProps={{ transition: "pop" }}
|
||||
withArrow
|
||||
position="bottom-end"
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => handleEdit(provider)}
|
||||
leftSection={<IconPencil size={16} />}
|
||||
>
|
||||
{t("Edit")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => openDeleteModal(provider.id)}
|
||||
leftSection={<IconTrash size={16} />}
|
||||
color="red"
|
||||
disabled={provider.type === SSO_PROVIDER.GOOGLE}
|
||||
>
|
||||
{t("Delete")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
} from "@/ee/security/services/security-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
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({
|
||||
queryKey: ["sso-providers"],
|
||||
queryFn: () => getSsoProviders(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import api from "@/lib/api-client.ts";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
|
||||
export async function getSsoProviderById(data: {
|
||||
providerId: string;
|
||||
@@ -8,8 +9,8 @@ export async function getSsoProviderById(data: {
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getSsoProviders(): Promise<IAuthProvider[]> {
|
||||
const req = await api.post<IAuthProvider[]>("/sso/providers");
|
||||
export async function getSsoProviders(): Promise<IPagination<IAuthProvider>> {
|
||||
const req = await api.post<IPagination<IAuthProvider>>("/sso/providers");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import { Table, Group, Text, Anchor } from "@mantine/core";
|
||||
import { useGetGroupsQuery } from "@/features/group/queries/group-query";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import { IGroup } from "@/features/group/types/group.types.ts";
|
||||
import Paginate from "@/components/common/paginate.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 { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
|
||||
|
||||
export default function GroupList() {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(1);
|
||||
const { data, isLoading } = useGetGroupsQuery({ page });
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { data, isLoading } = useGetGroupsQuery({ cursor });
|
||||
|
||||
const prefetchGroupMembers = (groupId: string) => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["groupMembers", groupId, { page: 1 }],
|
||||
queryFn: () => getGroupMembers(groupId, { page: 1 }),
|
||||
queryKey: ["groupMembers", groupId, {}],
|
||||
queryFn: () => getGroupMembers(groupId, {}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -85,10 +84,10 @@ export default function GroupList() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
useRemoveGroupMemberMutation,
|
||||
} from "@/features/group/queries/group-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { IconDots } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
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 { IUser } from "@/features/user/types/user.types.ts";
|
||||
import Paginate from "@/components/common/paginate.tsx";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
|
||||
export default function GroupMembersList() {
|
||||
const { t } = useTranslation();
|
||||
const { groupId } = useParams();
|
||||
const [page, setPage] = useState(1);
|
||||
const { data, isLoading } = useGroupMembersQuery(groupId, { page });
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { data, isLoading } = useGroupMembersQuery(groupId, { cursor });
|
||||
const removeGroupMember = useRemoveGroupMemberMutation();
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
@@ -107,10 +108,10 @@ export default function GroupMembersList() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -250,12 +250,10 @@ export function useGetSidebarPagesQuery(
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["sidebar-pages", data],
|
||||
enabled: !!data?.pageId || !!data?.spaceId,
|
||||
queryFn: ({ pageParam }) => getSidebarPages({ ...data, page: pageParam }),
|
||||
initialPageParam: 1,
|
||||
getPreviousPageParam: (firstPage) =>
|
||||
firstPage.meta.hasPrevPage ? firstPage.meta.page - 1 : undefined,
|
||||
queryFn: ({ pageParam }) => getSidebarPages({ ...data, cursor: pageParam }),
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.meta.hasNextPage ? lastPage.meta.page + 1 : undefined,
|
||||
lastPage.meta?.nextCursor ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,13 +261,11 @@ export function useGetRootSidebarPagesQuery(data: SidebarPagesParams) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["root-sidebar-pages", data.spaceId],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
return getSidebarPages({ spaceId: data.spaceId, page: pageParam });
|
||||
return getSidebarPages({ spaceId: data.spaceId, cursor: pageParam });
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getPreviousPageParam: (firstPage) =>
|
||||
firstPage.meta.hasPrevPage ? firstPage.meta.page - 1 : undefined,
|
||||
initialPageParam: undefined,
|
||||
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(
|
||||
params: SidebarPagesParams,
|
||||
): Promise<InfiniteData<IPagination<IPage>, unknown>> {
|
||||
let page = 1;
|
||||
let hasNextPage = false;
|
||||
let cursor: string | undefined = undefined;
|
||||
const pages: IPagination<IPage>[] = [];
|
||||
const pageParams: number[] = [];
|
||||
const pageParams: (string | undefined)[] = [];
|
||||
|
||||
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;
|
||||
pages.push(data);
|
||||
pageParams.push(page);
|
||||
pageParams.push(cursor);
|
||||
|
||||
hasNextPage = data.meta.hasNextPage;
|
||||
|
||||
page += 1;
|
||||
} while (hasNextPage);
|
||||
cursor = data.meta.nextCursor ?? undefined;
|
||||
} while (cursor);
|
||||
|
||||
return {
|
||||
pageParams,
|
||||
|
||||
@@ -30,15 +30,15 @@ import { useState } from "react";
|
||||
import TrashPageContentModal from "@/features/page/trash/components/trash-page-content-modal";
|
||||
import { UserInfo } from "@/components/common/user-info.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() {
|
||||
const { t } = useTranslation();
|
||||
const { spaceSlug } = useParams();
|
||||
const { page, setPage } = usePaginateAndSearch();
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
||||
const { data: deletedPages, isLoading } = useDeletedPagesQuery(space?.id, {
|
||||
page, limit: 50
|
||||
cursor, limit: 50
|
||||
});
|
||||
const restorePageMutation = useRestorePageMutation();
|
||||
const deletePageMutation = useDeletePageMutation();
|
||||
@@ -206,10 +206,10 @@ export default function Trash() {
|
||||
|
||||
{deletedPages && deletedPages.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={deletedPages.meta.hasPrevPage}
|
||||
hasNextPage={deletedPages.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={deletedPages.meta?.hasPrevPage}
|
||||
hasNextPage={deletedPages.meta?.hasNextPage}
|
||||
onNext={() => goNext(deletedPages.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface ICopyPageToSpace {
|
||||
export interface SidebarPagesParams {
|
||||
spaceId?: string;
|
||||
pageId?: string;
|
||||
page?: number; // pagination
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export interface IPageInput {
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
IconFileDescription,
|
||||
IconSearch,
|
||||
IconCheck,
|
||||
IconSparkles,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
@@ -26,7 +25,7 @@ import { useGetSpacesQuery } from "@/features/space/queries/space-query";
|
||||
import { useLicense } from "@/ee/hooks/use-license";
|
||||
import classes from "./search-spotlight-filters.module.css";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import { useAtom } from "jotai/index";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
|
||||
interface SearchSpotlightFiltersProps {
|
||||
@@ -53,7 +52,6 @@ export function SearchSpotlightFilters({
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
|
||||
const { data: spacesData } = useGetSpacesQuery({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
query: debouncedSpaceQuery,
|
||||
});
|
||||
@@ -265,7 +263,9 @@ export function SearchSpotlightFilters({
|
||||
contentType !== option.value &&
|
||||
handleFilterChange("contentType", option.value)
|
||||
}
|
||||
disabled={option.disabled || (isAiMode && option.value === "attachment")}
|
||||
disabled={
|
||||
option.disabled || (isAiMode && option.value === "attachment")
|
||||
}
|
||||
>
|
||||
<Group flex="1" gap="xs">
|
||||
<div>
|
||||
@@ -275,11 +275,13 @@ export function SearchSpotlightFilters({
|
||||
{t("Enterprise")}
|
||||
</Badge>
|
||||
)}
|
||||
{!option.disabled && isAiMode && option.value === "attachment" && (
|
||||
<Text size="xs" mt={4}>
|
||||
{t("Ask AI not available for attachments")}
|
||||
</Text>
|
||||
)}
|
||||
{!option.disabled &&
|
||||
isAiMode &&
|
||||
option.value === "attachment" && (
|
||||
<Text size="xs" mt={4}>
|
||||
{t("Ask AI not available for attachments")}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{contentType === option.value && <IconCheck size={20} />}
|
||||
</Group>
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
export async function searchPage(
|
||||
params: IPageSearchParams,
|
||||
): Promise<IPageSearch[]> {
|
||||
const req = await api.post<IPageSearch[]>("/search", params);
|
||||
return req.data;
|
||||
const req = await api.post<{ items: IPageSearch[] }>("/search", params);
|
||||
return req.data.items;
|
||||
}
|
||||
|
||||
export async function searchSuggestions(
|
||||
@@ -24,13 +24,13 @@ export async function searchSuggestions(
|
||||
export async function searchShare(
|
||||
params: IPageSearchParams,
|
||||
): Promise<IPageSearch[]> {
|
||||
const req = await api.post<IPageSearch[]>("/search/share-search", params);
|
||||
return req.data;
|
||||
const req = await api.post<{ items: IPageSearch[] }>("/search/share-search", params);
|
||||
return req.data.items;
|
||||
}
|
||||
|
||||
export async function searchAttachments(
|
||||
params: IPageSearchParams,
|
||||
): Promise<IAttachmentSearch[]> {
|
||||
const req = await api.post<IAttachmentSearch[]>("/search-attachments", params);
|
||||
return req.data;
|
||||
const req = await api.post<{ items: IAttachmentSearch[] }>("/search-attachments", params);
|
||||
return req.data.items;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Table, Group, Text, Anchor } from "@mantine/core";
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Paginate from "@/components/common/paginate.tsx";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
import { useGetSharesQuery } from "@/features/share/queries/share-query.ts";
|
||||
import { ISharedItem } from "@/features/share/types/share.types.ts";
|
||||
import { format } from "date-fns";
|
||||
@@ -14,8 +15,8 @@ import classes from "./share.module.css";
|
||||
|
||||
export default function ShareList() {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(1);
|
||||
const { data, isLoading } = useGetSharesQuery({ page });
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { data, isLoading } = useGetSharesQuery({ cursor });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -86,10 +87,10 @@ export default function ShareList() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useGetSharesQuery(
|
||||
params?: QueryParams,
|
||||
): UseQueryResult<IPagination<ISharedItem>, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["share-list"],
|
||||
queryKey: ["share-list", params],
|
||||
queryFn: () => getShares(params),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts
|
||||
|
||||
export default function SpaceGrid() {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading } = useGetSpacesQuery({ page: 1, limit: 10 });
|
||||
const { data, isLoading } = useGetSpacesQuery({ limit: 10 });
|
||||
|
||||
const cards = data?.items.slice(0, 9).map((space, index) => (
|
||||
<Card
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Group, Table, Text } from "@mantine/core";
|
||||
import React, { useState } from "react";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
@@ -12,8 +13,8 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
|
||||
|
||||
export default function SpaceList() {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(1);
|
||||
const { data, isLoading } = useGetSpacesQuery({ page });
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { data, isLoading } = useGetSpacesQuery({ cursor });
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
||||
|
||||
@@ -72,10 +73,10 @@ export default function SpaceList() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ export default function SpaceMembersList({
|
||||
readOnly,
|
||||
}: SpaceMembersProps) {
|
||||
const { t } = useTranslation();
|
||||
const { search, page, setPage, handleSearch } = usePaginateAndSearch();
|
||||
const { search, cursor, goNext, goPrev, handleSearch } = usePaginateAndSearch();
|
||||
const { data, isLoading } = useSpaceMembersQuery(spaceId, {
|
||||
page,
|
||||
cursor,
|
||||
limit: 100,
|
||||
query: search,
|
||||
});
|
||||
@@ -206,10 +206,10 @@ export default function SpaceMembersList({
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -28,19 +28,19 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
|
||||
interface AllSpacesListProps {
|
||||
spaces: any[];
|
||||
onSearch: (query: string) => void;
|
||||
page: number;
|
||||
hasPrevPage?: boolean;
|
||||
hasNextPage?: boolean;
|
||||
onPageChange: (page: number) => void;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
}
|
||||
|
||||
export default function AllSpacesList({
|
||||
spaces,
|
||||
onSearch,
|
||||
page,
|
||||
hasPrevPage,
|
||||
hasNextPage,
|
||||
onPageChange,
|
||||
onNext,
|
||||
onPrev,
|
||||
}: AllSpacesListProps) {
|
||||
const { t } = useTranslation();
|
||||
const [settingsOpened, { open: openSettings, close: closeSettings }] =
|
||||
@@ -145,10 +145,10 @@ export default function AllSpacesList({
|
||||
|
||||
{spaces.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={hasPrevPage}
|
||||
hasNextPage={hasNextPage}
|
||||
onPageChange={onPageChange}
|
||||
onNext={onNext}
|
||||
onPrev={onPrev}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+8
-7
@@ -1,6 +1,6 @@
|
||||
import { Group, Table, Avatar, Text, Alert } from "@mantine/core";
|
||||
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 InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import Paginate from "@/components/common/paginate.tsx";
|
||||
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
|
||||
|
||||
export default function WorkspaceInvitesTable() {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(1);
|
||||
const { cursor, goNext, goPrev } = useCursorPaginate();
|
||||
const { data, isLoading } = useWorkspaceInvitationsQuery({
|
||||
page,
|
||||
cursor,
|
||||
limit: 100,
|
||||
});
|
||||
const { isAdmin } = useUserRole();
|
||||
@@ -65,10 +66,10 @@ export default function WorkspaceInvitesTable() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+6
-6
@@ -21,9 +21,9 @@ import MemberActionMenu from "@/features/workspace/components/members/components
|
||||
|
||||
export default function WorkspaceMembersTable() {
|
||||
const { t } = useTranslation();
|
||||
const { search, page, setPage, handleSearch } = usePaginateAndSearch();
|
||||
const { search, cursor, goNext, goPrev, handleSearch } = usePaginateAndSearch();
|
||||
const { data, isLoading } = useWorkspaceMembersQuery({
|
||||
page,
|
||||
cursor,
|
||||
limit: 100,
|
||||
query: search,
|
||||
});
|
||||
@@ -111,10 +111,10 @@ export default function WorkspaceMembersTable() {
|
||||
|
||||
{data?.items.length > 0 && (
|
||||
<Paginate
|
||||
currentPage={page}
|
||||
hasPrevPage={data?.meta.hasPrevPage}
|
||||
hasNextPage={data?.meta.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
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 = "") {
|
||||
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 handleSearch = useCallback((newQuery: string) => {
|
||||
if (prevSearchRef.current !== newQuery) {
|
||||
prevSearchRef.current = 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 };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface QueryParams {
|
||||
query?: string;
|
||||
page?: number;
|
||||
cursor?: string;
|
||||
beforeCursor?: string;
|
||||
limit?: number;
|
||||
adminView?: boolean;
|
||||
}
|
||||
@@ -29,9 +30,10 @@ export interface ApiResponse<T> {
|
||||
|
||||
export type IPaginationMeta = {
|
||||
limit: number;
|
||||
page: number;
|
||||
hasNextPage: boolean;
|
||||
hasPrevPage: boolean;
|
||||
nextCursor: string | null;
|
||||
prevCursor: string | null;
|
||||
};
|
||||
export type IPagination<T> = {
|
||||
items: T[];
|
||||
|
||||
@@ -11,10 +11,10 @@ import useUserRole from "@/hooks/use-user-role";
|
||||
export default function Spaces() {
|
||||
const { t } = useTranslation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const { search, page, setPage, handleSearch } = usePaginateAndSearch();
|
||||
const { search, cursor, goNext, goPrev, handleSearch } = usePaginateAndSearch();
|
||||
|
||||
const { data, isLoading } = useGetSpacesQuery({
|
||||
page,
|
||||
cursor,
|
||||
limit: 30,
|
||||
query: search,
|
||||
});
|
||||
@@ -41,10 +41,10 @@ export default function Spaces() {
|
||||
<AllSpacesList
|
||||
spaces={data?.items || []}
|
||||
onSearch={handleSearch}
|
||||
page={page}
|
||||
hasPrevPage={data?.meta?.hasPrevPage}
|
||||
hasNextPage={data?.meta?.hasNextPage}
|
||||
onPageChange={setPage}
|
||||
onNext={() => goNext(data?.meta?.nextCursor)}
|
||||
onPrev={goPrev}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
Reference in New Issue
Block a user