Skip to content

Commit

Permalink
feat: display product attachments at the product detail page (#840)
Browse files Browse the repository at this point in the history
  • Loading branch information
suschneider authored Aug 31, 2021
1 parent ba895e7 commit d275b95
Show file tree
Hide file tree
Showing 19 changed files with 221 additions and 9 deletions.
9 changes: 9 additions & 0 deletions src/app/core/models/attachment/attachment.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Link } from 'ish-core/models/link/link.model';

export interface AttachmentData {
name: string;
type: string;
key: string;
description?: string;
link: Link;
}
48 changes: 48 additions & 0 deletions src/app/core/models/attachment/attachment.mapper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';

import { getICMServerURL } from 'ish-core/store/core/configuration';

import { AttachmentData } from './attachment.interface';
import { AttachmentMapper } from './attachment.mapper';

describe('Attachment Mapper', () => {
let attachmentMapper: AttachmentMapper;

const attachmentsMockData = [
{
name: 'attachment1',
type: 'Information',
key: 'key1',
description: 'descr1',
link: {
type: 'Link',
uri: 'inSPIRED-inTRONICS-Site/rest;loc=en_US/attachments/attachment1.pdf',
title: 'attachment1',
},
},
] as AttachmentData[];

beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideMockStore({ selectors: [{ selector: getICMServerURL, value: 'http://example.org' }] })],
});
attachmentMapper = TestBed.inject(AttachmentMapper);
});

describe('fromAttachments', () => {
it('should map attachment data to client array object', () => {
expect(attachmentMapper.fromAttachments(attachmentsMockData)).toMatchInlineSnapshot(`
Array [
Object {
"description": "descr1",
"key": "key1",
"name": "attachment1",
"type": "Information",
"url": "http://example.org/inSPIRED-inTRONICS-Site/rest;loc=en_US/attachments/attachment1.pdf",
},
]
`);
});
});
});
33 changes: 33 additions & 0 deletions src/app/core/models/attachment/attachment.mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';

import { Attachment } from 'ish-core/models/attachment/attachment.model';
import { getICMServerURL } from 'ish-core/store/core/configuration';

import { AttachmentData } from './attachment.interface';

@Injectable({ providedIn: 'root' })
export class AttachmentMapper {
private icmServerURL: string;

constructor(store: Store) {
store.pipe(select(getICMServerURL)).subscribe(url => (this.icmServerURL = url));
}

fromAttachments(attachments: AttachmentData[]): Attachment[] {
if (!attachments || attachments.length === 0) {
return;
}
return attachments.map(attachment => this.fromAttachment(attachment));
}

private fromAttachment(attachment: AttachmentData): Attachment {
return {
name: attachment.name,
type: attachment.type,
key: attachment.key,
description: attachment.description,
url: `${this.icmServerURL}/${attachment.link.uri}`,
};
}
}
7 changes: 7 additions & 0 deletions src/app/core/models/attachment/attachment.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Attachment {
name: string;
type: string;
key: string;
description?: string;
url: string;
}
3 changes: 2 additions & 1 deletion src/app/core/models/product/product.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AttachmentData } from 'ish-core/models/attachment/attachment.interface';
import { AttributeGroup } from 'ish-core/models/attribute-group/attribute-group.model';
import { Attribute } from 'ish-core/models/attribute/attribute.model';
import { CategoryData } from 'ish-core/models/category/category.interface';
Expand Down Expand Up @@ -54,7 +55,7 @@ export interface ProductData {
summedUpSalePrice?: PriceData;
// }

attachments?: unknown;
attachments?: AttachmentData[];
variations?: unknown;
crosssells?: unknown;
productMaster: boolean;
Expand Down
17 changes: 15 additions & 2 deletions src/app/core/models/product/product.mapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { anything, spy, verify } from 'ts-mockito';

import { AttachmentMapper } from 'ish-core/models/attachment/attachment.mapper';
import { Attribute } from 'ish-core/models/attribute/attribute.model';
import { ImageMapper } from 'ish-core/models/image/image.mapper';
import { Link } from 'ish-core/models/link/link.model';
import { getICMBaseURL } from 'ish-core/store/core/configuration';
import { getICMBaseURL, getICMServerURL } from 'ish-core/store/core/configuration';

import { ProductData, ProductDataStub } from './product.interface';
import { ProductMapper } from './product.mapper';
Expand All @@ -14,13 +15,22 @@ import { Product, ProductHelper, VariationProductMaster } from './product.model'
describe('Product Mapper', () => {
let productMapper: ProductMapper;
let imageMapper: ImageMapper;
let attachmentMapper: AttachmentMapper;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideMockStore({ selectors: [{ selector: getICMBaseURL, value: 'http://www.example.org' }] })],
providers: [
provideMockStore({
selectors: [
{ selector: getICMBaseURL, value: 'http://www.example.org' },
{ selector: getICMServerURL, value: 'http://www.example.org' },
],
}),
],
});
productMapper = TestBed.inject(ProductMapper);
imageMapper = spy(TestBed.inject(ImageMapper));
attachmentMapper = spy(TestBed.inject(AttachmentMapper));
});

