From 53553d57c23e59b34acee87dc1a002184804a875 Mon Sep 17 00:00:00 2001 From: Stefan Hauke Date: Wed, 15 May 2024 17:20:52 +0200 Subject: [PATCH 1/6] refactor: basket ui --- .../line-item-list-element.component.html | 44 +++++++++++-------- src/assets/i18n/en_US.json | 2 +- src/styles/pages/checkout/shopping-cart.scss | 8 ++++ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html index d754b51c30..d067251b2a 100644 --- a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html +++ b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html @@ -79,24 +79,6 @@ -
- - - -
@@ -132,6 +114,7 @@ {{ oldPrice | ishPrice }} +
@@ -146,6 +129,31 @@
{{ 'checkout.pli.freegift.text' | translate }}
+ + + +
+
+
+ + + +
+
+
diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index 40f92a42b0..e29374e6a7 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -823,7 +823,7 @@ "checkout.tax.text": "Tax", "checkout.termsandconditions.details.title": "Terms & conditions", "checkout.update.label": "Edit {{0}}", - "checkout.variation.edit.button.label": "Edit", + "checkout.variation.edit.button.label": "Edit variation", "checkout.widget.billing-address.heading": "Invoice address", "checkout.widget.buyer.TaxationID": "Taxation ID:", "checkout.widget.buyer.costcenter": "Cost center", diff --git a/src/styles/pages/checkout/shopping-cart.scss b/src/styles/pages/checkout/shopping-cart.scss index 8dc9fc4a58..3e9a7eacb0 100644 --- a/src/styles/pages/checkout/shopping-cart.scss +++ b/src/styles/pages/checkout/shopping-cart.scss @@ -186,3 +186,11 @@ max-width: 100%; } } + +.list-item-actions { + padding: $table-cell-padding * 0.2 0 $table-cell-padding * 0.9 0; + + .btn-tool.btn-link { + padding: 0 divide($space-default, 3); + } +} From 0a3dcce70319df44055343801c6f7a5950b08a5e Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Wed, 6 Dec 2023 10:07:22 +0100 Subject: [PATCH 2/6] feat: map data and configuration for custom fields --- .../requisition/requisition.mapper.spec.ts | 1 + src/app/core/facades/app.facade.ts | 11 +- .../core/models/basket/basket.interface.ts | 2 + src/app/core/models/basket/basket.mapper.ts | 2 + src/app/core/models/basket/basket.model.ts | 2 + .../custom-field/custom-field.interface.ts | 5 + .../custom-field/custom-field.mapper.spec.ts | 59 ++++ .../custom-field/custom-field.mapper.ts | 19 ++ .../models/custom-field/custom-field.model.ts | 9 + .../models/line-item/line-item.interface.ts | 2 + .../core/models/line-item/line-item.mapper.ts | 2 + .../core/models/line-item/line-item.model.ts | 3 +- src/app/core/models/order/order.mapper.ts | 2 + .../server-config/server-config.interface.ts | 18 +- .../server-config.mapper.spec.ts | 257 ++++++++++++++++-- .../server-config/server-config.mapper.ts | 47 +++- .../server-config/server-config.model.ts | 28 +- .../configuration/configuration.service.ts | 4 +- .../server-config/server-config.actions.ts | 4 +- .../server-config.effects.spec.ts | 5 +- .../server-config/server-config.effects.ts | 2 +- .../server-config/server-config.reducer.ts | 5 +- .../server-config.selectors.spec.ts | 107 +++++++- .../server-config/server-config.selectors.ts | 14 + 24 files changed, 563 insertions(+), 47 deletions(-) create mode 100644 src/app/core/models/custom-field/custom-field.interface.ts create mode 100644 src/app/core/models/custom-field/custom-field.mapper.spec.ts create mode 100644 src/app/core/models/custom-field/custom-field.mapper.ts create mode 100644 src/app/core/models/custom-field/custom-field.model.ts diff --git a/projects/requisition-management/src/app/models/requisition/requisition.mapper.spec.ts b/projects/requisition-management/src/app/models/requisition/requisition.mapper.spec.ts index a30c4893bc..b162c73eab 100644 --- a/projects/requisition-management/src/app/models/requisition/requisition.mapper.spec.ts +++ b/projects/requisition-management/src/app/models/requisition/requisition.mapper.spec.ts @@ -97,6 +97,7 @@ describe('Requisition Mapper', () => { "commonShippingMethod": undefined, "costCenter": "CostCenter123", "creationDate": 12345678, + "customFields": {}, "customerNo": "OilCorp", "dynamicMessages": undefined, "email": "bboldner@test.intershop.de", diff --git a/src/app/core/facades/app.facade.ts b/src/app/core/facades/app.facade.ts index 0c353cbc36..800fd74cf0 100644 --- a/src/app/core/facades/app.facade.ts +++ b/src/app/core/facades/app.facade.ts @@ -5,6 +5,7 @@ import { Store, select } from '@ngrx/store'; import { combineLatest, merge, noop } from 'rxjs'; import { filter, map, sample, shareReplay, startWith, withLatestFrom } from 'rxjs/operators'; +import { CustomFieldDefinitionScopes } from 'ish-core/models/server-config/server-config.interface'; import { getAvailableLocales, getCurrentCurrency, @@ -15,7 +16,11 @@ import { } from 'ish-core/store/core/configuration'; import { businessError, getGeneralError, getGeneralErrorType } from 'ish-core/store/core/error'; import { selectPath } from 'ish-core/store/core/router'; -import { getExtraConfigParameter, getServerConfigParameter } from 'ish-core/store/core/server-config'; +import { + getCustomFieldIdsForScope, + getExtraConfigParameter, + getServerConfigParameter, +} from 'ish-core/store/core/server-config'; import { getBreadcrumbData, getHeaderType, getWrapperClass, isStickyHeader } from 'ish-core/store/core/viewconf'; import { getLoggedInCustomer } from 'ish-core/store/customer/user'; import { getAllCountries, loadCountries } from 'ish-core/store/general/countries'; @@ -123,6 +128,10 @@ export class AppFacade { return this.store.pipe(select(getExtraConfigParameter(path))); } + customFieldsForScope$(scope: CustomFieldDefinitionScopes) { + return this.store.pipe(select(getCustomFieldIdsForScope(scope))); + } + /** * returns the currency symbol for the currency parameter in the current locale. * If no parameter is given, the the default currency is taken instead of it. diff --git a/src/app/core/models/basket/basket.interface.ts b/src/app/core/models/basket/basket.interface.ts index b90adbe20d..dfaddd788e 100644 --- a/src/app/core/models/basket/basket.interface.ts +++ b/src/app/core/models/basket/basket.interface.ts @@ -5,6 +5,7 @@ import { BasketInfo } from 'ish-core/models/basket-info/basket-info.model'; import { BasketRebateData } from 'ish-core/models/basket-rebate/basket-rebate.interface'; import { BasketTotalData } from 'ish-core/models/basket-total/basket-total.interface'; import { BasketWarrantyData } from 'ish-core/models/basket-warranty/basket-warranty.interface'; +import { CustomFieldData } from 'ish-core/models/custom-field/custom-field.interface'; import { LineItemData } from 'ish-core/models/line-item/line-item.interface'; import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model'; import { PaymentMethodBaseData } from 'ish-core/models/payment-method/payment-method.interface'; @@ -55,6 +56,7 @@ export interface BasketBaseData { firstName: string; lastName: string; }; + customFields?: CustomFieldData[]; } export interface BasketIncludedData { diff --git a/src/app/core/models/basket/basket.mapper.ts b/src/app/core/models/basket/basket.mapper.ts index a044d8f264..8b7d6e7c06 100644 --- a/src/app/core/models/basket/basket.mapper.ts +++ b/src/app/core/models/basket/basket.mapper.ts @@ -3,6 +3,7 @@ import { BasketRebateData } from 'ish-core/models/basket-rebate/basket-rebate.in import { BasketRebateMapper } from 'ish-core/models/basket-rebate/basket-rebate.mapper'; import { BasketTotal } from 'ish-core/models/basket-total/basket-total.model'; import { BasketBaseData, BasketData } from 'ish-core/models/basket/basket.interface'; +import { CustomFieldMapper } from 'ish-core/models/custom-field/custom-field.mapper'; import { LineItemMapper } from 'ish-core/models/line-item/line-item.mapper'; import { PaymentMapper } from 'ish-core/models/payment/payment.mapper'; import { PriceItemMapper } from 'ish-core/models/price-item/price-item.mapper'; @@ -74,6 +75,7 @@ export class BasketMapper { user: data.buyer, externalOrderReference: data.externalOrderReference, messageToMerchant: data.messageToMerchant, + customFields: CustomFieldMapper.fromData(data.customFields), }; } diff --git a/src/app/core/models/basket/basket.model.ts b/src/app/core/models/basket/basket.model.ts index 550fb9c709..545aff4ad2 100644 --- a/src/app/core/models/basket/basket.model.ts +++ b/src/app/core/models/basket/basket.model.ts @@ -4,6 +4,7 @@ import { BasketApproval } from 'ish-core/models/basket-approval/basket-approval. import { BasketInfo } from 'ish-core/models/basket-info/basket-info.model'; import { BasketTotal } from 'ish-core/models/basket-total/basket-total.model'; import { BasketValidationResultType } from 'ish-core/models/basket-validation/basket-validation.model'; +import { CustomFields } from 'ish-core/models/custom-field/custom-field.model'; import { LineItem, LineItemView } from 'ish-core/models/line-item/line-item.model'; import { Payment } from 'ish-core/models/payment/payment.model'; import { ShippingMethod } from 'ish-core/models/shipping-method/shipping-method.model'; @@ -36,6 +37,7 @@ export interface AbstractBasket { }; externalOrderReference?: string; messageToMerchant?: string; + customFields?: CustomFields; } export type Basket = AbstractBasket; diff --git a/src/app/core/models/custom-field/custom-field.interface.ts b/src/app/core/models/custom-field/custom-field.interface.ts new file mode 100644 index 0000000000..1708f3929b --- /dev/null +++ b/src/app/core/models/custom-field/custom-field.interface.ts @@ -0,0 +1,5 @@ +import { Attribute } from 'ish-core/models/attribute/attribute.model'; + +export interface CustomFieldData extends Attribute { + type: 'String'; +} diff --git a/src/app/core/models/custom-field/custom-field.mapper.spec.ts b/src/app/core/models/custom-field/custom-field.mapper.spec.ts new file mode 100644 index 0000000000..02d0d174e4 --- /dev/null +++ b/src/app/core/models/custom-field/custom-field.mapper.spec.ts @@ -0,0 +1,59 @@ +import { CustomFieldData } from './custom-field.interface'; +import { CustomFieldMapper } from './custom-field.mapper'; + +describe('Custom Field Mapper', () => { + describe('fromData', () => { + it('should return empty object if no data supplied', () => { + const mapped = CustomFieldMapper.fromData(); + expect(mapped).toMatchInlineSnapshot(`{}`); + }); + + it('should return empty object if empty data supplied', () => { + const mapped = CustomFieldMapper.fromData([]); + expect(mapped).toMatchInlineSnapshot(`{}`); + }); + + it('should map incoming data to model data', () => { + const data: CustomFieldData[] = [ + { + name: 'foo', + value: 'foo', + type: 'String', + }, + { + name: 'bar', + value: 'bar', + type: 'String', + }, + ]; + const mapped = CustomFieldMapper.fromData(data); + expect(mapped).toMatchInlineSnapshot(` + { + "bar": "bar", + "foo": "foo", + } + `); + }); + + it('should filter empty data', () => { + const data: CustomFieldData[] = [ + { + name: 'foo', + value: '', + type: 'String', + }, + { + name: 'bar', + value: 'bar', + type: 'String', + }, + ]; + const mapped = CustomFieldMapper.fromData(data); + expect(mapped).toMatchInlineSnapshot(` + { + "bar": "bar", + } + `); + }); + }); +}); diff --git a/src/app/core/models/custom-field/custom-field.mapper.ts b/src/app/core/models/custom-field/custom-field.mapper.ts new file mode 100644 index 0000000000..17f4eb1811 --- /dev/null +++ b/src/app/core/models/custom-field/custom-field.mapper.ts @@ -0,0 +1,19 @@ +import { CustomFieldData } from './custom-field.interface'; +import { CustomFields } from './custom-field.model'; + +export class CustomFieldMapper { + static fromData(customFieldData: CustomFieldData[] = []): CustomFields { + return customFieldData.filter(CustomFieldMapper.hasValue).reduce((customFields, customField) => { + customFields[customField.name] = customField.value; + return customFields; + }, {}); + } + + private static hasValue(customFieldData: CustomFieldData): boolean { + if (customFieldData.type === 'String') { + return !!customFieldData.value; + } + // eslint-disable-next-line unicorn/no-null + return customFieldData.value !== null; + } +} diff --git a/src/app/core/models/custom-field/custom-field.model.ts b/src/app/core/models/custom-field/custom-field.model.ts new file mode 100644 index 0000000000..5a3890b55e --- /dev/null +++ b/src/app/core/models/custom-field/custom-field.model.ts @@ -0,0 +1,9 @@ +import { CustomFieldDefinitionScopeType } from 'ish-core/models/server-config/server-config.model'; + +type CustomFieldValue = string; + +export type CustomFields = Record; + +export type CustomFieldsComponentInput = CustomFieldDefinitionScopeType & { + value: CustomFieldValue; +}; diff --git a/src/app/core/models/line-item/line-item.interface.ts b/src/app/core/models/line-item/line-item.interface.ts index b8fa26cd4f..243b7bbafb 100644 --- a/src/app/core/models/line-item/line-item.interface.ts +++ b/src/app/core/models/line-item/line-item.interface.ts @@ -1,3 +1,4 @@ +import { CustomFieldData } from 'ish-core/models/custom-field/custom-field.interface'; import { PriceItemData } from 'ish-core/models/price-item/price-item.interface'; import { PriceData } from 'ish-core/models/price/price.interface'; @@ -35,5 +36,6 @@ export interface LineItemData { quantityFixed?: boolean; quote?: string; desiredDelivery?: string; + customFields?: CustomFieldData[]; warranty?: string; } diff --git a/src/app/core/models/line-item/line-item.mapper.ts b/src/app/core/models/line-item/line-item.mapper.ts index 70c38ca7f2..feb0660723 100644 --- a/src/app/core/models/line-item/line-item.mapper.ts +++ b/src/app/core/models/line-item/line-item.mapper.ts @@ -2,6 +2,7 @@ import { BasketRebateData } from 'ish-core/models/basket-rebate/basket-rebate.in import { BasketRebateMapper } from 'ish-core/models/basket-rebate/basket-rebate.mapper'; import { BasketWarrantyData } from 'ish-core/models/basket-warranty/basket-warranty.interface'; import { BasketWarrantyMapper } from 'ish-core/models/basket-warranty/basket-warranty.mapper'; +import { CustomFieldMapper } from 'ish-core/models/custom-field/custom-field.mapper'; import { OrderItemData } from 'ish-core/models/order-item/order-item.interface'; import { OrderLineItem } from 'ish-core/models/order/order.model'; import { PriceItemMapper } from 'ish-core/models/price-item/price-item.mapper'; @@ -56,6 +57,7 @@ export class LineItemMapper { editable: !data.quantityFixed, quote: data.quote ? data.quote : undefined, desiredDeliveryDate: data.desiredDelivery, + customFields: CustomFieldMapper.fromData(data.customFields), warranty: data.warranty && warrantyData ? BasketWarrantyMapper.fromData(warrantyData[data.warranty]) : undefined, }; diff --git a/src/app/core/models/line-item/line-item.model.ts b/src/app/core/models/line-item/line-item.model.ts index 7a189569aa..ccaaab4e8e 100644 --- a/src/app/core/models/line-item/line-item.model.ts +++ b/src/app/core/models/line-item/line-item.model.ts @@ -1,6 +1,7 @@ import { BasketFeedback } from 'ish-core/models/basket-feedback/basket-feedback.model'; import { BasketRebate } from 'ish-core/models/basket-rebate/basket-rebate.model'; import { BasketWarranty } from 'ish-core/models/basket-warranty/basket-warranty.model'; +import { CustomFields } from 'ish-core/models/custom-field/custom-field.model'; import { PriceItem } from 'ish-core/models/price-item/price-item.model'; import { Price } from 'ish-core/models/price/price.model'; import { SkuQuantityType } from 'ish-core/models/product/product.model'; @@ -38,7 +39,7 @@ export interface LineItem { editable: boolean; quote?: string; desiredDeliveryDate?: string; - + customFields?: CustomFields; warranty?: BasketWarranty; } diff --git a/src/app/core/models/order/order.mapper.ts b/src/app/core/models/order/order.mapper.ts index 17e58bf052..fcaf795d93 100644 --- a/src/app/core/models/order/order.mapper.ts +++ b/src/app/core/models/order/order.mapper.ts @@ -1,6 +1,7 @@ import { AddressMapper } from 'ish-core/models/address/address.mapper'; import { AttributeHelper } from 'ish-core/models/attribute/attribute.helper'; import { BasketMapper } from 'ish-core/models/basket/basket.mapper'; +import { CustomFieldMapper } from 'ish-core/models/custom-field/custom-field.mapper'; import { LineItemMapper } from 'ish-core/models/line-item/line-item.mapper'; import { PaymentMapper } from 'ish-core/models/payment/payment.mapper'; import { ShippingMethodMapper } from 'ish-core/models/shipping-method/shipping-method.mapper'; @@ -91,6 +92,7 @@ export class OrderMapper { user: data.buyer, messageToMerchant: data.messageToMerchant, externalOrderReference: data.externalOrderReference, + customFields: CustomFieldMapper.fromData(data.customFields), }; } } diff --git a/src/app/core/models/server-config/server-config.interface.ts b/src/app/core/models/server-config/server-config.interface.ts index 11c0d671d0..19ac49f5ab 100644 --- a/src/app/core/models/server-config/server-config.interface.ts +++ b/src/app/core/models/server-config/server-config.interface.ts @@ -1,5 +1,21 @@ +export type CustomFieldDefinitionScopes = 'Basket' | 'BasketLineItem' | 'Order' | 'OrderLineItem'; + +export interface CustomFieldDefinitionsData extends ServerConfigDataEntry { + description: string; + displayName: string; + name: string; + position: number; + type: 'String'; + scopes: { + isEditable: boolean; + isVisible: boolean; + name: CustomFieldDefinitionScopes; + }[]; +} + export interface ServerConfigDataEntry { - [key: string]: string | boolean | number | string[] | ServerConfigDataEntry; + customFieldDefinitions?: CustomFieldDefinitionsData[]; + [key: string]: string | boolean | number | string[] | ServerConfigDataEntry[] | ServerConfigDataEntry; } export interface ServerConfigData { diff --git a/src/app/core/models/server-config/server-config.mapper.spec.ts b/src/app/core/models/server-config/server-config.mapper.spec.ts index b539ec0f67..da3d033565 100644 --- a/src/app/core/models/server-config/server-config.mapper.spec.ts +++ b/src/app/core/models/server-config/server-config.mapper.spec.ts @@ -25,44 +25,243 @@ describe('Server Config Mapper', () => { }); expect(config).toMatchInlineSnapshot(` - { - "application": { - "applicationType": "intershop.B2CResponsive", - "displayName": null, - "urlIdentifier": "-", + [ + { + "application": { + "applicationType": "intershop.B2CResponsive", + "displayName": null, + "urlIdentifier": "-", + }, + "basket": { + "acceleration": true, + }, + "general": { + "locales": [ + "en_US", + "de_DE", + ], + }, + "services": { + "captcha": { + "siteKey": "ASDF", + }, + "deeper": { + "hidden": { + "alt": 123, + "foo": "bar", + "num": 123, + }, + }, + "gtm": { + "monitor": true, + "token": "QWERTY", + }, + }, }, - "basket": { - "acceleration": true, + { + "entities": {}, + "scopes": {}, }, - "general": { - "locales": [ - "en_US", - "de_DE", - ], + ] + `); + }); + + it(`should return an empty object for falsy input`, () => { + expect(ServerConfigMapper.fromData(undefined)).toMatchInlineSnapshot(` + [ + {}, + undefined, + ] + `); + expect(ServerConfigMapper.fromData({} as ServerConfigData)).toMatchInlineSnapshot(` + [ + {}, + undefined, + ] + `); + }); + + it('should map custom fields', () => { + const config = ServerConfigMapper.fromData({ + data: { + customFieldDefinitions: [ + { + description: 'foo', + displayName: 'foo', + name: 'foo', + position: 1, + scopes: [ + { + isEditable: true, + isVisible: true, + name: 'Basket', + }, + { + isEditable: true, + isVisible: true, + name: 'Order', + }, + ], + type: 'String', + }, + { + description: 'bar', + displayName: 'bar', + name: 'bar', + position: 2, + scopes: [ + { + isEditable: true, + isVisible: true, + name: 'BasketLineItem', + }, + ], + type: 'String', + }, + { + description: 'baz', + displayName: 'baz', + name: 'baz', + position: 3, + scopes: [ + { + isEditable: true, + isVisible: true, + name: 'Order', + }, + ], + type: 'String', + }, + { + description: 'qux', + displayName: 'qux', + name: 'qux', + position: 4, + scopes: [ + { + isEditable: true, + isVisible: true, + name: 'BasketLineItem', + }, + { + isEditable: false, + isVisible: false, + name: 'OrderLineItem', + }, + ], + type: 'String', + }, + { + description: 'wob', + displayName: 'wob', + name: 'wob', + position: 2, + scopes: [ + { + isEditable: true, + isVisible: true, + name: 'BasketLineItem', + }, + { + isEditable: false, + isVisible: true, + name: 'OrderLineItem', + }, + ], + type: 'String', + }, + ], + general: { + id: 'general', + locales: ['en_US', 'de_DE'], }, - "services": { - "captcha": { - "siteKey": "ASDF", + }, + }); + + expect(config).toMatchInlineSnapshot(` + [ + { + "general": { + "locales": [ + "en_US", + "de_DE", + ], }, - "deeper": { - "hidden": { - "alt": 123, - "foo": "bar", - "num": 123, + }, + { + "entities": { + "bar": { + "description": "bar", + "displayName": "bar", + "name": "bar", + "type": "String", + }, + "baz": { + "description": "baz", + "displayName": "baz", + "name": "baz", + "type": "String", + }, + "foo": { + "description": "foo", + "displayName": "foo", + "name": "foo", + "type": "String", + }, + "qux": { + "description": "qux", + "displayName": "qux", + "name": "qux", + "type": "String", + }, + "wob": { + "description": "wob", + "displayName": "wob", + "name": "wob", + "type": "String", }, }, - "gtm": { - "monitor": true, - "token": "QWERTY", + "scopes": { + "Basket": [ + { + "editable": true, + "name": "foo", + }, + ], + "BasketLineItem": [ + { + "editable": true, + "name": "bar", + }, + { + "editable": true, + "name": "wob", + }, + { + "editable": true, + "name": "qux", + }, + ], + "Order": [ + { + "editable": true, + "name": "foo", + }, + { + "editable": true, + "name": "baz", + }, + ], + "OrderLineItem": [ + { + "editable": false, + "name": "wob", + }, + ], }, }, - } + ] `); }); - - it(`should return an empty object for falsy input`, () => { - expect(ServerConfigMapper.fromData(undefined)).toMatchInlineSnapshot(`{}`); - expect(ServerConfigMapper.fromData({} as ServerConfigData)).toMatchInlineSnapshot(`{}`); - }); }); }); diff --git a/src/app/core/models/server-config/server-config.mapper.ts b/src/app/core/models/server-config/server-config.mapper.ts index 87bf40e034..43921b5d35 100644 --- a/src/app/core/models/server-config/server-config.mapper.ts +++ b/src/app/core/models/server-config/server-config.mapper.ts @@ -1,14 +1,16 @@ import { omit } from 'ish-core/utils/functions'; -import { ServerConfigData, ServerConfigDataEntry } from './server-config.interface'; -import { ServerConfig } from './server-config.model'; +import { CustomFieldDefinitionsData, ServerConfigData, ServerConfigDataEntry } from './server-config.interface'; +import { CustomFieldDefinitions, ServerConfig } from './server-config.model'; export class ServerConfigMapper { - static fromData(payload: ServerConfigData): ServerConfig { + static fromData(payload: ServerConfigData): [ServerConfig, CustomFieldDefinitions | undefined] { if (payload?.data) { - return ServerConfigMapper.mapEntries(payload.data); + const config = ServerConfigMapper.mapEntries(omit(payload.data, 'customFieldDefinitions')); + const definitions = ServerConfigMapper.mapCustomFields(payload.data.customFieldDefinitions); + return [config, definitions]; } - return {}; + return [{}, undefined]; } private static transformType(val: unknown) { @@ -42,4 +44,39 @@ export class ServerConfigMapper { {} ); } + + private static mapCustomFields(data: CustomFieldDefinitionsData[] = []): CustomFieldDefinitions { + const entities = data.reduce( + (acc, entry) => ({ + ...acc, + [entry.name]: { + description: entry.description, + displayName: entry.displayName, + name: entry.name, + type: entry.type, + }, + }), + {} + ); + + const scopes = data + .sort((a, b) => a.position - b.position) + .reduce((acc, entry) => { + entry.scopes.forEach(scope => { + if (!scope.isVisible) { + return; + } + if (!acc[scope.name]) { + acc[scope.name] = []; + } + acc[scope.name].push({ + name: entry.name, + editable: scope.isEditable, + }); + }); + return acc; + }, {}); + + return { entities, scopes }; + } } diff --git a/src/app/core/models/server-config/server-config.model.ts b/src/app/core/models/server-config/server-config.model.ts index 8877d2cd8a..33458f2214 100644 --- a/src/app/core/models/server-config/server-config.model.ts +++ b/src/app/core/models/server-config/server-config.model.ts @@ -1,7 +1,33 @@ +import { CustomFieldDefinitionScopes } from './server-config.interface'; + +export type CustomFieldDefinitionScopeType = { + name: string; + editable?: boolean; +}; + +interface Scopes { + [key: CustomFieldDefinitionScopes | string]: CustomFieldDefinitionScopeType[]; +} + +export type CustomFieldDefinition = { + description: string; + displayName: string; + name: string; + type: 'String'; +}; + +interface Entities { + [key: string]: CustomFieldDefinition; +} + +export interface CustomFieldDefinitions { + scopes: Scopes; + entities: Entities; +} + /** * model for config data (parameters, services etc.) retrieved from the ICM server. */ - export interface ServerConfig { [key: string]: string | boolean | string[] | ServerConfig; } diff --git a/src/app/core/services/configuration/configuration.service.ts b/src/app/core/services/configuration/configuration.service.ts index 16cb71f3e5..099505668b 100644 --- a/src/app/core/services/configuration/configuration.service.ts +++ b/src/app/core/services/configuration/configuration.service.ts @@ -7,7 +7,7 @@ import { map } from 'rxjs/operators'; import { ContentConfigurationParameterMapper } from 'ish-core/models/content-configuration-parameter/content-configuration-parameter.mapper'; import { ContentPageletEntryPointData } from 'ish-core/models/content-pagelet-entry-point/content-pagelet-entry-point.interface'; import { ServerConfigMapper } from 'ish-core/models/server-config/server-config.mapper'; -import { ServerConfig } from 'ish-core/models/server-config/server-config.model'; +import { CustomFieldDefinitions, ServerConfig } from 'ish-core/models/server-config/server-config.model'; import { ApiService } from 'ish-core/services/api/api.service'; import { DomService } from 'ish-core/utils/dom/dom.service'; @@ -30,7 +30,7 @@ export class ConfigurationService { * * @returns The configuration object. */ - getServerConfiguration(): Observable { + getServerConfiguration(): Observable<[ServerConfig, CustomFieldDefinitions]> { return this.apiService .get(`configurations`, { headers: this.configHeaders, diff --git a/src/app/core/store/core/server-config/server-config.actions.ts b/src/app/core/store/core/server-config/server-config.actions.ts index df7d93df4d..ae9ecc0d5d 100644 --- a/src/app/core/store/core/server-config/server-config.actions.ts +++ b/src/app/core/store/core/server-config/server-config.actions.ts @@ -1,13 +1,13 @@ import { createAction } from '@ngrx/store'; -import { ServerConfig } from 'ish-core/models/server-config/server-config.model'; +import { CustomFieldDefinitions, ServerConfig } from 'ish-core/models/server-config/server-config.model'; import { httpError, payload } from 'ish-core/utils/ngrx-creators'; export const loadServerConfig = createAction('[Configuration Internal] Get the ICM configuration'); export const loadServerConfigSuccess = createAction( '[Configuration API] Get the ICM configuration Success', - payload<{ config: ServerConfig }>() + payload<{ config: ServerConfig; definitions?: CustomFieldDefinitions }>() ); export const loadServerConfigFail = createAction('[Configuration API] Get the ICM configuration Fail', httpError()); diff --git a/src/app/core/store/core/server-config/server-config.effects.spec.ts b/src/app/core/store/core/server-config/server-config.effects.spec.ts index 75bf2bbac5..59fbb94a10 100644 --- a/src/app/core/store/core/server-config/server-config.effects.spec.ts +++ b/src/app/core/store/core/server-config/server-config.effects.spec.ts @@ -30,7 +30,7 @@ describe('Server Config Effects', () => { const cookiesServiceMock = mock(CookiesService); const multiSiteServiceMock = mock(MultiSiteService); - when(configurationServiceMock.getServerConfiguration()).thenReturn(of({})); + when(configurationServiceMock.getServerConfiguration()).thenReturn(of([{}, undefined])); when(cookiesServiceMock.get(anything())).thenReturn('de_DE'); when(multiSiteServiceMock.getLangUpdatedUrl(anything(), anything())).thenReturn(of('/home;lang=de_DE')); when(multiSiteServiceMock.appendUrlParams(anything(), anything(), anything())).thenReturn('/home;lang=de_DE'); @@ -64,6 +64,7 @@ describe('Server Config Effects', () => { [Configuration Internal] Get the ICM configuration [Configuration API] Get the ICM configuration Success: config: {} + definitions: undefined `); }); }); @@ -123,7 +124,7 @@ describe('Server Config Effects', () => { describe('loadServerConfig$', () => { beforeEach(() => { - when(configurationServiceMock.getServerConfiguration()).thenReturn(of({})); + when(configurationServiceMock.getServerConfiguration()).thenReturn(of([{}, undefined])); }); it('should map to action of type LoadServerConfigSuccess', () => { diff --git a/src/app/core/store/core/server-config/server-config.effects.ts b/src/app/core/store/core/server-config/server-config.effects.ts index 45e3a50b5d..2bb3bf12ab 100644 --- a/src/app/core/store/core/server-config/server-config.effects.ts +++ b/src/app/core/store/core/server-config/server-config.effects.ts @@ -54,7 +54,7 @@ export class ServerConfigEffects { ofType(loadServerConfig), switchMap(() => this.configService.getServerConfiguration().pipe( - map(config => loadServerConfigSuccess({ config })), + map(([config, definitions]) => loadServerConfigSuccess({ config, definitions })), mapErrorToAction(loadServerConfigFail) ) ) diff --git a/src/app/core/store/core/server-config/server-config.reducer.ts b/src/app/core/store/core/server-config/server-config.reducer.ts index c9de2f3f66..eef016ba79 100644 --- a/src/app/core/store/core/server-config/server-config.reducer.ts +++ b/src/app/core/store/core/server-config/server-config.reducer.ts @@ -1,16 +1,18 @@ import { createReducer, on } from '@ngrx/store'; -import { ServerConfig } from 'ish-core/models/server-config/server-config.model'; +import { CustomFieldDefinitions, ServerConfig } from 'ish-core/models/server-config/server-config.model'; import { loadExtraConfigSuccess, loadServerConfigSuccess } from './server-config.actions'; export interface ServerConfigState { _config: ServerConfig; + _definitions: CustomFieldDefinitions; extra: ServerConfig; } const initialState: ServerConfigState = { _config: undefined, + _definitions: undefined, extra: undefined, }; @@ -21,6 +23,7 @@ export const serverConfigReducer = createReducer( (state, action): ServerConfigState => ({ ...state, _config: action.payload.config, + _definitions: action.payload.definitions, }) ), on( diff --git a/src/app/core/store/core/server-config/server-config.selectors.spec.ts b/src/app/core/store/core/server-config/server-config.selectors.spec.ts index 7672bc3a57..c6e27aa24b 100644 --- a/src/app/core/store/core/server-config/server-config.selectors.spec.ts +++ b/src/app/core/store/core/server-config/server-config.selectors.spec.ts @@ -1,10 +1,16 @@ import { TestBed } from '@angular/core/testing'; +import { isEqual } from 'lodash-es'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; import { StoreWithSnapshots, provideStoreSnapshots } from 'ish-core/utils/dev/ngrx-testing'; import { loadServerConfigSuccess } from './server-config.actions'; -import { getServerConfigParameter, isServerConfigurationLoaded } from './server-config.selectors'; +import { + getCustomFieldDefinition, + getCustomFieldIdsForScope, + getServerConfigParameter, + isServerConfigurationLoaded, +} from './server-config.selectors'; describe('Server Config Selectors', () => { let store$: StoreWithSnapshots; @@ -68,4 +74,103 @@ describe('Server Config Selectors', () => { `); }); }); + + describe('after setting config for custom fields', () => { + beforeEach(() => { + store$.dispatch( + loadServerConfigSuccess({ + config: {}, + definitions: { + entities: { + commissionNumber: { + name: 'commissionNumber', + description: 'Commission Number', + type: 'String', + displayName: 'Commission Number', + }, + projectNumber: { + name: 'projectNumber', + description: 'Project Number', + type: 'String', + displayName: 'Project Number', + }, + customerProductId: { + name: 'customerProductId', + description: 'Customer Product ID', + type: 'String', + displayName: 'Customer Product ID', + }, + }, + scopes: { + Basket: [ + { name: 'commissionNumber', editable: true }, + { name: 'projectNumber', editable: true }, + ], + Order: [ + { name: 'commissionNumber', editable: false }, + { name: 'projectNumber', editable: false }, + ], + BasketLineItem: [{ name: 'customerProductId', editable: true }], + OrderLineItem: [{ name: 'customerProductId', editable: false }], + }, + }, + }) + ); + }); + + it('should return custom field IDs definitions for scopes', () => { + expect.addSnapshotSerializer({ + test: val => + !Array.isArray(val) && + isEqual(Object.keys(val), ['name', 'editable']) && + typeof val.name === 'string' && + typeof val.editable === 'boolean', + print: (val: { name: string; editable: boolean }) => + `${val.name} (${val.editable ? 'editable' : 'not editable'})`, + }); + + expect(getCustomFieldIdsForScope('Basket')(store$.state)).toMatchInlineSnapshot(` + [ + commissionNumber (editable), + projectNumber (editable), + ] + `); + expect(getCustomFieldIdsForScope('BasketLineItem')(store$.state)).toMatchInlineSnapshot(` + [ + customerProductId (editable), + ] + `); + expect(getCustomFieldIdsForScope('Order')(store$.state)).toMatchInlineSnapshot(` + [ + commissionNumber (not editable), + projectNumber (not editable), + ] + `); + expect(getCustomFieldIdsForScope('OrderLineItem')(store$.state)).toMatchInlineSnapshot(` + [ + customerProductId (not editable), + ] + `); + }); + + it('should return the field for an ID', () => { + expect(getCustomFieldDefinition('commissionNumber')(store$.state)).toMatchInlineSnapshot(` + { + "description": "Commission Number", + "displayName": "Commission Number", + "name": "commissionNumber", + "type": "String", + } + `); + expect(getCustomFieldDefinition('customerProductId')(store$.state)).toMatchInlineSnapshot(` + { + "description": "Customer Product ID", + "displayName": "Customer Product ID", + "name": "customerProductId", + "type": "String", + } + `); + expect(getCustomFieldDefinition(undefined)(store$.state)).toBeUndefined(); + }); + }); }); diff --git a/src/app/core/store/core/server-config/server-config.selectors.ts b/src/app/core/store/core/server-config/server-config.selectors.ts index 9c97673abc..39efcb63a2 100644 --- a/src/app/core/store/core/server-config/server-config.selectors.ts +++ b/src/app/core/store/core/server-config/server-config.selectors.ts @@ -1,5 +1,6 @@ import { createSelector } from '@ngrx/store'; +import { CustomFieldDefinitionScopes } from 'ish-core/models/server-config/server-config.interface'; import { getCoreState } from 'ish-core/store/core/core-store'; const getServerConfigState = createSelector(getCoreState, state => state.serverConfig); @@ -29,3 +30,16 @@ export const getExtraConfigParameter = (path: string) => .split('.') .reduce((obj, key) => (obj?.[key] !== undefined ? obj[key] : undefined), extraConfig) as unknown as T ); + +const getCustomFieldDefinitions = createSelector(getServerConfigState, state => state._definitions); + +export const getCustomFieldIdsForScope = (scope: CustomFieldDefinitionScopes) => + createSelector(getCustomFieldDefinitions, customFieldDefinitions => customFieldDefinitions?.scopes?.[scope] ?? []); + +export const getCustomFieldsForScope = (scope: CustomFieldDefinitionScopes) => + createSelector(getCustomFieldDefinitions, getCustomFieldIdsForScope(scope), (customFieldDefinitions, ids) => + ids.map(({ name, editable }) => ({ name, editable, ...customFieldDefinitions?.entities?.[name] })) + ); + +export const getCustomFieldDefinition = (name: string) => + createSelector(getCustomFieldDefinitions, customFieldDefinitions => customFieldDefinitions?.entities?.[name]); From de964aeed1d3b84e502220a222ab5ba62de8f10a Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Wed, 13 Dec 2023 15:08:48 +0100 Subject: [PATCH 3/6] feat: display and edit custom fields on basket --- src/app/core/facades/app.facade.ts | 5 + src/app/core/facades/checkout.facade.ts | 6 + .../line-item-update.model.ts | 3 + .../basket-items/basket-items.service.ts | 16 +- .../core/services/basket/basket.service.ts | 4 +- .../basket/basket-items.effects.spec.ts | 5 +- .../customer/basket/basket-items.effects.ts | 43 ++++-- .../store/customer/basket/basket.actions.ts | 8 + .../store/customer/basket/basket.effects.ts | 18 +++ .../shopping-basket.component.html | 3 + .../shopping-basket.component.spec.ts | 2 + .../basket-custom-fields.component.html | 24 +++ .../basket-custom-fields.component.spec.ts | 46 ++++++ .../basket-custom-fields.component.ts | 47 ++++++ .../custom-fields-formly.component.html | 1 + .../custom-fields-formly.component.spec.ts | 33 +++++ .../custom-fields-formly.component.ts | 75 ++++++++++ .../custom-fields-view.component.html | 8 + .../custom-fields-view.component.scss | 22 +++ .../custom-fields-view.component.spec.ts | 31 ++++ .../custom-fields-view.component.ts | 39 +++++ .../line-item-custom-fields.component.html | 1 + .../line-item-custom-fields.component.spec.ts | 37 +++++ .../line-item-custom-fields.component.ts | 46 ++++++ .../line-item-edit-dialog.component.html | 37 ----- .../line-item-edit-dialog.component.spec.ts | 86 ----------- .../line-item-edit-dialog.component.ts | 29 ---- .../line-item-edit.component.html | 71 +++++++-- .../line-item-edit.component.spec.ts | 28 +++- .../line-item-edit.component.ts | 100 +++++++++++-- .../line-item-list-element.component.html | 11 +- .../line-item-list-element.component.spec.ts | 139 +++++++----------- .../line-item-list-element.component.ts | 31 +++- .../line-item-list.component.html | 2 +- .../line-item-list.component.ts | 4 - src/app/shared/shared.module.ts | 10 +- src/assets/i18n/de_DE.json | 1 + src/assets/i18n/en_US.json | 5 + 38 files changed, 766 insertions(+), 311 deletions(-) create mode 100644 src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.html create mode 100644 src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.spec.ts create mode 100644 src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.ts create mode 100644 src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.html create mode 100644 src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.spec.ts create mode 100644 src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.ts create mode 100644 src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.html create mode 100644 src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.scss create mode 100644 src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.spec.ts create mode 100644 src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.ts create mode 100644 src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.html create mode 100644 src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.spec.ts create mode 100644 src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.ts delete mode 100644 src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.html delete mode 100644 src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.spec.ts delete mode 100644 src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.ts diff --git a/src/app/core/facades/app.facade.ts b/src/app/core/facades/app.facade.ts index 800fd74cf0..9b4eef3e54 100644 --- a/src/app/core/facades/app.facade.ts +++ b/src/app/core/facades/app.facade.ts @@ -17,6 +17,7 @@ import { import { businessError, getGeneralError, getGeneralErrorType } from 'ish-core/store/core/error'; import { selectPath } from 'ish-core/store/core/router'; import { + getCustomFieldDefinition, getCustomFieldIdsForScope, getExtraConfigParameter, getServerConfigParameter, @@ -132,6 +133,10 @@ export class AppFacade { return this.store.pipe(select(getCustomFieldIdsForScope(scope))); } + customField$(name: string) { + return this.store.pipe(select(getCustomFieldDefinition(name))); + } + /** * returns the currency symbol for the currency parameter in the current locale. * If no parameter is given, the the default currency is taken instead of it. diff --git a/src/app/core/facades/checkout.facade.ts b/src/app/core/facades/checkout.facade.ts index ef4f7b3cd6..db5686abe1 100644 --- a/src/app/core/facades/checkout.facade.ts +++ b/src/app/core/facades/checkout.facade.ts @@ -7,6 +7,7 @@ import { debounceTime, distinctUntilChanged, map, sample, switchMap, take, tap } import { Address } from 'ish-core/models/address/address.model'; import { Attribute } from 'ish-core/models/attribute/attribute.model'; import { CheckoutStepType } from 'ish-core/models/checkout/checkout-step.type'; +import { CustomFields } from 'ish-core/models/custom-field/custom-field.model'; import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model'; import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model'; import { selectRouteData } from 'ish-core/store/core/router'; @@ -44,6 +45,7 @@ import { loadBasketWithId, removePromotionCodeFromBasket, setBasketAttribute, + setBasketCustomFields, setBasketDesiredDeliveryDate, setBasketPayment, startCheckout, @@ -160,6 +162,10 @@ export class CheckoutFacade { this.store.dispatch(addMessageToMerchant({ messageToMerchant: messageToMerchant || null })); } + setBasketCustomFields(customFields: CustomFields) { + this.store.dispatch(setBasketCustomFields({ customFields })); + } + // ORDERS private ordersError$ = this.store.pipe(select(getOrdersError)); diff --git a/src/app/core/models/line-item-update/line-item-update.model.ts b/src/app/core/models/line-item-update/line-item-update.model.ts index 043ec65184..83839c75df 100644 --- a/src/app/core/models/line-item-update/line-item-update.model.ts +++ b/src/app/core/models/line-item-update/line-item-update.model.ts @@ -1,7 +1,10 @@ +import { CustomFields } from 'ish-core/models/custom-field/custom-field.model'; + export interface LineItemUpdate { itemId: string; quantity?: number; sku?: string; unit?: string; + customFields?: CustomFields; warrantySku?: string; } diff --git a/src/app/core/services/basket-items/basket-items.service.ts b/src/app/core/services/basket-items/basket-items.service.ts index 780175b872..0398ff5edb 100644 --- a/src/app/core/services/basket-items/basket-items.service.ts +++ b/src/app/core/services/basket-items/basket-items.service.ts @@ -7,6 +7,7 @@ import { concatMap, first, map } from 'rxjs/operators'; import { BasketInfoMapper } from 'ish-core/models/basket-info/basket-info.mapper'; import { BasketInfo } from 'ish-core/models/basket-info/basket-info.model'; import { Basket } from 'ish-core/models/basket/basket.model'; +import { CustomFieldData } from 'ish-core/models/custom-field/custom-field.interface'; import { ErrorFeedback } from 'ish-core/models/http-error/http-error.model'; import { LineItemData } from 'ish-core/models/line-item/line-item.interface'; import { LineItemMapper } from 'ish-core/models/line-item/line-item.mapper'; @@ -14,12 +15,15 @@ import { AddLineItemType, LineItem, LineItemView } from 'ish-core/models/line-it import { ApiService } from 'ish-core/services/api/api.service'; import { getCurrentBasket } from 'ish-core/store/customer/basket'; -export type BasketItemUpdateType = - | { quantity?: { value: number; unit: string }; product?: string } - | { shippingMethod?: { id: string } } - | { desiredDelivery?: string } - | { calculated: boolean } - | { warranty?: string }; +export interface BasketItemUpdateType { + quantity?: { value: number; unit: string }; + product?: string; + shippingMethod?: { id: string }; + desiredDelivery?: string; + customFields?: CustomFieldData[]; + calculated?: boolean; + warranty?: string; +} /** * The Basket-Items Service handles basket line-item related calls for the 'baskets/items' REST API. diff --git a/src/app/core/services/basket/basket.service.ts b/src/app/core/services/basket/basket.service.ts index 854d549b26..8d92def94c 100644 --- a/src/app/core/services/basket/basket.service.ts +++ b/src/app/core/services/basket/basket.service.ts @@ -17,6 +17,7 @@ import { BasketValidation, BasketValidationScopeType } from 'ish-core/models/bas import { BasketBaseData, BasketData } from 'ish-core/models/basket/basket.interface'; import { BasketMapper } from 'ish-core/models/basket/basket.mapper'; import { Basket } from 'ish-core/models/basket/basket.model'; +import { CustomFieldData } from 'ish-core/models/custom-field/custom-field.interface'; import { ShippingMethodData } from 'ish-core/models/shipping-method/shipping-method.interface'; import { ShippingMethodMapper } from 'ish-core/models/shipping-method/shipping-method.mapper'; import { ShippingMethod } from 'ish-core/models/shipping-method/shipping-method.model'; @@ -31,7 +32,8 @@ export type BasketUpdateType = | { costCenter: string } | { externalOrderReference: string } | { invoiceToAddress: string } - | { messageToMerchant: string }; + | { messageToMerchant: string } + | { customFields: CustomFieldData[] }; /** * The Basket Service handles the interaction with the 'baskets' REST API. diff --git a/src/app/core/store/customer/basket/basket-items.effects.spec.ts b/src/app/core/store/customer/basket/basket-items.effects.spec.ts index 386fbb6992..d60dfd23c4 100644 --- a/src/app/core/store/customer/basket/basket-items.effects.spec.ts +++ b/src/app/core/store/customer/basket/basket-items.effects.spec.ts @@ -13,6 +13,7 @@ import { Product } from 'ish-core/models/product/product.model'; import { BasketItemsService } from 'ish-core/services/basket-items/basket-items.service'; import { BasketService } from 'ish-core/services/basket/basket.service'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; +import { loadServerConfigSuccess } from 'ish-core/store/core/server-config'; import { CustomerStoreModule } from 'ish-core/store/customer/customer-store.module'; import { loadProduct, loadProductSuccess } from 'ish-core/store/shopping/products'; import { ShoppingStoreModule } from 'ish-core/store/shopping/shopping-store.module'; @@ -52,7 +53,7 @@ describe('Basket Items Effects', () => { TestBed.configureTestingModule({ imports: [ - CoreStoreModule.forTesting(), + CoreStoreModule.forTesting(['serverConfig']), CustomerStoreModule.forTesting('basket'), RouterTestingModule.withRoutes([{ path: '**', children: [] }]), ShoppingStoreModule.forTesting('products', 'categories'), @@ -68,6 +69,8 @@ describe('Basket Items Effects', () => { effects = TestBed.inject(BasketItemsEffects); store = TestBed.inject(Store); location = TestBed.inject(Location); + + store.dispatch(loadServerConfigSuccess({ config: {} })); }); describe('addProductToBasket$', () => { diff --git a/src/app/core/store/customer/basket/basket-items.effects.ts b/src/app/core/store/customer/basket/basket-items.effects.ts index 778d982ac5..5e47ad89a5 100644 --- a/src/app/core/store/customer/basket/basket-items.effects.ts +++ b/src/app/core/store/customer/basket/basket-items.effects.ts @@ -6,8 +6,10 @@ import { from } from 'rxjs'; import { concatMap, debounceTime, filter, map, mergeMap, switchMap, toArray, window } from 'rxjs/operators'; import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model'; +import { CustomFieldDefinitionsData } from 'ish-core/models/server-config/server-config.interface'; import { BasketItemUpdateType, BasketItemsService } from 'ish-core/services/basket-items/basket-items.service'; import { BasketService } from 'ish-core/services/basket/basket.service'; +import { getCustomFieldsForScope } from 'ish-core/store/core/server-config'; import { getProductEntities, loadProduct } from 'ish-core/store/shopping/products'; import { mapErrorToAction, mapToPayload, mapToPayloadProperty } from 'ish-core/utils/operators'; @@ -103,11 +105,14 @@ export class BasketItemsEffects { mapToPayloadProperty('lineItemUpdate'), concatLatestFrom(() => this.store.pipe(select(getCurrentBasket))), filter(([payload, basket]) => !!basket.lineItems && !!payload), - concatMap(([lineItem]) => - this.basketItemsService.updateBasketItem(lineItem.itemId, this.determineUpdateItemPayload(lineItem)).pipe( - map(payload => updateBasketItemSuccess(payload)), - mapErrorToAction(updateBasketItemFail) - ) + concatLatestFrom(() => this.store.pipe(select(getCustomFieldsForScope('BasketLineItem')))), + concatMap(([[lineItem], customFieldDefinitions]) => + this.basketItemsService + .updateBasketItem(lineItem.itemId, this.mapLineItemUpdate(lineItem, customFieldDefinitions)) + .pipe( + map(payload => updateBasketItemSuccess(payload)), + mapErrorToAction(updateBasketItemFail) + ) ) ) ); @@ -176,16 +181,30 @@ export class BasketItemsEffects { { dispatch: false } ); - private determineUpdateItemPayload(lineItem: LineItemUpdate): BasketItemUpdateType { - const payload: BasketItemUpdateType = { - quantity: lineItem.quantity > 0 ? { value: lineItem.quantity, unit: lineItem.unit } : undefined, - product: lineItem.sku, + private mapLineItemUpdate( + update: LineItemUpdate, + customFieldDefinitions: { name: string; type: CustomFieldDefinitionsData['type'] }[] + ): BasketItemUpdateType { + const itemUpdate: Partial = { + product: update.sku, }; - if (lineItem.warrantySku || lineItem.warrantySku === '') { + if (update.quantity > 0) { + itemUpdate.quantity = { value: update.quantity, unit: update.unit }; + } + if (update.customFields) { + itemUpdate.customFields = customFieldDefinitions.map(({ name, type }) => ({ + name, + type, + value: update.customFields[name], + })); + } + + if (update.warrantySku || update.warrantySku === '') { // eslint-disable-next-line unicorn/no-null - return { ...payload, warranty: lineItem.warrantySku ? lineItem.warrantySku : null }; + itemUpdate.warranty = update.warrantySku ? update.warrantySku : null; } // undefined is not working here - return payload; + + return itemUpdate; } } diff --git a/src/app/core/store/customer/basket/basket.actions.ts b/src/app/core/store/customer/basket/basket.actions.ts index ab4fbe5c20..b6573c76f7 100644 --- a/src/app/core/store/customer/basket/basket.actions.ts +++ b/src/app/core/store/customer/basket/basket.actions.ts @@ -7,6 +7,7 @@ import { BasketInfo } from 'ish-core/models/basket-info/basket-info.model'; import { BasketValidation, BasketValidationScopeType } from 'ish-core/models/basket-validation/basket-validation.model'; import { Basket } from 'ish-core/models/basket/basket.model'; import { CheckoutStepType } from 'ish-core/models/checkout/checkout-step.type'; +import { CustomFields } from 'ish-core/models/custom-field/custom-field.model'; import { ErrorFeedback } from 'ish-core/models/http-error/http-error.model'; import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model'; import { AddLineItemType, LineItem } from 'ish-core/models/line-item/line-item.model'; @@ -68,10 +69,17 @@ export const updateBasketCostCenter = createAction( '[Basket] Assign a Cost Center at Basket ', payload<{ costCenter: string }>() ); + export const addMessageToMerchant = createAction( '[Basket] Message to Merchant', payload<{ messageToMerchant: string }>() ); + +export const setBasketCustomFields = createAction( + '[Basket] Set Basket Custom Fields', + payload<{ customFields: CustomFields }>() +); + export const updateBasket = createAction('[Basket] Update Basket', payload<{ update: BasketUpdateType }>()); export const updateBasketFail = createAction('[Basket API] Update Basket Fail', httpError()); diff --git a/src/app/core/store/customer/basket/basket.effects.ts b/src/app/core/store/customer/basket/basket.effects.ts index f9d8a5a206..1170a6396b 100644 --- a/src/app/core/store/customer/basket/basket.effects.ts +++ b/src/app/core/store/customer/basket/basket.effects.ts @@ -23,6 +23,7 @@ import { BasketItemsService } from 'ish-core/services/basket-items/basket-items. import { BasketService } from 'ish-core/services/basket/basket.service'; import { getCurrentCurrency } from 'ish-core/store/core/configuration'; import { mapToRouterState } from 'ish-core/store/core/router'; +import { getCustomFieldsForScope } from 'ish-core/store/core/server-config'; import { resetOrderErrors } from 'ish-core/store/customer/orders'; import { getLoggedInCustomer, loginUserSuccess, personalizationStatusDetermined } from 'ish-core/store/customer/user'; import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; @@ -53,6 +54,7 @@ import { setBasketAttribute, setBasketAttributeFail, setBasketAttributeSuccess, + setBasketCustomFields, setBasketDesiredDeliveryDate, setBasketDesiredDeliveryDateFail, setBasketDesiredDeliveryDateSuccess, @@ -222,6 +224,22 @@ export class BasketEffects { ) ); + setBasketCustomFields$ = createEffect(() => + this.actions$.pipe( + ofType(setBasketCustomFields), + mapToPayloadProperty('customFields'), + concatLatestFrom(() => this.store.pipe(select(getCustomFieldsForScope('Basket')))), + map(([customFields, definitions]) => + definitions.map(definition => ({ + name: definition.name, + value: customFields[definition.name], + type: definition.type, + })) + ), + map(customFields => updateBasket({ update: { customFields } })) + ) + ); + /** * Sets a desired delivery date at the current basket and each line item. */ diff --git a/src/app/pages/basket/shopping-basket/shopping-basket.component.html b/src/app/pages/basket/shopping-basket/shopping-basket.component.html index 71730e379f..8e94a772d7 100644 --- a/src/app/pages/basket/shopping-basket/shopping-basket.component.html +++ b/src/app/pages/basket/shopping-basket/shopping-basket.component.html @@ -45,6 +45,9 @@

+ + +
diff --git a/src/app/pages/basket/shopping-basket/shopping-basket.component.spec.ts b/src/app/pages/basket/shopping-basket/shopping-basket.component.spec.ts index 662ae4be37..f88f7efe5a 100644 --- a/src/app/pages/basket/shopping-basket/shopping-basket.component.spec.ts +++ b/src/app/pages/basket/shopping-basket/shopping-basket.component.spec.ts @@ -10,6 +10,7 @@ import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data'; import { ContentIncludeComponent } from 'ish-shared/cms/components/content-include/content-include.component'; import { BasketCostCenterSelectionComponent } from 'ish-shared/components/basket/basket-cost-center-selection/basket-cost-center-selection.component'; import { BasketCostSummaryComponent } from 'ish-shared/components/basket/basket-cost-summary/basket-cost-summary.component'; +import { BasketCustomFieldsComponent } from 'ish-shared/components/basket/basket-custom-fields/basket-custom-fields.component'; import { BasketErrorMessageComponent } from 'ish-shared/components/basket/basket-error-message/basket-error-message.component'; import { BasketInfoComponent } from 'ish-shared/components/basket/basket-info/basket-info.component'; import { BasketPromotionCodeComponent } from 'ish-shared/components/basket/basket-promotion-code/basket-promotion-code.component'; @@ -35,6 +36,7 @@ describe('Shopping Basket Component', () => { declarations: [ MockComponent(BasketCostCenterSelectionComponent), MockComponent(BasketCostSummaryComponent), + MockComponent(BasketCustomFieldsComponent), MockComponent(BasketErrorMessageComponent), MockComponent(BasketInfoComponent), MockComponent(BasketPromotionCodeComponent), diff --git a/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.html b/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.html new file mode 100644 index 0000000000..7f0fb809a3 --- /dev/null +++ b/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.html @@ -0,0 +1,24 @@ + +
+ + +
+ + +
+ + +
+
diff --git a/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.spec.ts b/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.spec.ts new file mode 100644 index 0000000000..9874c63b25 --- /dev/null +++ b/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { EMPTY } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; +import { CustomFieldsFormlyComponent } from 'ish-shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component'; +import { CustomFieldsViewComponent } from 'ish-shared/components/custom-fields/custom-fields-view/custom-fields-view.component'; + +import { BasketCustomFieldsComponent } from './basket-custom-fields.component'; + +describe('Basket Custom Fields Component', () => { + let component: BasketCustomFieldsComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + const checkoutFacade = mock(CheckoutFacade); + when(checkoutFacade.basket$).thenReturn(EMPTY); + + await TestBed.configureTestingModule({ + declarations: [ + BasketCustomFieldsComponent, + MockComponent(CustomFieldsFormlyComponent), + MockComponent(CustomFieldsViewComponent), + ], + providers: [ + { provide: AppFacade, useFactory: () => instance(mock(AppFacade)) }, + { provide: CheckoutFacade, useFactory: () => instance(checkoutFacade) }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BasketCustomFieldsComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.ts b/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.ts new file mode 100644 index 0000000000..3f7d74d2b5 --- /dev/null +++ b/src/app/shared/components/basket/basket-custom-fields/basket-custom-fields.component.ts @@ -0,0 +1,47 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { Observable, combineLatest, debounce, map } from 'rxjs'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; +import { CustomFieldsComponentInput } from 'ish-core/models/custom-field/custom-field.model'; +import { whenFalsy } from 'ish-core/utils/operators'; + +@Component({ + selector: 'ish-basket-custom-fields', + templateUrl: './basket-custom-fields.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BasketCustomFieldsComponent implements OnInit { + customFields$: Observable; + + visible$: Observable; + noValues$: Observable; + + constructor(private appFacade: AppFacade, private checkoutFacade: CheckoutFacade) {} + + form = new UntypedFormGroup({}); + + ngOnInit(): void { + this.customFields$ = combineLatest([ + this.appFacade.customFieldsForScope$('Basket'), + this.checkoutFacade.basket$.pipe(debounce(() => this.checkoutFacade.basketLoading$.pipe(whenFalsy()))), + ]).pipe( + map(([customFields, basket]) => + customFields.map(customField => ({ ...customField, value: basket.customFields[customField.name] })) + ) + ); + + this.visible$ = this.customFields$.pipe(map(fields => fields.length > 0)); + + this.noValues$ = this.customFields$.pipe(map(fields => fields.length > 0 && fields.every(field => !field.value))); + } + + submit() { + this.checkoutFacade.setBasketCustomFields(this.form.value); + } + + submitDisabled(): boolean { + return !this.form.valid || this.form.pristine; + } +} diff --git a/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.html b/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.html new file mode 100644 index 0000000000..c052397beb --- /dev/null +++ b/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.html @@ -0,0 +1 @@ + diff --git a/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.spec.ts b/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.spec.ts new file mode 100644 index 0000000000..3231160dcb --- /dev/null +++ b/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { instance, mock } from 'ts-mockito'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { FormlyTestingModule } from 'ish-shared/formly/dev/testing/formly-testing.module'; + +import { CustomFieldsFormlyComponent } from './custom-fields-formly.component'; + +describe('Custom Fields Formly Component', () => { + let component: CustomFieldsFormlyComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormlyTestingModule], + declarations: [CustomFieldsFormlyComponent], + providers: [{ provide: AppFacade, useFactory: () => instance(mock(AppFacade)) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CustomFieldsFormlyComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.ts b/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.ts new file mode 100644 index 0000000000..883b43b3c4 --- /dev/null +++ b/src/app/shared/components/custom-fields/custom-fields-formly/custom-fields-formly.component.ts @@ -0,0 +1,75 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { RxState } from '@rx-angular/state'; +import { Observable, combineLatest, map, switchMap } from 'rxjs'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { CustomFields, CustomFieldsComponentInput } from 'ish-core/models/custom-field/custom-field.model'; + +interface ComponentState { + form: UntypedFormGroup; + customFields: CustomFieldsComponentInput[]; + fields: FormlyFieldConfig[]; + model: CustomFields; +} + +@Component({ + selector: 'ish-custom-fields-formly', + templateUrl: './custom-fields-formly.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CustomFieldsFormlyComponent extends RxState implements OnInit { + @Input() set form(form: ComponentState['form']) { + this.set({ form }); + } + @Input() set fields(customFields: ComponentState['customFields']) { + this.set({ customFields }); + } + @Input() labelClass: string; + @Input() fieldClass: string; + + fields$: Observable; + model$: Observable; + form$: Observable; + + constructor(private appFacade: AppFacade) { + super(); + } + + ngOnInit(): void { + this.form$ = this.select('form'); + + this.connect( + 'fields', + this.select('customFields').pipe( + switchMap(fields => + combineLatest( + fields.map(field => + this.appFacade.customField$(field.name).pipe( + map(definition => ({ + key: field.name, + type: 'ish-text-input-field', + templateOptions: { + label: definition.displayName, + labelClass: this.labelClass, + fieldClass: this.fieldClass, + }, + })) + ) + ) + ) + ) + ) + ); + this.fields$ = this.select('fields'); + + this.connect( + 'model', + this.select('customFields').pipe( + map(fields => fields.reduce((acc, field) => ({ ...acc, [field.name]: field.value }), {})) + ) + ); + this.model$ = this.select('model'); + } +} diff --git a/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.html b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.html new file mode 100644 index 0000000000..ebadbe65a1 --- /dev/null +++ b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.html @@ -0,0 +1,8 @@ +
+
+ {{ + field.value || ('checkout.custom-field.no-value' | translate) + }} +
+
diff --git a/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.scss b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.scss new file mode 100644 index 0000000000..ce83a0a35a --- /dev/null +++ b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.scss @@ -0,0 +1,22 @@ +@import 'variables'; +@import 'bootstrap/scss/functions'; + +.custom-fields-view { + display: flex; + flex-direction: column; + gap: divide($space-default, 3); + + label { + padding-right: divide($space-default, 3); + margin: 0; + + &::after { + content: ':'; + } + } + + span.no-value { + font-style: italic; + color: $text-color-quaternary; + } +} diff --git a/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.spec.ts b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.spec.ts new file mode 100644 index 0000000000..6092b7683a --- /dev/null +++ b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { instance, mock } from 'ts-mockito'; + +import { AppFacade } from 'ish-core/facades/app.facade'; + +import { CustomFieldsViewComponent } from './custom-fields-view.component'; + +describe('Custom Fields View Component', () => { + let component: CustomFieldsViewComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CustomFieldsViewComponent], + providers: [{ provide: AppFacade, useFactory: () => instance(mock(AppFacade)) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CustomFieldsViewComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.ts b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.ts new file mode 100644 index 0000000000..6a8c8ef137 --- /dev/null +++ b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.ts @@ -0,0 +1,39 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable, ReplaySubject, combineLatest, map, switchMap } from 'rxjs'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { CustomFieldsComponentInput } from 'ish-core/models/custom-field/custom-field.model'; +import { CustomFieldDefinition } from 'ish-core/models/server-config/server-config.model'; + +@Component({ + selector: 'ish-custom-fields-view', + templateUrl: './custom-fields-view.component.html', + styleUrls: ['./custom-fields-view.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CustomFieldsViewComponent implements OnInit { + @Input() showEmpty = false; + + @Input() + set fields(val: CustomFieldsComponentInput[]) { + this.fields$.next(val); + } + + private fields$ = new ReplaySubject(1); + + data$: Observable<(CustomFieldsComponentInput & Pick)[]>; + + constructor(private appFacade: AppFacade) {} + + ngOnInit(): void { + this.data$ = this.fields$.pipe( + switchMap(fields => + combineLatest( + fields + .filter(field => this.showEmpty || field.value !== undefined) + .map(field => this.appFacade.customField$(field.name).pipe(map(def => ({ ...def, ...field })))) + ) + ) + ); + } +} diff --git a/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.html b/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.html new file mode 100644 index 0000000000..b4ce8744a6 --- /dev/null +++ b/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.html @@ -0,0 +1 @@ + diff --git a/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.spec.ts b/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.spec.ts new file mode 100644 index 0000000000..4c0eac2856 --- /dev/null +++ b/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { CustomFieldsViewComponent } from 'ish-shared/components/custom-fields/custom-fields-view/custom-fields-view.component'; + +import { LineItemCustomFieldsComponent } from './line-item-custom-fields.component'; + +describe('Line Item Custom Fields Component', () => { + let component: LineItemCustomFieldsComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + const appFacade = mock(AppFacade); + when(appFacade.customFieldsForScope$('BasketLineItem')).thenReturn(of([])); + + await TestBed.configureTestingModule({ + declarations: [LineItemCustomFieldsComponent, MockComponent(CustomFieldsViewComponent)], + providers: [{ provide: AppFacade, useFactory: () => instance(appFacade) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LineItemCustomFieldsComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.ts b/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.ts new file mode 100644 index 0000000000..d434d7f21d --- /dev/null +++ b/src/app/shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { RxState } from '@rx-angular/state'; +import { combineLatest, map } from 'rxjs'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { CustomFields, CustomFieldsComponentInput } from 'ish-core/models/custom-field/custom-field.model'; + +@Component({ + selector: 'ish-line-item-custom-fields', + templateUrl: './line-item-custom-fields.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LineItemCustomFieldsComponent + extends RxState<{ + fields: CustomFieldsComponentInput[]; + customFields: CustomFields; + editable: boolean; + }> + implements OnInit +{ + @Input() set lineItem(val: { customFields?: CustomFields }) { + this.set('customFields', () => val.customFields || {}); + } + @Input() set editable(val: boolean) { + this.set('editable', () => val); + } + + constructor(private appFacade: AppFacade) { + super(); + } + + ngOnInit(): void { + this.connect( + 'fields', + combineLatest([this.select('customFields'), this.appFacade.customFieldsForScope$('BasketLineItem')]).pipe( + map(([customFields, definitions]) => + definitions.map(({ name, editable }) => ({ + name, + editable, + value: customFields[name], + })) + ) + ) + ); + } +} diff --git a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.html b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.html deleted file mode 100644 index 28c48b6b31..0000000000 --- a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.html +++ /dev/null @@ -1,37 +0,0 @@ -
- - -
- -
-
-
- - -
- - - - -
{{ variationSalePrice$ | async | ishPrice }}
- - - - - -
- -
- - -
-
- -
- -
-
-
-
-
-
diff --git a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.spec.ts b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.spec.ts deleted file mode 100644 index 22387fa952..0000000000 --- a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent, MockPipe } from 'ng-mocks'; -import { EMPTY, of } from 'rxjs'; -import { instance, mock, when } from 'ts-mockito'; - -import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; -import { PricePipe } from 'ish-core/models/price/price.pipe'; -import { ProductView } from 'ish-core/models/product-view/product-view.model'; -import { ProductCompletenessLevel } from 'ish-core/models/product/product.model'; -import { findAllCustomElements } from 'ish-core/utils/dev/html-query-utils'; -import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component'; -import { ProductIdComponent } from 'ish-shared/components/product/product-id/product-id.component'; -import { ProductImageComponent } from 'ish-shared/components/product/product-image/product-image.component'; -import { ProductInventoryComponent } from 'ish-shared/components/product/product-inventory/product-inventory.component'; -import { ProductQuantityLabelComponent } from 'ish-shared/components/product/product-quantity-label/product-quantity-label.component'; -import { ProductQuantityComponent } from 'ish-shared/components/product/product-quantity/product-quantity.component'; -import { ProductVariationSelectComponent } from 'ish-shared/components/product/product-variation-select/product-variation-select.component'; - -import { LineItemEditDialogComponent } from './line-item-edit-dialog.component'; - -describe('Line Item Edit Dialog Component', () => { - let component: LineItemEditDialogComponent; - let fixture: ComponentFixture; - let element: HTMLElement; - let context: ProductContextFacade; - - beforeEach(async () => { - context = mock(ProductContextFacade); - - await TestBed.configureTestingModule({ - declarations: [ - LineItemEditDialogComponent, - MockComponent(LoadingComponent), - MockComponent(ProductIdComponent), - MockComponent(ProductImageComponent), - MockComponent(ProductInventoryComponent), - MockComponent(ProductQuantityComponent), - MockComponent(ProductQuantityLabelComponent), - MockComponent(ProductVariationSelectComponent), - MockPipe(PricePipe), - ], - providers: [{ provide: ProductContextFacade, useFactory: () => instance(context) }], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(LineItemEditDialogComponent); - component = fixture.componentInstance; - element = fixture.nativeElement; - - when(context.select('product')).thenReturn( - of({ - type: 'VariationProduct', - sku: 'SKU', - variableVariationAttributes: [], - available: true, - completenessLevel: ProductCompletenessLevel.List, - } as ProductView) - ); - when(context.select('prices')).thenReturn(EMPTY); - - when(context.select('loading')).thenReturn(of(false)); - }); - - it('should be created', () => { - expect(component).toBeTruthy(); - expect(element).toBeTruthy(); - expect(() => fixture.detectChanges()).not.toThrow(); - }); - - it('should give correct product id of variation to product id component', () => { - fixture.detectChanges(); - expect(element.querySelector('ish-product-id')).toMatchInlineSnapshot(``); - }); - - it('should display ish-components on the container', () => { - fixture.detectChanges(); - expect(findAllCustomElements(element)).toIncludeAllMembers(['ish-product-quantity', 'ish-product-image']); - }); - - it('should display loading-components on the container', () => { - when(context.select('loading')).thenReturn(of(true)); - fixture.detectChanges(); - expect(findAllCustomElements(element)).toIncludeAllMembers(['ish-product-quantity', 'ish-loading']); - }); -}); diff --git a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.ts b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.ts deleted file mode 100644 index 4394be83a8..0000000000 --- a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { Observable, map } from 'rxjs'; - -import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; -import { Price } from 'ish-core/models/price/price.model'; -import { ProductView } from 'ish-core/models/product-view/product-view.model'; - -/** - * The Line Item Edit Dialog Component displays an edit-dialog of a line items to edit quantity and variation. - */ -@Component({ - selector: 'ish-line-item-edit-dialog', - templateUrl: './line-item-edit-dialog.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class LineItemEditDialogComponent implements OnInit { - variation$: Observable; - variationSalePrice$: Observable; - loading$: Observable; - - constructor(private context: ProductContextFacade) {} - - ngOnInit() { - this.variation$ = this.context.select('product'); - this.variationSalePrice$ = this.context.select('prices').pipe(map(prices => prices?.salePrice)); - - this.loading$ = this.context.select('loading'); - } -} diff --git a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html index d02ce27917..6c3ce54eee 100644 --- a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html +++ b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html @@ -1,6 +1,5 @@ - + - - - - + + + +
+ + +
+ +
+
+
+ + +
+ + + + +
{{ select('lineItem', 'singleBasePrice') | async | ishPrice }}
+ + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+
+
+ + +
+
+
diff --git a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.spec.ts b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.spec.ts index fda2cee7cc..68c3f93e79 100644 --- a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.spec.ts +++ b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.spec.ts @@ -1,7 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { instance, mock } from 'ts-mockito'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { EMPTY } from 'rxjs'; +import { anyString, instance, mock, when } from 'ts-mockito'; +import { AppFacade } from 'ish-core/facades/app.facade'; import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; +import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.component'; import { LineItemEditComponent } from './line-item-edit.component'; @@ -11,10 +16,25 @@ describe('Line Item Edit Component', () => { let element: HTMLElement; beforeEach(async () => { + const appFacade = mock(AppFacade); + when(appFacade.serverSetting$(anyString())).thenReturn(EMPTY); + when(appFacade.customFieldsForScope$(anyString())).thenReturn(EMPTY); + + const context = mock(ProductContextFacade); + when(context.select(anyString())).thenReturn(EMPTY); + when(context.select(anyString(), anyString())).thenReturn(EMPTY); + await TestBed.configureTestingModule({ - declarations: [LineItemEditComponent], - providers: [{ provide: ProductContextFacade, useFactory: () => instance(mock(ProductContextFacade)) }], - }).compileComponents(); + imports: [TranslateModule.forRoot()], + declarations: [LineItemEditComponent, MockComponent(ModalDialogComponent)], + providers: [{ provide: AppFacade, useFactory: () => instance(appFacade) }], + }) + .overrideComponent(LineItemEditComponent, { + set: { + providers: [{ provide: ProductContextFacade, useFactory: () => instance(context) }], + }, + }) + .compileComponents(); }); beforeEach(() => { diff --git a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.ts b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.ts index 56adf1b00b..9d7d195152 100644 --- a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.ts +++ b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.ts @@ -1,11 +1,34 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { Observable } from 'rxjs'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnInit, + Output, + Self, + ViewChild, +} from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { RxState } from '@rx-angular/state'; +import { combineLatest, take } from 'rxjs'; +import { AppFacade } from 'ish-core/facades/app.facade'; import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; +import { CustomFieldsComponentInput } from 'ish-core/models/custom-field/custom-field.model'; import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model'; -import { ProductView } from 'ish-core/models/product-view/product-view.model'; +import { LineItemView } from 'ish-core/models/line-item/line-item.model'; +import { ProductHelper } from 'ish-core/models/product/product.model'; import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.component'; +interface ComponentState { + loading: boolean; + visible: boolean; + variationEditable: boolean; + lineItem: Partial>; + customFields: CustomFieldsComponentInput[]; +} + /** * The Line Item Edit Component displays an edit-link and edit-dialog. */ @@ -13,34 +36,81 @@ import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/ selector: 'ish-line-item-edit', templateUrl: './line-item-edit.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ProductContextFacade], }) -export class LineItemEditComponent implements OnInit { +export class LineItemEditComponent extends RxState implements OnInit, AfterViewInit { @ViewChild('modalDialog') modalDialogRef: ModalDialogComponent; - @Input({ required: true }) itemId: string; + @Input({ required: true }) set lineItem(lineItem: ComponentState['lineItem']) { + this.set({ lineItem }); + this.resetContext(); + } + @Output() updateItem = new EventEmitter(); - product$: Observable; + customFieldsForm = new UntypedFormGroup({}); - constructor(private context: ProductContextFacade) {} + constructor(@Self() private context: ProductContextFacade, private appFacade: AppFacade) { + super(); + } ngOnInit() { - this.product$ = this.context.select('product'); - } + this.connect( + 'variationEditable', + combineLatest([ + this.appFacade.serverSetting$('preferences.ChannelPreferences.EnableAdvancedVariationHandling'), + this.context.select('product'), + ]), + (_, [advancedVariationHandlingEnabled, product]) => + ProductHelper.isVariationProduct(product) && !advancedVariationHandlingEnabled + ); - show() { - this.modalDialogRef.show(); + this.connect( + 'visible', + combineLatest([this.select('variationEditable'), this.appFacade.customFieldsForScope$('BasketLineItem')]), + (_, [variationEditable, customFields]) => variationEditable || customFields.length > 0 + ); + + this.connect('loading', this.context.select('loading')); + + this.connect( + 'customFields', + combineLatest([this.appFacade.customFieldsForScope$('BasketLineItem'), this.select('lineItem', 'customFields')]), + (_, [customFields, customFieldsData]) => + customFields.map(field => ({ + name: field.name, + editable: field.editable, + value: customFieldsData[field.name], + })) + ); + } + ngAfterViewInit(): void { this.context.hold(this.context.select('product'), product => { this.modalDialogRef.options.confirmDisabled = !product.available; + this.modalDialogRef.options.titleText = product.name; }); + } + + private resetContext() { + const lineItem = this.get('lineItem'); + this.context.set({ quantity: lineItem.quantity.value, sku: lineItem.productSKU }); + } + + show() { + this.resetContext(); + + this.context.hold(this.modalDialogRef.confirmed.pipe(take(1)), () => { + const customFields = this.customFieldsForm.value; - this.context.hold(this.modalDialogRef.confirmed, () => this.updateItem.emit({ - itemId: this.itemId, + itemId: this.get('lineItem').id, sku: this.context.get('sku'), quantity: this.context.get('quantity'), - }) - ); + customFields: Object.keys(customFields).length > 0 ? customFields : undefined, + }); + }); + + this.modalDialogRef.show(); } } diff --git a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html index d067251b2a..abf51b007c 100644 --- a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html +++ b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html @@ -20,6 +20,8 @@ + + @@ -39,14 +41,7 @@ - + diff --git a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.spec.ts b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.spec.ts index 86dfeb71f0..d5dd810981 100644 --- a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.spec.ts +++ b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.spec.ts @@ -11,9 +11,9 @@ import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; import { PricePipe } from 'ish-core/models/price/price.pipe'; import { ProductView } from 'ish-core/models/product-view/product-view.model'; -import { ServerSettingPipe } from 'ish-core/pipes/server-setting.pipe'; import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data'; import { findAllCustomElements } from 'ish-core/utils/dev/html-query-utils'; +import { LineItemCustomFieldsComponent } from 'ish-shared/components/line-item/line-item-custom-fields/line-item-custom-fields.component'; import { LineItemEditComponent } from 'ish-shared/components/line-item/line-item-edit/line-item-edit.component'; import { LineItemWarrantyComponent } from 'ish-shared/components/line-item/line-item-warranty/line-item-warranty.component'; import { ProductBundleDisplayComponent } from 'ish-shared/components/product/product-bundle-display/product-bundle-display.component'; @@ -37,7 +37,7 @@ describe('Line Item List Element Component', () => { let element: HTMLElement; let context: ProductContextFacade; - async function prepareTestbed(serverSetting: boolean) { + beforeEach(async () => { context = mock(ProductContextFacade); when(context.select('product')).thenReturn(of({} as ProductView)); when(context.select('quantity')).thenReturn(EMPTY); @@ -49,6 +49,7 @@ describe('Line Item List Element Component', () => { MockComponent(FaIconComponent), MockComponent(LazyProductAddToOrderTemplateComponent), MockComponent(LazyProductAddToWishlistComponent), + MockComponent(LineItemCustomFieldsComponent), MockComponent(LineItemEditComponent), MockComponent(LineItemWarrantyComponent), MockComponent(ProductBundleDisplayComponent), @@ -63,84 +64,79 @@ describe('Line Item List Element Component', () => { MockDirective(NgbPopover), MockDirective(ProductContextDirective), MockPipe(PricePipe), - MockPipe(ServerSettingPipe, () => serverSetting), ], providers: [ { provide: CheckoutFacade, useFactory: () => instance(mock(CheckoutFacade)) }, { provide: ProductContextFacade, useFactory: () => instance(context) }, ], }).compileComponents(); - } + }); - describe('b2c variation handling', () => { - beforeEach(async () => { - prepareTestbed(false); - }); + beforeEach(() => { + fixture = TestBed.createComponent(LineItemListElementComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + component.pli = BasketMockData.getBasketItem(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + describe('editable', () => { beforeEach(() => { - fixture = TestBed.createComponent(LineItemListElementComponent); - component = fixture.componentInstance; - element = fixture.nativeElement; - component.pli = BasketMockData.getBasketItem(); + component.editable = true; }); - it('should be created', () => { - expect(component).toBeTruthy(); - expect(element).toBeTruthy(); - expect(() => fixture.detectChanges()).not.toThrow(); + it('should render item quantity change input field if editable === true', () => { + fixture.detectChanges(); + expect(element.querySelector('ish-product-quantity')).toBeTruthy(); }); - describe('editable', () => { - beforeEach(() => { - component.editable = true; - }); - - it('should render item quantity change input field if editable === true', () => { - fixture.detectChanges(); - expect(element.querySelector('ish-product-quantity')).toBeTruthy(); - }); - - it('should not render item quantity change input field if editable === false', () => { - component.editable = false; - fixture.detectChanges(); - expect(element.querySelector('ish-product-quantity')).not.toBeTruthy(); - }); - - it('should render item delete button if editable === true', () => { - fixture.detectChanges(); - expect(element.querySelector('fa-icon[ng-reflect-icon="fas,trash-alt"]')).toBeTruthy(); - }); - - it('should not render item delete button if editable === false', () => { - component.editable = false; - fixture.detectChanges(); - expect(element.querySelector('fa-icon[ng-reflect-icon="fas,trash-alt"]')).toBeFalsy(); - }); + it('should not render item quantity change input field if editable === false', () => { + component.editable = false; + fixture.detectChanges(); + expect(element.querySelector('ish-product-quantity')).not.toBeTruthy(); }); - it('should give correct sku to productIdComponent', () => { + it('should render item delete button if editable === true', () => { fixture.detectChanges(); - expect(element.querySelector('ish-product-id')).toMatchInlineSnapshot(``); + expect(element.querySelector('fa-icon[ng-reflect-icon="fas,trash-alt"]')).toBeTruthy(); }); - it('should hold itemSurcharges for the line item', () => { + it('should not render item delete button if editable === false', () => { + component.editable = false; fixture.detectChanges(); - expect(element.querySelectorAll('.details-tooltip')).toHaveLength(1); + expect(element.querySelector('fa-icon[ng-reflect-icon="fas,trash-alt"]')).toBeFalsy(); }); + }); - it('should not display itemSurcharges for the line item if not available', () => { - component.pli = { ...BasketMockData.getBasketItem(), itemSurcharges: undefined }; - expect(() => fixture.detectChanges()).not.toThrow(); - expect(element.querySelectorAll('.details-tooltip')).toHaveLength(0); - }); + it('should give correct sku to productIdComponent', () => { + fixture.detectChanges(); + expect(element.querySelector('ish-product-id')).toMatchInlineSnapshot(``); + }); - it('should display standard elements for normal products', () => { - fixture.detectChanges(); - expect(findAllCustomElements(element)).toMatchInlineSnapshot(` + it('should hold itemSurcharges for the line item', () => { + fixture.detectChanges(); + expect(element.querySelectorAll('.details-tooltip')).toHaveLength(1); + }); + + it('should not display itemSurcharges for the line item if not available', () => { + component.pli = { ...BasketMockData.getBasketItem(), itemSurcharges: undefined }; + expect(() => fixture.detectChanges()).not.toThrow(); + expect(element.querySelectorAll('.details-tooltip')).toHaveLength(0); + }); + + it('should display standard elements for normal products', () => { + fixture.detectChanges(); + expect(findAllCustomElements(element)).toMatchInlineSnapshot(` [ "ish-product-image", "ish-product-name", "ish-product-id", + "ish-line-item-custom-fields", "ish-product-variation-display", "ish-product-bundle-display", "ish-line-item-edit", @@ -156,38 +152,11 @@ describe('Line Item List Element Component', () => { "ish-line-item-warranty", ] `); - }); - - it('should display bundle parts for bundle products', () => { - when(context.select('product')).thenReturn(of({ type: 'Bundle' } as ProductView)); - fixture.detectChanges(); - expect(findAllCustomElements(element)).toContain('ish-product-bundle-display'); - }); }); - describe('advanced variation handling', () => { - beforeEach(async () => { - prepareTestbed(true); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(LineItemListElementComponent); - component = fixture.componentInstance; - element = fixture.nativeElement; - component.pli = BasketMockData.getBasketItem(); - }); - - it('should be created', () => { - expect(component).toBeTruthy(); - expect(element).toBeTruthy(); - expect(() => fixture.detectChanges()).not.toThrow(); - }); - - it('should not display edit component for variation products with advanced variation handling', () => { - when(context.select('product')).thenReturn(of({ type: 'VariationProduct' } as ProductView)); - - fixture.detectChanges(); - expect(findAllCustomElements(element)).not.toContain('ish-line-item-edit'); - }); + it('should display bundle parts for bundle products', () => { + when(context.select('product')).thenReturn(of({ type: 'Bundle' } as ProductView)); + fixture.detectChanges(); + expect(findAllCustomElements(element)).toContain('ish-product-bundle-display'); }); }); diff --git a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.ts b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.ts index 38cfd6c64e..bdea79e7ee 100644 --- a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.ts +++ b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.ts @@ -1,5 +1,6 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { isEqual } from 'lodash-es'; +import { Subject, Subscription, takeUntil } from 'rxjs'; import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; @@ -12,17 +13,30 @@ import { OrderLineItem } from 'ish-core/models/order/order.model'; templateUrl: './line-item-list-element.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LineItemListElementComponent implements OnInit { +export class LineItemListElementComponent implements OnChanges, OnDestroy { @Input({ required: true }) pli: Partial; @Input() editable = true; @Input() lineItemViewType: 'simple' | 'availability'; + private updateSubscription: Subscription; + private destroy$ = new Subject(); + constructor(private context: ProductContextFacade, private checkoutFacade: CheckoutFacade) {} - ngOnInit() { - this.context.hold(this.context.validDebouncedQuantityUpdate$(), quantity => { - this.checkoutFacade.updateBasketItem({ itemId: this.pli.id, quantity }); - }); + ngOnChanges(changes: SimpleChanges): void { + if (changes.pli) { + if (this.updateSubscription) { + // eslint-disable-next-line ban/ban + this.updateSubscription.unsubscribe(); + } + + this.updateSubscription = this.context + .validDebouncedQuantityUpdate$() + .pipe(takeUntil(this.destroy$)) + .subscribe(quantity => { + this.checkoutFacade.updateBasketItem({ itemId: this.pli.id, quantity }); + }); + } } get oldPrice() { @@ -38,4 +52,9 @@ export class LineItemListElementComponent implements OnInit { onDeleteItem(itemId: string) { this.checkoutFacade.deleteBasketItem(itemId); } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } 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 275124283d..ddedbac7c7 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 @@
this.pageSize; } - trackByFn(_: number, item: Partial) { - return item.id; - } - /** * Refresh items to display after changing the current page * diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 54afa72352..79c4fcf86d 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -68,6 +68,7 @@ import { BasketApprovalInfoComponent } from './components/basket/basket-approval import { BasketBuyerComponent } from './components/basket/basket-buyer/basket-buyer.component'; import { BasketCostCenterSelectionComponent } from './components/basket/basket-cost-center-selection/basket-cost-center-selection.component'; import { BasketCostSummaryComponent } from './components/basket/basket-cost-summary/basket-cost-summary.component'; +import { BasketCustomFieldsComponent } from './components/basket/basket-custom-fields/basket-custom-fields.component'; import { BasketDesiredDeliveryDateComponent } from './components/basket/basket-desired-delivery-date/basket-desired-delivery-date.component'; import { BasketErrorMessageComponent } from './components/basket/basket-error-message/basket-error-message.component'; import { BasketInfoComponent } from './components/basket/basket-info/basket-info.component'; @@ -97,6 +98,8 @@ import { ModalDialogLinkComponent } from './components/common/modal-dialog-link/ 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 { CustomFieldsFormlyComponent } from './components/custom-fields/custom-fields-formly/custom-fields-formly.component'; +import { CustomFieldsViewComponent } from './components/custom-fields/custom-fields-view/custom-fields-view.component'; import { FilterCheckboxComponent } from './components/filter/filter-checkbox/filter-checkbox.component'; import { FilterCollapsibleComponent } from './components/filter/filter-collapsible/filter-collapsible.component'; import { FilterDropdownComponent } from './components/filter/filter-dropdown/filter-dropdown.component'; @@ -106,7 +109,7 @@ import { FilterNavigationSidebarComponent } from './components/filter/filter-nav import { FilterNavigationComponent } from './components/filter/filter-navigation/filter-navigation.component'; import { FilterSwatchImagesComponent } from './components/filter/filter-swatch-images/filter-swatch-images.component'; import { FilterTextComponent } from './components/filter/filter-text/filter-text.component'; -import { LineItemEditDialogComponent } from './components/line-item/line-item-edit-dialog/line-item-edit-dialog.component'; +import { LineItemCustomFieldsComponent } from './components/line-item/line-item-custom-fields/line-item-custom-fields.component'; import { LineItemEditComponent } from './components/line-item/line-item-edit/line-item-edit.component'; import { LineItemListElementComponent } from './components/line-item/line-item-list-element/line-item-list-element.component'; import { LineItemListComponent } from './components/line-item/line-item-list/line-item-list.component'; @@ -227,7 +230,6 @@ const declaredComponents = [ FilterSwatchImagesComponent, FilterTextComponent, LineItemEditComponent, - LineItemEditDialogComponent, LineItemListElementComponent, LineItemWarrantyComponent, LoginModalComponent, @@ -247,6 +249,7 @@ const exportedComponents = [ AccordionComponent, AccordionItemComponent, AddressComponent, + BasketCustomFieldsComponent, BasketAddressSummaryComponent, BasketApprovalInfoComponent, BasketBuyerComponent, @@ -311,6 +314,9 @@ const exportedComponents = [ ProductVariationSelectSwatchComponent, ProductWarrantyComponent, ProductWarrantyDetailsComponent, + CustomFieldsFormlyComponent, + LineItemCustomFieldsComponent, + CustomFieldsViewComponent, PromotionDetailsComponent, PromotionRemoveComponent, SearchBoxComponent, diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index 79a5b00818..25d6fca5b5 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -715,6 +715,7 @@ "checkout.credit_card.user.lastname.missing.error": "Bitte geben Sie den Nachnamen des Karteninhabers an.", "checkout.credit_card.user.name.missing.error": "Bitte geben Sie den Namen des Karteninhabers an.", "checkout.credit_card.validityTime.error.notFound": "Die maximale Gültigkeitsdauer der Kartenprüfnummer wurde nicht gefunden.", + "checkout.custom-field.no-value": "Kein Wert", "checkout.desired_delivery_date.apply.button.label": "Übernehmen", "checkout.desired_delivery_date.error.date": "Das Datum ist ungültig. Bitte verwenden Sie das korrekte Format ({{ translate, common.placeholder.shortdate-caps }}).", "checkout.desired_delivery_date.error.max_date": "Ihr gewünschter Liefertermin kann nicht realisiert werden, weil das Datum zu weit in der Zukunft liegt.", diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index e29374e6a7..f7f5e6adb5 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -715,6 +715,11 @@ "checkout.credit_card.user.lastname.missing.error": "Please enter a cardholder last name.", "checkout.credit_card.user.name.missing.error": "Please enter a cardholder name.", "checkout.credit_card.validityTime.error.notFound": "The maximum validity time for CVC has not been found.", + "checkout.custom-field.no-value": "No value", + "checkout.custom-fields.basket.dialog.confirm": "OK", + "checkout.custom-fields.basket.dialog.reject": "Cancel", + "checkout.custom-fields.basket.dialog.title": "Custom Fields for Shopping Cart", + "checkout.custom-fields.define-cta": "Click here to define additional Shopping Cart attributes", "checkout.desired_delivery_date.apply.button.label": "Apply", "checkout.desired_delivery_date.error.date": "The date is invalid. Please use the correct format ({{ translate, common.placeholder.shortdate-caps }}).", "checkout.desired_delivery_date.error.max_date": "Your desired delivery date cannot be realized because the date is too far in the future.", From 93cc187d968d08b98e1944243457dcd007a75a67 Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Sun, 10 Dec 2023 13:14:41 +0100 Subject: [PATCH 4/6] chore: move line-item edit button to toolbar --- .../line-item-edit.component.html | 4 +- .../line-item-list-element.component.html | 8 ++-- .../line-item-list-element.component.spec.ts | 39 +++++++++---------- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html index 6c3ce54eee..54cba9053c 100644 --- a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html +++ b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html @@ -2,11 +2,11 @@ diff --git a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html index abf51b007c..5acf363f96 100644 --- a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html +++ b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html @@ -39,11 +39,6 @@ - - - - - @@ -148,6 +143,9 @@ > + + +
diff --git a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.spec.ts b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.spec.ts index d5dd810981..4396d2bb97 100644 --- a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.spec.ts +++ b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.spec.ts @@ -132,26 +132,25 @@ describe('Line Item List Element Component', () => { it('should display standard elements for normal products', () => { fixture.detectChanges(); expect(findAllCustomElements(element)).toMatchInlineSnapshot(` - [ - "ish-product-image", - "ish-product-name", - "ish-product-id", - "ish-line-item-custom-fields", - "ish-product-variation-display", - "ish-product-bundle-display", - "ish-line-item-edit", - "ish-product-inventory", - "ish-product-shipment", - "fa-icon", - "ish-lazy-product-add-to-order-template", - "ish-lazy-product-add-to-wishlist", - "fa-icon", - "ish-product-quantity-label", - "ish-product-quantity", - "ish-product-quantity", - "ish-line-item-warranty", - ] - `); + [ + "ish-product-image", + "ish-product-name", + "ish-product-id", + "ish-line-item-custom-fields", + "ish-product-variation-display", + "ish-product-bundle-display", + "ish-product-inventory", + "ish-product-shipment", + "fa-icon", + "ish-lazy-product-add-to-order-template", + "ish-lazy-product-add-to-wishlist", + "fa-icon", + "ish-line-item-edit", + "ish-product-quantity-label", + "ish-product-quantity", + "ish-product-quantity", + ] + `); }); it('should display bundle parts for bundle products', () => { From 70e14f28138028663d63f01bbf03cd512264b4e0 Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Mon, 11 Dec 2023 16:49:44 +0100 Subject: [PATCH 5/6] feat: display fields in checkout and myaccount --- .../requisition-detail-page.component.html | 2 + .../requisition-detail-page.component.spec.ts | 3 ++ .../account-order.component.html | 2 + .../account-order.component.spec.ts | 2 + .../checkout-receipt.component.html | 2 + .../checkout-receipt.component.spec.ts | 3 ++ .../checkout-review.component.html | 3 ++ .../checkout-review.component.spec.ts | 3 ++ .../basket-custom-fields-view.component.html | 7 ++++ ...asket-custom-fields-view.component.spec.ts | 35 ++++++++++++++++++ .../basket-custom-fields-view.component.ts | 37 +++++++++++++++++++ src/app/shared/shared.module.ts | 2 + src/assets/i18n/en_US.json | 1 + 13 files changed, 102 insertions(+) create mode 100644 src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.html create mode 100644 src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.spec.ts create mode 100644 src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.ts diff --git a/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.html b/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.html index 7ceba33faf..64efa325a2 100644 --- a/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.html +++ b/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.html @@ -42,6 +42,8 @@

{{ 'approval.detailspage.order_details.heading' | translate }}

+ +
diff --git a/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.spec.ts b/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.spec.ts index e07c7c85e4..e9318be682 100644 --- a/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.spec.ts +++ b/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.spec.ts @@ -13,6 +13,7 @@ import { AddressComponent } from 'ish-shared/components/address/address/address. import { BasketCostSummaryComponent } from 'ish-shared/components/basket/basket-cost-summary/basket-cost-summary.component'; import { BasketMerchantMessageViewComponent } from 'ish-shared/components/basket/basket-merchant-message-view/basket-merchant-message-view.component'; import { BasketShippingMethodComponent } from 'ish-shared/components/basket/basket-shipping-method/basket-shipping-method.component'; +import { BasketCustomFieldsViewComponent } from 'ish-shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component'; import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component'; import { InfoBoxComponent } from 'ish-shared/components/common/info-box/info-box.component'; @@ -38,6 +39,7 @@ describe('Requisition Detail Page Component', () => { declarations: [ MockComponent(AddressComponent), MockComponent(BasketCostSummaryComponent), + MockComponent(BasketCustomFieldsViewComponent), MockComponent(BasketMerchantMessageViewComponent), MockComponent(BasketShippingMethodComponent), MockComponent(ErrorMessageComponent), @@ -81,6 +83,7 @@ describe('Requisition Detail Page Component', () => { "ish-requisition-summary", "ish-requisition-cost-center-approval", "ish-basket-merchant-message-view", + "ish-basket-custom-fields-view", "ish-info-box", "ish-address", "ish-info-box", diff --git a/src/app/pages/account-order/account-order/account-order.component.html b/src/app/pages/account-order/account-order/account-order.component.html index 9d2187fd6c..184bbc8a90 100644 --- a/src/app/pages/account-order/account-order/account-order.component.html +++ b/src/app/pages/account-order/account-order/account-order.component.html @@ -62,6 +62,8 @@

{{ 'account.orderdetails.heading.default' | translate }}

+ +
diff --git a/src/app/pages/account-order/account-order/account-order.component.spec.ts b/src/app/pages/account-order/account-order/account-order.component.spec.ts index d67d16f0f6..f736c827c2 100644 --- a/src/app/pages/account-order/account-order/account-order.component.spec.ts +++ b/src/app/pages/account-order/account-order/account-order.component.spec.ts @@ -11,6 +11,7 @@ import { AddressComponent } from 'ish-shared/components/address/address/address. import { BasketCostSummaryComponent } from 'ish-shared/components/basket/basket-cost-summary/basket-cost-summary.component'; import { BasketMerchantMessageViewComponent } from 'ish-shared/components/basket/basket-merchant-message-view/basket-merchant-message-view.component'; import { BasketShippingMethodComponent } from 'ish-shared/components/basket/basket-shipping-method/basket-shipping-method.component'; +import { BasketCustomFieldsViewComponent } from 'ish-shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component'; import { InfoBoxComponent } from 'ish-shared/components/common/info-box/info-box.component'; import { LineItemListComponent } from 'ish-shared/components/line-item/line-item-list/line-item-list.component'; @@ -31,6 +32,7 @@ describe('Account Order Component', () => { MockComponent(AccountOrderToBasketComponent), MockComponent(AddressComponent), MockComponent(BasketCostSummaryComponent), + MockComponent(BasketCustomFieldsViewComponent), MockComponent(BasketMerchantMessageViewComponent), MockComponent(BasketShippingMethodComponent), MockComponent(FaIconComponent), diff --git a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html index 5c187abd30..93b60930e6 100644 --- a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html +++ b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html @@ -15,6 +15,8 @@
+ +
diff --git a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.spec.ts b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.spec.ts index eb8c66f005..a77686f863 100644 --- a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.spec.ts +++ b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.spec.ts @@ -11,6 +11,7 @@ import { AddressComponent } from 'ish-shared/components/address/address/address. import { BasketCostSummaryComponent } from 'ish-shared/components/basket/basket-cost-summary/basket-cost-summary.component'; import { BasketMerchantMessageViewComponent } from 'ish-shared/components/basket/basket-merchant-message-view/basket-merchant-message-view.component'; import { BasketShippingMethodComponent } from 'ish-shared/components/basket/basket-shipping-method/basket-shipping-method.component'; +import { BasketCustomFieldsViewComponent } from 'ish-shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component'; import { InfoBoxComponent } from 'ish-shared/components/common/info-box/info-box.component'; import { LineItemListComponent } from 'ish-shared/components/line-item/line-item-list/line-item-list.component'; @@ -27,6 +28,7 @@ describe('Checkout Receipt Component', () => { CheckoutReceiptComponent, MockComponent(AddressComponent), MockComponent(BasketCostSummaryComponent), + MockComponent(BasketCustomFieldsViewComponent), MockComponent(BasketMerchantMessageViewComponent), MockComponent(BasketShippingMethodComponent), MockComponent(FaIconComponent), @@ -66,6 +68,7 @@ describe('Checkout Receipt Component', () => { expect(findAllCustomElements(element)).toMatchInlineSnapshot(` [ "ish-basket-merchant-message-view", + "ish-basket-custom-fields-view", "ish-info-box", "ish-address", "ish-info-box", diff --git a/src/app/pages/checkout-review/checkout-review/checkout-review.component.html b/src/app/pages/checkout-review/checkout-review/checkout-review.component.html index 9bbf8e0547..af0c14dcf0 100644 --- a/src/app/pages/checkout-review/checkout-review/checkout-review.component.html +++ b/src/app/pages/checkout-review/checkout-review/checkout-review.component.html @@ -44,6 +44,9 @@

+ + +
{ MockComponent(AddressComponent), MockComponent(BasketApprovalInfoComponent), MockComponent(BasketCostSummaryComponent), + MockComponent(BasketCustomFieldsViewComponent), MockComponent(BasketMerchantMessageViewComponent), MockComponent(BasketShippingMethodComponent), MockComponent(BasketValidationResultsComponent), @@ -115,6 +117,7 @@ describe('Checkout Review Component', () => { "ish-error-message", "ish-basket-validation-results", "ish-basket-merchant-message-view", + "ish-basket-custom-fields-view", "ish-info-box", "ish-address", "ish-info-box", diff --git a/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.html b/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.html new file mode 100644 index 0000000000..8e785add8d --- /dev/null +++ b/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.html @@ -0,0 +1,7 @@ + + +

+ +

+
+
diff --git a/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.spec.ts b/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.spec.ts new file mode 100644 index 0000000000..15836ae1f8 --- /dev/null +++ b/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { EMPTY } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; + +import { AppFacade } from 'ish-core/facades/app.facade'; + +import { BasketCustomFieldsViewComponent } from './basket-custom-fields-view.component'; + +describe('Basket Custom Fields View Component', () => { + let component: BasketCustomFieldsViewComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + const appFacade = mock(AppFacade); + when(appFacade.customFieldsForScope$('Basket')).thenReturn(EMPTY); + + await TestBed.configureTestingModule({ + declarations: [BasketCustomFieldsViewComponent], + providers: [{ provide: AppFacade, useFactory: () => instance(appFacade) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BasketCustomFieldsViewComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.ts b/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.ts new file mode 100644 index 0000000000..111e559a06 --- /dev/null +++ b/src/app/shared/components/checkout/basket-custom-fields-view/basket-custom-fields-view.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable, ReplaySubject, combineLatest, map } from 'rxjs'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { CustomFields, CustomFieldsComponentInput } from 'ish-core/models/custom-field/custom-field.model'; + +@Component({ + selector: 'ish-basket-custom-fields-view', + templateUrl: './basket-custom-fields-view.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BasketCustomFieldsViewComponent implements OnInit { + @Input() set data(val: { customFields?: CustomFields }) { + this._customFields.next(val.customFields || {}); + } + @Input() editRouterLink: string; + + private _customFields = new ReplaySubject(1); + + fields$: Observable; + visible$: Observable; + + constructor(private appFacade: AppFacade) {} + + ngOnInit(): void { + this.fields$ = combineLatest([ + this._customFields.asObservable(), + this.appFacade.customFieldsForScope$('Basket'), + ]).pipe( + map(([customFields, customFieldsForScope]) => + customFieldsForScope.map(({ name, editable }) => ({ name, value: customFields[name], editable })) + ) + ); + + this.visible$ = this.fields$.pipe(map(fields => fields.some(field => field.value))); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 79c4fcf86d..bb7370df16 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -84,6 +84,7 @@ import { BasketValidationProductsComponent } from './components/basket/basket-va import { BasketValidationResultsComponent } from './components/basket/basket-validation-results/basket-validation-results.component'; import { ClearBasketComponent } from './components/basket/clear-basket/clear-basket.component'; import { MiniBasketContentComponent } from './components/basket/mini-basket-content/mini-basket-content.component'; +import { BasketCustomFieldsViewComponent } from './components/checkout/basket-custom-fields-view/basket-custom-fields-view.component'; import { BasketInvoiceAddressWidgetComponent } from './components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component'; import { BasketShippingAddressWidgetComponent } from './components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component'; import { AccordionItemComponent } from './components/common/accordion-item/accordion-item.component'; @@ -317,6 +318,7 @@ const exportedComponents = [ CustomFieldsFormlyComponent, LineItemCustomFieldsComponent, CustomFieldsViewComponent, + BasketCustomFieldsViewComponent, PromotionDetailsComponent, PromotionRemoveComponent, SearchBoxComponent, diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index f7f5e6adb5..4439e20d90 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -834,6 +834,7 @@ "checkout.widget.buyer.costcenter": "Cost center", "checkout.widget.buyer.heading": "Buyer", "checkout.widget.buyer.orderReferenceId": "Customer order ID", + "checkout.widget.custom_fields": "Custom fields", "checkout.widget.message_to_merchant": "Message to merchant", "checkout.widget.payment_method.heading": "Payment method", "checkout.widget.promotion.discount": "Discount", From 1bb88b1ffbe66e48ef361a8423933d2fe4668045 Mon Sep 17 00:00:00 2001 From: Stefan Hauke Date: Wed, 15 May 2024 18:39:49 +0200 Subject: [PATCH 6/6] adapt to new ui --- src/app/core/facades/checkout.facade.ts | 6 +- .../customer/basket/basket-items.effects.ts | 2 +- .../custom-fields-view.component.html | 10 +- .../custom-fields-view.component.scss | 8 +- .../line-item-edit-dialog.component.html | 37 +++++++ .../line-item-edit-dialog.component.spec.ts | 86 +++++++++++++++ .../line-item-edit-dialog.component.ts | 29 +++++ .../line-item-edit.component.html | 75 +++---------- .../line-item-edit.component.spec.ts | 28 +---- .../line-item-edit.component.ts | 100 +++--------------- .../line-item-information-edit.component.html | 71 +++++++++++++ ...ne-item-information-edit.component.spec.ts | 31 ++++++ .../line-item-information-edit.component.ts | 91 ++++++++++++++++ .../line-item-list-element.component.html | 23 +++- src/app/shared/shared.module.ts | 4 + src/assets/i18n/en_US.json | 5 + 16 files changed, 421 insertions(+), 185 deletions(-) create mode 100644 src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.html create mode 100644 src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.spec.ts create mode 100644 src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.ts create mode 100644 src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.html create mode 100644 src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.spec.ts create mode 100644 src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.ts diff --git a/src/app/core/facades/checkout.facade.ts b/src/app/core/facades/checkout.facade.ts index db5686abe1..b39f883999 100644 --- a/src/app/core/facades/checkout.facade.ts +++ b/src/app/core/facades/checkout.facade.ts @@ -126,10 +126,10 @@ export class CheckoutFacade { } updateBasketItem(update: LineItemUpdate) { - if (update.quantity) { - this.store.dispatch(updateBasketItem({ lineItemUpdate: update })); - } else { + if (update.quantity === 0) { this.store.dispatch(deleteBasketItem({ itemId: update.itemId })); + } else { + this.store.dispatch(updateBasketItem({ lineItemUpdate: update })); } } diff --git a/src/app/core/store/customer/basket/basket-items.effects.ts b/src/app/core/store/customer/basket/basket-items.effects.ts index 5e47ad89a5..3630aeced3 100644 --- a/src/app/core/store/customer/basket/basket-items.effects.ts +++ b/src/app/core/store/customer/basket/basket-items.effects.ts @@ -196,7 +196,7 @@ export class BasketItemsEffects { itemUpdate.customFields = customFieldDefinitions.map(({ name, type }) => ({ name, type, - value: update.customFields[name], + value: update.customFields[name] || '', })); } diff --git a/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.html b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.html index ebadbe65a1..8833afa324 100644 --- a/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.html +++ b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.html @@ -1,8 +1,8 @@
-
- {{ - field.value || ('checkout.custom-field.no-value' | translate) - }} +
+ + + {{ field.value || ('checkout.custom-field.no-value' | translate) }} +
diff --git a/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.scss b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.scss index ce83a0a35a..a55fe706cd 100644 --- a/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.scss +++ b/src/app/shared/components/custom-fields/custom-fields-view/custom-fields-view.component.scss @@ -2,9 +2,11 @@ @import 'bootstrap/scss/functions'; .custom-fields-view { - display: flex; - flex-direction: column; - gap: divide($space-default, 3); + .custom-field { + &:last-of-type { + margin-bottom: divide($space-default * 2, 3); + } + } label { padding-right: divide($space-default, 3); diff --git a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.html b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.html new file mode 100644 index 0000000000..28c48b6b31 --- /dev/null +++ b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.html @@ -0,0 +1,37 @@ +
+ + +
+ +
+
+
+ + +
+ + + + +
{{ variationSalePrice$ | async | ishPrice }}
+ + + + + +
+ +
+ + +
+
+ +
+ +
+
+
+
+
+
diff --git a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.spec.ts b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.spec.ts new file mode 100644 index 0000000000..22387fa952 --- /dev/null +++ b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.spec.ts @@ -0,0 +1,86 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent, MockPipe } from 'ng-mocks'; +import { EMPTY, of } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; + +import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; +import { PricePipe } from 'ish-core/models/price/price.pipe'; +import { ProductView } from 'ish-core/models/product-view/product-view.model'; +import { ProductCompletenessLevel } from 'ish-core/models/product/product.model'; +import { findAllCustomElements } from 'ish-core/utils/dev/html-query-utils'; +import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component'; +import { ProductIdComponent } from 'ish-shared/components/product/product-id/product-id.component'; +import { ProductImageComponent } from 'ish-shared/components/product/product-image/product-image.component'; +import { ProductInventoryComponent } from 'ish-shared/components/product/product-inventory/product-inventory.component'; +import { ProductQuantityLabelComponent } from 'ish-shared/components/product/product-quantity-label/product-quantity-label.component'; +import { ProductQuantityComponent } from 'ish-shared/components/product/product-quantity/product-quantity.component'; +import { ProductVariationSelectComponent } from 'ish-shared/components/product/product-variation-select/product-variation-select.component'; + +import { LineItemEditDialogComponent } from './line-item-edit-dialog.component'; + +describe('Line Item Edit Dialog Component', () => { + let component: LineItemEditDialogComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let context: ProductContextFacade; + + beforeEach(async () => { + context = mock(ProductContextFacade); + + await TestBed.configureTestingModule({ + declarations: [ + LineItemEditDialogComponent, + MockComponent(LoadingComponent), + MockComponent(ProductIdComponent), + MockComponent(ProductImageComponent), + MockComponent(ProductInventoryComponent), + MockComponent(ProductQuantityComponent), + MockComponent(ProductQuantityLabelComponent), + MockComponent(ProductVariationSelectComponent), + MockPipe(PricePipe), + ], + providers: [{ provide: ProductContextFacade, useFactory: () => instance(context) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LineItemEditDialogComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + when(context.select('product')).thenReturn( + of({ + type: 'VariationProduct', + sku: 'SKU', + variableVariationAttributes: [], + available: true, + completenessLevel: ProductCompletenessLevel.List, + } as ProductView) + ); + when(context.select('prices')).thenReturn(EMPTY); + + when(context.select('loading')).thenReturn(of(false)); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should give correct product id of variation to product id component', () => { + fixture.detectChanges(); + expect(element.querySelector('ish-product-id')).toMatchInlineSnapshot(``); + }); + + it('should display ish-components on the container', () => { + fixture.detectChanges(); + expect(findAllCustomElements(element)).toIncludeAllMembers(['ish-product-quantity', 'ish-product-image']); + }); + + it('should display loading-components on the container', () => { + when(context.select('loading')).thenReturn(of(true)); + fixture.detectChanges(); + expect(findAllCustomElements(element)).toIncludeAllMembers(['ish-product-quantity', 'ish-loading']); + }); +}); diff --git a/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.ts b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.ts new file mode 100644 index 0000000000..4394be83a8 --- /dev/null +++ b/src/app/shared/components/line-item/line-item-edit-dialog/line-item-edit-dialog.component.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { Observable, map } from 'rxjs'; + +import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; +import { Price } from 'ish-core/models/price/price.model'; +import { ProductView } from 'ish-core/models/product-view/product-view.model'; + +/** + * The Line Item Edit Dialog Component displays an edit-dialog of a line items to edit quantity and variation. + */ +@Component({ + selector: 'ish-line-item-edit-dialog', + templateUrl: './line-item-edit-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LineItemEditDialogComponent implements OnInit { + variation$: Observable; + variationSalePrice$: Observable; + loading$: Observable; + + constructor(private context: ProductContextFacade) {} + + ngOnInit() { + this.variation$ = this.context.select('product'); + this.variationSalePrice$ = this.context.select('prices').pipe(map(prices => prices?.salePrice)); + + this.loading$ = this.context.select('loading'); + } +} diff --git a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html index 54cba9053c..d02ce27917 100644 --- a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html +++ b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.html @@ -1,67 +1,24 @@ - + - - - - -
- - -
- -
-
-
- - -
- - - - -
{{ select('lineItem', 'singleBasePrice') | async | ishPrice }}
- - - - -
- - - - -
- - -
-
- -
- -
-
-
-
-
- - -
-
-
+ + + +
diff --git a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.spec.ts b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.spec.ts index 68c3f93e79..fda2cee7cc 100644 --- a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.spec.ts +++ b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.spec.ts @@ -1,12 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { MockComponent } from 'ng-mocks'; -import { EMPTY } from 'rxjs'; -import { anyString, instance, mock, when } from 'ts-mockito'; +import { instance, mock } from 'ts-mockito'; -import { AppFacade } from 'ish-core/facades/app.facade'; import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; -import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.component'; import { LineItemEditComponent } from './line-item-edit.component'; @@ -16,25 +11,10 @@ describe('Line Item Edit Component', () => { let element: HTMLElement; beforeEach(async () => { - const appFacade = mock(AppFacade); - when(appFacade.serverSetting$(anyString())).thenReturn(EMPTY); - when(appFacade.customFieldsForScope$(anyString())).thenReturn(EMPTY); - - const context = mock(ProductContextFacade); - when(context.select(anyString())).thenReturn(EMPTY); - when(context.select(anyString(), anyString())).thenReturn(EMPTY); - await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [LineItemEditComponent, MockComponent(ModalDialogComponent)], - providers: [{ provide: AppFacade, useFactory: () => instance(appFacade) }], - }) - .overrideComponent(LineItemEditComponent, { - set: { - providers: [{ provide: ProductContextFacade, useFactory: () => instance(context) }], - }, - }) - .compileComponents(); + declarations: [LineItemEditComponent], + providers: [{ provide: ProductContextFacade, useFactory: () => instance(mock(ProductContextFacade)) }], + }).compileComponents(); }); beforeEach(() => { diff --git a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.ts b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.ts index 9d7d195152..56adf1b00b 100644 --- a/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.ts +++ b/src/app/shared/components/line-item/line-item-edit/line-item-edit.component.ts @@ -1,34 +1,11 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnInit, - Output, - Self, - ViewChild, -} from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; -import { RxState } from '@rx-angular/state'; -import { combineLatest, take } from 'rxjs'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { Observable } from 'rxjs'; -import { AppFacade } from 'ish-core/facades/app.facade'; import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; -import { CustomFieldsComponentInput } from 'ish-core/models/custom-field/custom-field.model'; import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model'; -import { LineItemView } from 'ish-core/models/line-item/line-item.model'; -import { ProductHelper } from 'ish-core/models/product/product.model'; +import { ProductView } from 'ish-core/models/product-view/product-view.model'; import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.component'; -interface ComponentState { - loading: boolean; - visible: boolean; - variationEditable: boolean; - lineItem: Partial>; - customFields: CustomFieldsComponentInput[]; -} - /** * The Line Item Edit Component displays an edit-link and edit-dialog. */ @@ -36,81 +13,34 @@ interface ComponentState { selector: 'ish-line-item-edit', templateUrl: './line-item-edit.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ProductContextFacade], }) -export class LineItemEditComponent extends RxState implements OnInit, AfterViewInit { +export class LineItemEditComponent implements OnInit { @ViewChild('modalDialog') modalDialogRef: ModalDialogComponent; - @Input({ required: true }) set lineItem(lineItem: ComponentState['lineItem']) { - this.set({ lineItem }); - this.resetContext(); - } - + @Input({ required: true }) itemId: string; @Output() updateItem = new EventEmitter(); - customFieldsForm = new UntypedFormGroup({}); + product$: Observable; - constructor(@Self() private context: ProductContextFacade, private appFacade: AppFacade) { - super(); - } + constructor(private context: ProductContextFacade) {} ngOnInit() { - this.connect( - 'variationEditable', - combineLatest([ - this.appFacade.serverSetting$('preferences.ChannelPreferences.EnableAdvancedVariationHandling'), - this.context.select('product'), - ]), - (_, [advancedVariationHandlingEnabled, product]) => - ProductHelper.isVariationProduct(product) && !advancedVariationHandlingEnabled - ); - - this.connect( - 'visible', - combineLatest([this.select('variationEditable'), this.appFacade.customFieldsForScope$('BasketLineItem')]), - (_, [variationEditable, customFields]) => variationEditable || customFields.length > 0 - ); - - this.connect('loading', this.context.select('loading')); - - this.connect( - 'customFields', - combineLatest([this.appFacade.customFieldsForScope$('BasketLineItem'), this.select('lineItem', 'customFields')]), - (_, [customFields, customFieldsData]) => - customFields.map(field => ({ - name: field.name, - editable: field.editable, - value: customFieldsData[field.name], - })) - ); + this.product$ = this.context.select('product'); } - ngAfterViewInit(): void { + show() { + this.modalDialogRef.show(); + this.context.hold(this.context.select('product'), product => { this.modalDialogRef.options.confirmDisabled = !product.available; - this.modalDialogRef.options.titleText = product.name; }); - } - - private resetContext() { - const lineItem = this.get('lineItem'); - this.context.set({ quantity: lineItem.quantity.value, sku: lineItem.productSKU }); - } - - show() { - this.resetContext(); - - this.context.hold(this.modalDialogRef.confirmed.pipe(take(1)), () => { - const customFields = this.customFieldsForm.value; + this.context.hold(this.modalDialogRef.confirmed, () => this.updateItem.emit({ - itemId: this.get('lineItem').id, + itemId: this.itemId, sku: this.context.get('sku'), quantity: this.context.get('quantity'), - customFields: Object.keys(customFields).length > 0 ? customFields : undefined, - }); - }); - - this.modalDialogRef.show(); + }) + ); } } diff --git a/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.html b/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.html new file mode 100644 index 0000000000..2f8adfc501 --- /dev/null +++ b/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.html @@ -0,0 +1,71 @@ + + + +
+
+ + + +
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+
+
+
diff --git a/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.spec.ts b/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.spec.ts new file mode 100644 index 0000000000..fda2cee7cc --- /dev/null +++ b/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { instance, mock } from 'ts-mockito'; + +import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; + +import { LineItemEditComponent } from './line-item-edit.component'; + +describe('Line Item Edit Component', () => { + let component: LineItemEditComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LineItemEditComponent], + providers: [{ provide: ProductContextFacade, useFactory: () => instance(mock(ProductContextFacade)) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LineItemEditComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.ts b/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.ts new file mode 100644 index 0000000000..f0d9fe03f2 --- /dev/null +++ b/src/app/shared/components/line-item/line-item-information-edit/line-item-information-edit.component.ts @@ -0,0 +1,91 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, Self } from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { RxState } from '@rx-angular/state'; +import { combineLatest } from 'rxjs'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; +import { CustomFieldsComponentInput } from 'ish-core/models/custom-field/custom-field.model'; +import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model'; +import { LineItemView } from 'ish-core/models/line-item/line-item.model'; + +interface ComponentState { + lineItem: Partial>; + loading: boolean; + visible: boolean; + customFields: CustomFieldsComponentInput[]; + editableFieldsMode: 'edit' | 'add'; // 'edit' for editable custom fields with existing values, else 'add' new values (relevant for translations) +} + +/** + * The Line Item Edit Component displays an edit-link and edit-dialog. + */ +@Component({ + selector: 'ish-line-item-information-edit', + templateUrl: './line-item-information-edit.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ProductContextFacade], +}) +export class LineItemInformationEditComponent extends RxState implements OnInit { + @Input({ required: true }) set lineItem(lineItem: ComponentState['lineItem']) { + this.set({ lineItem }); + this.resetContext(); + } + @Input() editable = true; + + @Output() updateItem = new EventEmitter(); + + customFieldsForm = new UntypedFormGroup({}); + editMode = false; + + constructor(@Self() private context: ProductContextFacade, private appFacade: AppFacade) { + super(); + } + + ngOnInit() { + this.connect( + 'visible', + this.appFacade.customFieldsForScope$('BasketLineItem'), + (_, customFields) => customFields.length > 0 + ); + + this.connect('loading', this.context.select('loading')); + + this.connect( + 'customFields', + combineLatest([this.appFacade.customFieldsForScope$('BasketLineItem'), this.select('lineItem', 'customFields')]), + (_, [customFields, customFieldsData]) => + customFields.map(field => ({ + name: field.name, + editable: field.editable, + value: customFieldsData[field.name], + })) + ); + + this.connect('editableFieldsMode', this.select('lineItem', 'customFields'), (_, customFieldsData) => + Object.keys(customFieldsData).length > 0 ? 'edit' : 'add' + ); + } + + private resetContext() { + const lineItem = this.get('lineItem'); + this.context.set({ sku: lineItem.productSKU }); + } + + save() { + const customFields = this.customFieldsForm.value; + + this.updateItem.emit({ + itemId: this.get('lineItem').id, + customFields: Object.keys(customFields).length > 0 ? customFields : undefined, + }); + } + cancel() { + this.editMode = false; + this.resetContext(); + } + + edit() { + this.editMode = !this.editMode; + } +} diff --git a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html index 5acf363f96..056ffa53c9 100644 --- a/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html +++ b/src/app/shared/components/line-item/line-item-list-element/line-item-list-element.component.html @@ -20,8 +20,6 @@ - - @@ -39,6 +37,18 @@ + + + + + @@ -125,6 +135,12 @@
+
+
+ +
+
+
- - -
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index bb7370df16..b068957871 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -111,7 +111,9 @@ import { FilterNavigationComponent } from './components/filter/filter-navigation import { FilterSwatchImagesComponent } from './components/filter/filter-swatch-images/filter-swatch-images.component'; import { FilterTextComponent } from './components/filter/filter-text/filter-text.component'; import { LineItemCustomFieldsComponent } from './components/line-item/line-item-custom-fields/line-item-custom-fields.component'; +import { LineItemEditDialogComponent } from './components/line-item/line-item-edit-dialog/line-item-edit-dialog.component'; import { LineItemEditComponent } from './components/line-item/line-item-edit/line-item-edit.component'; +import { LineItemInformationEditComponent } from './components/line-item/line-item-information-edit/line-item-information-edit.component'; import { LineItemListElementComponent } from './components/line-item/line-item-list-element/line-item-list-element.component'; import { LineItemListComponent } from './components/line-item/line-item-list/line-item-list.component'; import { LineItemWarrantyComponent } from './components/line-item/line-item-warranty/line-item-warranty.component'; @@ -231,6 +233,8 @@ const declaredComponents = [ FilterSwatchImagesComponent, FilterTextComponent, LineItemEditComponent, + LineItemEditDialogComponent, + LineItemInformationEditComponent, LineItemListElementComponent, LineItemWarrantyComponent, LoginModalComponent, diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index 4439e20d90..c533a219bc 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -716,10 +716,15 @@ "checkout.credit_card.user.name.missing.error": "Please enter a cardholder name.", "checkout.credit_card.validityTime.error.notFound": "The maximum validity time for CVC has not been found.", "checkout.custom-field.no-value": "No value", + "checkout.custom-fields.add.button.label": "Add", + "checkout.custom-fields.add.link.label": "Add product information", "checkout.custom-fields.basket.dialog.confirm": "OK", "checkout.custom-fields.basket.dialog.reject": "Cancel", "checkout.custom-fields.basket.dialog.title": "Custom Fields for Shopping Cart", + "checkout.custom-fields.cancel.button.label": "Cancel", "checkout.custom-fields.define-cta": "Click here to define additional Shopping Cart attributes", + "checkout.custom-fields.edit.link.label": "Edit product information", + "checkout.custom-fields.save.button.label": "Save changes", "checkout.desired_delivery_date.apply.button.label": "Apply", "checkout.desired_delivery_date.error.date": "The date is invalid. Please use the correct format ({{ translate, common.placeholder.shortdate-caps }}).", "checkout.desired_delivery_date.error.max_date": "Your desired delivery date cannot be realized because the date is too far in the future.",