From 3ca1c6a8494cf2360c49a193ea670eedc9dd549e Mon Sep 17 00:00:00 2001 From: Christoph Hinssen <33626130+ChristophHi@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:24:41 +0200 Subject: [PATCH] feat: support of date attribute type (#18730) Closes CXSPA-6790 --- .../assets/translations/en/configurator.json | 6 +- .../rulebased/components/attribute/index.ts | 2 +- ...te-single-selection-base.component.spec.ts | 14 ++ ...tribute-single-selection-base.component.ts | 2 +- ...rator-attribute-input-field.component.html | 27 ++- ...or-attribute-input-field.component.spec.ts | 184 ++++++++++++++++-- ...gurator-attribute-input-field.component.ts | 86 ++++++-- ...nfigurator-attribute-input-field.module.ts | 1 + .../config/configurator-ui-settings.config.ts | 1 + ...default-configurator-ui-settings.config.ts | 1 + .../group/configurator-group.module.ts | 4 +- .../core/model/configurator.model.ts | 2 + ...cc-configurator-variant-normalizer.spec.ts | 58 ++++++ .../occ-configurator-variant-normalizer.ts | 122 ++++++++---- ...cc-configurator-variant-serializer.spec.ts | 16 ++ .../occ-configurator-variant-serializer.ts | 87 ++++++--- .../variant-configurator-occ.models.ts | 1 + 17 files changed, 511 insertions(+), 103 deletions(-) diff --git a/feature-libs/product-configurator/common/assets/translations/en/configurator.json b/feature-libs/product-configurator/common/assets/translations/en/configurator.json index 58684f2396a..3b9db968a5b 100644 --- a/feature-libs/product-configurator/common/assets/translations/en/configurator.json +++ b/feature-libs/product-configurator/common/assets/translations/en/configurator.json @@ -155,13 +155,15 @@ "nameOfAttribute": "Name of Attribute", "valueOfAttribute": "Value of attribute {{ attribute }}", "forAttribute": "{{ value }} for attribute {{ attribute }}", - "valueOfAttributeFull": "Value {{ value }} of attribute {{ attribute }}", + "valueOfAttributeFull": "Value {{ value }} of attribute {{ attribute }}.", + "valueOfDateAttributeFull": "Value {{ value }} of date attribute {{ attribute }}. Press space key to enter date picker.", "valueOfAttributeFullWithPrice": "Value {{ value }} of attribute {{ attribute }}, Surcharge {{ price }}", "selectedValueOfAttributeFull": "Selected value {{ value }} of attribute {{ attribute }}", "selectedValueOfAttributeFullWithPrice": "Selected value {{ value }} of attribute {{ attribute }}, Surcharge {{ price }}", "readOnlyValueOfAttributeFullWithPrice": "Read-only value {{ value }} of attribute {{ attribute }}, Surcharge {{ price }}", "readOnlyValueOfAttributeFull": "Read-only value {{ value }} of attribute {{ attribute }}", - "valueOfAttributeBlank": "Value of attribute {{ attribute }} is blank", + "valueOfAttributeBlank": "Value of attribute {{ attribute }} is blank.", + "valueOfDateAttributeBlank": "Value of date attribute {{ attribute }} is blank. Press space key to enter date picker", "value": "Value {{ value }}", "attribute": "Attribute {{ attribute }}", "requiredAttribute": "Attribute {{param}} is required", diff --git a/feature-libs/product-configurator/rulebased/components/attribute/index.ts b/feature-libs/product-configurator/rulebased/components/attribute/index.ts index e7e2dd04e18..01f27f30d56 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/index.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/index.ts @@ -5,9 +5,9 @@ */ export * from './composition/index'; -export * from './price-change/index'; export * from './footer/index'; export * from './header/index'; +export * from './price-change/index'; export * from './product-card/index'; export * from './quantity/index'; export * from './types/base/index'; diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.spec.ts index dae970ce6e8..89c5ee87501 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.spec.ts @@ -451,6 +451,13 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { component.attribute.validationType = Configurator.ValidationType.NUMERIC; expect(component.isAdditionalValueNumeric).toBe(true); }); + + it('should return false for UI type Radio button additional input and validation type sap date', () => { + component.attribute.uiType = + Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT; + component.attribute.validationType = Configurator.ValidationType.SAP_DATE; + expect(component.isAdditionalValueNumeric).toBe(false); + }); }); describe('IsAdditionalValueAlphaNumeric', () => { @@ -467,6 +474,13 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { component.attribute.validationType = Configurator.ValidationType.NONE; expect(component.isAdditionalValueAlphaNumeric).toBe(true); }); + + it('should return true for UI type Radio button additional input and validation type sap date', () => { + component.attribute.uiType = + Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT; + component.attribute.validationType = Configurator.ValidationType.SAP_DATE; + expect(component.isAdditionalValueAlphaNumeric).toBe(true); + }); }); describe('getAriaLabel', () => { it('should return aria label for additional value', () => { diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.ts index 95c746b809b..806533989c9 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.ts @@ -223,7 +223,7 @@ export abstract class ConfiguratorAttributeSingleSelectionBaseComponent extends get isAdditionalValueAlphaNumeric(): boolean { return ( this.isWithAdditionalValues(this.attribute) && - this.attribute.validationType === Configurator.ValidationType.NONE + this.attribute.validationType !== Configurator.ValidationType.NUMERIC ); } diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.html b/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.html index c2fc8713d92..d5fb3b6d88a 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.html +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.html @@ -1,7 +1,28 @@
+ { - return of(isCartEntryOrGroupVisited); + return isCartEntryOrGroupVisited; } } +const DEBOUNCE_TIME: number = 600; +const DEBOUNCE_TIME_DATE: number = 1600; describe('ConfiguratorAttributeInputFieldComponent', () => { let component: ConfiguratorAttributeInputFieldComponent; let fixture: ComponentFixture; let htmlElem: HTMLElement; - let DEBOUNCE_TIME: number; const ownerKey = 'theOwnerKey'; const name = 'attributeName'; const groupId = 'theGroupId'; @@ -72,12 +74,6 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { provide: ConfiguratorStorefrontUtilsService, useClass: MockConfigUtilsService, }, - { - provide: FeaturesConfig, - useValue: { - features: { level: '*' }, - }, - }, ], }) .overrideComponent(ConfiguratorAttributeInputFieldComponent, { @@ -104,9 +100,14 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { component.ownerType = CommonConfigurator.OwnerType.CART_ENTRY; component.ownerKey = ownerKey; fixture.detectChanges(); - DEBOUNCE_TIME = - defaultConfiguratorUISettingsConfig.productConfigurator - ?.updateDebounceTime?.input ?? component['FALLBACK_DEBOUNCE_TIME']; + + defaultConfiguratorUISettingsConfig.productConfigurator = { + updateDebounceTime: { + input: DEBOUNCE_TIME, + date: DEBOUNCE_TIME_DATE, + }, + }; + spyOn( component['configuratorCommonsService'], 'updateConfiguration' @@ -270,6 +271,78 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { }); }); + describe('Accessibility support for attributes of type sap_date', () => { + beforeEach(() => { + component.attribute.uiType = Configurator.UiType.SAP_DATE; + fixture.detectChanges(); + }); + describe('in case value is empty', () => { + it('should render input element with aria-label attribute that defines an accessible name to label the current element', () => { + CommonConfiguratorTestUtilsService.expectElementContainsA11y( + expect, + htmlElem, + 'input', + 'form-control', + 0, + 'aria-label', + 'configurator.a11y.valueOfAttributeBlank attribute:' + + component.attribute.label + ); + }); + + it('should render hidden label with aria-label attribute that explains that date picker can be reached by hitting space ', () => { + fixture.detectChanges(); + CommonConfiguratorTestUtilsService.expectElementContainsA11y( + expect, + htmlElem, + 'label', + 'cx-visually-hidden', + 0, + 'aria-label', + 'configurator.a11y.valueOfDateAttributeBlank attribute:' + + component.attribute.label + ); + }); + }); + describe('in case value is present', () => { + beforeEach(() => { + component.attribute.userInput = '2024-12-31'; + fixture.detectChanges(); + }); + it("should contain input element with class name 'form-control' with an 'aria-label' attribute that also mentions the value", () => { + fixture.detectChanges(); + CommonConfiguratorTestUtilsService.expectElementContainsA11y( + expect, + htmlElem, + 'input', + 'form-control', + 0, + 'aria-label', + 'configurator.a11y.valueOfAttributeFull attribute:' + + component.attribute.label + + ' value:' + + component.attribute.userInput + ); + }); + + it('should render hidden label with aria-label attribute that explains that date picker can be reached by hitting space ', () => { + fixture.detectChanges(); + CommonConfiguratorTestUtilsService.expectElementContainsA11y( + expect, + htmlElem, + 'label', + 'cx-visually-hidden', + 0, + 'aria-label', + 'configurator.a11y.valueOfDateAttributeFull attribute:' + + component.attribute.label + + ' value:' + + component.attribute.userInput + ); + }); + }); + }); + describe('isRequired', () => { it('should tell from attribute if form input required for string and numeric attribute without domain', () => { expect(component.isRequired).toBe(true); @@ -315,4 +388,89 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { expect(component.isUserInputEmpty).toBe(true); }); }); + + describe('compileShowRequiredErrorMessage', () => { + it('should create observable that emits false in case configuration is in initial state', () => { + const falseEmittingObs = cold('a', { a: false }); + isCartEntryOrGroupVisited = falseEmittingObs; + component['compileShowRequiredErrorMessage'](); + expect(component.showRequiredErrorMessage$).toBeObservable( + falseEmittingObs + ); + }); + }); + + describe('calculateDebounceTime', () => { + it('should return debounce time from configuration if available', () => { + expect(component['calculateDebounceTime']()).toBe(DEBOUNCE_TIME); + }); + + it('should return fallback if nothing available in configuration', () => { + component['config'].productConfigurator = {}; + expect(component['calculateDebounceTime']()).toBe( + component['FALLBACK_DEBOUNCE_TIME'] + ); + }); + + it('should return debounce time for dates in case provided', () => { + component['debounceForDateActive'] = true; + component.attribute.uiType = Configurator.UiType.SAP_DATE; + expect(component['calculateDebounceTime']()).toBe(DEBOUNCE_TIME_DATE); + }); + + it('should return zero if debounceForDateActive is false', () => { + component['debounceForDateActive'] = false; + component.attribute.uiType = Configurator.UiType.SAP_DATE; + expect(component['calculateDebounceTime']()).toBe(0); + }); + + it('should return fallback debounce time for dates in case not provided in configuration', () => { + component['debounceForDateActive'] = true; + component.attribute.uiType = Configurator.UiType.SAP_DATE; + component['config'].productConfigurator = {}; + expect(component['calculateDebounceTime']()).toBe( + component['FALLBACK_DEBOUNCE_TIME_DATE'] + ); + }); + }); + + describe('activateDebounceDate', () => { + it('should activate the debounce time for date input', () => { + expect(component['debounceForDateActive']).toBe(false); + component.activateDebounceDate(); + expect(component['debounceForDateActive']).toBe(true); + }); + }); + + describe('inputType', () => { + it('should return "text" for UI type string', () => { + expect(component.inputType).toBe('text'); + }); + + it('should return "date" for UI type sap_date', () => { + component.attribute.uiType = Configurator.UiType.SAP_DATE; + expect(component.inputType).toBe('date'); + }); + + it('should return "date" for UI type DDLB with additional value and validation type sap date', () => { + component.attribute.uiType = + Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT; + component.attribute.validationType = Configurator.ValidationType.SAP_DATE; + expect(component.inputType).toBe('date'); + }); + + it('should return "date" for UI type RB with additional value and validation type sap date', () => { + component.attribute.uiType = + Configurator.UiType.RADIOBUTTON_ADDITIONAL_INPUT; + component.attribute.validationType = Configurator.ValidationType.SAP_DATE; + expect(component.inputType).toBe('date'); + }); + + it('should return "text" for UI type DDLB with additional value and validation type NONE', () => { + component.attribute.uiType = + Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT; + component.attribute.validationType = Configurator.ValidationType.NONE; + expect(component.inputType).toBe('text'); + }); + }); }); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.ts index 4d0fcfd3171..25233d274d8 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.ts @@ -41,12 +41,25 @@ export class ConfiguratorAttributeInputFieldComponent showRequiredErrorMessage$: Observable = of(false); + /** + * Initially, changes to a date control are sent immediately + * (when using the date picker. Only after any key is pressed, the + * debounce time kicks in) + * ) + */ + protected debounceForDateActive = false; /** * In case no config is injected, or when the debounce time is not configured at all, * this value will be used as fallback. */ protected readonly FALLBACK_DEBOUNCE_TIME = 500; + /** + * In case no config is injected, or if the debounce time for dates is not configured at all, + * this value will be used as fallback. + */ + protected readonly FALLBACK_DEBOUNCE_TIME_DATE = 1500; + constructor( protected config: ConfiguratorUISettingsConfig, protected attributeComponentContext: ConfiguratorAttributeCompositionContext, @@ -61,16 +74,7 @@ export class ConfiguratorAttributeInputFieldComponent this.ownerKey = attributeComponentContext.owner.key; this.ownerType = attributeComponentContext.owner.type; - this.showRequiredErrorMessage$ = this.configuratorStorefrontUtilsService - .isCartEntryOrGroupVisited(this.owner, this.group) - .pipe( - map((result) => - result - ? this.isRequiredErrorMsg(this.attribute) && - this.isUserInput(this.attribute) - : false - ) - ); + this.compileShowRequiredErrorMessage(); } ngOnInit() { @@ -84,15 +88,10 @@ export class ConfiguratorAttributeInputFieldComponent this.attributeInputForm.markAsTouched(); } this.sub = this.attributeInputForm.valueChanges - .pipe( - debounce(() => - timer( - this.config.productConfigurator?.updateDebounceTime?.input ?? - this.FALLBACK_DEBOUNCE_TIME - ) - ) - ) - .subscribe(() => this.onChange()); + .pipe(debounce(() => timer(this.calculateDebounceTime()))) + .subscribe(() => { + this.onChange(); + }); } onChange(): void { @@ -135,4 +134,53 @@ export class ConfiguratorAttributeInputFieldComponent ? this.attribute.required ?? false : false; } + /** + * Returns the type for the input field. Depending on the UI type, that is text or date. + */ + get inputType(): string { + return this.isDateBased(this.attribute) ? 'date' : 'text'; + } + /** + * Activates a waiting period until date changes are sent. We only + * want to enable that once the user tries to enter something + * directly (when using date picker, changes should be sent instantly) + */ + activateDebounceDate(): void { + this.debounceForDateActive = true; + } + + protected isDateBased(attribute: Configurator.Attribute) { + return ( + attribute.uiType === Configurator.UiType.SAP_DATE || + (this.isWithAdditionalValues(attribute) && + attribute.validationType === Configurator.ValidationType.SAP_DATE) + ); + } + + protected compileShowRequiredErrorMessage(): void { + this.showRequiredErrorMessage$ = this.configuratorStorefrontUtilsService + .isCartEntryOrGroupVisited(this.owner, this.group) + .pipe( + map((result) => + result + ? this.isRequiredErrorMsg(this.attribute) && + this.isUserInput(this.attribute) + : false + ) + ); + } + + protected calculateDebounceTime(): number { + if (this.isDateBased(this.attribute)) { + return this.debounceForDateActive + ? this.config.productConfigurator?.updateDebounceTime?.date ?? + this.FALLBACK_DEBOUNCE_TIME_DATE + : 0; + } else { + return ( + this.config.productConfigurator?.updateDebounceTime?.input ?? + this.FALLBACK_DEBOUNCE_TIME + ); + } + } } diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.module.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.module.ts index def528fb37e..8ca983a4154 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.module.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.module.ts @@ -25,6 +25,7 @@ import { ConfiguratorAttributeInputFieldComponent } from './configurator-attribu productConfigurator: { assignment: { AttributeType_string: ConfiguratorAttributeInputFieldComponent, + AttributeType_sap_date: ConfiguratorAttributeInputFieldComponent, }, }, }), diff --git a/feature-libs/product-configurator/rulebased/components/config/configurator-ui-settings.config.ts b/feature-libs/product-configurator/rulebased/components/config/configurator-ui-settings.config.ts index 765970e81f7..c5cc5425653 100644 --- a/feature-libs/product-configurator/rulebased/components/config/configurator-ui-settings.config.ts +++ b/feature-libs/product-configurator/rulebased/components/config/configurator-ui-settings.config.ts @@ -11,6 +11,7 @@ export interface ProductConfiguratorUISettingsConfig { updateDebounceTime?: { quantity?: number; input?: number; + date?: number; }; addRetractOption?: boolean; enableNavigationToConflict?: boolean; diff --git a/feature-libs/product-configurator/rulebased/components/config/default-configurator-ui-settings.config.ts b/feature-libs/product-configurator/rulebased/components/config/default-configurator-ui-settings.config.ts index 96bd3235128..634a831e773 100644 --- a/feature-libs/product-configurator/rulebased/components/config/default-configurator-ui-settings.config.ts +++ b/feature-libs/product-configurator/rulebased/components/config/default-configurator-ui-settings.config.ts @@ -12,6 +12,7 @@ export const defaultConfiguratorUISettingsConfig: ConfiguratorUISettingsConfig = updateDebounceTime: { quantity: 750, input: 500, + date: 1500, }, addRetractOption: false, enableNavigationToConflict: true, diff --git a/feature-libs/product-configurator/rulebased/components/group/configurator-group.module.ts b/feature-libs/product-configurator/rulebased/components/group/configurator-group.module.ts index 1189f08d8c9..692b3b60aa6 100644 --- a/feature-libs/product-configurator/rulebased/components/group/configurator-group.module.ts +++ b/feature-libs/product-configurator/rulebased/components/group/configurator-group.module.ts @@ -9,6 +9,7 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; import { CmsConfig, I18nModule, provideDefaultConfig } from '@spartacus/core'; +import { ConfiguratorAttributeCompositionModule } from '../attribute/composition/configurator-attribute-composition.module'; import { ConfiguratorAttributeFooterModule } from '../attribute/footer/configurator-attribute-footer.module'; import { ConfiguratorAttributeHeaderModule } from '../attribute/header/configurator-attribute-header.module'; import { ConfiguratorAttributeCheckboxListModule } from '../attribute/types/checkbox-list/configurator-attribute-checkbox-list.module'; @@ -17,14 +18,13 @@ import { ConfiguratorAttributeDropDownModule } from '../attribute/types/drop-dow import { ConfiguratorAttributeInputFieldModule } from '../attribute/types/input-field/configurator-attribute-input-field.module'; import { ConfiguratorAttributeMultiSelectionBundleModule } from '../attribute/types/multi-selection-bundle/configurator-attribute-multi-selection-bundle.module'; import { ConfiguratorAttributeMultiSelectionImageModule } from '../attribute/types/multi-selection-image/configurator-attribute-multi-selection-image.module'; -import { ConfiguratorAttributeNumericInputFieldModule } from '../attribute/types/numeric-input-field/configurator-attribute-numeric-input-field.module'; import { ConfiguratorAttributeNotSupportedModule } from '../attribute/types/not-supported/configurator-attribute-not-supported.module'; +import { ConfiguratorAttributeNumericInputFieldModule } from '../attribute/types/numeric-input-field/configurator-attribute-numeric-input-field.module'; import { ConfiguratorAttributeRadioButtonModule } from '../attribute/types/radio-button/configurator-attribute-radio-button.module'; import { ConfiguratorAttributeReadOnlyModule } from '../attribute/types/read-only/configurator-attribute-read-only.module'; import { ConfiguratorAttributeSingleSelectionBundleDropdownModule } from '../attribute/types/single-selection-bundle-dropdown/configurator-attribute-single-selection-bundle-dropdown.module'; import { ConfiguratorAttributeSingleSelectionBundleModule } from '../attribute/types/single-selection-bundle/configurator-attribute-single-selection-bundle.module'; import { ConfiguratorAttributeSingleSelectionImageModule } from '../attribute/types/single-selection-image/configurator-attribute-single-selection-image.module'; -import { ConfiguratorAttributeCompositionModule } from '../attribute/composition/configurator-attribute-composition.module'; import { ConfiguratorConflictDescriptionModule } from '../conflict-description/configurator-conflict-description.module'; import { ConfiguratorConflictSuggestionModule } from '../conflict-suggestion/configurator-conflict-suggestion.module'; import { ConfiguratorGroupComponent } from './configurator-group.component'; diff --git a/feature-libs/product-configurator/rulebased/core/model/configurator.model.ts b/feature-libs/product-configurator/rulebased/core/model/configurator.model.ts index 2ed975964e7..512476ccb98 100644 --- a/feature-libs/product-configurator/rulebased/core/model/configurator.model.ts +++ b/feature-libs/product-configurator/rulebased/core/model/configurator.model.ts @@ -231,6 +231,7 @@ export namespace Configurator { READ_ONLY_MULTI_SELECTION_IMAGE = 'read_only_multi_selection_image', STRING = 'string', NUMERIC = 'numeric', + SAP_DATE = 'sap_date', AUTO_COMPLETE_CUSTOM = 'input_autocomplete', MULTI_SELECTION_IMAGE = 'multi_selection_image', SINGLE_SELECTION_IMAGE = 'single_selection_image', @@ -274,6 +275,7 @@ export namespace Configurator { export enum ValidationType { NONE = 'NONE', NUMERIC = 'NUMERIC', + SAP_DATE = 'SAP_DATE', } export enum OverviewFilter { VISIBLE = 'PRIMARY', diff --git a/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.spec.ts b/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.spec.ts index ef9324c7965..7c7a3596312 100644 --- a/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.spec.ts +++ b/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.spec.ts @@ -811,6 +811,13 @@ describe('OccConfiguratorVariantNormalizer', () => { ).toBe(Configurator.UiType.NUMERIC); }); + it('should convert date attribute type correctly', () => { + sourceAttribute.type = OccConfigurator.UiType.SAP_DATE; + expect( + occConfiguratorVariantNormalizer.convertAttributeType(sourceAttribute) + ).toBe(Configurator.UiType.SAP_DATE); + }); + it('should convert read-only attribute type correctly', () => { sourceAttribute.type = OccConfigurator.UiType.READ_ONLY; expect( @@ -983,6 +990,48 @@ describe('OccConfiguratorVariantNormalizer', () => { }); }); + describe('compileUserInput', () => { + const value = '2025-01-01'; + const formattedValue = '01/01/2025'; + const sourceAttribute: OccConfigurator.Attribute = { + name: attributeName, + key: attributeName, + value: value, + formattedValue: formattedValue, + type: OccConfigurator.UiType.SAP_DATE, + }; + it('should return attribute value in case of date UI type', () => { + expect( + occConfiguratorVariantNormalizer['compileUserInput'](sourceAttribute) + ).toBe(value); + }); + it('should return formatted attribute value in case of readOnly UI type', () => { + expect( + occConfiguratorVariantNormalizer['compileUserInput']({ + ...sourceAttribute, + type: OccConfigurator.UiType.READ_ONLY, + }) + ).toBe(formattedValue); + }); + it('should default to blank if no value is present for date UI type', () => { + expect( + occConfiguratorVariantNormalizer['compileUserInput']({ + ...sourceAttribute, + value: undefined, + }) + ).toBe(''); + }); + it('should default to blank if no formatted value is present for read-only UI type', () => { + expect( + occConfiguratorVariantNormalizer['compileUserInput']({ + ...sourceAttribute, + formattedValue: undefined, + type: OccConfigurator.UiType.READ_ONLY, + }) + ).toBe(''); + }); + }); + describe('convertGroupType', () => { it('should convert group types properly', () => { expect( @@ -1212,6 +1261,15 @@ describe('OccConfiguratorVariantNormalizer', () => { ); expect(attributeDDWithValues.incomplete).toBe(true); }); + + it('should not touch flag in case uiType is not defined ', () => { + //a previous user input is always be part of the domain after a roundtrip + attributeDDWithValues.uiType = undefined; + occConfiguratorVariantNormalizer.compileAttributeIncomplete( + attributeDDWithValues + ); + expect(attributeDDWithValues.incomplete).toBe(false); + }); }); describe('isRetractValueSelected', () => { diff --git a/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.ts b/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.ts index c323fac3866..c175c6b16f6 100644 --- a/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.ts +++ b/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.ts @@ -137,13 +137,7 @@ export class OccConfiguratorVariantNormalizer uiType: uiType, uiTypeVariation: sourceAttribute.type, groupId: this.getGroupId(sourceAttribute.key, sourceAttribute.name), - userInput: - uiType === Configurator.UiType.NUMERIC || - uiType === Configurator.UiType.STRING - ? sourceAttribute.formattedValue - ? sourceAttribute.formattedValue - : '' - : undefined, + userInput: this.compileUserInput(sourceAttribute), maxlength: (sourceAttribute.maxlength ?? 0) + (sourceAttribute.negativeAllowed ? 1 : 0), @@ -167,7 +161,24 @@ export class OccConfiguratorVariantNormalizer this.compileAttributeIncomplete(attribute); attributeList.push(attribute); } - + protected compileUserInput( + sourceAttribute: OccConfigurator.Attribute + ): string | undefined { + let userInput; + if ( + sourceAttribute.type === OccConfigurator.UiType.NUMERIC || + sourceAttribute.type === OccConfigurator.UiType.STRING || + sourceAttribute.type === OccConfigurator.UiType.READ_ONLY + ) { + userInput = sourceAttribute.formattedValue + ? sourceAttribute.formattedValue + : ''; + } + if (sourceAttribute.type === OccConfigurator.UiType.SAP_DATE) { + userInput = sourceAttribute.value ? sourceAttribute.value : ''; + } + return userInput; + } setSelectedSingleValue(attribute: Configurator.Attribute) { if (attribute.values) { const selectedValues = attribute.values @@ -414,6 +425,10 @@ export class OccConfiguratorVariantNormalizer uiType = Configurator.UiType.NUMERIC; break; } + case OccConfigurator.UiType.SAP_DATE: { + uiType = Configurator.UiType.SAP_DATE; + break; + } } return uiType; } @@ -516,41 +531,64 @@ export class OccConfiguratorVariantNormalizer //Default value for incomplete is false attribute.incomplete = false; - switch (attribute.uiType) { - case Configurator.UiType.RADIOBUTTON: - case Configurator.UiType.RADIOBUTTON_ADDITIONAL_INPUT: - case Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT: - case Configurator.UiType.DROPDOWN: { - if ( - !attribute.selectedSingleValue || - attribute.selectedSingleValue === Configurator.RetractValueCode - ) { - attribute.incomplete = true; - } - break; - } - case Configurator.UiType.SINGLE_SELECTION_IMAGE: { - if (!attribute.selectedSingleValue) { - attribute.incomplete = true; - } - break; - } - case Configurator.UiType.NUMERIC: - case Configurator.UiType.STRING: { - if (!attribute.userInput) { - attribute.incomplete = true; - } - break; - } + const singleValueTypes = [ + Configurator.UiType.RADIOBUTTON, + Configurator.UiType.RADIOBUTTON_ADDITIONAL_INPUT, + Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT, + Configurator.UiType.DROPDOWN, + ]; + const inputTypes = [ + Configurator.UiType.NUMERIC, + Configurator.UiType.SAP_DATE, + Configurator.UiType.STRING, + ]; + const multiValueTypes = [ + Configurator.UiType.CHECKBOXLIST, + Configurator.UiType.CHECKBOX, + Configurator.UiType.MULTI_SELECTION_IMAGE, + ]; + const uiType = attribute.uiType ?? Configurator.UiType.NOT_IMPLEMENTED; + if (singleValueTypes.includes(uiType)) { + this.compileAttributeIncompleteSingleLevel(attribute); + } else if (uiType === Configurator.UiType.SINGLE_SELECTION_IMAGE) { + this.compileAttributeIncompleteSingleSelectionImage(attribute); + } else if (inputTypes.includes(uiType)) { + this.compileAttributeIncompleteInputTypes(attribute); + } else if (multiValueTypes.includes(uiType)) { + this.compileAttributeIncompleteMultiSelect(attribute); + } + } - case Configurator.UiType.CHECKBOXLIST: - case Configurator.UiType.CHECKBOX: - case Configurator.UiType.MULTI_SELECTION_IMAGE: { - const isOneValueSelected = - attribute.values?.find((value) => value.selected) !== undefined; - attribute.incomplete = !isOneValueSelected; - break; - } + protected compileAttributeIncompleteSingleLevel( + attribute: Configurator.Attribute + ): void { + if ( + !attribute.selectedSingleValue || + attribute.selectedSingleValue === Configurator.RetractValueCode + ) { + attribute.incomplete = true; } } + protected compileAttributeIncompleteSingleSelectionImage( + attribute: Configurator.Attribute + ): void { + if (!attribute.selectedSingleValue) { + attribute.incomplete = true; + } + } + protected compileAttributeIncompleteInputTypes( + attribute: Configurator.Attribute + ): void { + if (!attribute.userInput) { + attribute.incomplete = true; + } + } + + protected compileAttributeIncompleteMultiSelect( + attribute: Configurator.Attribute + ): void { + const isOneValueSelected = + attribute.values?.find((value) => value.selected) !== undefined; + attribute.incomplete = !isOneValueSelected; + } } diff --git a/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-serializer.spec.ts b/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-serializer.spec.ts index 9a5572fb719..a65116b0d30 100644 --- a/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-serializer.spec.ts +++ b/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-serializer.spec.ts @@ -447,6 +447,12 @@ describe('OccConfiguratorVariantSerializer', () => { ) ).toBe(OccConfigurator.UiType.NUMERIC); + expect( + occConfiguratorVariantSerializer.convertCharacteristicType( + Configurator.UiType.SAP_DATE + ) + ).toBe(OccConfigurator.UiType.SAP_DATE); + expect( occConfiguratorVariantSerializer.convertCharacteristicType( Configurator.UiType.RADIOBUTTON @@ -507,4 +513,14 @@ describe('OccConfiguratorVariantSerializer', () => { ) ).toBe(OccConfigurator.UiType.DROPDOWN_ADDITIONAL_INPUT); }); + + describe('convertCharacteristicTypeSingleValue', () => { + it('should default to NOT_IMPLEMENTED', () => { + expect( + occConfiguratorVariantSerializer[ + 'convertCharacteristicTypeSingleValue' + ](Configurator.UiType.MULTI_SELECTION_IMAGE) + ).toBe(OccConfigurator.UiType.NOT_IMPLEMENTED); + }); + }); }); diff --git a/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-serializer.ts b/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-serializer.ts index 1e7da3f639b..9821a70fab8 100644 --- a/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-serializer.ts +++ b/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-serializer.ts @@ -93,34 +93,43 @@ export class OccConfiguratorVariantSerializer attribute: Configurator.Attribute, occAttributes: OccConfigurator.Attribute[] ): void { + const uiType = attribute.uiType ?? Configurator.UiType.NOT_IMPLEMENTED; const targetAttribute: OccConfigurator.Attribute = { key: attribute.name, name: attribute.name, langDepName: attribute.label, required: attribute.required, retractTriggered: attribute.retractTriggered, - type: this.convertCharacteristicType( - attribute.uiType ?? Configurator.UiType.NOT_IMPLEMENTED - ), + type: this.convertCharacteristicType(uiType), }; - - if ( - attribute.uiType === Configurator.UiType.DROPDOWN || - attribute.uiType === Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT || - attribute.uiType === Configurator.UiType.RADIOBUTTON || - attribute.uiType === Configurator.UiType.RADIOBUTTON_ADDITIONAL_INPUT || - attribute.uiType === Configurator.UiType.SINGLE_SELECTION_IMAGE - ) { + const singleValueTypes = [ + Configurator.UiType.RADIOBUTTON, + Configurator.UiType.RADIOBUTTON_ADDITIONAL_INPUT, + Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT, + Configurator.UiType.DROPDOWN, + Configurator.UiType.SINGLE_SELECTION_IMAGE, + ]; + + const multiValueTypes = [ + Configurator.UiType.CHECKBOXLIST, + Configurator.UiType.CHECKBOX, + Configurator.UiType.MULTI_SELECTION_IMAGE, + ]; + + const inputTypesSetValue = [ + Configurator.UiType.STRING, + Configurator.UiType.SAP_DATE, + ]; + + const inputTypesSetFormattedValue = [Configurator.UiType.NUMERIC]; + + if (singleValueTypes.includes(uiType)) { this.retractValue(attribute, targetAttribute); - } else if (attribute.uiType === Configurator.UiType.STRING) { + } else if (inputTypesSetValue.includes(uiType)) { targetAttribute.value = attribute.userInput; - } else if (attribute.uiType === Configurator.UiType.NUMERIC) { + } else if (inputTypesSetFormattedValue.includes(uiType)) { targetAttribute.formattedValue = attribute.userInput; - } else if ( - attribute.uiType === Configurator.UiType.CHECKBOXLIST || - attribute.uiType === Configurator.UiType.CHECKBOX || - attribute.uiType === Configurator.UiType.MULTI_SELECTION_IMAGE - ) { + } else if (multiValueTypes.includes(uiType)) { const domainValues: OccConfigurator.Value[] = []; if (attribute.values) { attribute.values.forEach((value) => { @@ -143,6 +152,24 @@ export class OccConfiguratorVariantSerializer } convertCharacteristicType(type: Configurator.UiType): OccConfigurator.UiType { + const singleValueTypes = [ + Configurator.UiType.RADIOBUTTON, + Configurator.UiType.RADIOBUTTON_ADDITIONAL_INPUT, + Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT, + Configurator.UiType.DROPDOWN, + Configurator.UiType.SINGLE_SELECTION_IMAGE, + ]; + + if (singleValueTypes.includes(type)) { + return this.convertCharacteristicTypeSingleValue(type); + } else { + return this.convertCharacteristicTypeMultiValueAndInput(type); + } + } + + protected convertCharacteristicTypeSingleValue( + type: Configurator.UiType + ): OccConfigurator.UiType { let uiType: OccConfigurator.UiType; switch (type) { case Configurator.UiType.RADIOBUTTON: { @@ -161,6 +188,22 @@ export class OccConfiguratorVariantSerializer uiType = OccConfigurator.UiType.DROPDOWN_ADDITIONAL_INPUT; break; } + case Configurator.UiType.SINGLE_SELECTION_IMAGE: { + uiType = OccConfigurator.UiType.SINGLE_SELECTION_IMAGE; + break; + } + default: { + uiType = OccConfigurator.UiType.NOT_IMPLEMENTED; + } + } + return uiType; + } + + protected convertCharacteristicTypeMultiValueAndInput( + type: Configurator.UiType + ): OccConfigurator.UiType { + let uiType: OccConfigurator.UiType; + switch (type) { case Configurator.UiType.STRING: { uiType = OccConfigurator.UiType.STRING; break; @@ -169,6 +212,10 @@ export class OccConfiguratorVariantSerializer uiType = OccConfigurator.UiType.NUMERIC; break; } + case Configurator.UiType.SAP_DATE: { + uiType = OccConfigurator.UiType.SAP_DATE; + break; + } case Configurator.UiType.CHECKBOX: { uiType = OccConfigurator.UiType.CHECK_BOX; break; @@ -181,10 +228,6 @@ export class OccConfiguratorVariantSerializer uiType = OccConfigurator.UiType.MULTI_SELECTION_IMAGE; break; } - case Configurator.UiType.SINGLE_SELECTION_IMAGE: { - uiType = OccConfigurator.UiType.SINGLE_SELECTION_IMAGE; - break; - } default: { uiType = OccConfigurator.UiType.NOT_IMPLEMENTED; } diff --git a/feature-libs/product-configurator/rulebased/occ/variant/variant-configurator-occ.models.ts b/feature-libs/product-configurator/rulebased/occ/variant/variant-configurator-occ.models.ts index 04166c4e37c..dd38e457500 100644 --- a/feature-libs/product-configurator/rulebased/occ/variant/variant-configurator-occ.models.ts +++ b/feature-libs/product-configurator/rulebased/occ/variant/variant-configurator-occ.models.ts @@ -199,6 +199,7 @@ export namespace OccConfigurator { export enum UiType { STRING = 'STRING', NUMERIC = 'NUMERIC', + SAP_DATE = 'SAP_DATE', CHECK_BOX = 'CHECK_BOX', CHECK_BOX_LIST = 'CHECK_BOX_LIST', RADIO_BUTTON = 'RADIO_BUTTON',