From afd7995dbf7a27b19ea996419db4f38792df84b8 Mon Sep 17 00:00:00 2001 From: Silke Date: Wed, 20 Jul 2022 14:26:56 +0200 Subject: [PATCH] feat: add paging bar to the list of line items (basket, requisition, order, quote) --- src/app/core/icon.module.ts | 2 + .../common/paging/paging.component.html | 44 ++++++++++ .../common/paging/paging.component.scss | 19 ++++ .../common/paging/paging.component.spec.ts | 87 +++++++++++++++++++ .../common/paging/paging.component.ts | 53 +++++++++++ .../line-item-list.component.html | 11 ++- .../line-item-list.component.spec.ts | 21 +++++ .../line-item-list.component.ts | 36 +++++++- src/app/shared/shared.module.ts | 3 +- src/assets/i18n/de_DE.json | 1 + src/assets/i18n/en_US.json | 1 + src/assets/i18n/fr_FR.json | 1 + 12 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/components/common/paging/paging.component.html create mode 100644 src/app/shared/components/common/paging/paging.component.scss create mode 100644 src/app/shared/components/common/paging/paging.component.spec.ts create mode 100644 src/app/shared/components/common/paging/paging.component.ts diff --git a/src/app/core/icon.module.ts b/src/app/core/icon.module.ts index d1b0537ced..519792fbab 100644 --- a/src/app/core/icon.module.ts +++ b/src/app/core/icon.module.ts @@ -4,6 +4,7 @@ import { config } from '@fortawesome/fontawesome-svg-core'; import { faAddressBook, faAngleDown, + faAngleLeft, faAngleRight, faAngleUp, faArrowAltCircleRight, @@ -59,6 +60,7 @@ export class IconModule { library.addIcons( faAddressBook, faAngleDown, + faAngleLeft, faAngleRight, faAngleUp, faArrowsAlt, diff --git a/src/app/shared/components/common/paging/paging.component.html b/src/app/shared/components/common/paging/paging.component.html new file mode 100644 index 0000000000..5eaee5130a --- /dev/null +++ b/src/app/shared/components/common/paging/paging.component.html @@ -0,0 +1,44 @@ + diff --git a/src/app/shared/components/common/paging/paging.component.scss b/src/app/shared/components/common/paging/paging.component.scss new file mode 100644 index 0000000000..01830837bb --- /dev/null +++ b/src/app/shared/components/common/paging/paging.component.scss @@ -0,0 +1,19 @@ +@import 'variables'; + +.pagination { + margin: 0; + list-style: none; + + li { + margin-right: ($space-default); + + &.active a { + color: $text-color-primary; + cursor: unset; + } + + &:last-child { + margin-right: 0; + } + } +} diff --git a/src/app/shared/components/common/paging/paging.component.spec.ts b/src/app/shared/components/common/paging/paging.component.spec.ts new file mode 100644 index 0000000000..e96f06dd8c --- /dev/null +++ b/src/app/shared/components/common/paging/paging.component.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MockComponent } from 'ng-mocks'; +import { spy, verify } from 'ts-mockito'; + +import { PagingComponent } from './paging.component'; + +describe('Paging Component', () => { + let component: PagingComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MockComponent(FaIconComponent), PagingComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PagingComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + component.currentPage = 1; + component.lastPage = 10; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should display paging navigation links if current page = 1', () => { + component.ngOnChanges(); + fixture.detectChanges(); + + expect(JSON.stringify(component.pageIndices)).toMatchInlineSnapshot(`"[1,2,3,4,5,6,-1,10]"`); + expect(element.querySelectorAll('button.btn')).toHaveLength(2); + expect(element.querySelectorAll('[data-testing-id=paging-link] a')).toHaveLength(7); + expect(element.innerHTML).toContain('...'); + }); + + it('should display paging navigation links if current page = last page', () => { + component.currentPage = 10; + component.ngOnChanges(); + fixture.detectChanges(); + + expect(JSON.stringify(component.pageIndices)).toMatchInlineSnapshot(`"[1,-1,5,6,7,8,9,10]"`); + expect(element.querySelectorAll('button.btn')).toHaveLength(2); + expect(element.querySelectorAll('[data-testing-id=paging-link] a')).toHaveLength(7); + expect(element.innerHTML).toContain('...'); + }); + + it('should display paging navigation links if current page is in the center', () => { + component.currentPage = 5; + component.ngOnChanges(); + fixture.detectChanges(); + + expect(JSON.stringify(component.pageIndices)).toMatchInlineSnapshot(`"[1,-1,3,4,5,6,7,-1,10]"`); + expect(element.querySelectorAll('[data-testing-id=paging-link] a')).toHaveLength(7); + expect(element.innerHTML).toContain('...'); + }); + + it('should navigate to the next page if the next button is clicked', () => { + component.ngOnChanges(); + fixture.detectChanges(); + + const emitter = spy(component.goToPage); + + (element.querySelector('button[data-testing-id="paging-next-button"]') as HTMLElement).click(); + + verify(emitter.emit(2)).once(); + }); + + it('should navigate to the previous page if the previous button is clicked', () => { + component.currentPage = 5; + component.ngOnChanges(); + fixture.detectChanges(); + + const emitter = spy(component.goToPage); + + (element.querySelector('button[data-testing-id="paging-previous-button"]') as HTMLElement).click(); + + verify(emitter.emit(4)).once(); + }); +}); diff --git a/src/app/shared/components/common/paging/paging.component.ts b/src/app/shared/components/common/paging/paging.component.ts new file mode 100644 index 0000000000..18bd5f7bce --- /dev/null +++ b/src/app/shared/components/common/paging/paging.component.ts @@ -0,0 +1,53 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; + +@Component({ + selector: 'ish-paging', + templateUrl: './paging.component.html', + styleUrls: ['./paging.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PagingComponent implements OnChanges { + @Input() currentPage: number; + @Input() lastPage: number; + + @Output() goToPage: EventEmitter = new EventEmitter(); + + pageIndices: number[] = []; + + ngOnChanges(): void { + if (this.currentPage && this.lastPage) { + this.pageIndices = this.getPages(this.currentPage, this.lastPage); + } + } + /** + * If the user changes the page the goToPage event is emitted + * + * @param page : changed page number + */ + setPage(page: number) { + this.goToPage.emit(page); + } + + /** + * Determines the page array - elements with the value of -1 will be shown as ... + * + * @param current current page + * @param total number of pages + * @returns pages array + */ + private getPages(current: number, total: number): number[] { + if (total <= 8) { + return [...Array(total).keys()].map(x => x + 1); + } + + if (current > 4) { + if (current >= total - 3) { + return [1, -1, total - 5, total - 4, total - 3, total - 2, total - 1, total]; + } else { + return [1, -1, current - 2, current - 1, current, current + 1, current + 2, -1, total]; + } + } + + return [1, 2, 3, 4, 5, 6, -1, total]; + } +} diff --git a/src/app/shared/components/line-item/line-item-list/line-item-list.component.html b/src/app/shared/components/line-item/line-item-list/line-item-list.component.html index 903863d1dd..f5a3a2e8fc 100644 --- a/src/app/shared/components/line-item/line-item-list/line-item-list.component.html +++ b/src/app/shared/components/line-item/line-item-list/line-item-list.component.html @@ -12,7 +12,7 @@
+
+
+ {{ 'shopping_cart.paging.items.label' | translate: { '0': lineItems.length } }} + +
+
+
{{ 'quote.items.total.label' | translate }}
diff --git a/src/app/shared/components/line-item/line-item-list/line-item-list.component.spec.ts b/src/app/shared/components/line-item/line-item-list/line-item-list.component.spec.ts index d4ac9372ce..f11abf0667 100644 --- a/src/app/shared/components/line-item/line-item-list/line-item-list.component.spec.ts +++ b/src/app/shared/components/line-item/line-item-list/line-item-list.component.spec.ts @@ -1,3 +1,4 @@ +import { SimpleChange, SimpleChanges } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; @@ -7,6 +8,7 @@ import { Price } from 'ish-core/models/price/price.model'; import { PricePipe } from 'ish-core/models/price/price.pipe'; import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data'; import { findAllCustomElements } from 'ish-core/utils/dev/html-query-utils'; +import { PagingComponent } from 'ish-shared/components/common/paging/paging.component'; import { LineItemListElementComponent } from 'ish-shared/components/line-item/line-item-list-element/line-item-list-element.component'; import { LineItemListComponent } from './line-item-list.component'; @@ -21,6 +23,7 @@ describe('Line Item List Component', () => { declarations: [ LineItemListComponent, MockComponent(LineItemListElementComponent), + MockComponent(PagingComponent), MockDirective(ProductContextDirective), MockPipe(PricePipe), ], @@ -42,12 +45,30 @@ describe('Line Item List Component', () => { }); it('should render sub components if basket changes', () => { + const changes: SimpleChanges = { + lineItems: new SimpleChange(false, component.lineItems, false), + }; + + component.ngOnChanges(changes); fixture.detectChanges(); expect(findAllCustomElements(element)).toMatchInlineSnapshot(` Array [ "ish-line-item-list-element", ] `); + expect(element.querySelector('ish-paging')).toBeFalsy(); + }); + + it('should display the paging bar if the number of lineitems exceeds the page size', () => { + component.pageSize = 1; + component.lineItems = [BasketMockData.getBasketItem(), BasketMockData.getBasketItem()]; + const changes: SimpleChanges = { + lineItems: new SimpleChange(false, component.lineItems, false), + }; + + component.ngOnChanges(changes); + fixture.detectChanges(); + expect(element.querySelector('ish-paging')).toBeTruthy(); }); describe('totals', () => { diff --git a/src/app/shared/components/line-item/line-item-list/line-item-list.component.ts b/src/app/shared/components/line-item/line-item-list/line-item-list.component.ts index 0954fc0c3e..0defb7ca2c 100644 --- a/src/app/shared/components/line-item/line-item-list/line-item-list.component.ts +++ b/src/app/shared/components/line-item/line-item-list/line-item-list.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { LineItemView } from 'ish-core/models/line-item/line-item.model'; import { OrderLineItem } from 'ish-core/models/order/order.model'; @@ -23,13 +23,45 @@ import { Price } from 'ish-core/models/price/price.model'; templateUrl: './line-item-list.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LineItemListComponent { +export class LineItemListComponent implements OnChanges { @Input() lineItems: Partial[]; @Input() editable = true; @Input() total: Price; @Input() lineItemViewType: 'simple' | 'availability'; + /** + * If pageSize > 0 only items are shown at once and a paging bar is shown below the line item list. + */ + @Input() pageSize = 25; + + currentPage = 1; + lastPage: number; + displayItems: Partial[] = []; + + ngOnChanges(c: SimpleChanges) { + if (c.lineItems) { + this.lastPage = Math.ceil(this.lineItems?.length / this.pageSize); + this.goToPage(this.currentPage); + } + } + + get showPagingBar() { + return this.pageSize && this.lineItems?.length > this.pageSize; + } trackByFn(_: number, item: Partial) { return item.productSKU; } + + /** + * Refresh items to display after changing the current page + * + * @param page current page + */ + goToPage(page: number) { + this.currentPage = page; + + this.displayItems = this.pageSize + ? this.lineItems.slice((page - 1) * this.pageSize, page * this.pageSize) + : this.lineItems; + } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 7d4ee13455..360239e981 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -83,6 +83,7 @@ import { InfoMessageComponent } from './components/common/info-message/info-mess import { LoadingComponent } from './components/common/loading/loading.component'; import { ModalDialogLinkComponent } from './components/common/modal-dialog-link/modal-dialog-link.component'; import { ModalDialogComponent } from './components/common/modal-dialog/modal-dialog.component'; +import { PagingComponent } from './components/common/paging/paging.component'; import { SuccessMessageComponent } from './components/common/success-message/success-message.component'; import { FilterCheckboxComponent } from './components/filter/filter-checkbox/filter-checkbox.component'; import { FilterCollapsibleComponent } from './components/filter/filter-collapsible/filter-collapsible.component'; @@ -282,7 +283,7 @@ const exportedComponents = [ @NgModule({ imports: [...importExportModules], - declarations: [...declaredComponents, ...exportedComponents], + declarations: [...declaredComponents, ...exportedComponents, PagingComponent], exports: [...exportedComponents, ...importExportModules], }) export class SharedModule { diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index 93d35d9bb4..36388bea47 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -1058,6 +1058,7 @@ "shopping_cart.ministatus.show_all_items.text": "Gehen Sie zum Warenkorb, um alle Ihre Artikel anzusehen.", "shopping_cart.ministatus.view_cart.link": "Warenkorb ansehen", "shopping_cart.oci.transfer_basket.button": "Warenkorb übertragen", + "shopping_cart.paging.items.label": "{{0, plural, one{# Artikel} other{# Artikel}}}", "shopping_cart.payment.creditCardExpiryDate.invalid.error": "Das Ablaufdatum der Kreditkarte muss eine Länge von 5 Zeichen und das Format nn/nn haben, wobei n eine Zahl ist.", "shopping_cart.payment.creditCardNumberRange.invalid.error": "Die eingegebene Nummer ist für die gewählte Karte ungültig.", "shopping_cart.pli.qty.label": "Anzahl:", diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index 8bf05a4166..18dfe689fd 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -1058,6 +1058,7 @@ "shopping_cart.ministatus.show_all_items.text": "Go to the shopping cart to view all your items.", "shopping_cart.ministatus.view_cart.link": "View Cart", "shopping_cart.oci.transfer_basket.button": "Transfer Cart", + "shopping_cart.paging.items.label": "{{0, plural, one{# line item} other{# line items}}}", "shopping_cart.payment.creditCardExpiryDate.invalid.error": "The credit card expiration date must be 5 characters long and in the format nn/nn, where n is a number.", "shopping_cart.payment.creditCardNumberRange.invalid.error": "You entered an invalid number for the selected card.", "shopping_cart.pli.qty.label": "Quantity:", diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json index 2ab5041717..592960a777 100644 --- a/src/assets/i18n/fr_FR.json +++ b/src/assets/i18n/fr_FR.json @@ -1058,6 +1058,7 @@ "shopping_cart.ministatus.show_all_items.text": "Accédez au panier pour afficher tous vos articles.", "shopping_cart.ministatus.view_cart.link": "Afficher le panier", "shopping_cart.oci.transfer_basket.button": "Transférer le panier", + "shopping_cart.paging.items.label": "{{0, plural, one{# article} other{# articles}}}", "shopping_cart.payment.creditCardExpiryDate.invalid.error": "La date d’expiration de la carte de crédit doit contenir 5 caractères et être au format nn/nn, où n est un nombre.", "shopping_cart.payment.creditCardNumberRange.invalid.error": "Vous avez saisi un numéro non valide pour la carte sélectionnée.", "shopping_cart.pli.qty.label": "Quantité :",