Skip to content

Commit

Permalink
feat: add paging bar to the list of line items (basket, requisition, …
Browse files Browse the repository at this point in the history
…order, quote)
  • Loading branch information
SGrueber authored and MaxKless committed Jul 27, 2022
1 parent b6422c4 commit afd7995
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 4 deletions.
2 changes: 2 additions & 0 deletions src/app/core/icon.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { config } from '@fortawesome/fontawesome-svg-core';
import {
faAddressBook,
faAngleDown,
faAngleLeft,
faAngleRight,
faAngleUp,
faArrowAltCircleRight,
Expand Down Expand Up @@ -59,6 +60,7 @@ export class IconModule {
library.addIcons(
faAddressBook,
faAngleDown,
faAngleLeft,
faAngleRight,
faAngleUp,
faArrowsAlt,
Expand Down
44 changes: 44 additions & 0 deletions src/app/shared/components/common/paging/paging.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<nav *ngIf="currentPage && lastPage" aria-label="Pagination">
<ul class="pagination">
<li class="page-item">
<button
class="btn btn-primary mb-0"
[disabled]="currentPage === 1"
(click)="setPage(currentPage - 1)"
aria-label="Go To Previous Page"
[attr.aria-disabled]="currentPage === 1"
data-testing-id="paging-previous-button"
>
<fa-icon [icon]="['fas', 'angle-left']"></fa-icon>
</button>
</li>
<li
*ngFor="let p of pageIndices"
class="page-item pt-2"
[ngClass]="{ active: p === currentPage }"
data-testing-id="paging-link"
>
<a
*ngIf="p !== -1; else more"
[attr.aria-current]="p === currentPage ? 'page' : null"
[attr.aria-label]="p === currentPage ? 'Current Page, Page ' + p : 'Go to Page ' + p"
(click)="setPage(p)"
(keyup.enter)="setPage(p)"
>{{ p }}</a
>
<ng-template #more>...</ng-template>
</li>
<li class="page-item">
<button
class="btn btn-primary m-0"
[disabled]="currentPage === lastPage"
(click)="setPage(currentPage + 1)"
aria-label="Go To Next Page"
[attr.aria-disabled]="currentPage === lastPage"
data-testing-id="paging-next-button"
>
<fa-icon [icon]="['fas', 'angle-right']"></fa-icon>
</button>
</li>
</ul>
</nav>
19 changes: 19 additions & 0 deletions src/app/shared/components/common/paging/paging.component.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
87 changes: 87 additions & 0 deletions src/app/shared/components/common/paging/paging.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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<PagingComponent>;
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();
});
});
53 changes: 53 additions & 0 deletions src/app/shared/components/common/paging/paging.component.ts
Original file line number Diff line number Diff line change
@@ -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<number> = new EventEmitter<number>();

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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</div>
<div class="list-body">
<ish-line-item-list-element
*ngFor="let pli of lineItems; index as i; trackBy: trackByFn"
*ngFor="let pli of displayItems; index as i; trackBy: trackByFn"
ishProductContext
[sku]="pli.productSKU"
[quantity]="pli.quantity.value"
Expand All @@ -23,6 +23,15 @@
></ish-line-item-list-element>
</div>

<div *ngIf="showPagingBar" class="row">
<div
class="col-12 d-flex grey-panel align-items-center flex-column flex-md-row justify-content-between px-3 py-2 mb-3"
>
<span>{{ 'shopping_cart.paging.items.label' | translate: { '0': lineItems.length } }}</span>
<ish-paging [currentPage]="currentPage" [lastPage]="lastPage" (goToPage)="goToPage($event)"></ish-paging>
</div>
</div>

<div *ngIf="total?.value && lineItems.length > 0" class="clearfix section">
<div class="row justify-content-end list-body">
<div class="col-sm-4 col-md-3 col-lg-2 text-right pr-0">{{ 'quote.items.total.label' | translate }}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -21,6 +23,7 @@ describe('Line Item List Component', () => {
declarations: [
LineItemListComponent,
MockComponent(LineItemListElementComponent),
MockComponent(PagingComponent),
MockDirective(ProductContextDirective),
MockPipe(PricePipe),
],
Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<LineItemView & OrderLineItem>[];
@Input() editable = true;
@Input() total: Price;
@Input() lineItemViewType: 'simple' | 'availability';
/**
* If pageSize > 0 only <pageSize> 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<LineItemView & OrderLineItem>[] = [];

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<LineItemView & OrderLineItem>) {
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;
}
}
3 changes: 2 additions & 1 deletion src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -282,7 +283,7 @@ const exportedComponents = [

@NgModule({
imports: [...importExportModules],
declarations: [...declaredComponents, ...exportedComponents],
declarations: [...declaredComponents, ...exportedComponents, PagingComponent],
exports: [...exportedComponents, ...importExportModules],
})
export class SharedModule {
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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é :",
Expand Down

0 comments on commit afd7995

Please sign in to comment.