From e43245f1d96ab24dd502cfc22d8226b2d0d2ce7b Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Tue, 5 Mar 2024 16:30:47 +0100 Subject: [PATCH] feat: handle complex values of variation attributes --- .../product-variation.helper.spec.ts | 161 ++++++++++++------ .../product-variation.helper.ts | 37 +++- .../variation-attribute.model.ts | 2 +- src/app/core/pipes.module.ts | 2 + .../pipes/variation-attribute.pipe.spec.ts | 67 ++++++++ .../core/pipes/variation-attribute.pipe.ts | 26 +++ .../product-variation-display.component.html | 2 +- 7 files changed, 237 insertions(+), 60 deletions(-) create mode 100644 src/app/core/pipes/variation-attribute.pipe.spec.ts create mode 100644 src/app/core/pipes/variation-attribute.pipe.ts diff --git a/src/app/core/models/product-variation/product-variation.helper.spec.ts b/src/app/core/models/product-variation/product-variation.helper.spec.ts index eff3231a8e..0696fabbdd 100644 --- a/src/app/core/models/product-variation/product-variation.helper.spec.ts +++ b/src/app/core/models/product-variation/product-variation.helper.spec.ts @@ -11,6 +11,7 @@ const productVariations = [ variableVariationAttributes: [ { name: 'Attr 1', value: 'A', variationAttributeId: 'a1' }, { name: 'Attr 2', value: 'A', variationAttributeId: 'a2' }, + { name: 'Attr 3', value: { value: 3, unit: 'm' }, variationAttributeId: 'a3' }, ], }, { @@ -20,6 +21,7 @@ const productVariations = [ variableVariationAttributes: [ { name: 'Attr 1', value: 'A', variationAttributeId: 'a1' }, { name: 'Attr 2', value: 'B', variationAttributeId: 'a2' }, + { name: 'Attr 3', value: { value: 1, unit: 'm' }, variationAttributeId: 'a3' }, ], }, { @@ -28,6 +30,7 @@ const productVariations = [ variableVariationAttributes: [ { name: 'Attr 1', value: 'B', variationAttributeId: 'a1' }, { name: 'Attr 2', value: 'A', variationAttributeId: 'a2' }, + { name: 'Attr 3', value: { value: 2, unit: 'm' }, variationAttributeId: 'a3' }, ], }, { @@ -36,6 +39,7 @@ const productVariations = [ variableVariationAttributes: [ { name: 'Attr 1', value: 'B', variationAttributeId: 'a1' }, { name: 'Attr 2', value: 'B', variationAttributeId: 'a2' }, + { name: 'Attr 3', value: { value: 3, unit: 'm' }, variationAttributeId: 'a3' }, ], }, { @@ -44,6 +48,7 @@ const productVariations = [ variableVariationAttributes: [ { name: 'Attr 1', value: 'B', variationAttributeId: 'a1' }, { name: 'Attr 2', value: 'C', variationAttributeId: 'a2' }, + { name: 'Attr 3', value: { value: 3, unit: 'm' }, variationAttributeId: 'a3' }, ], }, ] as VariationProduct[]; @@ -56,6 +61,9 @@ const productMaster = { { name: 'Attr 2', value: 'A', variationAttributeId: 'a2' }, { name: 'Attr 2', value: 'B', variationAttributeId: 'a2' }, { name: 'Attr 2', value: 'C', variationAttributeId: 'a2' }, + { name: 'Attr 3', value: '1', variationAttributeId: 'a3' }, + { name: 'Attr 3', value: '2', variationAttributeId: 'a3' }, + { name: 'Attr 3', value: '3', variationAttributeId: 'a3' }, ], } as VariationProductMaster; @@ -73,58 +81,96 @@ const masterProductView = { describe('Product Variation Helper', () => { describe('buildVariationOptionGroups', () => { it('should build variation option groups for variation product', () => { - const expectedGroups = [ - { - id: 'a1', - label: 'Attr 1', - options: [ - { - label: 'A', - value: 'A', - type: 'a1', - alternativeCombination: false, - active: true, - }, - { - label: 'B', - value: 'B', - type: 'a1', - alternativeCombination: false, - active: false, - }, - ], - }, - { - id: 'a2', - label: 'Attr 2', - options: [ - { - label: 'A', - value: 'A', - type: 'a2', - alternativeCombination: false, - active: true, - }, - { - label: 'B', - value: 'B', - type: 'a2', - alternativeCombination: false, - active: false, - }, - { - label: 'C', - value: 'C', - type: 'a2', - alternativeCombination: true, - active: false, - }, - ], - }, - ]; - const result = ProductVariationHelper.buildVariationOptionGroups(variationProductView); - expect(result).toEqual(expectedGroups); + expect(result).toMatchInlineSnapshot(` + [ + { + "attributeType": undefined, + "id": "a1", + "label": "Attr 1", + "options": [ + { + "active": true, + "alternativeCombination": true, + "label": "A", + "metaData": undefined, + "type": "a1", + "value": "A", + }, + { + "active": false, + "alternativeCombination": true, + "label": "B", + "metaData": undefined, + "type": "a1", + "value": "B", + }, + ], + }, + { + "attributeType": undefined, + "id": "a2", + "label": "Attr 2", + "options": [ + { + "active": true, + "alternativeCombination": true, + "label": "A", + "metaData": undefined, + "type": "a2", + "value": "A", + }, + { + "active": false, + "alternativeCombination": true, + "label": "B", + "metaData": undefined, + "type": "a2", + "value": "B", + }, + { + "active": false, + "alternativeCombination": true, + "label": "C", + "metaData": undefined, + "type": "a2", + "value": "C", + }, + ], + }, + { + "attributeType": undefined, + "id": "a3", + "label": "Attr 3", + "options": [ + { + "active": false, + "alternativeCombination": true, + "label": "1", + "metaData": undefined, + "type": "a3", + "value": "1", + }, + { + "active": false, + "alternativeCombination": true, + "label": "2", + "metaData": undefined, + "type": "a3", + "value": "2", + }, + { + "active": true, + "alternativeCombination": true, + "label": "3", + "metaData": undefined, + "type": "a3", + "value": "3", + }, + ], + }, + ] + `); }); }); @@ -188,6 +234,19 @@ describe('Product Variation Helper', () => { expect(ProductVariationHelper.productVariationCount(masterProductView, filters)).toEqual(2); }); + it('should filter for products matching complex value attributes', () => { + const filters = { + filter: [ + { + id: 'a1', + facets: [{ selected: true, name: 'a3=3' }], + }, + ], + } as FilterNavigation; + + expect(ProductVariationHelper.productVariationCount(masterProductView, filters)).toEqual(3); + }); + it('should filter for products matching multiple selected attributes', () => { const filters = { filter: [ diff --git a/src/app/core/models/product-variation/product-variation.helper.ts b/src/app/core/models/product-variation/product-variation.helper.ts index 29260b4b6d..d82bf9515a 100644 --- a/src/app/core/models/product-variation/product-variation.helper.ts +++ b/src/app/core/models/product-variation/product-variation.helper.ts @@ -30,11 +30,11 @@ export class ProductVariationHelper { // each with information about alternative combinations and active status (active status comes from currently selected variation) const options: VariationSelectOption[] = (product.productMaster?.variationAttributeValues || []) .map(attr => ({ - label: attr.value, - value: attr.value, + label: ProductVariationHelper.toDisplayValue(attr.value), + value: ProductVariationHelper.toValue(attr.value)?.toString(), type: attr.variationAttributeId, metaData: attr.metaData, - active: currentSettings?.[attr.variationAttributeId]?.value === attr.value, + active: ProductVariationHelper.isEqual(currentSettings?.[attr.variationAttributeId]?.value, attr.value), })) .map(option => ({ ...option, @@ -65,7 +65,9 @@ export class ProductVariationHelper { const candidates = product.variations .filter(variation => - variation.variableVariationAttributes.some(attr => attr.variationAttributeId === name && attr.value === value) + variation.variableVariationAttributes.some( + attr => attr.variationAttributeId === name && ProductVariationHelper.isEqual(attr.value, value) + ) ) .map(variation => ({ sku: variation.sku, @@ -109,7 +111,9 @@ export class ProductVariationHelper { // attribute is not selected !selectedFacets.find(([key]) => key === attr.variationAttributeId) || // selection is variation - selectedFacets.find(([key, val]) => key === attr.variationAttributeId && val === attr.value.toString()) + selectedFacets.find( + ([key, val]) => key === attr.variationAttributeId && ProductVariationHelper.isEqual(val, attr.value) + ) ) ).length; } @@ -140,14 +144,17 @@ export class ProductVariationHelper { // increment quality if variation attribute matches selected product attribute. if ( attribute.variationAttributeId === selectedAttribute.variationAttributeId && - attribute.value === selectedAttribute.value + ProductVariationHelper.isEqual(attribute.value, selectedAttribute.value) ) { quality += 1; continue; } // increment quality if variation attribute matches currently checked option. - if (attribute.variationAttributeId === option.type && attribute.value === option.value) { + if ( + attribute.variationAttributeId === option.type && + ProductVariationHelper.isEqual(attribute.value, option.value) + ) { quality += 1; continue; } @@ -164,6 +171,22 @@ export class ProductVariationHelper { return true; } + private static toValue(input: string | number | { value: number }): string | number { + return typeof input === 'object' ? input.value : input; + } + + private static toDisplayValue(input: string | number | { value: number; unit: string }): string { + return typeof input === 'object' ? `${input.value} ${input.unit}` : input.toString(); + } + + private static isEqual( + obj1: string | number | { value: number }, + obj2: string | number | { value: number } + ): boolean { + // eslint-disable-next-line eqeqeq -- needed for comparison of string, integers and floats + return ProductVariationHelper.toValue(obj1) == ProductVariationHelper.toValue(obj2); + } + private static simplifyVariableVariationAttributes(attrs: VariationAttribute[]): { [name: string]: string } { return attrs .map(attr => ({ diff --git a/src/app/core/models/product-variation/variation-attribute.model.ts b/src/app/core/models/product-variation/variation-attribute.model.ts index e93dd8bc4e..6179d1e710 100644 --- a/src/app/core/models/product-variation/variation-attribute.model.ts +++ b/src/app/core/models/product-variation/variation-attribute.model.ts @@ -1,7 +1,7 @@ export interface VariationAttribute { variationAttributeId: string; name: string; - value: string; + value: string | number | { value: number; unit: string }; attributeType: VariationAttributeType; metaData?: string; } diff --git a/src/app/core/pipes.module.ts b/src/app/core/pipes.module.ts index 5dbf31c86a..ed532a3500 100644 --- a/src/app/core/pipes.module.ts +++ b/src/app/core/pipes.module.ts @@ -9,6 +9,7 @@ import { HtmlEncodePipe } from './pipes/html-encode.pipe'; import { MakeHrefPipe } from './pipes/make-href.pipe'; import { SanitizePipe } from './pipes/sanitize.pipe'; import { ServerSettingPipe } from './pipes/server-setting.pipe'; +import { VariationAttributePipe } from './pipes/variation-attribute.pipe'; import { CategoryRoutePipe } from './routing/category/category-route.pipe'; import { ContentPageRoutePipe } from './routing/content-page/content-page-route.pipe'; import { ProductRoutePipe } from './routing/product/product-route.pipe'; @@ -26,6 +27,7 @@ const pipes = [ ProductRoutePipe, SanitizePipe, ServerSettingPipe, + VariationAttributePipe, ]; @NgModule({ diff --git a/src/app/core/pipes/variation-attribute.pipe.spec.ts b/src/app/core/pipes/variation-attribute.pipe.spec.ts new file mode 100644 index 0000000000..fe9afb31bb --- /dev/null +++ b/src/app/core/pipes/variation-attribute.pipe.spec.ts @@ -0,0 +1,67 @@ +import { registerLocaleData } from '@angular/common'; +import localeDe from '@angular/common/locales/de'; +import { TestBed } from '@angular/core/testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; + +import { VariationAttribute } from 'ish-core/models/product-variation/variation-attribute.model'; + +import { VariationAttributePipe } from './variation-attribute.pipe'; + +describe('Variation Attribute Pipe', () => { + let variationAttributePipe: VariationAttributePipe; + let translateService: TranslateService; + + beforeEach(() => { + registerLocaleData(localeDe); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [VariationAttributePipe], + }); + + variationAttributePipe = TestBed.inject(VariationAttributePipe); + translateService = TestBed.inject(TranslateService); + translateService.setDefaultLang('en'); + translateService.use('en'); + }); + + it('should be created', () => { + expect(variationAttributePipe).toBeTruthy(); + }); + + it('should transform undefined to undefined', () => { + expect(variationAttributePipe.transform(undefined)).toMatchInlineSnapshot(`"undefined"`); + }); + + it('should transform string attribute to string', () => { + const attr = { value: 'test' } as VariationAttribute; + expect(variationAttributePipe.transform(attr)).toMatchInlineSnapshot(`"test"`); + }); + + it('should transform number attribute to number', () => { + const attr = { value: 123 } as VariationAttribute; + expect(variationAttributePipe.transform(attr)).toMatchInlineSnapshot(`"123"`); + }); + + it('should transform float attribute to formatted locale en', () => { + const attr = { value: 123.4 } as VariationAttribute; + expect(variationAttributePipe.transform(attr)).toMatchInlineSnapshot(`"123.4"`); + }); + + it('should transform float attribute to formatted locale de', () => { + translateService.use('de'); + const attr = { value: 123.4 } as VariationAttribute; + expect(variationAttributePipe.transform(attr)).toMatchInlineSnapshot(`"123,4"`); + }); + + it('should transform object attribute to formatted locale en', () => { + const attr = { value: { value: 123.4, unit: 'mm' } } as VariationAttribute; + expect(variationAttributePipe.transform(attr)).toMatchInlineSnapshot(`"123.4\xA0mm"`); + }); + + it('should transform object attribute to formatted locale de', () => { + translateService.use('de'); + const attr = { value: { value: 123.4, unit: 'mm' } } as VariationAttribute; + expect(variationAttributePipe.transform(attr)).toMatchInlineSnapshot(`"123,4\xA0mm"`); + }); +}); diff --git a/src/app/core/pipes/variation-attribute.pipe.ts b/src/app/core/pipes/variation-attribute.pipe.ts new file mode 100644 index 0000000000..1b20c437bc --- /dev/null +++ b/src/app/core/pipes/variation-attribute.pipe.ts @@ -0,0 +1,26 @@ +import { formatNumber } from '@angular/common'; +import { Pipe, PipeTransform } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { VariationAttribute } from 'ish-core/models/product-variation/variation-attribute.model'; + +@Pipe({ name: 'ishVariationAttribute', pure: false }) +export class VariationAttributePipe implements PipeTransform { + constructor(private translateService: TranslateService) {} + + private toDecimal(val: number): string { + return formatNumber(val, this.translateService.currentLang); + } + + transform(attr: VariationAttribute): string { + if (!this.translateService.currentLang) { + return 'undefined'; + } + + return typeof attr?.value === 'object' + ? `${this.toDecimal(attr.value.value)}\xA0${attr.value.unit}` + : typeof attr?.value === 'number' + ? this.toDecimal(attr.value) + : attr?.value || 'undefined'; + } +} diff --git a/src/app/shared/components/product/product-variation-display/product-variation-display.component.html b/src/app/shared/components/product/product-variation-display/product-variation-display.component.html index bf0749c7d5..c088b606aa 100644 --- a/src/app/shared/components/product/product-variation-display/product-variation-display.component.html +++ b/src/app/shared/components/product/product-variation-display/product-variation-display.component.html @@ -2,7 +2,7 @@
{{ attr.name }} - {{ attr.value }} + {{ attr | ishVariationAttribute }}