From 906881b37a4306d358ac93df3b9749ba71ef6c3f Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 22 Jul 2024 16:20:58 +0300 Subject: [PATCH] WIP: server select to load infinite data (#322) * wip: inifinite loading select * fix: select - add limit 10 for VolunteerSelect and remove the hardcoded value in listing/volunteer ctrl * fix: select - allow listing/events to limit, set limit to 10 in EventSelect * fix: select - add page and limit to listing/admins, set limit to 10 * fix: select - add pagination to location/city --------- Co-authored-by: luciatugui --- .../activity-type/activity-type.controller.ts | 5 +- backend/src/api/listing/listing.controller.ts | 3 - backend/src/api/location/dto/get-city.dto.ts | 16 ++-- .../src/api/location/location.controller.ts | 17 ++-- .../repository-with-pagination.interface.ts | 3 +- .../base/repository-with-pagination.class.ts | 3 +- .../location-repository.interface.ts | 3 +- .../src/modules/location/model/city.model.ts | 6 +- .../repositories/location.repository.ts | 59 +++++++++---- .../location/services/location.facade.ts | 5 +- .../repositories/volunteer.repository.ts | 5 +- .../usecases/location/get-citties.usecase.ts | 9 +- frontend/package-lock.json | 65 ++++++++++++++ frontend/package.json | 1 + frontend/src/components/PaginatedSelect.tsx | 87 +++++++++++++++++++ frontend/src/containers/AdminSelect.tsx | 37 ++++++-- frontend/src/containers/EventSelect.tsx | 45 ++++++++-- frontend/src/containers/LocationSelect.tsx | 43 ++++++--- frontend/src/containers/VolunteerSelect.tsx | 66 +++++++++----- frontend/src/services/admin/admin.api.ts | 2 + frontend/src/services/event/event.api.ts | 2 + .../src/services/location/location.api.ts | 1 + .../src/services/volunteer/volunteer.api.ts | 2 + mobile/src/components/PaginatedSelect.tsx | 7 ++ 24 files changed, 386 insertions(+), 106 deletions(-) create mode 100644 frontend/src/components/PaginatedSelect.tsx create mode 100644 mobile/src/components/PaginatedSelect.tsx diff --git a/backend/src/api/activity-type/activity-type.controller.ts b/backend/src/api/activity-type/activity-type.controller.ts index 7cf3cbe77..d32f7f9c5 100644 --- a/backend/src/api/activity-type/activity-type.controller.ts +++ b/backend/src/api/activity-type/activity-type.controller.ts @@ -56,9 +56,8 @@ export class ActivityTypeController { async get( @Param('id', UuidValidationPipe) activityTypeId: string, ): Promise { - const accessRequest = await this.getOneActivityTypeUseCase.execute( - activityTypeId, - ); + const accessRequest = + await this.getOneActivityTypeUseCase.execute(activityTypeId); return new ActivityTypePresenter(accessRequest); } diff --git a/backend/src/api/listing/listing.controller.ts b/backend/src/api/listing/listing.controller.ts index e7f445315..9819cd2f4 100644 --- a/backend/src/api/listing/listing.controller.ts +++ b/backend/src/api/listing/listing.controller.ts @@ -45,7 +45,6 @@ export class ListingController { ): Promise>> { const events = await this.getManyEventUseCase.execute({ ...filters, - limit: 50, organizationId, status: EventStatus.PUBLISHED, }); @@ -67,7 +66,6 @@ export class ListingController { const volunteers = await this.getManyVolunteersUseCase.execute({ ...filters, organizationId: user.organizationId, - limit: 50, }); return new PaginatedPresenter({ @@ -103,7 +101,6 @@ export class ListingController { ): Promise>> { const admins = await this.getManyAdminUserUseCase.execute({ ...filters, - limit: 50, organizationId, }); diff --git a/backend/src/api/location/dto/get-city.dto.ts b/backend/src/api/location/dto/get-city.dto.ts index 4e53c72fd..807310763 100644 --- a/backend/src/api/location/dto/get-city.dto.ts +++ b/backend/src/api/location/dto/get-city.dto.ts @@ -1,16 +1,12 @@ -import { IsOptional, IsString, MinLength } from 'class-validator'; +import { IsOptional, IsPositive, IsString } from 'class-validator'; +import { BasePaginationFilterDto } from 'src/infrastructure/base/base-pagination-filter.dto'; -export class GetCityDto { +export class GetCityDto extends BasePaginationFilterDto { @IsString() @IsOptional() - @MinLength(3) - search: string; + search?: string; - @IsString() - @IsOptional() - city: string; - - @IsString() + @IsPositive() @IsOptional() - county: string; + countyId?: number; } diff --git a/backend/src/api/location/location.controller.ts b/backend/src/api/location/location.controller.ts index 741f29978..1201677e8 100644 --- a/backend/src/api/location/location.controller.ts +++ b/backend/src/api/location/location.controller.ts @@ -7,6 +7,7 @@ import { CountyPresenter } from './presenters/county.presenter'; import { Public } from 'src/common/decorators/public.decorator'; import { ApiParam } from '@nestjs/swagger'; import { GetCitiesByCountyIdUseCase } from 'src/usecases/location/get-cities-by-county-id.usecase'; +import { PaginatedPresenter } from 'src/infrastructure/presenters/generic-paginated.presenter'; @Public() @Controller('location') @@ -19,14 +20,14 @@ export class LocationController { @Get('city') async getCities( - @Query() { search, city, county }: GetCityDto, - ): Promise { - const cities = await this.getCitiesUseCase.execute({ - search, - city, - county, - }); - return cities.map((city) => new CityPresenter(city)); + @Query() options: GetCityDto, + ): Promise> { + const cities = await this.getCitiesUseCase.execute(options); + + return { + items: cities.items.map((city) => new CityPresenter(city)), + meta: cities.meta, + }; } @ApiParam({ name: 'countyId', type: 'number' }) diff --git a/backend/src/common/interfaces/repository-with-pagination.interface.ts b/backend/src/common/interfaces/repository-with-pagination.interface.ts index c600922f6..6dd0af4ae 100644 --- a/backend/src/common/interfaces/repository-with-pagination.interface.ts +++ b/backend/src/common/interfaces/repository-with-pagination.interface.ts @@ -1,8 +1,7 @@ -import { BaseEntity } from 'src/infrastructure/base/base-entity'; import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class'; import { SelectQueryBuilder } from 'typeorm'; -export interface IRepositoryWithPagination { +export interface IRepositoryWithPagination { paginateQuery( query: SelectQueryBuilder, limit: number, diff --git a/backend/src/infrastructure/base/repository-with-pagination.class.ts b/backend/src/infrastructure/base/repository-with-pagination.class.ts index 3df34e602..abcac23fd 100644 --- a/backend/src/infrastructure/base/repository-with-pagination.class.ts +++ b/backend/src/infrastructure/base/repository-with-pagination.class.ts @@ -2,7 +2,6 @@ import { Brackets, Repository, SelectQueryBuilder } from 'typeorm'; import { format } from 'date-fns'; import { DATE_CONSTANTS } from 'src/common/constants/constants'; import { IRepositoryWithPagination } from 'src/common/interfaces/repository-with-pagination.interface'; -import { BaseEntity } from './base-entity'; export interface IPaginationMeta { itemCount: number; @@ -17,7 +16,7 @@ export interface Pagination { meta: IPaginationMeta; } -export abstract class RepositoryWithPagination +export abstract class RepositoryWithPagination implements IRepositoryWithPagination { constructor(private readonly repository: Repository) {} diff --git a/backend/src/modules/location/interfaces/location-repository.interface.ts b/backend/src/modules/location/interfaces/location-repository.interface.ts index fb030b65f..4ab8bcf8b 100644 --- a/backend/src/modules/location/interfaces/location-repository.interface.ts +++ b/backend/src/modules/location/interfaces/location-repository.interface.ts @@ -1,8 +1,9 @@ +import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class'; import { FindLocationOptions, ICityModel } from '../model/city.model'; import { ICountyModel } from '../model/county.model'; export interface ILocationRepository { findCounties(): Promise; - findCities(options: FindLocationOptions): Promise; + findCities(options: FindLocationOptions): Promise>; findCitiesByCountyId(countyId: number): Promise; } diff --git a/backend/src/modules/location/model/city.model.ts b/backend/src/modules/location/model/city.model.ts index bd3b566ae..1bc552a8e 100644 --- a/backend/src/modules/location/model/city.model.ts +++ b/backend/src/modules/location/model/city.model.ts @@ -1,3 +1,4 @@ +import { IBasePaginationFilterModel } from 'src/infrastructure/base/base-pagination-filter.model'; import { CityEntity } from '../entities/city.entity'; import { ICountyModel } from './county.model'; @@ -9,9 +10,8 @@ export interface ICityModel { export type FindLocationOptions = { search?: string; - city?: string; - county?: string; -}; + countyId?: number; +} & IBasePaginationFilterModel; export class CityTransformer { static fromEntity(entity: CityEntity): ICityModel { diff --git a/backend/src/modules/location/repositories/location.repository.ts b/backend/src/modules/location/repositories/location.repository.ts index e93bad3a0..6fbe49e10 100644 --- a/backend/src/modules/location/repositories/location.repository.ts +++ b/backend/src/modules/location/repositories/location.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ILike, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { CityEntity } from '../entities/city.entity'; import { CountyEntity } from '../entities/county.entity'; import { ILocationRepository } from '../interfaces/location-repository.interface'; @@ -10,36 +10,59 @@ import { ICityModel, } from '../model/city.model'; import { CountyTransformer, ICountyModel } from '../model/county.model'; +import { + Pagination, + RepositoryWithPagination, +} from 'src/infrastructure/base/repository-with-pagination.class'; +import { OrderDirection } from 'src/common/enums/order-direction.enum'; @Injectable() -export class LocationRepositoryService implements ILocationRepository { +export class LocationRepositoryService + extends RepositoryWithPagination + implements ILocationRepository +{ constructor( @InjectRepository(CityEntity) private readonly cityRepository: Repository, @InjectRepository(CountyEntity) private readonly countyRepository: Repository, - ) {} + ) { + super(cityRepository); + } async findCounties(): Promise { const countyEntities = await this.countyRepository.find(); return countyEntities.map(CountyTransformer.fromEntity); } - // This will only find the cities which start with the search word - async findCities(options: FindLocationOptions): Promise { - const { search, city, county } = options; - const cityEntities = await this.cityRepository.find({ - where: { - name: ILike(`${search}%`), - ...(city && county - ? { county: { abbreviation: county }, name: city } - : {}), - }, - relations: { - county: true, - }, - }); - return cityEntities.map(CityTransformer.fromEntity); + async findCities( + options: FindLocationOptions, + ): Promise> { + const { search, countyId } = options; + + const query = this.cityRepository + .createQueryBuilder('city') + .leftJoinAndMapOne('city.county', 'city.county', 'county') + .select() + .orderBy( + this.buildOrderByQuery(options.orderBy || 'name', 'city'), + options.orderDirection || OrderDirection.ASC, + ); + + if (search) { + query.andWhere(this.buildBracketSearchQuery(['city.name'], search)); + } + + if (countyId) { + query.andWhere('city.countyId = :countyId', { countyId }); + } + + return this.paginateQuery( + query, + options.limit, + options.page, + CityTransformer.fromEntity, + ); } async findCitiesByCountyId(countyId: number): Promise { diff --git a/backend/src/modules/location/services/location.facade.ts b/backend/src/modules/location/services/location.facade.ts index d39b97f93..bff812ae9 100644 --- a/backend/src/modules/location/services/location.facade.ts +++ b/backend/src/modules/location/services/location.facade.ts @@ -2,12 +2,15 @@ import { Injectable } from '@nestjs/common'; import { FindLocationOptions, ICityModel } from '../model/city.model'; import { ICountyModel } from '../model/county.model'; import { LocationRepositoryService } from '../repositories/location.repository'; +import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class'; @Injectable() export class LocationFacade { constructor(private readonly locationRepository: LocationRepositoryService) {} - public async findCities(options: FindLocationOptions): Promise { + public async findCities( + options: FindLocationOptions, + ): Promise> { return this.locationRepository.findCities(options); } diff --git a/backend/src/modules/volunteer/repositories/volunteer.repository.ts b/backend/src/modules/volunteer/repositories/volunteer.repository.ts index a66e81b73..093478b6c 100644 --- a/backend/src/modules/volunteer/repositories/volunteer.repository.ts +++ b/backend/src/modules/volunteer/repositories/volunteer.repository.ts @@ -331,9 +331,8 @@ export class VolunteerRepositoryService ); } - const deletedVolunteerRecords = await this.volunteerRepository.softRemove( - volunteerRecords, - ); + const deletedVolunteerRecords = + await this.volunteerRepository.softRemove(volunteerRecords); return { deletedProfiles: deletedProfiles?.map((dp) => dp.id) || [], diff --git a/backend/src/usecases/location/get-citties.usecase.ts b/backend/src/usecases/location/get-citties.usecase.ts index 94b593e15..423868c97 100644 --- a/backend/src/usecases/location/get-citties.usecase.ts +++ b/backend/src/usecases/location/get-citties.usecase.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; +import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class'; import { FindLocationOptions, ICityModel, @@ -7,10 +8,14 @@ import { import { LocationFacade } from 'src/modules/location/services/location.facade'; @Injectable() -export class GetCitiesUseCase implements IUseCaseService { +export class GetCitiesUseCase + implements IUseCaseService> +{ constructor(private readonly locationFacade: LocationFacade) {} - public async execute(options: FindLocationOptions): Promise { + public async execute( + options: FindLocationOptions, + ): Promise> { return this.locationFacade.findCities(options); } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dbdf1fa66..ad99605f6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "react-query": "^3.39.3", "react-router-dom": "6.24.1", "react-select": "^5.8.0", + "react-select-async-paginate": "0.7.4", "react-toastify": "^10.0.5", "recharts": "^2.12.7", "use-query-params": "^2.2.1", @@ -3563,6 +3564,11 @@ "win32" ] }, + "node_modules/@seznam/compose-react-refs": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@seznam/compose-react-refs/-/compose-react-refs-1.0.6.tgz", + "integrity": "sha512-izzOXQfeQLonzrIQb8u6LQ8dk+ymz3WXTIXjvOlTXHq6sbzROg3NWU+9TTAOpEoK9Bth24/6F/XrfHJ5yR5n6Q==" + }, "node_modules/@smithy/abort-controller": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.2.0.tgz", @@ -4759,6 +4765,14 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@vtaits/use-lazy-ref": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@vtaits/use-lazy-ref/-/use-lazy-ref-0.1.3.tgz", + "integrity": "sha512-ZTLuFBHSivPcgWrwkXe5ExVt6R3/ybD+N0yFPy4ClzCztk/9bUD/1udKQ/jd7eCal+lapSrRWXbffqI9jkpDlg==", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@xstate/react": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@xstate/react/-/react-3.2.2.tgz", @@ -8717,6 +8731,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/krustykrab": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/krustykrab/-/krustykrab-1.0.0.tgz", + "integrity": "sha512-cn9vpa5YLWF8WtgCzrWu9nII9O2AB5gXMpbrAPuDjlytPVdopnPBBAGyoa6101EHIy2ZyII+w0BeG4mWc5RyEg==" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10138,6 +10157,23 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-select-async-paginate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/react-select-async-paginate/-/react-select-async-paginate-0.7.4.tgz", + "integrity": "sha512-ffsMyajBx8sS4Hqf3oZYhNXnrD4GZTZtJ9snX8DSrspmSH3v72r+gSBDlRep5nbJIoLDhFWJQlG8R6CqnIoDFA==", + "dependencies": { + "@seznam/compose-react-refs": "^1.0.6", + "@vtaits/use-lazy-ref": "^0.1.3", + "krustykrab": "^1.0.0", + "sleep-promise": "^9.1.0", + "use-is-mounted-ref": "^1.5.0", + "use-latest": "^1.2.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0", + "react-select": "^5.0.0" + } + }, "node_modules/react-smooth": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", @@ -10708,6 +10744,11 @@ "node": ">=8" } }, + "node_modules/sleep-promise": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz", + "integrity": "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==" + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -11580,6 +11621,14 @@ } } }, + "node_modules/use-is-mounted-ref": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-is-mounted-ref/-/use-is-mounted-ref-1.5.0.tgz", + "integrity": "sha512-p5FksHf/ospZUr5KU9ese6u3jp9fzvZ3wuSb50i0y6fdONaHWgmOqQtxR/PUcwi6hnhQDbNxWSg3eTK3N6m+dg==", + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", @@ -11593,6 +11642,22 @@ } } }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-query-params": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 987b4beaa..fefcda885 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "react-query": "^3.39.3", "react-router-dom": "6.24.1", "react-select": "^5.8.0", + "react-select-async-paginate": "0.7.4", "react-toastify": "^10.0.5", "recharts": "^2.12.7", "use-query-params": "^2.2.1", diff --git a/frontend/src/components/PaginatedSelect.tsx b/frontend/src/components/PaginatedSelect.tsx new file mode 100644 index 000000000..e91e1a891 --- /dev/null +++ b/frontend/src/components/PaginatedSelect.tsx @@ -0,0 +1,87 @@ +import React, { + AriaAttributes, + ComponentPropsWithoutRef, + ReactNode, + useEffect, + useState, +} from 'react'; +import { AsyncPaginate } from 'react-select-async-paginate'; +import { ListItem } from '../common/interfaces/list-item.interface'; +import { useTranslation } from 'react-i18next'; + +interface PaginatedSelectProps extends Omit, 'value'> { + label: string; + value?: ListItem; + isMulti?: boolean; + helper?: ReactNode; + isClearable?: boolean; + placeholder?: string; + loadOptions: ( + searchQuery: string, + loadedOptions: ListItem[], + { page }: { page: number }, + ) => Promise<{ + options: { value: string; label: string }[]; + hasMore: boolean; + additional: { page: number }; + }>; + /** Indicate if the value entered in the field is invalid **/ + 'aria-invalid'?: AriaAttributes['aria-invalid']; +} + +const PaginatedSelect = ({ + id, + isMulti, + loadOptions, + value, + onChange, + placeholder, + label, + isClearable, + helper, + disabled, + ...props +}: PaginatedSelectProps) => { + const { t } = useTranslation('general'); + + const [defaultValue, setDefaultValue] = useState(); + + useEffect(() => { + setDefaultValue(value); + }, [value]); + + return ( +
+ {label && } + { + if (props['aria-invalid'] && state.isFocused) return 'error-and-focused'; + if (props['aria-invalid']) return 'error'; + if (state.isFocused) return 'focused'; + return ''; + }, + }} + placeholder={placeholder} + classNamePrefix="reactselect" + loadOptions={loadOptions} + onChange={onChange} + isClearable={isClearable} + isMulti={isMulti} + value={defaultValue || null} + isDisabled={disabled} + noOptionsMessage={({ inputValue }) => + inputValue.length < 2 ? `${t('type_for_options')}` : `${t('no_options')}` + } + additional={{ + page: 1, + }} + debounceTimeout={500} + /> + {helper} +
+ ); +}; + +export default PaginatedSelect; diff --git a/frontend/src/containers/AdminSelect.tsx b/frontend/src/containers/AdminSelect.tsx index 0c3ab7eb3..86b50140b 100644 --- a/frontend/src/containers/AdminSelect.tsx +++ b/frontend/src/containers/AdminSelect.tsx @@ -3,8 +3,9 @@ import React from 'react'; import i18n from '../common/config/i18n'; import { OrderDirection } from '../common/enums/order-direction.enum'; import { ListItem } from '../common/interfaces/list-item.interface'; -import ServerSelect from '../components/ServerSelect'; +// import ServerSelect from '../components/ServerSelect'; import { getAdminsListItems } from '../services/admin/admin.api'; +import PaginatedSelect from '../components/PaginatedSelect'; export interface AdminSelectProps { label: string; @@ -12,31 +13,51 @@ export interface AdminSelectProps { onSelect: (item: ListItem) => void; } +interface LoadAdminsParams { + options: ListItem[]; + hasMore: boolean; + additional: { page: number }; +} + const AdminSelect = ({ label, defaultValue, onSelect }: AdminSelectProps) => { // load admins from the database - const loadVolunteers = async (search: string): Promise => { + const loadVolunteers = async ( + search: string, + loadedOptions: ListItem[], + { page }: { page: number }, + ): Promise => { try { const admins = await getAdminsListItems({ + page, + limit: 10, search, orderBy: 'name', orderDirection: OrderDirection.ASC, }); // map admins to server select data type - return admins.items.map((admin) => ({ - value: admin.id, - label: admin.name, - })); + return { + options: admins.items.map((admin) => ({ + value: admin.id, + label: admin.name, + })), + hasMore: page < admins.meta.totalPages, + additional: { page: page + 1 }, + }; } catch (error) { // show error console.error(error); // return empty error - return []; + return { + options: [], + hasMore: false, + additional: { page: page }, + }; } }; return ( - { // load events from the database - const loadEvents = async (search: string): Promise => { + const loadEvents = async ( + search: string, + loadedOptions: unknown, + { page }: { page: number }, + ): Promise => { try { const events = await getEventListItems({ search, orderBy: 'name', orderDirection: OrderDirection.ASC, + page: page, + limit: 10, }); + console.log(events); + console.log(loadedOptions); + console.log(page); // map events to server select data type - return events.items.map((event) => ({ - value: event.id, - label: event.name, - })); + return { + options: events.items.map((event) => ({ + value: event.id, + label: event.name, + })), + hasMore: page < events.meta.totalPages, + additional: { + page: page + 1, + }, + }; } catch (error) { // show error console.error(error); // return empty error - return []; + // return []; + return { + options: [], + hasMore: false, + additional: { + page: page, + }, + }; } }; return ( - void; } +interface LoadCitiesParams { + options: ListItem[]; + hasMore: boolean; + additional: { page: number }; +} const LocationSelect = ({ label, @@ -21,20 +29,35 @@ const LocationSelect = ({ onSelect, }: LocationSelectProps) => { // load cities from the database - const loadCities = async (search: string): Promise => { + const loadCities = async ( + search: string, + loadedOptions: ListItem[], + { page }: { page: number }, + ): Promise => { try { - const cities = await getCities({ search }); - + const cities = await getCities({ search: 'gal' }); + console.log(cities); // map cities to server select data type - return cities.map((city) => ({ - value: city.id.toString(), - label: `${city.name}, ${city.county.abbreviation}`, - })); + return { + options: cities.map((city) => ({ + value: city.id.toString(), + label: `${city.name}, ${city.county.abbreviation}`, + })), + // todo: hasMore + hasMore: true, + additional: { page: page + 1 }, + }; } catch (error) { // show error console.error(error); // return empty error - return []; + return { + options: [], + hasMore: false, + additional: { + page: page, + }, + }; } }; @@ -56,7 +79,7 @@ const LocationSelect = ({ }, []); return ( - { - // load volunteers from the database - const loadVolunteers = async (search: string): Promise => { - try { - const volunteers = await getVolunteerListItems({ - search, - status: VolunteerStatus.ACTIVE, - orderBy: 'user.name', - orderDirection: OrderDirection.ASC, - }); + const loadVolunteers = useCallback( + async (searchQuery: string = '', loadedOptions: unknown, { page }: { page: number }) => { + try { + //get response from api + const response = await getVolunteerListItems({ + search: searchQuery, + status: VolunteerStatus.ACTIVE, + orderBy: 'user.name', + orderDirection: OrderDirection.ASC, + page: page, + limit: 10, + }); - // map volunteers to server select data type - return volunteers.items.map((volunteer) => ({ - value: volunteer.id, - label: volunteer.name, - })); - } catch (error) { - // show error - console.error(error); - // return empty error - return []; - } - }; + // get the options that will be displayed inside the select list + const options = response.items.map((volunteer) => ({ + value: volunteer.id, + label: volunteer.name, + })); + + return { + options, + hasMore: page < response.meta.totalPages, + additional: { + page: page + 1, + }, + }; + } catch (error) { + console.error(error); + //TODO: Inteleg de ce, dar poate ar trebui sa punem si ceva error + return { + options: [], + hasMore: false, + additional: { + page: page, + }, + }; + } + }, + [], + ); return ( - => { //Listing events export const getEventListItems = async (params: { + page: number; + limit: number; search?: string; orderBy?: string; orderDirection?: OrderDirection; diff --git a/frontend/src/services/location/location.api.ts b/frontend/src/services/location/location.api.ts index 85d0089eb..94fc0f177 100644 --- a/frontend/src/services/location/location.api.ts +++ b/frontend/src/services/location/location.api.ts @@ -6,6 +6,7 @@ export const getCities = async (params: { search?: string; city?: string; county?: string; + page?: number; }): Promise => { return API.get(`/location/city`, { params }).then((res) => res.data); }; diff --git a/frontend/src/services/volunteer/volunteer.api.ts b/frontend/src/services/volunteer/volunteer.api.ts index 7bd13307e..a84d7b496 100644 --- a/frontend/src/services/volunteer/volunteer.api.ts +++ b/frontend/src/services/volunteer/volunteer.api.ts @@ -113,6 +113,8 @@ export const blockVolunteer = async (id: string): Promise => { export const getVolunteerListItems = async (params: { status: VolunteerStatus; + page: number; + limit: number; search?: string; orderBy?: string; orderDirection?: OrderDirection; diff --git a/mobile/src/components/PaginatedSelect.tsx b/mobile/src/components/PaginatedSelect.tsx new file mode 100644 index 000000000..f5d760e3d --- /dev/null +++ b/mobile/src/components/PaginatedSelect.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const PaginatedSelect = () => { + return; +}; + +export default PaginatedSelect;