Skip to content

Commit

Permalink
Clean up how pagination parameters are handled
Browse files Browse the repository at this point in the history
  • Loading branch information
sandhose committed Oct 1, 2024
1 parent 093809c commit 811e168
Show file tree
Hide file tree
Showing 16 changed files with 248 additions and 95 deletions.
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

0 comments on commit 811e168

Please sign in to comment.