From e83792edd5e589faec1c517095944b7a819e8b62 Mon Sep 17 00:00:00 2001 From: Evgeny Chaban Date: Mon, 4 Mar 2024 14:17:30 +0300 Subject: [PATCH] Pagination Improvements (#283) --- .../api/src/resources/user/actions/list.ts | 87 ++++++++++--------- template/apps/api/src/utils/index.ts | 2 + template/apps/api/src/utils/string.util.ts | 9 ++ template/apps/web/src/pages/home/index.tsx | 55 ++++++------ .../apps/web/src/resources/user/user.api.ts | 18 ++-- template/apps/web/src/types.ts | 20 +++++ .../packages/app-types/src/common.types.ts | 7 ++ template/packages/app-types/src/index.ts | 2 + template/packages/schemas/src/index.ts | 2 + .../packages/schemas/src/pagination.schema.ts | 12 +++ 10 files changed, 131 insertions(+), 83 deletions(-) create mode 100644 template/apps/api/src/utils/string.util.ts create mode 100644 template/packages/app-types/src/common.types.ts create mode 100644 template/packages/schemas/src/pagination.schema.ts diff --git a/template/apps/api/src/resources/user/actions/list.ts b/template/apps/api/src/resources/user/actions/list.ts index 06942e53..30f7592c 100644 --- a/template/apps/api/src/resources/user/actions/list.ts +++ b/template/apps/api/src/resources/user/actions/list.ts @@ -1,64 +1,65 @@ import { z } from 'zod'; -import { AppKoaContext, AppRouter } from 'types'; +import { AppKoaContext, AppRouter, NestedKeys, User } from 'types'; import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; -const schema = z.object({ - page: z.string().transform(Number).default('1'), - perPage: z.string().transform(Number).default('10'), - sort: z.object({ - createdOn: z.enum(['asc', 'desc']), - }).default({ createdOn: 'desc' }), +import { stringUtil } from 'utils'; + +import { paginationSchema } from 'schemas'; + +const schema = paginationSchema.extend({ filter: z.object({ createdOn: z.object({ - sinceDate: z.string(), - dueDate: z.string(), - }).nullable().default(null), - }).nullable().default(null), - searchValue: z.string().default(''), + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().optional(), + }).optional(), + }).optional(), }); type ValidatedData = z.infer; async function handler(ctx: AppKoaContext) { - const { - perPage, page, sort, searchValue, filter, - } = ctx.validatedData; - - const validatedSearch = searchValue.split('\\').join('\\\\').split('.').join('\\.'); - const regExp = new RegExp(validatedSearch, 'gi'); - - const users = await userService.find( - { - $and: [ - { - $or: [ - { firstName: { $regex: regExp } }, - { lastName: { $regex: regExp } }, - { email: { $regex: regExp } }, - { createdOn: {} }, - ], + const { perPage, page, sort, searchValue, filter } = ctx.validatedData; + + const filterOptions = []; + + if (searchValue) { + const searchPattern = stringUtil.escapeRegExpString(searchValue); + + const searchFields: NestedKeys[] = ['firstName', 'lastName', 'email']; + + filterOptions.push({ + $or: searchFields.map((field) => ({ [field]: { $regex: searchPattern } })), + }); + } + + if (filter) { + const { createdOn, ...otherFilters } = filter; + + if (createdOn) { + const { startDate, endDate } = createdOn; + + filterOptions.push({ + createdOn: { + ...(startDate && ({ $gte: startDate })), + ...(endDate && ({ $lt: endDate })), }, - filter?.createdOn ? { - createdOn: { - $gte: new Date(filter.createdOn.sinceDate as string), - $lt: new Date(filter.createdOn.dueDate as string), - }, - } : {}, - ], - }, + }); + } + + Object.entries(otherFilters).forEach(([key, value]) => { + filterOptions.push({ [key]: value }); + }); + } + + ctx.body = await userService.find( + { ...(filterOptions.length && { $and: filterOptions }) }, { page, perPage }, { sort }, ); - - ctx.body = { - items: users.results, - totalPages: users.pagesCount, - count: users.count, - }; } export default (router: AppRouter) => { diff --git a/template/apps/api/src/utils/index.ts b/template/apps/api/src/utils/index.ts index 742c799f..225d6e64 100644 --- a/template/apps/api/src/utils/index.ts +++ b/template/apps/api/src/utils/index.ts @@ -2,10 +2,12 @@ import configUtil from './config.util'; import promiseUtil from './promise.util'; import routeUtil from './routes.util'; import securityUtil from './security.util'; +import stringUtil from './string.util'; export { configUtil, promiseUtil, routeUtil, securityUtil, + stringUtil, }; diff --git a/template/apps/api/src/utils/string.util.ts b/template/apps/api/src/utils/string.util.ts new file mode 100644 index 00000000..e1411ead --- /dev/null +++ b/template/apps/api/src/utils/string.util.ts @@ -0,0 +1,9 @@ +const escapeRegExpString = (searchString: string): RegExp => { + const escapedString = searchString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + return new RegExp(escapedString, 'gi'); +}; + +export default { + escapeRegExpString, +}; diff --git a/template/apps/web/src/pages/home/index.tsx b/template/apps/web/src/pages/home/index.tsx index fb9102da..957b14fa 100644 --- a/template/apps/web/src/pages/home/index.tsx +++ b/template/apps/web/src/pages/home/index.tsx @@ -15,30 +15,30 @@ import { import { useDebouncedValue, useInputState } from '@mantine/hooks'; import { IconSearch, IconX, IconSelector } from '@tabler/icons-react'; import { RowSelectionState, SortingState } from '@tanstack/react-table'; -import { DatePickerInput, DatesRangeValue } from '@mantine/dates'; +import { DatePickerInput, DatesRangeValue, DateValue } from '@mantine/dates'; import { userApi } from 'resources/user'; import { Table } from 'components'; +import { ListParams, SortOrder } from 'types'; + import { PER_PAGE, columns, selectOptions } from './constants'; import classes from './index.module.css'; -interface UsersListParams { - page?: number; - perPage?: number; - searchValue?: string; - sort?: { - createdOn: 'asc' | 'desc'; - }; - filter?: { - createdOn?: { - sinceDate: Date | null; - dueDate: Date | null; - }; +type FilterParams = { + createdOn?: { + startDate: DateValue; + endDate: DateValue; }; -} +}; + +type SortParams = { + createdOn?: SortOrder; +}; + +type UsersListParams = ListParams; const Home: NextPage = () => { const [search, setSearch] = useInputState(''); @@ -58,20 +58,20 @@ const Home: NextPage = () => { })); }, []); - const handleFilter = useCallback(([sinceDate, dueDate]: DatesRangeValue) => { - setFilterDate([sinceDate, dueDate]); + const handleFilter = useCallback(([startDate, endDate]: DatesRangeValue) => { + setFilterDate([startDate, endDate]); - if (!sinceDate) { + if (!startDate) { setParams((prev) => ({ ...prev, filter: {}, })); } - if (dueDate) { + if (endDate) { setParams((prev) => ({ ...prev, - filter: { createdOn: { sinceDate, dueDate } }, + filter: { createdOn: { startDate, endDate } }, })); } }, []); @@ -80,13 +80,14 @@ const Home: NextPage = () => { setParams((prev) => ({ ...prev, page: 1, searchValue: debouncedSearch, perPage: PER_PAGE })); }, [debouncedSearch]); - const { data, isLoading: isListLoading } = userApi.useList(params); + const { data: users, isLoading: isUserListLoading } = userApi.useList(params); return ( <> Home + Users @@ -96,7 +97,7 @@ const Home: NextPage = () => { className={classes.inputSkeleton} height={42} radius="sm" - visible={isListLoading} + visible={isUserListLoading} width="auto" > { width="auto" height={42} radius="sm" - visible={isListLoading} + visible={isUserListLoading} >