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',