Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up how pagination parameters are handled #3272

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@radix-ui/react-collapsible": "1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@tanstack/react-router": "^1.58.15",
"@tanstack/router-zod-adapter": "^1.58.15",
"@urql/core": "^5.0.6",
"@urql/devtools": "^2.0.3",
"@urql/exchange-graphcache": "^7.1.3",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/UserProfile/UserEmailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import { useTransition } from "react";
import { useQuery } from "urql";

import { FragmentType, graphql, useFragment } from "../../gql";
import { type FragmentType, graphql, useFragment } from "../../gql";
import {
FIRST_PAGE,
Pagination,
type Pagination,
usePages,
usePagination,
} from "../../pagination";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const BrowserSessionsOverview: React.FC<{
})}
</Text>
</div>
<Link to="/sessions/browsers" search={{ first: 6 }}>
<Link to="/sessions/browsers">
{t("frontend.browser_sessions_overview.view_all_button")}
</Link>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ exports[`BrowserSessionsOverview > renders with no browser sessions 1`] = `
<a
class="_link_1mzip_17"
data-kind="primary"
href="/sessions/browsers?first=6"
href="/sessions/browsers"
rel="noreferrer noopener"
>
View all
Expand Down Expand Up @@ -53,7 +53,7 @@ exports[`BrowserSessionsOverview > renders with sessions 1`] = `
<a
class="_link_1mzip_17"
data-kind="primary"
href="/sessions/browsers?first=6"
href="/sessions/browsers"
rel="noreferrer noopener"
>
View all
Expand Down
58 changes: 43 additions & 15 deletions frontend/src/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,43 @@
import { useState } from "react";
import * as z from "zod";

import { PageInfo } from "./gql/graphql";
import type { PageInfo } from "./gql/graphql";

export const FIRST_PAGE = Symbol("FIRST_PAGE");
export const LAST_PAGE = Symbol("LAST_PAGE");

export const anyPaginationSchema = z.object({
first: z.number().optional(),
after: z.string().optional(),
last: z.number().optional(),
before: z.string().optional(),
});

export const forwardPaginationSchema = z.object({
first: z.number(),
after: z.string().optional(),
});

