From b799e33db3a1ebe9543300d925f1838b8778d6dd Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 9 Dec 2024 17:54:37 +0100 Subject: [PATCH] feat(layout): use pagination controls in components with lists --- .../src/e2e/datasetDetailPage.cy.ts | 4 +- .../organization-details.component.html | 21 ++--- .../organization-details.component.spec.ts | 58 ++----------- .../organization-details.component.ts | 83 ++++++++----------- .../record-apis/record-apis.component.html | 12 +-- .../record-apis/record-apis.component.ts | 26 +----- .../record-otherlinks.component.html | 6 +- .../record-otherlinks.component.ts | 33 ++------ .../app/records/records-list.component.html | 6 +- .../records/records-list.component.spec.ts | 7 +- .../src/app/records/records-list.component.ts | 44 ++++++++-- .../organisations.component.html | 6 +- .../organisations.component.spec.ts | 24 ++---- .../organisations/organisations.component.ts | 35 ++++++-- .../search/src/lib/state/search.facade.ts | 8 -- .../search/src/lib/state/selectors.spec.ts | 78 ----------------- .../feature/search/src/lib/state/selectors.ts | 18 ---- libs/ui/layout/src/index.ts | 1 + .../lib/block-list/block-list.component.css | 16 ---- .../lib/block-list/block-list.component.html | 16 +--- .../block-list/block-list.component.spec.ts | 14 ++-- .../block-list.component.stories.ts | 2 +- .../lib/block-list/block-list.component.ts | 31 ++++--- .../src/lib/carousel/carousel.component.html | 16 +--- .../lib/carousel/carousel.component.spec.ts | 22 ++--- .../src/lib/carousel/carousel.component.ts | 41 ++++----- 26 files changed, 212 insertions(+), 416 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 088b06fb4e..ec2ee577dc 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -529,12 +529,12 @@ describe('dataset pages', () => { }) it('should not display carousel dot button for 4 link cards', () => { cy.get('datahub-record-otherlinks') - .find('.carousel-step-dot') + .find('.pagination-dot') .should('exist') }) it('should not display carousel dot button for 2 API cards', () => { cy.get('datahub-record-apis') - .find('.carousel-step-dot') + .find('.pagination-dot') .should('not.exist') }) }) diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.html b/apps/datahub/src/app/organization/organization-details/organization-details.component.html index 6c27e7f32c..68d4b78084 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.html +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.html @@ -77,9 +77,7 @@
-
- -
+ diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts b/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts index 339dec1b85..4d43c8f2ac 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts @@ -1,9 +1,4 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - DebugElement, - NO_ERRORS_SCHEMA, -} from '@angular/core' +import { ChangeDetectionStrategy, DebugElement } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { SearchFacade } from '@geonetwork-ui/feature/search' import { TranslateModule } from '@ngx-translate/core' @@ -14,22 +9,6 @@ import { datasetRecordsFixture, someOrganizationsFixture, } from '@geonetwork-ui/common/fixtures' -import { AsyncPipe, NgForOf, NgIf } from '@angular/common' -import { - ButtonComponent, - PreviousNextButtonsComponent, -} from '@geonetwork-ui/ui/inputs' -import { - BlockListComponent, - CarouselComponent, - MaxLinesComponent, -} from '@geonetwork-ui/ui/layout' -import { LetDirective } from '@ngrx/component' -import { LinkCardComponent, UiElementsModule } from '@geonetwork-ui/ui/elements' -import { UiSearchModule } from '@geonetwork-ui/ui/search' -import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' -import { RouterLink } from '@angular/router' -import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { Organization } from '@geonetwork-ui/common/domain/model/record' import { RouterTestingModule } from '@angular/router/testing' import { By } from '@angular/platform-browser' @@ -37,10 +16,6 @@ import { ROUTER_ROUTE_SEARCH } from '@geonetwork-ui/feature/router' let getHTMLElement: (dataTest: string) => HTMLElement | undefined -const changeDetectorRefMock: Partial = { - markForCheck: jest.fn(), -} - class OrganisationsServiceMock { getFiltersForOrgs = jest.fn((orgs) => of({ @@ -58,7 +33,7 @@ const manyDatasets = datasetRecordsFixture().concat(datasetRecordsFixture()[0]) const organizationIsLoading = new BehaviorSubject(false) const totalPages = new BehaviorSubject(10) -const currentPage = new BehaviorSubject(0) +const currentPage = new BehaviorSubject(1) const results = new BehaviorSubject(manyDatasets) const desiredPageSize = 3 @@ -72,11 +47,9 @@ class SearchFacadeMock { results$ = results.asObservable() isLoading$ = organizationIsLoading.asObservable() totalPages$ = totalPages.asObservable() - isBeginningOfResults$ = of(currentPage.getValue() === 1) - isEndOfResults$ = of(totalPages.getValue() === currentPage.getValue()) currentPage$ = currentPage.asObservable() - paginate = jest.fn(() => { - currentPage.next(currentPage.getValue() + 1) + paginate = jest.fn((newPage) => { + currentPage.next(newPage) return new SearchFacadeMock() }) } @@ -89,25 +62,8 @@ describe('OrganizationDetailsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [], - schemas: [NO_ERRORS_SCHEMA], imports: [ - AsyncPipe, - NgIf, - ButtonComponent, - TranslateModule, - CarouselComponent, - BlockListComponent, - LetDirective, - LinkCardComponent, - NgForOf, - PreviousNextButtonsComponent, - UiElementsModule, - UiSearchModule, - MaxLinesComponent, - UiDatavizModule, - RouterLink, - UiWidgetsModule, + OrganizationDetailsComponent, TranslateModule.forRoot(), RouterTestingModule, ], @@ -120,10 +76,6 @@ describe('OrganizationDetailsComponent', () => { provide: SearchFacade, useClass: SearchFacadeMock, }, - { - provide: ChangeDetectorRef, - useValue: changeDetectorRefMock, - }, ], }) .overrideComponent(OrganizationDetailsComponent, { diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.ts b/apps/datahub/src/app/organization/organization-details/organization-details.component.ts index a089aca958..17245bf9f0 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.ts +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.ts @@ -4,28 +4,23 @@ import { Input, OnDestroy, OnInit, - ViewChild, } from '@angular/core' import { CommonModule } from '@angular/common' import { CatalogRecord, Organization, } from '@geonetwork-ui/common/domain/model/record' -import { - ButtonComponent, - PreviousNextButtonsComponent, -} from '@geonetwork-ui/ui/inputs' import { TranslateModule } from '@ngx-translate/core' import { - BlockListComponent, - CarouselComponent, MaxLinesComponent, + Paginable, + PaginationDotsComponent, + PreviousNextButtonsComponent, } from '@geonetwork-ui/ui/layout' import { LetDirective } from '@ngrx/component' import { ErrorComponent, ErrorType, - LinkCardComponent, RelatedRecordCardComponent, UiElementsModule, } from '@geonetwork-ui/ui/elements' @@ -57,12 +52,8 @@ import { startWith } from 'rxjs/operators' standalone: true, imports: [ CommonModule, - ButtonComponent, TranslateModule, - CarouselComponent, - BlockListComponent, LetDirective, - LinkCardComponent, PreviousNextButtonsComponent, UiElementsModule, UiSearchModule, @@ -73,33 +64,25 @@ import { startWith } from 'rxjs/operators' ErrorComponent, SpinningLoaderComponent, RelatedRecordCardComponent, + PaginationDotsComponent, ], }) -export class OrganizationDetailsComponent implements OnInit, OnDestroy { +export class OrganizationDetailsComponent + implements OnInit, OnDestroy, Paginable +{ protected readonly ErrorType = ErrorType protected readonly ROUTER_ROUTE_SEARCH = ROUTER_ROUTE_SEARCH - protected get pages() { - return new Array(this.totalPages).fill(0).map((_, i) => i + 1) - } - subscriptions$: Subscription = new Subscription() isSearchFacadeLoading = true - totalPages = 0 - currentPage = 1 - isFirstPage = this.currentPage === 1 - isLastPage = false - currentOrganization$ = new BehaviorSubject(null) @Input() set organization(value: Organization) { this.currentOrganization$.next(value) } @Input() paginationContainerClass = 'w-full bottom-0 top-auto' - @ViewChild(BlockListComponent) list: BlockListComponent - lastPublishedDatasets$: Observable = this.currentOrganization$.pipe( switchMap((organization) => @@ -129,17 +112,31 @@ export class OrganizationDetailsComponent implements OnInit, OnDestroy { } get hasPagination() { - return this.totalPages > 1 + return this.pagesCount > 1 } - changeStepOrPage(direction: string) { - if (direction === 'next') { - this.searchFacade.paginate(this.currentPage + 1) - } else { - this.searchFacade.paginate(this.currentPage - 1) - } - } + pagesCount_ = 0 + currentPage_ = 1 + // Paginable API + get currentPage() { + return this.currentPage_ + } + get pagesCount() { + return this.pagesCount_ + } + get isFirstPage() { + return this.currentPage === 1 + } + get isLastPage() { + return this.currentPage === this.pagesCount + } + goToPrevPage() { + this.searchFacade.paginate(this.currentPage - 1) + } + goToNextPage() { + this.searchFacade.paginate(this.currentPage + 1) + } goToPage(page: number) { this.searchFacade.paginate(page) } @@ -149,24 +146,12 @@ export class OrganizationDetailsComponent implements OnInit, OnDestroy { combineLatest([ this.searchFacade.isLoading$.pipe(distinctUntilChanged()), this.searchFacade.totalPages$.pipe(distinctUntilChanged()), - this.searchFacade.isBeginningOfResults$.pipe(distinctUntilChanged()), - this.searchFacade.isEndOfResults$.pipe(distinctUntilChanged()), this.searchFacade.currentPage$.pipe(distinctUntilChanged()), - ]).subscribe( - ([ - isSearchFacadeLoading, - totalPages, - isBeginningOfResults, - isEndOfResults, - currentPage, - ]) => { - this.isSearchFacadeLoading = isSearchFacadeLoading - this.totalPages = totalPages - this.isFirstPage = isBeginningOfResults - this.isLastPage = isEndOfResults - this.currentPage = currentPage - } - ) + ]).subscribe(([isSearchFacadeLoading, totalPages, currentPage]) => { + this.isSearchFacadeLoading = isSearchFacadeLoading + this.pagesCount_ = totalPages + this.currentPage_ = currentPage + }) ) } diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.html b/apps/datahub/src/app/record/record-apis/record-apis.component.html index dd22ded982..3ad41c9fba 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.html +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.html @@ -6,13 +6,15 @@ record.metadata.api

- + 1 - } - updateView() { this.changeDetector.detectChanges() } - get isFirstStep() { - return this.carousel?.isFirstStep - } - - get isLastStep() { - return this.carousel?.isLastStep - } - openRecordApiForm(link: DatasetServiceDistribution) { this.selectedApiLink = link this.setStyle(link) @@ -88,12 +78,4 @@ export class RecordApisComponent implements OnInit { this.maxHeight = link === undefined ? '0px' : '500px' this.opacity = link === undefined ? 0 : 1 } - - changeStepOrPage(direction: string) { - if (direction === 'next') { - this.carousel?.slideToNext() - } else { - this.carousel?.slideToPrevious() - } - } } diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html index b97bc8a202..90ca03c0d7 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html @@ -6,10 +6,8 @@ record.metadata.links

diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts index cef2c555c7..799d6dea65 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts @@ -6,8 +6,12 @@ import { ViewChild, } from '@angular/core' import { MdViewFacade } from '@geonetwork-ui/feature/record' -import { BlockListComponent, CarouselComponent } from '@geonetwork-ui/ui/layout' -import { PreviousNextButtonsComponent } from '@geonetwork-ui/ui/inputs' +import { + BlockListComponent, + CarouselComponent, + Paginable, + PreviousNextButtonsComponent, +} from '@geonetwork-ui/ui/layout' import { CommonModule } from '@angular/common' import { LinkCardComponent } from '@geonetwork-ui/ui/elements' import { LetDirective } from '@ngrx/component' @@ -34,34 +38,15 @@ export class RecordOtherlinksComponent implements AfterViewInit { @ViewChild(CarouselComponent) carousel: CarouselComponent @ViewChild(BlockListComponent) list: BlockListComponent + get paginableElement(): Paginable { + return this.carousel || this.list + } constructor( public facade: MdViewFacade, private changeDetector: ChangeDetectorRef ) {} - get isFirstStepOrPage() { - return this.carousel?.isFirstStep ?? this.list?.isFirstPage ?? true - } - - get isLastStepOrPage() { - return this.carousel?.isLastStep ?? this.list?.isLastPage ?? false - } - - get hasPagination() { - return (this.carousel?.stepsCount || this.list?.pagesCount) > 1 - } - - changeStepOrPage(direction: string) { - if (direction === 'next') { - this.list?.nextPage() - this.carousel?.slideToNext() - } else { - this.carousel?.slideToPrevious() - this.list?.previousPage() - } - } - updateView() { this.changeDetector.detectChanges() } diff --git a/apps/metadata-editor/src/app/records/records-list.component.html b/apps/metadata-editor/src/app/records/records-list.component.html index 215e2e950f..b5be0a5672 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.html +++ b/apps/metadata-editor/src/app/records/records-list.component.html @@ -7,10 +7,6 @@ >
- +
diff --git a/apps/metadata-editor/src/app/records/records-list.component.spec.ts b/apps/metadata-editor/src/app/records/records-list.component.spec.ts index 2a277e8594..f7e8f9a501 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.spec.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.spec.ts @@ -10,7 +10,7 @@ import { Router } from '@angular/router' import { BehaviorSubject } from 'rxjs' import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures' import { MockBuilder } from 'ng-mocks' -import { PaginationButtonsComponent } from '@geonetwork-ui/ui/elements' +import { PaginationButtonsComponent } from '@geonetwork-ui/ui/layout' const results = [{ md: true }] const currentPage = 5 @@ -97,8 +97,7 @@ describe('RecordsListComponent', () => { }) it('displays pagination', () => { expect(pagination).toBeTruthy() - expect(pagination.currentPage).toEqual(currentPage) - expect(pagination.totalPages).toEqual(totalPages) + expect(pagination.listComponent).toBe(component) }) describe('when click on a record', () => { const uniqueIdentifier = 123 @@ -128,7 +127,7 @@ describe('RecordsListComponent', () => { }) describe('when click on pagination', () => { beforeEach(() => { - pagination.newCurrentPageEvent.emit(3) + component.goToPage(3) }) it('paginates', () => { expect(searchService.setPage).toHaveBeenCalledWith(3) diff --git a/apps/metadata-editor/src/app/records/records-list.component.ts b/apps/metadata-editor/src/app/records/records-list.component.ts index 8f53f6d992..958b943f88 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.ts @@ -8,12 +8,10 @@ import { SearchService, } from '@geonetwork-ui/feature/search' import { UiSearchModule } from '@geonetwork-ui/ui/search' -import { - PaginationButtonsComponent, - UiElementsModule, -} from '@geonetwork-ui/ui/elements' +import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { TranslateModule } from '@ngx-translate/core' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { Paginable, PaginationButtonsComponent } from '@geonetwork-ui/ui/layout' export const allSearchFields = [ 'uuid', @@ -41,7 +39,7 @@ export const allSearchFields = [ PaginationButtonsComponent, ], }) -export class RecordsListComponent implements OnInit { +export class RecordsListComponent implements OnInit, Paginable { constructor( private router: Router, public searchFacade: SearchFacade, @@ -51,10 +49,13 @@ export class RecordsListComponent implements OnInit { ngOnInit(): void { this.searchFacade.setConfigRequestFields(allSearchFields) this.searchFacade.setPageSize(15) - } - paginate(page: number) { - this.searchService.setPage(page) + this.searchFacade.currentPage$.subscribe((page) => { + this.currentPage_ = page + }) + this.searchFacade.totalPages$.subscribe((total) => { + this.totalPages_ = total + }) } editRecord(record: CatalogRecord) { @@ -64,4 +65,31 @@ export class RecordsListComponent implements OnInit { duplicateRecord(record: CatalogRecord) { this.router.navigate(['/duplicate', record.uniqueIdentifier]) } + + // these are 0 based + totalPages_: number + currentPage_: number + + // Paginable API + get isFirstPage() { + return this.currentPage_ === 1 + } + get isLastPage() { + return this.currentPage_ === this.totalPages_ + } + get pagesCount() { + return this.totalPages_ + } + get currentPage() { + return this.currentPage_ + } + goToPage(page: number) { + this.searchService.setPage(page) + } + goToNextPage() { + this.searchService.setPage(this.currentPage_ + 1) + } + goToPrevPage() { + this.searchService.setPage(this.currentPage_ - 1) + } } diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.html b/libs/feature/catalog/src/lib/organisations/organisations.component.html index 3d253d16f3..0e6e6c1c2b 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.html +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.html @@ -26,9 +26,5 @@
- +
diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts b/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts index 8844d1ccb0..ea9ed82c75 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts @@ -15,7 +15,6 @@ import { OrganisationsFilterComponent, OrganisationsResultComponent, } from '@geonetwork-ui/ui/catalog' -import { PaginationComponent } from '@geonetwork-ui/ui/elements' class OrganisationsServiceMock { organisations$ = of(someOrganizationsFixture()) @@ -67,12 +66,7 @@ describe('OrganisationsComponent', () => { describe('on component init', () => { let orgPreviewComponents: OrganisationPreviewComponent[] let orgResultComponent: OrganisationsResultComponent - let paginationComponentDE: DebugElement - let setCurrentPageSpy let setSortBySpy - beforeEach(() => { - paginationComponentDE = de.query(By.directive(PaginationComponent)) - }) describe('pass organisations to ui preview components', () => { beforeEach(() => { orgPreviewComponents = de @@ -88,17 +82,16 @@ describe('OrganisationsComponent', () => { }) describe('pass params to ui pagination component', () => { it('should init ui pagination component with currentPage = 1', () => { - expect(paginationComponentDE.componentInstance.currentPage).toEqual(1) + expect(component.currentPage).toEqual(1) }) it('should init ui pagination component with correct value for total nPages', () => { - expect(paginationComponentDE.componentInstance.nPages).toEqual( + expect(component.pagesCount).toEqual( Math.ceil(someOrganizationsFixture().length / ITEMS_ON_PAGE) ) }) - describe('navigate to second page (and trigger newCurrentPageEvent output)', () => { + describe('navigate to second page', () => { beforeEach(() => { - setCurrentPageSpy = jest.spyOn(component, 'setCurrentPage') - paginationComponentDE.triggerEventHandler('newCurrentPageEvent', 2) + component.goToPage(2) fixture.detectChanges() orgPreviewComponents = de .queryAll(By.directive(OrganisationPreviewComponent)) @@ -107,11 +100,8 @@ describe('OrganisationsComponent', () => { afterEach(() => { jest.restoreAllMocks() }) - it('should call setcurrentPage() with correct value', () => { - expect(setCurrentPageSpy).toHaveBeenCalledWith(2) - }) it('should set currentPage in ui component to correct value', () => { - expect(paginationComponentDE.componentInstance.currentPage).toEqual(2) + expect(component.currentPage).toEqual(2) }) it('should pass first organisation of second page (sorted by name-asc) to first ui preview component', () => { expect(orgPreviewComponents[0].organization.name).toEqual( @@ -127,12 +117,12 @@ describe('OrganisationsComponent', () => { it('should not change currentPage when sorting results', () => { component['setSortBy'](['desc', 'recordCount']) fixture.detectChanges() - expect(paginationComponentDE.componentInstance.currentPage).toEqual(2) + expect(component.currentPage).toEqual(2) }) it('should set currentPage to 1 when filtering to display results', () => { component['setFilterBy']('Data') fixture.detectChanges() - expect(paginationComponentDE.componentInstance.currentPage).toEqual(1) + expect(component.currentPage).toEqual(1) }) }) }) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.ts b/libs/feature/catalog/src/lib/organisations/organisations.component.ts index 565a631c68..e7517f6f1e 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.ts @@ -14,16 +14,14 @@ import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/orga import { SortByField } from '@geonetwork-ui/common/domain/model/search' import { createFuzzyFilter } from '@geonetwork-ui/util/shared' import { ORGANIZATION_PAGE_URL_TOKEN } from '../organization-url.token' -import { - ContentGhostComponent, - PaginationComponent, -} from '@geonetwork-ui/ui/elements' +import { ContentGhostComponent } from '@geonetwork-ui/ui/elements' import { CommonModule } from '@angular/common' import { OrganisationPreviewComponent, OrganisationsFilterComponent, OrganisationsResultComponent, } from '@geonetwork-ui/ui/catalog' +import { Paginable, PaginationComponent } from '@geonetwork-ui/ui/layout' @Component({ selector: 'gn-ui-organisations', @@ -40,7 +38,7 @@ import { PaginationComponent, ], }) -export class OrganisationsComponent { +export class OrganisationsComponent implements Paginable { @Input() itemsOnPage = 12 @Output() orgSelect = new EventEmitter() @@ -89,10 +87,6 @@ export class OrganisationsComponent { ) ) - protected setCurrentPage(page: number): void { - this.currentPage$.next(page) - } - protected setFilterBy(value: string): void { this.currentPage$.next(1) this.filterBy$.next(value) @@ -140,4 +134,27 @@ export class OrganisationsComponent { if (!this.urlTemplate) return null return this.urlTemplate.replace('${name}', organisation.name) } + + // Paginable API + get isFirstPage() { + return this.currentPage === 1 + } + get isLastPage() { + return this.currentPage === this.totalPages + } + get pagesCount() { + return this.totalPages + } + get currentPage() { + return this.currentPage$.value + } + goToPage(index: number) { + this.currentPage$.next(index) + } + goToNextPage() { + this.goToPage(this.currentPage + 1) + } + goToPrevPage() { + this.goToPage(this.currentPage - 1) + } } diff --git a/libs/feature/search/src/lib/state/search.facade.ts b/libs/feature/search/src/lib/state/search.facade.ts index 44c9c34e1e..1c03fb0848 100644 --- a/libs/feature/search/src/lib/state/search.facade.ts +++ b/libs/feature/search/src/lib/state/search.facade.ts @@ -38,8 +38,6 @@ import { getSearchResultsLoading, getSearchSortBy, getSpatialFilterEnabled, - isBeginningOfResults, - isEndOfResults, totalPages, } from './selectors' import { FILTER_GEOMETRY } from '../filter-geometry.token' @@ -60,8 +58,6 @@ export class SearchFacade { layout$: Observable sortBy$: Observable isLoading$: Observable - isBeginningOfResults$: Observable - isEndOfResults$: Observable totalPages$: Observable currentPage$: Observable pageSize$: Observable @@ -101,10 +97,6 @@ export class SearchFacade { this.isLoading$ = this.store.pipe(select(getSearchResultsLoading, searchId)) this.searchFilters$ = this.store.pipe(select(getSearchFilters, searchId)) this.resultsHits$ = this.store.pipe(select(getSearchResultsHits, searchId)) - this.isBeginningOfResults$ = this.store.pipe( - select(isBeginningOfResults, searchId) - ) - this.isEndOfResults$ = this.store.pipe(select(isEndOfResults, searchId)) this.totalPages$ = this.store.pipe(select(totalPages, searchId)) this.currentPage$ = this.store.pipe(select(currentPage, searchId)) this.pageSize$ = this.store.pipe(select(getPageSize, searchId)) diff --git a/libs/feature/search/src/lib/state/selectors.spec.ts b/libs/feature/search/src/lib/state/selectors.spec.ts index 4f610fc05d..210f3dd5ce 100644 --- a/libs/feature/search/src/lib/state/selectors.spec.ts +++ b/libs/feature/search/src/lib/state/selectors.spec.ts @@ -76,84 +76,6 @@ describe('Search Selectors', () => { }) }) - describe('isBeginningOfResults', () => { - it('should return true once at the beginning of results list', () => { - const beginningResult = fromSelectors.isBeginningOfResults.projector({ - ...initialStateSearch, - params: { - ...initialStateSearch.params, - currentPage: 0, - pageSize: 20, - }, - results: { - ...initialStateSearch.results, - count: 62, - }, - }) - expect(beginningResult).toEqual(true) - - const notBeginningResult = fromSelectors.isBeginningOfResults.projector({ - ...initialStateSearch, - params: { - ...initialStateSearch.params, - currentPage: 3, - pageSize: 20, - }, - results: { - ...initialStateSearch.results, - count: 62, - }, - }) - expect(notBeginningResult).toEqual(false) - }) - }) - - describe('isEndOfResults', () => { - it('should return true once at the end of results list', () => { - const result = fromSelectors.isEndOfResults.projector({ - ...initialStateSearch, - params: { - ...initialStateSearch.params, - currentPage: 0, - pageSize: 20, - }, - results: { - ...initialStateSearch.results, - count: 62, - }, - }) - expect(result).toEqual(false) - - const endResult = fromSelectors.isEndOfResults.projector({ - ...initialStateSearch, - params: { - ...initialStateSearch.params, - currentPage: 3, - pageSize: 20, - }, - results: { - ...initialStateSearch.results, - count: 62, - }, - }) - expect(endResult).toEqual(true) - - const exactEndOfResult = fromSelectors.isEndOfResults.projector({ - ...initialStateSearch, - params: { - ...initialStateSearch.params, - currentPage: 2, - pageSize: 20, - }, - results: { - ...initialStateSearch.results, - count: 60, - }, - }) - expect(exactEndOfResult).toEqual(true) - }) - }) - describe('totalPages', () => { it('returns correct page amount', () => { const result = fromSelectors.totalPages.projector({ diff --git a/libs/feature/search/src/lib/state/selectors.ts b/libs/feature/search/src/lib/state/selectors.ts index b2215ba3fc..338ee281cb 100644 --- a/libs/feature/search/src/lib/state/selectors.ts +++ b/libs/feature/search/src/lib/state/selectors.ts @@ -50,24 +50,6 @@ export const getSearchResultsHits = createSelector( (state: SearchStateSearch) => state.results.count ) -export const isBeginningOfResults = createSelector( - getSearchStateSearch, - (state: SearchStateSearch) => { - return state.params.currentPage === 0 - } -) - -export const isEndOfResults = createSelector( - getSearchStateSearch, - (state: SearchStateSearch) => { - return ( - state.params.currentPage * state.params.pageSize + - state.params.pageSize >= - state.results.count - ) - } -) - export const totalPages = createSelector( getSearchStateSearch, (state: SearchStateSearch) => { diff --git a/libs/ui/layout/src/index.ts b/libs/ui/layout/src/index.ts index 8233c4e3f1..c8e05e4498 100644 --- a/libs/ui/layout/src/index.ts +++ b/libs/ui/layout/src/index.ts @@ -12,6 +12,7 @@ export * from './lib/sortable-list/sortable-list.component' export * from './lib/modal-dialog/modal-dialog.component' export * from './lib/pagination/pagination.component' export * from './lib/pagination-buttons/pagination-buttons.component' +export * from './lib/pagination-dots/pagination-dots.component' export * from './lib/previous-next-buttons/previous-next-buttons.component' export * from './lib/paginable.interface' export * from './lib/ui-layout.module' diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.css b/libs/ui/layout/src/lib/block-list/block-list.component.css index 0d37f18585..c0add777fc 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.css +++ b/libs/ui/layout/src/lib/block-list/block-list.component.css @@ -5,19 +5,3 @@ :host { position: relative; } - -.list-page-dot { - width: 6px; - height: 6px; - border-radius: 6px; - position: relative; -} - -.list-page-dot:after { - content: ''; - position: absolute; - left: -7px; - top: -7px; - width: 20px; - height: 20px; -} diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.html b/libs/ui/layout/src/lib/block-list/block-list.component.html index 17e706ddd6..ca420ca667 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.html +++ b/libs/ui/layout/src/lib/block-list/block-list.component.html @@ -6,15 +6,7 @@ > -
- -
+ diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts index 2b28278dde..e3a945d87a 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts +++ b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts @@ -56,7 +56,7 @@ describe('BlockListComponent', () => { }) describe('click on step', () => { beforeEach(() => { - component.goToPage(1) + component.goToPage(2) }) it('updates visibility', () => { const blocksVisibility = blockEls.map( @@ -73,7 +73,7 @@ describe('BlockListComponent', () => { ]) }) it('emits the selected page', () => { - expect(component['currentPage']).toEqual(1) + expect(component['currentPage']).toEqual(2) }) }) describe('custom page size', () => { @@ -103,7 +103,7 @@ describe('BlockListComponent', () => { beforeEach(() => { component.pageSize = 2 component.goToPage(2) - component.previousPage() + component.goToPrevPage() }) it('changes to previous page', () => { expect(component['currentPage']).toEqual(1) @@ -114,7 +114,7 @@ describe('BlockListComponent', () => { beforeEach(() => { component.pageSize = 2 component.goToPage(1) - component.nextPage() + component.goToNextPage() }) it('changes to next page', () => { expect(component['currentPage']).toEqual(2) @@ -129,7 +129,7 @@ describe('BlockListComponent', () => { expect(component.isFirstPage).toBe(true) }) it('returns false if the current page is not the first one', () => { - component.goToPage(1) + component.goToPage(2) expect(component.isFirstPage).toBe(false) }) }) @@ -139,11 +139,11 @@ describe('BlockListComponent', () => { component.pageSize = 3 }) it('returns true if the current page is the last one', () => { - component.goToPage(2) + component.goToPage(3) expect(component.isLastPage).toBe(true) }) it('returns false if the current page is not the last one', () => { - component.goToPage(1) + component.goToPage(2) expect(component.isLastPage).toBe(false) }) }) diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts index 31f06fdd74..7e921a6115 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts +++ b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/angular' -import { BlockListComponent } from './block-list.component' import { componentWrapperDecorator } from '@storybook/angular' +import { BlockListComponent } from './block-list.component' const meta: Meta = { component: BlockListComponent, diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.ts b/libs/ui/layout/src/lib/block-list/block-list.component.ts index 5a0937c761..8a89f27eea 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.ts +++ b/libs/ui/layout/src/lib/block-list/block-list.component.ts @@ -10,6 +10,8 @@ import { ViewChild, } from '@angular/core' import { CommonModule } from '@angular/common' +import { Paginable } from '../paginable.interface' +import { PaginationDotsComponent } from '../pagination-dots/pagination-dots.component' @Component({ selector: 'gn-ui-block-list', @@ -17,9 +19,9 @@ import { CommonModule } from '@angular/common' styleUrls: ['./block-list.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule], + imports: [CommonModule, PaginationDotsComponent], }) -export class BlockListComponent implements AfterViewInit { +export class BlockListComponent implements AfterViewInit, Paginable { @Input() pageSize = 5 @Input() containerClass = '' @Input() paginationContainerClass = 'w-full bottom-0 top-auto' @@ -30,20 +32,23 @@ export class BlockListComponent implements AfterViewInit { protected minHeight = 0 - protected currentPage = 0 + protected currentPage_ = 0 protected get pages() { return new Array(this.pagesCount).fill(0).map((_, i) => i) } get isFirstPage() { - return this.currentPage === 0 + return this.currentPage_ === 0 } get isLastPage() { - return this.currentPage === this.pagesCount - 1 + return this.currentPage_ === this.pagesCount - 1 } get pagesCount() { return this.blocks ? Math.ceil(this.blocks.length / this.pageSize) : 1 } + get currentPage() { + return this.currentPage_ + 1 // this is 1-based + } constructor(private changeDetector: ChangeDetectorRef) {} @@ -59,25 +64,29 @@ export class BlockListComponent implements AfterViewInit { protected refreshBlocksVisibility = () => { this.blocks.forEach((block, index) => { block.nativeElement.style.display = - index >= this.currentPage * this.pageSize && - index < (this.currentPage + 1) * this.pageSize + index >= this.currentPage_ * this.pageSize && + index < (this.currentPage_ + 1) * this.pageSize ? null : 'none' }) } - public goToPage(index: number) { - this.currentPage = Math.max(Math.min(index, this.pagesCount - 1), 0) + // pageIndex is 1-based + public goToPage(pageIndex: number) { + this.currentPage_ = Math.max( + Math.min(pageIndex - 1, this.pagesCount - 1), + 0 + ) this.changeDetector.detectChanges() this.refreshBlocksVisibility() } - public previousPage() { + public goToPrevPage() { if (this.isFirstPage) return this.goToPage(this.currentPage - 1) } - public nextPage() { + public goToNextPage() { if (this.isLastPage) return this.goToPage(this.currentPage + 1) } diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.html b/libs/ui/layout/src/lib/carousel/carousel.component.html index bc78c8c816..a56a62cf71 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.html +++ b/libs/ui/layout/src/lib/carousel/carousel.component.html @@ -3,15 +3,7 @@ -
- -
+ diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts b/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts index 7301cb0d1a..87c18a7f89 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts +++ b/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts @@ -73,7 +73,7 @@ describe('CarouselComponent', () => { }) describe('click on step', () => { beforeEach(() => { - component.scrollToStep(2) + component.goToPage(3) }) it('calls #scrollTo', () => { expect(component.emblaApi.scrollTo).toHaveBeenCalledWith(2) @@ -88,30 +88,30 @@ describe('CarouselComponent', () => { it('emits the current step index', () => { const spy = jest.fn() component.currentStepChange.subscribe(spy) - component.scrollToStep(2) + component.goToPage(3) expect(spy).toHaveBeenCalledWith(2) expect(spy).toHaveBeenCalledTimes(1) }) }) - describe('isFirstStep', () => { + describe('isFirstPage', () => { it('returns true if the current step is the first one', () => { - expect(component.isFirstStep).toBe(true) + expect(component.isFirstPage).toBe(true) }) it('returns false if the current step is not the first one', () => { - component.scrollToStep(2) - expect(component.isFirstStep).toBe(false) + component.goToPage(3) + expect(component.isFirstPage).toBe(false) }) }) - describe('isLastStep', () => { + describe('isLastPage', () => { it('returns true if the current step is the last one', () => { - component.scrollToStep(3) - expect(component.isLastStep).toBe(true) + component.goToPage(4) + expect(component.isLastPage).toBe(true) }) it('returns false if the current step is not the last one', () => { - component.scrollToStep(1) - expect(component.isLastStep).toBe(false) + component.goToPage(2) + expect(component.isLastPage).toBe(false) }) }) }) diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.ts b/libs/ui/layout/src/lib/carousel/carousel.component.ts index abb751af2a..1e3b212ea7 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.ts +++ b/libs/ui/layout/src/lib/carousel/carousel.component.ts @@ -11,6 +11,8 @@ import { } from '@angular/core' import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel' import { CommonModule } from '@angular/common' +import { Paginable } from '../paginable.interface' +import { PaginationDotsComponent } from '../pagination-dots/pagination-dots.component' @Component({ selector: 'gn-ui-carousel', @@ -18,9 +20,9 @@ import { CommonModule } from '@angular/common' styleUrls: ['./carousel.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule], + imports: [CommonModule, PaginationDotsComponent], }) -export class CarouselComponent implements AfterViewInit { +export class CarouselComponent implements AfterViewInit, Paginable { @ViewChild('carouselOverflowContainer') carouselOverflowContainer: ElementRef @Input() containerClass = '' @@ -38,15 +40,30 @@ export class CarouselComponent implements AfterViewInit { this.changeDetector.detectChanges() } - get isFirstStep() { + // Paginable API + get isFirstPage() { return this.currentStep === 0 } - get isLastStep() { + get isLastPage() { return this.currentStep === this.steps.length - 1 } - get stepsCount() { + get currentPage() { + return this.currentStep + 1 // this is 1-based + } + get pagesCount() { return this.steps.length } + public goToPage(stepIndex: number) { + this.emblaApi.scrollTo(stepIndex - 1) // this is 0-based + } + public goToPrevPage() { + if (this.isFirstPage) return + this.emblaApi.scrollPrev() + } + public goToNextPage() { + if (this.isLastPage) return + this.emblaApi.scrollNext() + } constructor(private changeDetector: ChangeDetectorRef) {} @@ -63,18 +80,4 @@ export class CarouselComponent implements AfterViewInit { .on('reInit', this.refreshSteps) .on('select', this.refreshSteps) } - - public scrollToStep(stepIndex: number) { - this.emblaApi.scrollTo(stepIndex) - } - - public slideToPrevious() { - if (this.isFirstStep) return - this.emblaApi.scrollPrev() - } - - public slideToNext() { - if (this.isLastStep) return - this.emblaApi.scrollNext() - } }