diff --git a/src/auth/user-directory.ts b/src/auth/user-directory.ts index 2ead492..870e791 100644 --- a/src/auth/user-directory.ts +++ b/src/auth/user-directory.ts @@ -226,6 +226,7 @@ export async function listUsers(actorId: string, search: UserSearch): Promise row.id); const linked = ids.length @@ -333,7 +334,7 @@ export async function listUsers(actorId: string, search: UserSearch): Promise { }); it("drops malformed values instead of refusing the whole request", () => { - const params = new URLSearchParams("q=%20%20&sort=random&page=0&student=maybe&extra=1"); + const params = new URLSearchParams( + "q=%20%20&sort=random&page=0&pageSize=30&student=maybe&extra=1", + ); expect(parseUserSearch(params)).toEqual({}); expect(parseUserSearch(new URLSearchParams("page=two"))).toEqual({}); }); it("round-trips through the API query string", () => { - const search = { q: "123456789", student: "yes", page: 2 } as const; + const search = { q: "123456789", student: "yes", page: 2, pageSize: 50 } as const; expect(parseUserSearch(userSearchParams(search))).toEqual(search); }); + it("defaults to 25 rows and accepts the admin page sizes", () => { + expect(USER_PAGE_SIZE).toBe(25); + for (const pageSize of [10, 20, 25, 50, 100]) { + expect(parseUserSearch(new URLSearchParams(`pageSize=${pageSize}`)).pageSize).toBe(pageSize); + } + }); + it("keeps a numeric search from the page URL as text", () => { // The router parses `?q=123456789` into a number before validating it. expect(userSearchSchema.parse({ q: 123456789 })).toEqual({ q: "123456789" }); }); it("tells filters apart from sorting and paging", () => { - expect(hasUserFilters({ sort: "newest", page: 2 })).toBe(false); + expect(hasUserFilters({ sort: "newest", page: 2, pageSize: 50 })).toBe(false); expect(hasUserFilters({ telegram: "no" })).toBe(true); expect(hasUserFilters({ role: "moderator" })).toBe(true); }); diff --git a/src/auth/users.ts b/src/auth/users.ts index 1eb9e00..0c8954a 100644 --- a/src/auth/users.ts +++ b/src/auth/users.ts @@ -24,7 +24,8 @@ export type GroupTrait = "socio" | "direttivo"; export const USER_SORTS = ["name", "newest", "oldest"] as const; -export const USER_PAGE_SIZE = 50; +export const USER_PAGE_SIZES = [10, 20, 25, 50, 100] as const; +export const USER_PAGE_SIZE = 25; // The router parses search values as JSON, so a Telegram ID typed into the search box // arrives as a number. @@ -48,6 +49,12 @@ export const userSearchSchema = z.object({ role: text(MAX_KEY_LENGTH), sort: z.enum(USER_SORTS).optional().catch(undefined), page: z.coerce.number().int().min(1).max(100_000).optional().catch(undefined), + pageSize: z.coerce + .number() + .int() + .refine((value) => USER_PAGE_SIZES.some((size) => size === value)) + .optional() + .catch(undefined), socio: presence, direttivo: presence, student: presence, diff --git a/src/routes/users/index.tsx b/src/routes/users/index.tsx index c90bfbd..f146ab2 100644 --- a/src/routes/users/index.tsx +++ b/src/routes/users/index.tsx @@ -26,6 +26,7 @@ import { type UserListPage, type UserSearch, type UserTrait, + USER_PAGE_SIZES, USER_TRAITS, hasUserFilters, userSearchSchema, @@ -351,7 +352,10 @@ function UsersIndex() { className="h-7 rounded-full px-3" onClick={() => { setQuery(""); - void navigate({ search: { sort: search.sort }, replace: true }); + void navigate({ + search: { sort: search.sort, pageSize: search.pageSize }, + replace: true, + }); }} >