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/results-list/results-list.container.component.html b/libs/feature/search/src/lib/results-list/results-list.container.component.html index 45ba6814aa..40ff0b746a 100644 --- a/libs/feature/search/src/lib/results-list/results-list.container.component.html +++ b/libs/feature/search/src/lib/results-list/results-list.container.component.html @@ -9,12 +9,7 @@ [recordUrlGetter]="recordUrlGetter" (mdSelect)="onMetadataSelection($event)" > - +
{ }) describe('when there are no more results', () => { beforeEach(() => { - searchFacade.isEndOfResults$.next(true) + searchFacade.currentPage$.next(5) fixture.detectChanges() }) it('show-more element is hidden', () => { diff --git a/libs/feature/search/src/lib/results-list/results-list.container.component.ts b/libs/feature/search/src/lib/results-list/results-list.container.component.ts index 992f71ac29..697a9f04d2 100644 --- a/libs/feature/search/src/lib/results-list/results-list.container.component.ts +++ b/libs/feature/search/src/lib/results-list/results-list.container.component.ts @@ -7,7 +7,7 @@ import { Optional, Output, } from '@angular/core' -import { Observable, tap } from 'rxjs' +import { combineLatest, Observable, tap } from 'rxjs' import { filter, map } from 'rxjs/operators' import { SearchFacade } from '../state/search.facade' import { SearchError } from '../state/reducer' @@ -39,6 +39,7 @@ export class ResultsListContainerComponent implements OnInit { errorCode$: Observable errorMessage$: Observable pipelineForQualityScoreActivated: Observable + allowShowMore$: Observable errorTypes = ErrorType recordUrlGetter = this.getRecordUrl.bind(this) @@ -81,6 +82,16 @@ export class ResultsListContainerComponent implements OnInit { filter((error) => error !== null), map((error) => error.message) ) + this.allowShowMore$ = combineLatest([ + this.facade.isLoading$, + this.facade.currentPage$, + this.facade.totalPages$, + ]).pipe( + map( + ([loading, currentPage, totalPages]) => + !loading && currentPage < totalPages + ) + ) } onMetadataSelection(metadata: CatalogRecord): void { 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/elements/src/index.ts b/libs/ui/elements/src/index.ts index 1e62e06968..7d8c2c6537 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -15,8 +15,6 @@ export * from './lib/metadata-info/metadata-info.component' export * from './lib/metadata-quality-item/metadata-quality-item.component' export * from './lib/metadata-quality/metadata-quality.component' export * from './lib/notification/notification.component' -export * from './lib/pagination-buttons/pagination-buttons.component' -export * from './lib/pagination/pagination.component' export * from './lib/record-api-form/record-api-form.component' export * from './lib/related-record-card/related-record-card.component' export * from './lib/thumbnail/thumbnail.component' diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html b/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html deleted file mode 100644 index a6dbd5bac5..0000000000 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
- - - - - - {{ page }} - - - {{ page }} - - - - - -
-
diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts b/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts deleted file mode 100644 index 70cc9ecee5..0000000000 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { TranslateModule } from '@ngx-translate/core' -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { PaginationButtonsComponent } from './pagination-buttons.component' -import { FormsModule } from '@angular/forms' -import { action } from '@storybook/addon-actions' - -export default { - title: 'Elements/PaginationButtonsComponent', - component: PaginationButtonsComponent, - decorators: [ - moduleMetadata({ - imports: [ - ButtonComponent, - UtilI18nModule, - FormsModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], - }), - ], - render: (args: PaginationButtonsComponent) => ({ - props: { - ...args, - newCurrentPageEvent: action('newCurrentPageEvent'), - }, - }), -} as Meta - -export const Primary: StoryObj = { - args: { - currentPage: 1, - totalPages: 10, - }, - parameters: { - layout: 'centered', - }, -} diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts b/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts deleted file mode 100644 index b69928c602..0000000000 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - Component, - EventEmitter, - Input, - OnChanges, - Output, -} from '@angular/core' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { NgIcon, provideIcons } from '@ng-icons/core' -import { CommonModule } from '@angular/common' -import { iconoirNavArrowLeft, iconoirNavArrowRight } from '@ng-icons/iconoir' - -@Component({ - selector: 'gn-ui-pagination-buttons', - templateUrl: './pagination-buttons.component.html', - styleUrls: ['./pagination-buttons.component.css'], - standalone: true, - imports: [CommonModule, ButtonComponent, NgIcon], - viewProviders: [ - provideIcons({ - iconoirNavArrowRight, - iconoirNavArrowLeft, - }), - ], -}) -export class PaginationButtonsComponent implements OnChanges { - @Input() currentPage: number - @Input() totalPages: number - visiblePages: (number | '...')[] = [] - @Output() newCurrentPageEvent = new EventEmitter() - - ngOnChanges(): void { - this.calculateVisiblePages() - } - - calculateVisiblePages(): void { - const maxVisiblePages = 5 - const halfVisible = Math.floor(maxVisiblePages / 2) - const startPage = Math.max(this.currentPage - halfVisible, 1) - const endPage = Math.min(this.currentPage + halfVisible, this.totalPages) - - const visiblePages: (number | '...')[] = [] - if (startPage > 1) { - visiblePages.push(1) - if (startPage > 2) { - visiblePages.push('...') - } - } - for (let page = startPage; page <= endPage; page++) { - visiblePages.push(page) - } - if (endPage < this.totalPages) { - if (endPage < this.totalPages - 1) { - visiblePages.push('...') - } - visiblePages.push(this.totalPages) - } - - this.visiblePages = visiblePages - } - - changePage(page) { - this.setPage(page) - } - - nextPage() { - this.setPage(this.currentPage + 1) - } - - previousPage() { - this.setPage(this.currentPage - 1) - } - - setPage(newPage) { - if (!Number.isInteger(newPage)) return - this.currentPage = newPage - this.calculateVisiblePages() - this.newCurrentPageEvent.emit(this.currentPage) - } -} diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts b/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts deleted file mode 100644 index 8d81779b3d..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ChangeDetectionStrategy } from '@angular/core' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { By } from '@angular/platform-browser' -import { PaginationComponent } from './pagination.component' -import { TranslateModule } from '@ngx-translate/core' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' - -describe('PaginationComponent', () => { - let component: PaginationComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PaginationComponent, TranslateModule.forRoot()], - }) - .overrideComponent(PaginationComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() - - fixture = TestBed.createComponent(PaginationComponent) - component = fixture.componentInstance - component.currentPage = 10 - component.nPages = 10 - component.hideButton = false - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - describe('next button', () => { - let btn - describe('by default', () => { - beforeEach(() => { - btn = fixture.debugElement.query(By.css('gn-ui-button[type=secondary]')) - }) - it('is displayed', () => { - expect(btn).toBeTruthy() - }) - }) - describe('if hidden', () => { - beforeEach(() => { - component.hideButton = true - fixture.detectChanges() - btn = fixture.debugElement.query(By.css('gn-ui-button[type=secondary]')) - }) - it('is displayed', () => { - expect(btn).toBeFalsy() - }) - }) - }) - - it('should navigate_next be disabled', () => { - const isDisabled = fixture.debugElement.queryAll( - By.directive(ButtonComponent) - )[0].componentInstance.disabled - expect(isDisabled).toBeTruthy() - }) - - it('should navigate_previous be enabled', () => { - const isDisabled = fixture.debugElement.queryAll( - By.directive(ButtonComponent) - )[1].componentInstance.disabled - expect(isDisabled).toBeFalsy() - }) -}) diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts b/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts deleted file mode 100644 index 2879c62f0e..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { TranslateModule } from '@ngx-translate/core' -import { - componentWrapperDecorator, - Meta, - moduleMetadata, - StoryObj, -} from '@storybook/angular' -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' -import { PaginationComponent } from './pagination.component' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { FormsModule } from '@angular/forms' - -export default { - title: 'Elements/PaginationComponent', - component: PaginationComponent, - decorators: [ - moduleMetadata({ - imports: [ - ButtonComponent, - UtilI18nModule, - FormsModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], - }), - componentWrapperDecorator( - (story) => `
${story}
` - ), - ], -} as Meta - -export const Primary: StoryObj = { - args: { - currentPage: 1, - nPages: 10, - }, -} diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.ts b/libs/ui/elements/src/lib/pagination/pagination.component.ts deleted file mode 100644 index 67f5bd7546..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnChanges, - Output, - SimpleChanges, -} from '@angular/core' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { NgIcon, provideIcons } from '@ng-icons/core' -import { FormsModule } from '@angular/forms' -import { - matChevronLeft, - matChevronRight, -} from '@ng-icons/material-icons/baseline' -import { CommonModule } from '@angular/common' -import { TranslateModule } from '@ngx-translate/core' - -@Component({ - selector: 'gn-ui-pagination', - templateUrl: './pagination.component.html', - styleUrls: ['./pagination.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - CommonModule, - ButtonComponent, - NgIcon, - FormsModule, - TranslateModule, - ], - viewProviders: [ - provideIcons({ - matChevronLeft, - matChevronRight, - }), - ], -}) -export class PaginationComponent implements OnChanges { - @Input() currentPage = 1 - @Input() nPages = 1 - @Input() hideButton = false - @Output() newCurrentPageEvent = new EventEmitter() - - private applyPageBounds() { - // make sure this works with NaN inputs as well by adding `|| 1` - this.nPages = Math.max(1, this.nPages || 1) - this.currentPage = Math.max(1, Math.min(this.nPages, this.currentPage || 1)) - } - - ngOnChanges(changes: SimpleChanges) { - // make sure the inputs are valid - if ('currentPage' in changes || 'nPages' in changes) { - this.applyPageBounds() - } - } - - setPage(newPage) { - if (!Number.isInteger(newPage)) return - this.currentPage = newPage - this.applyPageBounds() - this.newCurrentPageEvent.emit(this.currentPage) - } - - nextPage() { - this.setPage(this.currentPage + 1) - } - - previousPage() { - this.setPage(this.currentPage - 1) - } -} diff --git a/libs/ui/inputs/src/index.ts b/libs/ui/inputs/src/index.ts index e87fa440fe..c55f95dca2 100644 --- a/libs/ui/inputs/src/index.ts +++ b/libs/ui/inputs/src/index.ts @@ -22,7 +22,6 @@ export * from './lib/text-input/text-input.component' export * from './lib/ui-inputs.module' export * from './lib/url-input/url-input.component' export * from './lib/viewport-intersector/viewport-intersector.component' -export * from './lib/previous-next-buttons/previous-next-buttons.component' export * from './lib/switch-toggle/switch-toggle.component' export * from './lib/file-input/file-input.component' export * from './lib/image-input/image-input.component' diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts deleted file mode 100644 index 47d8a272ba..0000000000 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' -import { PreviousNextButtonsComponent } from './previous-next-buttons.component' -import { TranslateModule } from '@ngx-translate/core' -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' - -export default { - title: 'Inputs/PreviousNextButtonsComponent', - component: PreviousNextButtonsComponent, - parameters: { - backgrounds: { - default: 'dark', - }, - }, - decorators: [ - moduleMetadata({ - imports: [ - UtilI18nModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], - }), - ], -} as Meta - -export const Primary: StoryObj = { - args: { - isFirst: true, - isLast: false, - }, - render: (args) => ({ - props: args, - template: - '', - }), -} diff --git a/libs/ui/layout/src/index.ts b/libs/ui/layout/src/index.ts index 00930a80ee..c8e05e4498 100644 --- a/libs/ui/layout/src/index.ts +++ b/libs/ui/layout/src/index.ts @@ -10,4 +10,9 @@ export * from './lib/sticky-header/sticky-header.component' export * from './lib/block-list/block-list.component' 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.css b/libs/ui/layout/src/lib/carousel/carousel.component.css index 33364704bc..09abc47b36 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.css +++ b/libs/ui/layout/src/lib/carousel/carousel.component.css @@ -6,19 +6,3 @@ position: relative; display: block; } - -.carousel-step-dot { - width: 6px; - height: 6px; - border-radius: 6px; - position: relative; -} - -.carousel-step-dot:after { - content: ''; - position: absolute; - left: -7px; - top: -7px; - width: 20px; - height: 20px; -} diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.html b/libs/ui/layout/src/lib/carousel/carousel.component.html index 0c2ae552d9..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() - } } diff --git a/libs/ui/layout/src/lib/paginable.interface.ts b/libs/ui/layout/src/lib/paginable.interface.ts new file mode 100644 index 0000000000..a696dbfa40 --- /dev/null +++ b/libs/ui/layout/src/lib/paginable.interface.ts @@ -0,0 +1,14 @@ +/** + * This interface is used for components that want to offer pagination + * Note: pages indexes are 1-based!! so `isLastPage` means `currentPage === pagesCount` + * and `isFirstPage` means `currentPage === 1` + */ +export interface Paginable { + isFirstPage: boolean + isLastPage: boolean + pagesCount: number + currentPage: number + goToPage(index: number): void + goToNextPage(): void + goToPrevPage(): void +} diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.css b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.css similarity index 100% rename from libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.css rename to libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.css diff --git a/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html new file mode 100644 index 0000000000..237fff4344 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html @@ -0,0 +1,51 @@ +
+
+ + + + + +
+ {{ page }} +
+
+ + ... + + + {{ page }} + +
+ + + +
+
diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts similarity index 74% rename from libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts rename to libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts index 71735ad743..dd63fd4793 100644 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts @@ -1,14 +1,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { PaginationButtonsComponent } from './pagination-buttons.component' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 1 + pagesCount = 5 + isFirstPage = true + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} describe('PaginationButtonsComponent', () => { let component: PaginationButtonsComponent let fixture: ComponentFixture - const mockChangePage = (page) => { - component.setPage(page) - } - beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PaginationButtonsComponent], @@ -16,19 +23,16 @@ describe('PaginationButtonsComponent', () => { fixture = TestBed.createComponent(PaginationButtonsComponent) component = fixture.componentInstance - component.currentPage = 3 - component.totalPages = 10 - component.calculateVisiblePages() - component.changePage = mockChangePage + component.listComponent = new MockPaginable() fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + describe('when using next arrow', () => { beforeEach(() => { - component.currentPage = 2 const paginationButtons = fixture.nativeElement.querySelectorAll('gn-ui-button') paginationButtons.forEach((buttonElement) => { @@ -39,12 +43,11 @@ describe('PaginationButtonsComponent', () => { }) }) it('should access next page on click', () => { - expect(component.currentPage).toBe(3) + expect(component.listComponent.goToNextPage).toHaveBeenCalled() }) }) describe('when using previous arrow', () => { beforeEach(() => { - component.currentPage = 4 const paginationButtons = fixture.nativeElement.querySelectorAll('gn-ui-button') paginationButtons.forEach((buttonElement) => { @@ -54,13 +57,13 @@ describe('PaginationButtonsComponent', () => { } }) }) - it('is should access previous page', () => { - expect(component.currentPage).toBe(3) + it('should access previous page on click', () => { + expect(component.listComponent.goToPrevPage).toHaveBeenCalled() }) }) describe('when accessing first page', () => { beforeEach(() => { - component.currentPage = 1 + component.listComponent.isFirstPage = true fixture.detectChanges() }) it('is should disable the previous arrow', () => { @@ -77,7 +80,7 @@ describe('PaginationButtonsComponent', () => { }) describe('when accessing last page', () => { beforeEach(() => { - component.currentPage = 10 + component.listComponent.isLastPage = true fixture.detectChanges() }) it('is should disable the next arrow', () => { @@ -94,19 +97,12 @@ describe('PaginationButtonsComponent', () => { }) describe('when clicking on page button', () => { beforeEach(() => { - const paginationButtons = - fixture.nativeElement.querySelectorAll('gn-ui-button') - const pageBtnList = [] - paginationButtons.forEach((buttonElement) => { - const ngIcon = buttonElement.querySelector('ng-icon') - if (!ngIcon) { - pageBtnList.push(buttonElement) - } - }) - pageBtnList[1].dispatchEvent(new Event('buttonClick')) + const paginationButton = + fixture.nativeElement.querySelector('[data-test=page-2]') + paginationButton.dispatchEvent(new Event('buttonClick')) }) - it('is should access the requested page', () => { - expect(component.currentPage).toBe(2) + it('should access the requested page', () => { + expect(component.listComponent.goToPage).toHaveBeenCalledWith(2) }) }) }) diff --git a/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts new file mode 100644 index 0000000000..b44214027c --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts @@ -0,0 +1,21 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { PaginationButtonsComponent } from './pagination-buttons.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PaginationButtonsComponent', + component: PaginationButtonsComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts new file mode 100644 index 0000000000..d696a1e89f --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts @@ -0,0 +1,50 @@ +import { Component, Input } from '@angular/core' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { iconoirNavArrowLeft, iconoirNavArrowRight } from '@ng-icons/iconoir' +import { Paginable } from '../paginable.interface' + +@Component({ + selector: 'gn-ui-pagination-buttons', + templateUrl: './pagination-buttons.component.html', + styleUrls: ['./pagination-buttons.component.css'], + standalone: true, + imports: [CommonModule, ButtonComponent, NgIcon], + viewProviders: [ + provideIcons({ + iconoirNavArrowRight, + iconoirNavArrowLeft, + }), + ], +}) +export class PaginationButtonsComponent { + @Input() listComponent: Paginable + + get visiblePages(): (number | '...')[] { + const maxVisiblePages = 5 + const halfVisible = Math.floor(maxVisiblePages / 2) + const startPage = Math.max(this.listComponent.currentPage - halfVisible, 1) + const endPage = Math.min( + this.listComponent.currentPage + halfVisible, + this.listComponent.pagesCount + ) + + const allPages = new Array(this.listComponent.pagesCount) + .fill(0) + .map((_, i) => i + 1) // pages are 1-based + return allPages.reduce((pages, page) => { + if (page === 1 || page === this.listComponent.pagesCount) { + // first and last page + pages.push(page) + } else if (page >= startPage && page <= endPage) { + // pages around current one + pages.push(page) + } else if (pages[pages.length - 1] !== '...') { + // dots between pages + pages.push('...') + } + return pages + }, []) + } +} diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css new file mode 100644 index 0000000000..7263eb6669 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css @@ -0,0 +1,16 @@ +.pagination-dot { + width: 6px; + height: 6px; + border-radius: 6px; + position: relative; + flex-shrink: 0; +} + +.pagination-dot:after { + content: ''; + position: absolute; + left: -7px; + top: -7px; + width: 20px; + height: 20px; +} diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html new file mode 100644 index 0000000000..923c8093f0 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html @@ -0,0 +1,15 @@ +
+ +
diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts new file mode 100644 index 0000000000..1a4795d31f --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { PaginationDotsComponent } from './pagination-dots.component' +import { By } from '@angular/platform-browser' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 4 + pagesCount = 5 + isFirstPage = false + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} + +describe('PaginationDotsComponent', () => { + let component: PaginationDotsComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PaginationDotsComponent], + }).compileComponents() + + fixture = TestBed.createComponent(PaginationDotsComponent) + component = fixture.componentInstance + component.listComponent = new MockPaginable() + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('dots', () => { + let dots: HTMLElement[] + beforeEach(() => { + dots = fixture.debugElement + .queryAll(By.css('.pagination-dot')) + .map((dot) => dot.nativeElement) + }) + it('has 1 dot per page', () => { + expect(dots.length).toBe(component.listComponent.pagesCount) + }) + it('switches to a page on click', () => { + dots[2].click() + expect(component.listComponent.goToPage).toHaveBeenCalledWith(3) // page index is 1-based + }) + it('shows selected page as active', () => { + expect(dots[2].classList).not.toContain('bg-primary') + expect(dots[3].classList).toContain('bg-primary') + expect(dots[4].classList).not.toContain('bg-primary') + }) + }) +}) diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts new file mode 100644 index 0000000000..cecd216eee --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts @@ -0,0 +1,30 @@ +import { + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { PaginationDotsComponent } from './pagination-dots.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PaginationDotsComponent', + component: PaginationDotsComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + componentWrapperDecorator( + (story) => + `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts new file mode 100644 index 0000000000..e989b0dc07 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts @@ -0,0 +1,31 @@ +import { Component, Input } from '@angular/core' +import { provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { iconoirNavArrowLeft, iconoirNavArrowRight } from '@ng-icons/iconoir' +import { Paginable } from '../paginable.interface' + +@Component({ + selector: 'gn-ui-pagination-dots', + templateUrl: './pagination-dots.component.html', + styleUrls: ['./pagination-dots.component.css'], + standalone: true, + imports: [CommonModule], + viewProviders: [ + provideIcons({ + iconoirNavArrowRight, + iconoirNavArrowLeft, + }), + ], +}) +export class PaginationDotsComponent { + @Input() listComponent: Paginable + @Input() containerClass = '' + + // 1-based + get steps() { + return Array.from( + { length: this.listComponent.pagesCount }, + (_, i) => i + 1 + ) + } +} diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.css b/libs/ui/layout/src/lib/pagination/pagination.component.css similarity index 100% rename from libs/ui/elements/src/lib/pagination/pagination.component.css rename to libs/ui/layout/src/lib/pagination/pagination.component.css diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.html b/libs/ui/layout/src/lib/pagination/pagination.component.html similarity index 72% rename from libs/ui/elements/src/lib/pagination/pagination.component.html rename to libs/ui/layout/src/lib/pagination/pagination.component.html index 9c9ac2a487..99d55d0a1e 100644 --- a/libs/ui/elements/src/lib/pagination/pagination.component.html +++ b/libs/ui/layout/src/lib/pagination/pagination.component.html @@ -1,9 +1,9 @@
pagination.pageOf {{ nPages }}pagination.pageOf + {{ listComponent.pagesCount }} { + let component: PaginationComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PaginationComponent, TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(PaginationComponent) + component = fixture.componentInstance + component.listComponent = new MockPaginable() + component.hideButton = false + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('next button', () => { + let btn: ButtonComponent + beforeEach(() => { + btn = fixture.debugElement.queryAll(By.directive(ButtonComponent))[0] + ?.componentInstance + }) + it('is displayed by default', () => { + expect(btn).toBeTruthy() + }) + it('is hidden if hideButton = true', () => { + component.hideButton = true + fixture.detectChanges() + expect( + fixture.debugElement.queryAll(By.directive(ButtonComponent)).length + ).toBe(2) + }) + it('is disabled if last page', () => { + component.listComponent.isLastPage = true + fixture.detectChanges() + expect(btn.disabled).toBe(true) + }) + it('goes to next page', () => { + btn.buttonClick.emit() + expect(component.listComponent.goToNextPage).toHaveBeenCalled() + }) + }) + + describe('prev and next buttons', () => { + let prevButton: ButtonComponent + let nextButton: ButtonComponent + beforeEach(() => { + prevButton = fixture.debugElement.queryAll( + By.directive(ButtonComponent) + )[1].componentInstance + nextButton = fixture.debugElement.queryAll( + By.directive(ButtonComponent) + )[2].componentInstance + }) + it('prev button disabled if first page', () => { + component.listComponent.isFirstPage = true + fixture.detectChanges() + expect(prevButton.disabled).toBe(true) + }) + it('prev button enabled if not first page', () => { + component.listComponent.isFirstPage = false + fixture.detectChanges() + expect(prevButton.disabled).toBe(false) + }) + it('calls goToPrevPage', () => { + prevButton.buttonClick.emit() + expect(component.listComponent.goToPrevPage).toHaveBeenCalled() + }) + it('next button disabled if last page', () => { + component.listComponent.isLastPage = true + fixture.detectChanges() + expect(nextButton.disabled).toBe(true) + }) + it('next button enabled if not last page', () => { + component.listComponent.isLastPage = false + fixture.detectChanges() + expect(nextButton.disabled).toBe(false) + }) + it('calls goToNextPage', () => { + nextButton.buttonClick.emit() + expect(component.listComponent.goToNextPage).toHaveBeenCalled() + }) + }) +}) diff --git a/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts b/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts new file mode 100644 index 0000000000..7f60821d94 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts @@ -0,0 +1,102 @@ +import { + applicationConfig, + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { PaginationComponent } from './pagination.component' +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + importProvidersFrom, +} from '@angular/core' +import { Paginable } from '../paginable.interface' +import { + TRANSLATE_DEFAULT_CONFIG, + UtilI18nModule, +} from '@geonetwork-ui/util/i18n' +import { TranslateModule } from '@ngx-translate/core' + +@Component({ + selector: 'gn-ui-mock-list', + template: `current page: {{ currentPage }}
+   +
+
pages count: {{ pagesCount }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class MockListComponent implements Paginable { + currentPage = 1 + pagesCount = 8 + constructor(private changeDetector: ChangeDetectorRef) {} + get isFirstPage() { + return this.currentPage == 1 + } + get isLastPage() { + return this.currentPage == this.pagesCount + } + goToPage(index: number) { + this.currentPage = index + this.changeDetector.detectChanges() + } + goToPrevPage() { + if (this.isFirstPage) return + this.goToPage(this.currentPage - 1) + } + goToNextPage() { + if (this.isLastPage) return + this.goToPage(this.currentPage + 1) + } +} + +export default { + title: 'Layout/Pagination/PaginationComponent', + component: PaginationComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + applicationConfig({ + providers: [ + importProvidersFrom(UtilI18nModule), + importProvidersFrom(TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG)), + ], + }), + componentWrapperDecorator( + (story) => + `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + hideButton: false, + }, + argTypes: { + hideButton: { + control: 'boolean', + }, + }, + render: (args) => ({ + props: args, + template: ` + +`, + }), +} diff --git a/libs/ui/layout/src/lib/pagination/pagination.component.ts b/libs/ui/layout/src/lib/pagination/pagination.component.ts new file mode 100644 index 0000000000..62bebe78d9 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination/pagination.component.ts @@ -0,0 +1,45 @@ +import { Component, Input } from '@angular/core' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { FormsModule } from '@angular/forms' +import { + matChevronLeft, + matChevronRight, +} from '@ng-icons/material-icons/baseline' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' +import { Paginable } from '../paginable.interface' + +@Component({ + selector: 'gn-ui-pagination', + templateUrl: './pagination.component.html', + styleUrls: ['./pagination.component.css'], + standalone: true, + imports: [ + CommonModule, + ButtonComponent, + NgIcon, + FormsModule, + TranslateModule, + ], + viewProviders: [ + provideIcons({ + matChevronLeft, + matChevronRight, + }), + ], +}) +export class PaginationComponent { + @Input() listComponent: Paginable + @Input() hideButton = false + + private applyPageBounds(page: number): number { + // make sure this works with NaN inputs as well by adding `|| 1` + return Math.max(1, Math.min(this.listComponent.pagesCount, page || 1)) + } + + setPage(newPage) { + if (!Number.isInteger(newPage)) return + this.listComponent.goToPage(this.applyPageBounds(newPage)) + } +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.css similarity index 100% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.css diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html similarity index 55% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html index ed38ca89ca..4ce91cf319 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html @@ -1,17 +1,17 @@
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts similarity index 80% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts index 95ac4c3eb5..588e4b5264 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts @@ -1,9 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' - import { PreviousNextButtonsComponent } from './previous-next-buttons.component' import { TranslateModule } from '@ngx-translate/core' import { By } from '@angular/platform-browser' import { DebugElement } from '@angular/core' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 1 + pagesCount = 5 + isFirstPage = true + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} describe('PreviousNextButtonsComponent', () => { let component: PreviousNextButtonsComponent @@ -19,6 +29,7 @@ describe('PreviousNextButtonsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(PreviousNextButtonsComponent) component = fixture.componentInstance + component.listComponent = new MockPaginable() compiled = fixture.debugElement }) @@ -28,8 +39,8 @@ describe('PreviousNextButtonsComponent', () => { describe('onFirstElement', () => { beforeEach(() => { - component.isFirst = true - component.isLast = false + component.listComponent.isFirstPage = true + component.listComponent.isLastPage = false fixture.detectChanges() }) @@ -48,8 +59,8 @@ describe('PreviousNextButtonsComponent', () => { describe('onLastElement', () => { beforeEach(() => { - component.isFirst = false - component.isLast = true + component.listComponent.isFirstPage = false + component.listComponent.isLastPage = true fixture.detectChanges() }) diff --git a/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts new file mode 100644 index 0000000000..a65bafa6ac --- /dev/null +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts @@ -0,0 +1,26 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { PreviousNextButtonsComponent } from './previous-next-buttons.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PreviousNextButtonsComponent', + component: PreviousNextButtonsComponent, + parameters: { + backgrounds: { + default: 'dark', + }, + }, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts similarity index 53% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts index 9e95996105..889428f6cf 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts @@ -1,11 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core' -import { ButtonComponent } from '../button/button.component' +import { Component, Input } from '@angular/core' import { NgIconComponent, provideIcons, @@ -15,12 +8,13 @@ import { matArrowBack, matArrowForward, } from '@ng-icons/material-icons/baseline' +import { Paginable } from '../paginable.interface' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' @Component({ selector: 'gn-ui-previous-next-buttons', templateUrl: './previous-next-buttons.component.html', styleUrls: ['./previous-next-buttons.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ButtonComponent, NgIconComponent], providers: [ @@ -31,16 +25,5 @@ import { ], }) export class PreviousNextButtonsComponent { - @Input() isFirst: boolean - @Input() isLast: boolean - - @Output() directionButtonClicked: EventEmitter = new EventEmitter() - - previousButtonClicked() { - this.directionButtonClicked.next('previous') - } - - nextButtonClicked() { - this.directionButtonClicked.next('next') - } + @Input() listComponent: Paginable }