Skip to content

Commit

Permalink
feat: introduce variation attribute mapping with extended /variations…
Browse files Browse the repository at this point in the history
… call (#1317)

* changed products service to use `extended=true` REST calls for product details and variations
* added VariationAttributeMapper to map variation attributes with attributeType and metaData
* backwards compatible preparation for distinct variation select displays

BREAKING CHANGES: `ProductsService` was changed to use an `extended=true` details and variations call. `VariationAttribute` model was cleaned up and extended (see [Migrations / 3.1 to 3.2](https://github.com/intershop/intershop-pwa/blob/develop/docs/guides/migrations.md#31-to-32) for more details).
  • Loading branch information
shauke committed Dec 16, 2022
1 parent 6add3d2 commit d9e4b17
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 37 deletions.
3 changes: 3 additions & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ After entering a desired delivery date on the checkout shipping page and after s
In case of large basket (> 20 items) this might cause (unacceptable) long response times.
You can keep the existing behavior by modifying the updateBasketItemsDesiredDeliveryDate() method of the basket service to always return an empty array without doing anything.

The `ProductsService` was changed to use `extended=true` REST calls for product details and variations to fetch variation attributes with additional `attributeType` and `metaData` information that can be used to control the rendering of different variation select types.
The added `VariationAttributeMapper` maps the additional information in a backwards compatible way.

## 3.0 to 3.1

The SSR environment variable 'ICM_IDENTITY_PROVIDER' will be removed in a future release ( PWA 5.0 ).
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/models/image/image.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class ImageMapper {
* @param url The relative or absolute image URL.
* @returns The URL.
*/
private fromEffectiveUrl(url: string): string {
fromEffectiveUrl(url: string): string {
if (!url) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,53 @@ const productVariations = [
sku: '222',
productMasterSKU: 'M111',
variableVariationAttributes: [
{ name: 'Attr 1', type: 'String', value: 'A', variationAttributeId: 'a1' },
{ name: 'Attr 2', type: 'String', value: 'A', variationAttributeId: 'a2' },
{ name: 'Attr 1', value: 'A', variationAttributeId: 'a1' },
{ name: 'Attr 2', value: 'A', variationAttributeId: 'a2' },
],
},
{
sku: '333',
productMasterSKU: 'M111',
attributes: [{ name: 'defaultVariation', type: 'Boolean', value: true }],
variableVariationAttributes: [
{ name: 'Attr 1', type: 'String', value: 'A', variationAttributeId: 'a1' },
{ name: 'Attr 2', type: 'String', value: 'B', variationAttributeId: 'a2' },
{ name: 'Attr 1', value: 'A', variationAttributeId: 'a1' },
{ name: 'Attr 2', value: 'B', variationAttributeId: 'a2' },
],
},
{
sku: '444',
productMasterSKU: 'M111',
variableVariationAttributes: [
{ name: 'Attr 1', type: 'String', value: 'B', variationAttributeId: 'a1' },
{ name: 'Attr 2', type: 'String', value: 'A', variationAttributeId: 'a2' },
{ name: 'Attr 1', value: 'B', variationAttributeId: 'a1' },
{ name: 'Attr 2', value: 'A', variationAttributeId: 'a2' },
],
},
{
sku: '555',
productMasterSKU: 'M111',
variableVariationAttributes: [
{ name: 'Attr 1', type: 'String', value: 'B', variationAttributeId: 'a1' },
{ name: 'Attr 2', type: 'String', value: 'B', variationAttributeId: 'a2' },
{ name: 'Attr 1', value: 'B', variationAttributeId: 'a1' },
{ name: 'Attr 2', value: 'B', variationAttributeId: 'a2' },
],
},
{
sku: '666',
productMasterSKU: 'M111',
variableVariationAttributes: [
{ name: 'Attr 1', type: 'String', value: 'B', variationAttributeId: 'a1' },
{ name: 'Attr 2', type: 'String', value: 'C', variationAttributeId: 'a2' },
{ name: 'Attr 1', value: 'B', variationAttributeId: 'a1' },
{ name: 'Attr 2', value: 'C', variationAttributeId: 'a2' },
],
},
] as VariationProduct[];

const productMaster = {
sku: 'M111',
variationAttributeValues: [
{ name: 'Attr 1', type: 'String', value: 'A', variationAttributeId: 'a1' },
{ name: 'Attr 1', type: 'String', value: 'B', variationAttributeId: 'a1' },
{ name: 'Attr 2', type: 'String', value: 'A', variationAttributeId: 'a2' },
{ name: 'Attr 2', type: 'String', value: 'B', variationAttributeId: 'a2' },
{ name: 'Attr 2', type: 'String', value: 'C', variationAttributeId: 'a2' },
{ name: 'Attr 1', value: 'A', variationAttributeId: 'a1' },
{ name: 'Attr 1', value: 'B', variationAttributeId: 'a1' },
{ name: 'Attr 2', value: 'A', variationAttributeId: 'a2' },
{ name: 'Attr 2', value: 'B', variationAttributeId: 'a2' },
{ name: 'Attr 2', value: 'C', variationAttributeId: 'a2' },
],
} as VariationProductMaster;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class ProductVariationHelper {
label: attr.value,
value: attr.value,
type: attr.variationAttributeId,
metaData: attr.metaData,
active: currentSettings?.[attr.variationAttributeId]?.value === attr.value,
}))
.map(option => ({
Expand All @@ -50,6 +51,7 @@ export class ProductVariationHelper {
return {
id: attribute.variationAttributeId,
label: attribute.name,
attributeType: attribute.attributeType,
options: groupedOptions[attrId],
};
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { VariationAttributeType } from './variation-attribute.model';

export interface VariationAttributeData {
variationAttributeId: string;
name: string;
value?: VariationAttributeValue;
values?: {
value: VariationAttributeValue;
metadata?: VariationAttributeMetaData;
}[];
attributeType?: VariationAttributeType;
metadata?: VariationAttributeMetaData;
}

interface VariationAttributeValue {
name: string;
value: string;
}

export interface VariationAttributeMetaData {
colorCode?: string;
imagePath?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable ish-custom-rules/project-structure */
import { Injectable } from '@angular/core';

import { ImageMapper } from 'ish-core/models/image/image.mapper';

import { VariationAttributeData, VariationAttributeMetaData } from './variation-attribute.interface';
import { VariationAttribute, VariationAttributeType } from './variation-attribute.model';

/**
* Maps variation attributes data of HTTP requests to client side model instance.
*/
@Injectable({ providedIn: 'root' })
export class VariationAttributeMapper {
constructor(private imageMapper: ImageMapper) {}

fromData(data: VariationAttributeData[]): VariationAttribute[] {
return data?.map(varAttr => ({
variationAttributeId: varAttr.variationAttributeId,
name: varAttr.name,
value: varAttr.value.value,
attributeType: varAttr.attributeType,
metaData: this.mapMetaData(varAttr.attributeType, varAttr.metadata),
}));
}

fromMasterData(variationAttributes: VariationAttributeData[]): VariationAttribute[] {
return variationAttributes
?.map(varAttr =>
varAttr.values.map(value => ({
variationAttributeId: varAttr.variationAttributeId,
name: varAttr.name,
value: value.value.value,
attributeType: varAttr.attributeType,
metaData: this.mapMetaData(varAttr.attributeType, value.metadata),
}))
)
.flat();
}

private mapMetaData(attributeType: VariationAttributeType, metaData: VariationAttributeMetaData): string {
switch (attributeType) {
case 'colorCode':
case 'defaultAndColorCode':
return metaData?.colorCode;
case 'swatchImage':
case 'defaultAndSwatchImage':
return this.imageMapper.fromEffectiveUrl(metaData?.imagePath);
default:
return;
}
}
}
15 changes: 12 additions & 3 deletions src/app/core/models/product-variation/variation-attribute.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Attribute } from 'ish-core/models/attribute/attribute.model';

export interface VariationAttribute extends Attribute<string> {
export interface VariationAttribute {
variationAttributeId: string;
name: string;
value: string;
attributeType: VariationAttributeType;
metaData?: string;
}

export type VariationAttributeType =
| 'default'
| 'colorCode'
| 'defaultAndColorCode'
| 'swatchImage'
| 'defaultAndSwatchImage';
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface VariationOptionGroup {
options: VariationSelectOption[];
label: string;
id: string;
attributeType?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface VariationSelectOption {
type: string;
alternativeCombination?: boolean;
active?: boolean;
metaData?: string;
}
7 changes: 3 additions & 4 deletions src/app/core/models/product/product.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CategoryData } from 'ish-core/models/category/category.interface';
import { Image } from 'ish-core/models/image/image.model';
import { Link } from 'ish-core/models/link/link.model';
import { PriceData } from 'ish-core/models/price/price.interface';
import { VariationAttribute } from 'ish-core/models/product-variation/variation-attribute.model';
import { VariationAttributeData } from 'ish-core/models/product-variation/variation-attribute.interface';
import { SeoAttributesData } from 'ish-core/models/seo-attributes/seo-attributes.interface';
import { Warranty } from 'ish-core/models/warranty/warranty.model';

Expand Down Expand Up @@ -41,8 +41,7 @@ export interface ProductData {
stepOrderQuantity?: number;
packingUnit: string;

variationAttributeValues?: VariationAttribute[];
variableVariationAttributes?: VariationAttribute[];
variationAttributeValuesExtended?: VariationAttributeData[];
partOfRetailSet: boolean;

attachments?: AttachmentData[];
Expand Down Expand Up @@ -74,5 +73,5 @@ export interface ProductDataStub {
}

export interface ProductVariationLink extends Link {
variableVariationAttributeValues: VariationAttribute[];
variableVariationAttributeValuesExtended: VariationAttributeData[];
}
4 changes: 2 additions & 2 deletions src/app/core/models/product/product.mapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('Product Mapper', () => {
const product: Product = productMapper.fromData({
sku: '1',
productMaster: true,
variationAttributeValues: [],
variationAttributeValuesExtended: [],
} as ProductData);
expect(product).toBeTruthy();
expect(product.type).toEqual('VariationProductMaster');
Expand All @@ -78,7 +78,7 @@ describe('Product Mapper', () => {
const product: Product = productMapper.fromData({
sku: '1',
productMaster: false,
variableVariationAttributes: [],
variationAttributeValuesExtended: [],
} as ProductData);
expect(product).toBeTruthy();
expect(product.type).toEqual('Product');
Expand Down
12 changes: 8 additions & 4 deletions src/app/core/models/product/product.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CategoryData } from 'ish-core/models/category/category.interface';
import { CategoryMapper } from 'ish-core/models/category/category.mapper';
import { ImageMapper } from 'ish-core/models/image/image.mapper';
import { Link } from 'ish-core/models/link/link.model';
import { VariationAttributeMapper } from 'ish-core/models/product-variation/variation-attribute.mapper';
import { SeoAttributesMapper } from 'ish-core/models/seo-attributes/seo-attributes.mapper';

import { SkuQuantityType } from './product.helper';
Expand Down Expand Up @@ -43,7 +44,8 @@ export class ProductMapper {
constructor(
private imageMapper: ImageMapper,
private attachmentMapper: AttachmentMapper,
private categoryMapper: CategoryMapper
private categoryMapper: CategoryMapper,
private variationAttributeMapper: VariationAttributeMapper
) {}

static parseSkuFromURI(uri: string): string {
Expand Down Expand Up @@ -91,7 +93,9 @@ export class ProductMapper {
fromVariationLink(link: ProductVariationLink, productMasterSKU: string): Partial<VariationProduct> {
return {
...this.fromLink(link),
variableVariationAttributes: link.variableVariationAttributeValues,
variableVariationAttributes: this.variationAttributeMapper.fromData(
link.variableVariationAttributeValuesExtended
),
productMasterSKU,
type: 'VariationProduct',
failed: false,
Expand Down Expand Up @@ -227,14 +231,14 @@ export class ProductMapper {
if (data.productMaster) {
return {
...product,
variationAttributeValues: data.variationAttributeValues,
variationAttributeValues: this.variationAttributeMapper.fromMasterData(data.variationAttributeValuesExtended),
type: 'VariationProductMaster',
};
} else if (data.mastered) {
return {
...product,
productMasterSKU: data.productMasterSKU,
variableVariationAttributes: data.variableVariationAttributes,
variableVariationAttributes: this.variationAttributeMapper.fromData(data.variationAttributeValuesExtended),
type: 'VariationProduct',
};
} else if (data.productTypes?.includes('BUNDLE') || data.productBundle) {
Expand Down
12 changes: 7 additions & 5 deletions src/app/core/services/products/products.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,23 @@ describe('Products Service', () => {
it("should get all product variations data when 'getProductVariations' is called and more than 50 variations exist", done => {
const total = 156;
when(apiServiceMock.get(`products/${productSku}/variations`, anything())).thenCall((_, opts) =>
!opts.params ? of({ elements: [], amount: 40, total }) : of({ elements: [], total })
!opts?.params.has('amount') ? of({ elements: [], amount: 40, total }) : of({ elements: [], total })
);

productsService.getProductVariations(productSku).subscribe(() => {
verify(apiServiceMock.get(`products/${productSku}/variations`, anything())).times(4);
expect(capture<string, AvailableOptions>(apiServiceMock.get).byCallIndex(0)?.[1]?.params).toBeUndefined();
expect(
capture<string, AvailableOptions>(apiServiceMock.get).byCallIndex(0)?.[1]?.params?.toString()
).toMatchInlineSnapshot(`"extended=true"`);
expect(
capture<string, AvailableOptions>(apiServiceMock.get).byCallIndex(1)?.[1]?.params?.toString()
).toMatchInlineSnapshot(`"amount=40&offset=40"`);
).toMatchInlineSnapshot(`"extended=true&amount=40&offset=40"`);
expect(
capture<string, AvailableOptions>(apiServiceMock.get).byCallIndex(2)?.[1]?.params?.toString()
).toMatchInlineSnapshot(`"amount=40&offset=80"`);
).toMatchInlineSnapshot(`"extended=true&amount=40&offset=80"`);
expect(
capture<string, AvailableOptions>(apiServiceMock.get).byCallIndex(3)?.[1]?.params?.toString()
).toMatchInlineSnapshot(`"amount=36&offset=120"`);
).toMatchInlineSnapshot(`"extended=true&amount=36&offset=120"`);
done();
});
});
Expand Down
11 changes: 8 additions & 3 deletions src/app/core/services/products/products.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class ProductsService {
return throwError(() => new Error('getProduct() called without a sku'));
}

const params = new HttpParams().set('allImages', 'true');
const params = new HttpParams().set('allImages', true).set('extended', true);

return this.apiService
.get<ProductData>(`products/${sku}`, { sendSPGID: true, params })
Expand Down Expand Up @@ -260,8 +260,13 @@ export class ProductsService {
return throwError(() => new Error('getProductVariations() called without a sku'));
}

const params = new HttpParams().set('extended', true);

return this.apiService
.get<{ elements: Link[]; total: number; amount: number }>(`products/${sku}/variations`, { sendSPGID: true })
.get<{ elements: Link[]; total: number; amount: number }>(`products/${sku}/variations`, {
sendSPGID: true,
params,
})
.pipe(
switchMap(resp =>
!resp.total
Expand All @@ -277,7 +282,7 @@ export class ProductsService {
this.apiService
.get<{ elements: Link[] }>(`products/${sku}/variations`, {
sendSPGID: true,
params: new HttpParams().set('amount', length).set('offset', offset),
params: params.set('amount', length).set('offset', offset),
})
.pipe(mapToProperty('elements'))
)
Expand Down

0 comments on commit d9e4b17

Please sign in to comment.