describe('fromData', () => {
Expand All @@ -29,6 +39,7 @@ describe('Product Mapper', () => {
expect(product).toBeTruthy();
expect(product.type).toEqual('Product');
verify(imageMapper.fromImages(anything())).once();
verify(attachmentMapper.fromAttachments(anything())).once();
});

it(`should return VariationProduct when getting a ProductData with mastered = true`, () => {
Expand All @@ -41,6 +52,7 @@ describe('Product Mapper', () => {
expect(product.type).toEqual('VariationProduct');
expect(ProductHelper.isMasterProduct(product)).toBeFalsy();
verify(imageMapper.fromImages(anything())).once();
verify(attachmentMapper.fromAttachments(anything())).once();
});

it(`should return VariationProductMaster when getting a ProductData with productMaster = true`, () => {
Expand Down Expand Up @@ -168,6 +180,7 @@ describe('Product Mapper', () => {
expect(stub.shortDescription).toEqual('productDescription');
expect(stub.sku).toEqual('productSKU');
verify(imageMapper.fromImages(anything())).once();
verify(attachmentMapper.fromAttachments(anything())).never();
});

it('should construct a stub when supplied with a complex API response', () => {
Expand Down
8 changes: 7 additions & 1 deletion src/app/core/models/product/product.mapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { flatten } from 'lodash-es';

import { AttachmentMapper } from 'ish-core/models/attachment/attachment.mapper';
import { AttributeGroup } from 'ish-core/models/attribute-group/attribute-group.model';
import { AttributeHelper } from 'ish-core/models/attribute/attribute.helper';
import { CategoryData } from 'ish-core/models/category/category.interface';
Expand Down Expand Up @@ -40,7 +41,11 @@ function mapAttributeGroups(data: ProductDataStub): { [id: string]: AttributeGro
*/
@Injectable({ providedIn: 'root' })
export class ProductMapper {
constructor(private imageMapper: ImageMapper, private categoryMapper: CategoryMapper) {}
constructor(
private imageMapper: ImageMapper,
private attachmentMapper: AttachmentMapper,
private categoryMapper: CategoryMapper
) {}

static parseSkuFromURI(uri: string): string {
const match = /products[^\/]*\/([^\?]*)/.exec(uri);
Expand Down Expand Up @@ -215,6 +220,7 @@ export class ProductMapper {
data.attributeGroups.PRODUCT_DETAIL_ATTRIBUTES.attributes) ||
data.attributes,
attributeGroups: data.attributeGroups,
attachments: this.attachmentMapper.fromAttachments(data.attachments),
images: this.imageMapper.fromImages(data.images),
listPrice: PriceMapper.fromData(data.listPrice),
salePrice: PriceMapper.fromData(data.salePrice),
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/models/product/product.model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Attachment } from 'ish-core/models/attachment/attachment.model';
import { AttributeGroup } from 'ish-core/models/attribute-group/attribute-group.model';
import { Attribute } from 'ish-core/models/attribute/attribute.model';
import { Image } from 'ish-core/models/image/image.model';
Expand All @@ -16,6 +17,7 @@ export interface Product {
stepOrderQuantity: number;
attributes: Attribute[];
attributeGroups?: { [id: string]: AttributeGroup };
attachments?: Attachment[];
images: Image[];
listPrice: Price;
salePrice: Price;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
>
<ish-product-attributes [product]="product"></ish-product-attributes>
</ish-accordion-item>

<ish-accordion-item *ngIf="product.attachments?.length" [heading]="'product.attachments.heading' | translate">
<ish-product-attachments></ish-product-attachments>
</ish-accordion-item>
<ish-accordion-item
*ngIf="configuration$('shipment') | async"
[heading]="'product.shipping.heading' | translate"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { instance, mock } from 'ts-mockito';
import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { AccordionItemComponent } from 'ish-shared/components/common/accordion-item/accordion-item.component';
import { AccordionComponent } from 'ish-shared/components/common/accordion/accordion.component';
import { ProductAttachmentsComponent } from 'ish-shared/components/product/product-attachments/product-attachments.component';
import { ProductAttributesComponent } from 'ish-shared/components/product/product-attributes/product-attributes.component';
import { ProductShipmentComponent } from 'ish-shared/components/product/product-shipment/product-shipment.component';

Expand All @@ -22,6 +23,7 @@ describe('Product Detail Info Accordion Component', () => {
declarations: [
MockComponent(AccordionComponent),
MockComponent(AccordionItemComponent),
MockComponent(ProductAttachmentsComponent),
MockComponent(ProductAttributesComponent),
MockComponent(ProductShipmentComponent),
ProductDetailInfoAccordionComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<ng-container *ngIf="attachments$ | async as attachments">
<div *ngIf="attachments?.length" class="product-attachments">
<div *ngFor="let attachment of attachments" class="product-attachment">
<strong>{{ attachment.name }}</strong>
<a class="download-link" [href]="attachment.url" rel="enclosure">{{
'product.attachments.download.link' | translate
}}</a>
<p *ngIf="attachment.description" class="product-attachment-description">{{ attachment.description }}</p>
</div>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { instance, mock } from 'ts-mockito';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { Product } from 'ish-core/models/product/product.model';

import { ProductAttachmentsComponent } from './product-attachments.component';

describe('Product Attachments Component', () => {
let component: ProductAttachmentsComponent;
let fixture: ComponentFixture<ProductAttachmentsComponent>;
let element: HTMLElement;
let product: Product;

beforeEach(async () => {
product = { sku: 'sku' } as Product;
product.attachments = [
{
name: 'A',
type: 'typeA',
key: 'keyA',
description: 'descriptionA',
url: 'urlA',
},
];
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [ProductAttachmentsComponent],
providers: [{ provide: ProductContextFacade, useFactory: () => instance(mock(ProductContextFacade)) }],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ProductAttachmentsComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { Attachment } from 'ish-core/models/attachment/attachment.model';

@Component({
selector: 'ish-product-attachments',
templateUrl: './product-attachments.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductAttachmentsComponent implements OnInit {
attachments$: Observable<Attachment[]>;

constructor(private context: ProductContextFacade) {}

ngOnInit() {
this.attachments$ = this.context.select('product', 'attachments');
}
}
2 changes: 2 additions & 0 deletions src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import { OrderListComponent } from './components/order/order-list/order-list.com
import { OrderWidgetComponent } from './components/order/order-widget/order-widget.component';
import { ProductAddToBasketComponent } from './components/product/product-add-to-basket/product-add-to-basket.component';
import { ProductAddToCompareComponent } from './components/product/product-add-to-compare/product-add-to-compare.component';
import { ProductAttachmentsComponent } from './components/product/product-attachments/product-attachments.component';
import { ProductAttributesComponent } from './components/product/product-attributes/product-attributes.component';
import { ProductBundleDisplayComponent } from './components/product/product-bundle-display/product-bundle-display.component';
import { ProductChooseVariationComponent } from './components/product/product-choose-variation/product-choose-variation.component';
Expand Down Expand Up @@ -247,6 +248,7 @@ const exportedComponents = [
OrderWidgetComponent,
ProductAddToBasketComponent,
ProductAddToCompareComponent,
ProductAttachmentsComponent,
ProductAttributesComponent,
ProductBundleDisplayComponent,
ProductIdComponent,
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,8 @@
"product.add_to_cart.link": "In den Warenkorb",
"product.add_to_cart.retailset.link": "Artikel in den Warenkorb legen",
"product.add_to_wishlist.link": "Auf die Wunschliste",
"product.attachments.download.link": "Download",
"product.attachments.heading": "Dokumente",
"product.available_in_different_configuration": "Nicht Verfügbar",
"product.choose_another_variation.link": "Wählen Sie eine andere Variante",
"product.choose_variation.link": "Variation auswählen",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,8 @@
"product.add_to_cart.link": "Add to Cart",
"product.add_to_cart.retailset.link": "Add item(s) to Cart",
"product.add_to_wishlist.link": "Add to Wish List",
"product.attachments.download.link": "Download",
"product.attachments.heading": "Documents",
"product.available_in_different_configuration": "Not Available",
"product.choose_another_variation.link": "Choose Another Variation",
"product.choose_variation.link": "Choose Variation",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,8 @@
"product.add_to_cart.link": "Ajouter au panier",
"product.add_to_cart.retailset.link": "Ajouter les articles au panier",
"product.add_to_wishlist.link": "Ajouter à la liste de souhaits",
"product.attachments.download.link": "Télécharger",
"product.attachments.heading": "Documents",
"product.available_in_different_configuration": "Non disponible",
"product.choose_another_variation.link": "Sélectionnez une autre variation",
"product.choose_variation.link": "Sélectionner une variation",
Expand Down
5 changes: 5 additions & 0 deletions src/styles/global/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ img.marketing {
white-space: nowrap;
}

.download-link {
display: inline-block;
padding-left: $space-default * 0.5;
}

.item-count-container {
position: relative;

Expand Down
8 changes: 4 additions & 4 deletions src/styles/pages/productdetail/productdetail.scss
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@
padding: $space-default 0;
color: $text-color-primary;

.product-attachments-list-item {
.ng-fa-icon {
margin-left: math.div($space-default * 2, 3);
font-size: 22px;
.product-attachment {
margin-bottom: $space-default;
.product-attachment-description {
margin-bottom: 0;
}
}
}
Expand Down

0 comments on commit d275b95

Please sign in to comment.