export const backwardPaginationSchema = z.object({
const backwardPaginationSchema = z.object({
last: z.number(),
before: z.string().optional(),
});

export const paginationSchema = z.union([
const paginationSchema = z.union([
forwardPaginationSchema,
backwardPaginationSchema,
]);

export type ForwardPagination = z.infer<typeof forwardPaginationSchema>;
export type BackwardPagination = z.infer<typeof backwardPaginationSchema>;
type ForwardPagination = z.infer<typeof forwardPaginationSchema>;
type BackwardPagination = z.infer<typeof backwardPaginationSchema>;
export type Pagination = z.infer<typeof paginationSchema>;
export type AnyPagination = z.infer<typeof anyPaginationSchema>;

// Check if the pagination is a valid pagination
export const isValidPagination = (
pagination: AnyPagination,
): pagination is Pagination =>
typeof pagination.first === "number" || typeof pagination.last === "number";

// Check if the pagination is forward pagination.
export const isForwardPagination = (
Expand All @@ -47,26 +61,40 @@ export const isBackwardPagination = (

type Action = typeof FIRST_PAGE | typeof LAST_PAGE | Pagination;

// Normalize pagination parameters to a valid pagination object
export const normalizePagination = (
pagination: AnyPagination,
pageSize = 6,
type: "forward" | "backward" = "forward",
): Pagination => {
if (isValidPagination(pagination)) {
return pagination;
}

if (type === "forward") {
return { first: pageSize } satisfies ForwardPagination;
}

return { last: pageSize } satisfies BackwardPagination;
};

// Hook to handle pagination state.
export const usePagination = (
pageSize = 6,
): [Pagination, (action: Action) => void] => {
const [pagination, setPagination] = useState<Pagination>({
first: pageSize,
after: undefined,
});

const handlePagination = (action: Action): void => {
if (action === FIRST_PAGE) {
setPagination({
first: pageSize,
after: undefined,
});
} satisfies ForwardPagination);
} else if (action === LAST_PAGE) {
setPagination({
last: pageSize,
before: undefined,
});
} satisfies BackwardPagination);
} else {
setPagination(action);
}
Expand All @@ -78,7 +106,7 @@ export const usePagination = (
// Compute the next backward and forward pagination parameters based on the current pagination and the page info.
export const usePages = (
currentPagination: Pagination,
pageInfo: PageInfo | null,
pageInfo: PageInfo,
pageSize = 6,
): [BackwardPagination | null, ForwardPagination | null] => {
const hasProbablyPreviousPage =
Expand All @@ -90,17 +118,17 @@ export const usePages = (

let previousPagination: BackwardPagination | null = null;
let nextPagination: ForwardPagination | null = null;
if (pageInfo?.hasPreviousPage || hasProbablyPreviousPage) {
if (pageInfo.hasPreviousPage || hasProbablyPreviousPage) {
previousPagination = {
last: pageSize,
before: pageInfo?.startCursor ?? undefined,
before: pageInfo.startCursor ?? undefined,
};
}

if (pageInfo?.hasNextPage || hasProbablyNextPage) {
if (pageInfo.hasNextPage || hasProbablyNextPage) {
nextPagination = {
first: pageSize,
after: pageInfo?.endCursor ?? undefined,
after: pageInfo.endCursor ?? undefined,
};
}

Expand Down
147 changes: 132 additions & 15 deletions frontend/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,21 +204,138 @@ declare module '@tanstack/react-router' {

// Create and export the route tree

export const routeTree = rootRoute.addChildren({
AccountRoute: AccountRoute.addChildren({
AccountIndexRoute,
AccountSessionsIdRoute,
AccountSessionsBrowsersRoute,
AccountSessionsIndexRoute,
}),
ResetCrossSigningRoute,
ClientsIdRoute,
DevicesSplatRoute,
EmailsIdVerifyRoute,
PasswordChangeSuccessLazyRoute,
PasswordChangeIndexRoute,
PasswordRecoveryIndexRoute,
})
interface AccountRouteChildren {
AccountIndexRoute: typeof AccountIndexRoute
AccountSessionsIdRoute: typeof AccountSessionsIdRoute
AccountSessionsBrowsersRoute: typeof AccountSessionsBrowsersRoute
AccountSessionsIndexRoute: typeof AccountSessionsIndexRoute
}

const AccountRouteChildren: AccountRouteChildren = {
AccountIndexRoute: AccountIndexRoute,
AccountSessionsIdRoute: AccountSessionsIdRoute,
AccountSessionsBrowsersRoute: AccountSessionsBrowsersRoute,
AccountSessionsIndexRoute: AccountSessionsIndexRoute,
}

const AccountRouteWithChildren =
AccountRoute._addFileChildren(AccountRouteChildren)

export interface FileRoutesByFullPath {
'': typeof AccountRouteWithChildren
'/reset-cross-signing': typeof ResetCrossSigningRoute
'/clients/$id': typeof ClientsIdRoute
'/devices/$': typeof DevicesSplatRoute
'/': typeof AccountIndexRoute
'/sessions/$id': typeof AccountSessionsIdRoute
'/sessions/browsers': typeof AccountSessionsBrowsersRoute
'/emails/$id/verify': typeof EmailsIdVerifyRoute
'/password/change/success': typeof PasswordChangeSuccessLazyRoute
'/sessions': typeof AccountSessionsIndexRoute
'/password/change': typeof PasswordChangeIndexRoute
'/password/recovery': typeof PasswordRecoveryIndexRoute
}

export interface FileRoutesByTo {
'/reset-cross-signing': typeof ResetCrossSigningRoute
'/clients/$id': typeof ClientsIdRoute
'/devices/$': typeof DevicesSplatRoute
'/': typeof AccountIndexRoute
'/sessions/$id': typeof AccountSessionsIdRoute
'/sessions/browsers': typeof AccountSessionsBrowsersRoute
'/emails/$id/verify': typeof EmailsIdVerifyRoute
'/password/change/success': typeof PasswordChangeSuccessLazyRoute
'/sessions': typeof AccountSessionsIndexRoute
'/password/change': typeof PasswordChangeIndexRoute
'/password/recovery': typeof PasswordRecoveryIndexRoute
}

export interface FileRoutesById {
__root__: typeof rootRoute
'/_account': typeof AccountRouteWithChildren
'/reset-cross-signing': typeof ResetCrossSigningRoute
'/clients/$id': typeof ClientsIdRoute
'/devices/$': typeof DevicesSplatRoute
'/_account/': typeof AccountIndexRoute
'/_account/sessions/$id': typeof AccountSessionsIdRoute
'/_account/sessions/browsers': typeof AccountSessionsBrowsersRoute
'/emails/$id/verify': typeof EmailsIdVerifyRoute
'/password/change/success': typeof PasswordChangeSuccessLazyRoute
'/_account/sessions/': typeof AccountSessionsIndexRoute
'/password/change/': typeof PasswordChangeIndexRoute
'/password/recovery/': typeof PasswordRecoveryIndexRoute
}

export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| ''
| '/reset-cross-signing'
| '/clients/$id'
| '/devices/$'
| '/'
| '/sessions/$id'
| '/sessions/browsers'
| '/emails/$id/verify'
| '/password/change/success'
| '/sessions'
| '/password/change'
| '/password/recovery'
fileRoutesByTo: FileRoutesByTo
to:
| '/reset-cross-signing'
| '/clients/$id'
| '/devices/$'
| '/'
| '/sessions/$id'
| '/sessions/browsers'
| '/emails/$id/verify'
| '/password/change/success'
| '/sessions'
| '/password/change'
| '/password/recovery'
id:
| '__root__'
| '/_account'
| '/reset-cross-signing'
| '/clients/$id'
| '/devices/$'
| '/_account/'
| '/_account/sessions/$id'
| '/_account/sessions/browsers'
| '/emails/$id/verify'
| '/password/change/success'
| '/_account/sessions/'
| '/password/change/'
| '/password/recovery/'
fileRoutesById: FileRoutesById
}

export interface RootRouteChildren {
AccountRoute: typeof AccountRouteWithChildren
ResetCrossSigningRoute: typeof ResetCrossSigningRoute
ClientsIdRoute: typeof ClientsIdRoute
DevicesSplatRoute: typeof DevicesSplatRoute
EmailsIdVerifyRoute: typeof EmailsIdVerifyRoute
PasswordChangeSuccessLazyRoute: typeof PasswordChangeSuccessLazyRoute
PasswordChangeIndexRoute: typeof PasswordChangeIndexRoute
PasswordRecoveryIndexRoute: typeof PasswordRecoveryIndexRoute
}

const rootRouteChildren: RootRouteChildren = {
AccountRoute: AccountRouteWithChildren,
ResetCrossSigningRoute: ResetCrossSigningRoute,
ClientsIdRoute: ClientsIdRoute,
DevicesSplatRoute: DevicesSplatRoute,
EmailsIdVerifyRoute: EmailsIdVerifyRoute,
PasswordChangeSuccessLazyRoute: PasswordChangeSuccessLazyRoute,
PasswordChangeIndexRoute: PasswordChangeIndexRoute,
PasswordRecoveryIndexRoute: PasswordRecoveryIndexRoute,
}

export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

/* prettier-ignore-end */

Expand Down
Loading
Loading