From 0c768296d160e6492ce48838990787f79310afd2 Mon Sep 17 00:00:00 2001 From: styve Lioumba Date: Sun, 30 Nov 2025 18:39:42 +0100 Subject: [PATCH] feat : #11 pagination --- .../guard/authentication/auth.guard.spec.ts | 4 -- src/app/domain/profiles/profile.model.ts | 8 ++++ src/app/domain/profiles/profile.repository.ts | 4 +- src/app/domain/search/search-filters.ts | 4 +- .../profiles/pb-profile.repository.ts | 10 +++-- .../infrastructure/search/search.service.ts | 2 + .../profile-list/profile-list.component.html | 8 +++- .../profile-list/profile-list.component.ts | 22 +++++++---- .../pagination/pagination.component.html | 36 ------------------ .../pagination/pagination.component.ts | 10 ----- .../project-list/project-list.component.html | 2 +- .../pagination/pagination.component.html | 38 +++++++++++++++++++ .../pagination/pagination.component.scss | 0 .../pagination/pagination.component.spec.ts | 0 .../pagination/pagination.component.ts | 34 +++++++++++++++++ .../profiles/fake-profile.repository.ts | 8 ++-- .../profiles/pb-profile.repository.spec.ts | 12 +++--- src/app/testing/profile.mock.ts | 10 ++++- .../profiles/list-profiles.usecase.spec.ts | 6 +-- src/app/ui/profiles/profile.facade.ts | 26 +++++++++++-- .../usecase/profiles/list-profiles.usecase.ts | 4 +- 21 files changed, 163 insertions(+), 85 deletions(-) delete mode 100644 src/app/shared/components/pagination/pagination.component.html delete mode 100644 src/app/shared/components/pagination/pagination.component.ts create mode 100644 src/app/shared/features/pagination/pagination.component.html rename src/app/shared/{components => features}/pagination/pagination.component.scss (100%) rename src/app/shared/{components => features}/pagination/pagination.component.spec.ts (100%) create mode 100644 src/app/shared/features/pagination/pagination.component.ts diff --git a/src/app/core/guard/authentication/auth.guard.spec.ts b/src/app/core/guard/authentication/auth.guard.spec.ts index a40d13f..fbc7e4b 100644 --- a/src/app/core/guard/authentication/auth.guard.spec.ts +++ b/src/app/core/guard/authentication/auth.guard.spec.ts @@ -2,10 +2,6 @@ import { TestBed } from '@angular/core/testing'; import { authGuard } from './auth.guard'; import { CanActivateFn, Router, UrlTree } from '@angular/router'; -import { AuthRepository } from '@app/domain/authentification/auth.repository'; -import { ProfileRepository } from '@app/domain/profiles/profile.repository'; -import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token'; -import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token'; import { AuthFacade } from '@app/ui/authentification/auth.facade'; describe('authGuard', () => { diff --git a/src/app/domain/profiles/profile.model.ts b/src/app/domain/profiles/profile.model.ts index b8b4796..e02e718 100644 --- a/src/app/domain/profiles/profile.model.ts +++ b/src/app/domain/profiles/profile.model.ts @@ -12,3 +12,11 @@ export interface Profile { projets: string[]; apropos: string; } + +export interface ProfilePaginated { + page: number; + perPage: number; + totalPages: number; + totalItems: number; + items: any[]; +} diff --git a/src/app/domain/profiles/profile.repository.ts b/src/app/domain/profiles/profile.repository.ts index 713a00a..440bad0 100644 --- a/src/app/domain/profiles/profile.repository.ts +++ b/src/app/domain/profiles/profile.repository.ts @@ -1,9 +1,9 @@ -import { Profile } from '@app/domain/profiles/profile.model'; +import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model'; import { Observable } from 'rxjs'; import { SearchFilters } from '@app/domain/search/search-filters'; export interface ProfileRepository { - list(params?: SearchFilters): Observable; + list(params?: SearchFilters): Observable; getByUserId(userId: string): Observable; create(profile: Profile): Observable; update(profileId: string, profile: Partial): Observable; diff --git a/src/app/domain/search/search-filters.ts b/src/app/domain/search/search-filters.ts index bb394e3..aad9154 100644 --- a/src/app/domain/search/search-filters.ts +++ b/src/app/domain/search/search-filters.ts @@ -5,5 +5,7 @@ export interface SearchFilters { profession: string | null; sort: string; page?: number; - pageSize?: number; + perPage?: number; + totalPages?: number; + totalItems?: number; } diff --git a/src/app/infrastructure/profiles/pb-profile.repository.ts b/src/app/infrastructure/profiles/pb-profile.repository.ts index 5daa54f..7d04ac8 100644 --- a/src/app/infrastructure/profiles/pb-profile.repository.ts +++ b/src/app/infrastructure/profiles/pb-profile.repository.ts @@ -1,6 +1,6 @@ import { ProfileRepository } from '@app/domain/profiles/profile.repository'; import { from, Observable } from 'rxjs'; -import { Profile } from '@app/domain/profiles/profile.model'; +import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model'; import { Injectable } from '@angular/core'; import PocketBase from 'pocketbase'; import { environment } from '@env/environment'; @@ -15,14 +15,18 @@ export class PbProfileRepository implements ProfileRepository { expand: 'utilisateur', }; - list(params?: SearchFilters): Observable { + list(params?: SearchFilters): Observable { const requestOptions = { ...this.defaultOptions, sort: this.onSortSetting(params), filter: this.onFilterSetting(params).join(' && '), }; - return from(this.pb.collection('profiles').getFullList(requestOptions)); + return from( + this.pb + .collection('profiles') + .getList(params?.page, params?.perPage, requestOptions) + ); } getByUserId(userId: string): Observable { diff --git a/src/app/infrastructure/search/search.service.ts b/src/app/infrastructure/search/search.service.ts index d56eef7..c38c392 100644 --- a/src/app/infrastructure/search/search.service.ts +++ b/src/app/infrastructure/search/search.service.ts @@ -12,6 +12,8 @@ export class SearchService implements SearchRepository { secteur: null, profession: null, sort: 'recent', + page: 1, + perPage: 25, }); setFilters(filters: SearchFilters) { diff --git a/src/app/routes/profile/profile-list/profile-list.component.html b/src/app/routes/profile/profile-list/profile-list.component.html index 5a80dab..f41b10f 100644 --- a/src/app/routes/profile/profile-list/profile-list.component.html +++ b/src/app/routes/profile/profile-list/profile-list.component.html @@ -70,13 +70,17 @@ d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" /> - {{ profiles().length }} profil(s) + {{ profilePaginated().items.length }} profil(s)
- + +
+ +
+
} diff --git a/src/app/routes/profile/profile-list/profile-list.component.ts b/src/app/routes/profile/profile-list/profile-list.component.ts index 250813a..9e9ba17 100644 --- a/src/app/routes/profile/profile-list/profile-list.component.ts +++ b/src/app/routes/profile/profile-list/profile-list.component.ts @@ -6,34 +6,42 @@ import { ProfileFacade } from '@app/ui/profiles/profile.facade'; import { LoadingComponent } from '@app/shared/components/loading/loading.component'; import { Router } from '@angular/router'; import { SearchFilters } from '@app/domain/search/search-filters'; -import { SearchService } from '@app/infrastructure/search/search.service'; import { FilterComponent } from '@app/shared/features/filter/filter.component'; +import { PaginationComponent } from '@app/shared/features/pagination/pagination.component'; @Component({ selector: 'app-profile-list', standalone: true, - imports: [SearchComponent, VerticalProfileListComponent, LoadingComponent, FilterComponent], + imports: [ + SearchComponent, + VerticalProfileListComponent, + LoadingComponent, + FilterComponent, + PaginationComponent, + ], templateUrl: './profile-list.component.html', styleUrl: './profile-list.component.scss', }) @UntilDestroy() export class ProfileListComponent { - private readonly searchService = inject(SearchService); private readonly facade = inject(ProfileFacade); private readonly router = inject(Router); - protected readonly profiles = this.facade.profiles; + protected readonly searchFilters = this.facade.searchFilters; + protected readonly profilePaginated = this.facade.profilePaginated; protected readonly loading = this.facade.loading; protected readonly error = this.facade.error; - protected readonly searchFilters = this.searchService.getFilters(); - showNewQuery(filters: SearchFilters) { - this.facade.load(this.searchFilters()); + this.facade.load(filters); this.router.navigate(['/profiles'], { queryParams: { search: filters.search } }); } onFilterChange(filters: SearchFilters) { this.facade.load(filters); } + + onPageChange(filters: SearchFilters) { + this.facade.load(filters); + } } diff --git a/src/app/shared/components/pagination/pagination.component.html b/src/app/shared/components/pagination/pagination.component.html deleted file mode 100644 index 37d1375..0000000 --- a/src/app/shared/components/pagination/pagination.component.html +++ /dev/null @@ -1,36 +0,0 @@ - - diff --git a/src/app/shared/components/pagination/pagination.component.ts b/src/app/shared/components/pagination/pagination.component.ts deleted file mode 100644 index f75e00d..0000000 --- a/src/app/shared/components/pagination/pagination.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-pagination', - standalone: true, - imports: [], - templateUrl: './pagination.component.html', - styleUrl: './pagination.component.scss', -}) -export class PaginationComponent {} diff --git a/src/app/shared/components/project-list/project-list.component.html b/src/app/shared/components/project-list/project-list.component.html index 000848a..0996bd1 100644 --- a/src/app/shared/components/project-list/project-list.component.html +++ b/src/app/shared/components/project-list/project-list.component.html @@ -1,6 +1,6 @@
-
+
@for (project of projects(); track project.id) { } @empty { diff --git a/src/app/shared/features/pagination/pagination.component.html b/src/app/shared/features/pagination/pagination.component.html new file mode 100644 index 0000000..0c753d3 --- /dev/null +++ b/src/app/shared/features/pagination/pagination.component.html @@ -0,0 +1,38 @@ + +
+
    +
  • + +
  • + +
  • + + {{ currentPage }} / {{ filters.totalPages! }} + +
  • + +
  • + +
  • +
+
diff --git a/src/app/shared/components/pagination/pagination.component.scss b/src/app/shared/features/pagination/pagination.component.scss similarity index 100% rename from src/app/shared/components/pagination/pagination.component.scss rename to src/app/shared/features/pagination/pagination.component.scss diff --git a/src/app/shared/components/pagination/pagination.component.spec.ts b/src/app/shared/features/pagination/pagination.component.spec.ts similarity index 100% rename from src/app/shared/components/pagination/pagination.component.spec.ts rename to src/app/shared/features/pagination/pagination.component.spec.ts diff --git a/src/app/shared/features/pagination/pagination.component.ts b/src/app/shared/features/pagination/pagination.component.ts new file mode 100644 index 0000000..165633d --- /dev/null +++ b/src/app/shared/features/pagination/pagination.component.ts @@ -0,0 +1,34 @@ +import { Component, Input, OnInit, output } from '@angular/core'; +import { SearchFilters } from '@app/domain/search/search-filters'; + +@Component({ + selector: 'app-pagination', + standalone: true, + imports: [], + templateUrl: './pagination.component.html', + styleUrl: './pagination.component.scss', +}) +export class PaginationComponent implements OnInit { + @Input({ required: true }) filters: SearchFilters = {} as SearchFilters; + onPageChange = output(); + currentPage = 1; + + ngOnInit() { + this.currentPage = this.filters.page!; + } + + goToPreviousPage() { + this.currentPage = this.currentPage - 1; + this.emitPageChange(); + } + + goToNextPage() { + this.currentPage = this.currentPage + 1; + this.emitPageChange(); + } + + private emitPageChange() { + const filters = { ...this.filters, page: this.currentPage }; + this.onPageChange.emit(filters); + } +} diff --git a/src/app/testing/domain/profiles/fake-profile.repository.ts b/src/app/testing/domain/profiles/fake-profile.repository.ts index 920a1f1..c54291c 100644 --- a/src/app/testing/domain/profiles/fake-profile.repository.ts +++ b/src/app/testing/domain/profiles/fake-profile.repository.ts @@ -1,11 +1,11 @@ -import { Profile } from '@app/domain/profiles/profile.model'; -import { mockProfiles } from '@app/testing/profile.mock'; +import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model'; +import { mockProfilePaginated, mockProfiles } from '@app/testing/profile.mock'; import { ProfileRepository } from '@app/domain/profiles/profile.repository'; import { Observable, of } from 'rxjs'; export class FakeProfileRepository implements ProfileRepository { - list(): Observable { - return of(mockProfiles); + list(): Observable { + return of(mockProfilePaginated); } getByUserId(userId: string): Observable { diff --git a/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts b/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts index 40db7ba..108be45 100644 --- a/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts +++ b/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts @@ -2,7 +2,7 @@ import { PbProfileRepository } from '@app/infrastructure/profiles/pb-profile.rep import { Profile } from '@app/domain/profiles/profile.model'; import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto'; import PocketBase from 'pocketbase'; -import { mockProfiles } from '@app/testing/profile.mock'; +import { mockProfilePaginated, mockProfiles } from '@app/testing/profile.mock'; jest.mock('pocketbase'); // on mock le module PocketBase @@ -14,7 +14,7 @@ describe('PbProfileRepository', () => { beforeEach(() => { // Création d’un faux client PocketBase avec les méthodes dont on a besoin mockCollection = { - getFullList: jest.fn(), + getList: jest.fn(), getFirstListItem: jest.fn(), create: jest.fn(), update: jest.fn(), @@ -35,8 +35,8 @@ describe('PbProfileRepository', () => { // ------------------------------------------ // 🔹 TEST : list() // ------------------------------------------ - it('devrait appeler pb.collection("profiles").getFullList() avec un tri par profession', (done) => { - mockCollection.getFullList.mockResolvedValue(mockProfiles); + it('devrait appeler pb.collection("profiles").getList() avec un tri par profession', (done) => { + mockCollection.getList.mockResolvedValue(mockProfilePaginated); const options = { expand: 'utilisateur', filter: @@ -45,8 +45,8 @@ describe('PbProfileRepository', () => { }; repo.list().subscribe((result) => { expect(mockPocketBase.collection).toHaveBeenCalledWith('profiles'); - expect(mockCollection.getFullList).toHaveBeenCalledWith(options); - expect(result).toEqual(mockProfiles); + expect(mockCollection.getList).toHaveBeenCalledWith(undefined, undefined, options); + expect(result).toEqual(mockProfilePaginated); done(); }); }); diff --git a/src/app/testing/profile.mock.ts b/src/app/testing/profile.mock.ts index 79d3c79..7d316da 100644 --- a/src/app/testing/profile.mock.ts +++ b/src/app/testing/profile.mock.ts @@ -1,4 +1,4 @@ -import { Profile } from '@app/domain/profiles/profile.model'; +import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model'; export const mockProfiles: Profile[] = [ { @@ -30,3 +30,11 @@ export const mockProfiles: Profile[] = [ apropos: 'Designer Freelance', }, ]; + +export const mockProfilePaginated: ProfilePaginated = { + page: 1, + perPage: 10, + totalPages: 1, + totalItems: 1, + items: mockProfiles, +}; diff --git a/src/app/testing/usecase/profiles/list-profiles.usecase.spec.ts b/src/app/testing/usecase/profiles/list-profiles.usecase.spec.ts index 13f9819..9943e8d 100644 --- a/src/app/testing/usecase/profiles/list-profiles.usecase.spec.ts +++ b/src/app/testing/usecase/profiles/list-profiles.usecase.spec.ts @@ -1,6 +1,6 @@ import { ListProfilesUseCase } from '@app/usecase/profiles/list-profiles.usecase'; import { FakeProfileRepository } from '@app/testing/domain/profiles/fake-profile.repository'; -import { mockProfiles } from '@app/testing/profile.mock'; +import { mockProfilePaginated, mockProfiles } from '@app/testing/profile.mock'; describe('ListProfilesUseCase', () => { it('doit retourner la liste des profils', () => { @@ -9,8 +9,8 @@ describe('ListProfilesUseCase', () => { useCase.execute().subscribe({ next: (profiles) => { - expect(profiles.length).toBe(2); - expect(profiles).toEqual(mockProfiles); + expect(profiles.items.length).toBe(2); + expect(profiles).toEqual(mockProfilePaginated); }, }); }); diff --git a/src/app/ui/profiles/profile.facade.ts b/src/app/ui/profiles/profile.facade.ts index aa3fba3..9521a75 100644 --- a/src/app/ui/profiles/profile.facade.ts +++ b/src/app/ui/profiles/profile.facade.ts @@ -1,7 +1,7 @@ import { ListProfilesUseCase } from '@app/usecase/profiles/list-profiles.usecase'; import { inject, Injectable, signal } from '@angular/core'; import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token'; -import { Profile } from '@app/domain/profiles/profile.model'; +import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model'; import { ProfilePresenter } from '@app/ui/profiles/profile.presenter'; import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model'; import { LoaderAction } from '@app/domain/loader-action.util'; @@ -12,19 +12,23 @@ import { UpdateProfileUseCase } from '@app/usecase/profiles/update-profile.useca import { GetProfileUseCase } from '@app/usecase/profiles/get-profile.usecase'; import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto'; import { SearchFilters } from '@app/domain/search/search-filters'; +import { SearchService } from '@app/infrastructure/search/search.service'; @Injectable({ providedIn: 'root', }) export class ProfileFacade { private profileRepository = inject(PROFILE_REPOSITORY_TOKEN); + private readonly searchService = inject(SearchService); private listUseCase = new ListProfilesUseCase(this.profileRepository); private createUseCase = new CreateProfileUseCase(this.profileRepository); private updateUseCase = new UpdateProfileUseCase(this.profileRepository); private getUseCase = new GetProfileUseCase(this.profileRepository); + readonly searchFilters = this.searchService.getFilters(); readonly profiles = signal([]); + readonly profilePaginated = signal({} as ProfilePaginated); readonly profile = signal({} as ProfileViewModel); readonly loading = signal({ isLoading: false, action: ActionType.NONE }); readonly error = signal({ @@ -36,9 +40,25 @@ export class ProfileFacade { load(search?: SearchFilters) { this.handleError(ActionType.READ, false, null, true); + if (search === undefined || search === null) { + search = this.searchFilters(); + } + this.listUseCase.execute(search).subscribe({ - next: (profiles) => { - this.profiles.set(ProfilePresenter.toViewModels(profiles)); + next: (profilePaginated: ProfilePaginated) => { + const filters = { + ...this.searchFilters(), + page: profilePaginated.page, + perPage: profilePaginated.perPage, + totalItems: profilePaginated.totalItems, + totalPages: profilePaginated.totalPages, + }; + + this.searchService.setFilters(filters); + this.searchFilters.set(filters); + + this.profilePaginated.set(profilePaginated); + this.profiles.set(ProfilePresenter.toViewModels(profilePaginated.items as Profile[])); this.handleError(ActionType.READ, false, null, false); }, error: (err) => { diff --git a/src/app/usecase/profiles/list-profiles.usecase.ts b/src/app/usecase/profiles/list-profiles.usecase.ts index 155d87a..efdfcfe 100644 --- a/src/app/usecase/profiles/list-profiles.usecase.ts +++ b/src/app/usecase/profiles/list-profiles.usecase.ts @@ -1,12 +1,12 @@ import { ProfileRepository } from '@app/domain/profiles/profile.repository'; import { Observable } from 'rxjs'; -import { Profile } from '@app/domain/profiles/profile.model'; +import { ProfilePaginated } from '@app/domain/profiles/profile.model'; import { SearchFilters } from '@app/domain/search/search-filters'; export class ListProfilesUseCase { constructor(private readonly repo: ProfileRepository) {} - execute(params?: SearchFilters): Observable { + execute(params?: SearchFilters): Observable { return this.repo.list(params); } }