From 692356c407dc97abea442b27f3056d5b605f313a Mon Sep 17 00:00:00 2001 From: samhere06 <118881732+samhere06@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:37:42 +0530 Subject: [PATCH] Added multi select combobox and checkbox support (#167) Co-authored-by: mohas22 --- .../_bridge/helpers/sdk-pega-component-map.ts | 2 + .../field/check-box/check-box.component.html | 31 +- .../field/check-box/check-box.component.scss | 15 +- .../field/check-box/check-box.component.ts | 171 ++++++--- .../multiselect/multiselect.component.html | 34 ++ .../multiselect/multiselect.component.scss | 7 + .../multiselect/multiselect.component.spec.ts | 21 + .../multiselect/multiselect.component.ts | 363 ++++++++++++++++++ .../_components/field/multiselect/utils.ts | 209 ++++++++++ .../src/lib/_helpers/instructions-utils.ts | 38 ++ .../DigV2/ComplexFields/DataReference.spec.js | 61 +++ 11 files changed, 892 insertions(+), 60 deletions(-) create mode 100644 packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.html create mode 100644 packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.scss create mode 100644 packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.spec.ts create mode 100644 packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.ts create mode 100644 packages/angular-sdk-components/src/lib/_components/field/multiselect/utils.ts create mode 100644 packages/angular-sdk-components/src/lib/_helpers/instructions-utils.ts diff --git a/packages/angular-sdk-components/src/lib/_bridge/helpers/sdk-pega-component-map.ts b/packages/angular-sdk-components/src/lib/_bridge/helpers/sdk-pega-component-map.ts index 4666af66..ba595e9d 100644 --- a/packages/angular-sdk-components/src/lib/_bridge/helpers/sdk-pega-component-map.ts +++ b/packages/angular-sdk-components/src/lib/_bridge/helpers/sdk-pega-component-map.ts @@ -68,6 +68,7 @@ import { InlineDashboardPageComponent } from '../../_components/template/inline- import { ListPageComponent } from '../../_components/template/list-page/list-page.component'; import { ListViewComponent } from '../../_components/template/list-view/list-view.component'; import { MultiReferenceReadonlyComponent } from '../../_components/template/multi-reference-readonly/multi-reference-readonly.component'; +import { MultiselectComponent } from '../../_components/field/multiselect/multiselect.component'; import { NarrowWideFormComponent } from '../../_components/template/narrow-wide-form/narrow-wide-form.component'; import { OneColumnComponent } from '../../_components/template/one-column/one-column.component'; import { OneColumnPageComponent } from '../../_components/template/one-column-page/one-column-page.component'; @@ -185,6 +186,7 @@ const pegaSdkComponentMap = { MaterialUtility: MaterialUtilityComponent, ModalViewContainer: ModalViewContainerComponent, MultiReferenceReadOnly: MultiReferenceReadonlyComponent, + Multiselect: MultiselectComponent, MultiStep: MultiStepComponent, // 'NarrowWide': NarrowWideFormComponent, NarrowWideDetails: DetailsNarrowWideComponent, diff --git a/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.html b/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.html index 3ab8860d..012a3fbd 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.html +++ b/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.html @@ -6,36 +6,37 @@ > -
+
- {{ caption$ }}
-
+
+ + {{ item.text ?? item.value }} + + +
+ {{ caption$ }} -
- {{ helperText }} +

{{ helperText }}

+ {{ getErrorMessage() }}
diff --git a/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.scss b/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.scss index 767c7526..4abc8a90 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.scss +++ b/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.scss @@ -8,10 +8,10 @@ top: 0rem; margin-top: 0.625rem; font-size: 0.875rem; - display: block; transform: translateY(-1.28125em) scale(0.75) perspective(100px) translateZ(0.001px); -ms-transform: translateY(-1.28125em) scale(0.75); width: 133.33333%; + color: rgba(0, 0, 0, 0.6); } .psdk-data-readonly { @@ -22,3 +22,16 @@ ::ng-deep .mat-mdc-form-field-infix { width: auto; } + +p { + font-size: 0.75rem; + color: rgba(0, 0, 0, 0.58); +} + +mat-checkbox { + margin-left: -11px; +} + +.mat-mdc-option { + margin-left: -16px; +} diff --git a/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.ts b/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.ts index 9dbbc756..c455e196 100644 --- a/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.ts +++ b/packages/angular-sdk-components/src/lib/_components/field/check-box/check-box.component.ts @@ -3,10 +3,14 @@ import { CommonModule } from '@angular/common'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatOptionModule } from '@angular/material/core'; +import { interval } from 'rxjs'; import { AngularPConnectData, AngularPConnectService } from '../../../_bridge/angular-pconnect'; import { Utils } from '../../../_helpers/utils'; import { ComponentMapperComponent } from '../../../_bridge/component-mapper/component-mapper.component'; import { PConnFieldProps } from '../../../_types/PConnProps.interface'; +import { deleteInstruction, insertInstruction, updateNewInstructions } from '../../../_helpers/instructions-utils'; +import { handleEvent } from '../../../_helpers/event-util'; interface CheckboxProps extends Omit { // If any, enter additional props that only exist on Checkbox here @@ -15,6 +19,13 @@ interface CheckboxProps extends Omit { caption?: string; trueLabel?: string; falseLabel?: string; + selectionMode?: string; + datasource?: any; + selectionKey?: string; + selectionList?: any; + primaryField: string; + readonlyContextList: any; + referenceList: string; } @Component({ @@ -22,7 +33,7 @@ interface CheckboxProps extends Omit { templateUrl: './check-box.component.html', styleUrls: ['./check-box.component.scss'], standalone: true, - imports: [CommonModule, ReactiveFormsModule, MatCheckboxModule, MatFormFieldModule, forwardRef(() => ComponentMapperComponent)] + imports: [CommonModule, ReactiveFormsModule, MatCheckboxModule, MatFormFieldModule, MatOptionModule, forwardRef(() => ComponentMapperComponent)] }) export class CheckBoxComponent implements OnInit, OnDestroy { @Input() pConn$: typeof PConnect; @@ -50,6 +61,17 @@ export class CheckBoxComponent implements OnInit, OnDestroy { trueLabel$?: string; falseLabel$?: string; + selectionMode?: string; + datasource?: any; + selectionKey?: string; + selectionList?: any; + primaryField: string; + selectedvalues: any; + referenceList: string; + listOfCheckboxes: any[] = []; + actionsApi: any; + propName: any; + fieldControl = new FormControl('', null); constructor( @@ -69,6 +91,11 @@ export class CheckBoxComponent implements OnInit, OnDestroy { // this.updateSelf(); this.checkAndUpdate(); + if (this.selectionMode === 'multi' && this.referenceList?.length > 0) { + this.pConn$.setReferenceList(this.selectionList); + updateNewInstructions(this.pConn$, this.selectionList); + } + if (this.formGroup$) { // add control to formGroup this.formGroup$.addControl(this.controlName$, this.fieldControl); @@ -111,68 +138,124 @@ export class CheckBoxComponent implements OnInit, OnDestroy { // moved this from ngOnInit() and call this from there instead... this.configProps$ = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()) as CheckboxProps; - if (this.configProps$.value != undefined) { - this.value$ = this.configProps$.value; - } this.testId = this.configProps$.testId; - this.label$ = this.configProps$.label; this.displayMode$ = this.configProps$.displayMode; + this.label$ = this.configProps$.label; + if (this.label$ != '') { + this.showLabel$ = true; + } - this.caption$ = this.configProps$.caption; - this.helperText = this.configProps$.helperText; - this.trueLabel$ = this.configProps$.trueLabel; - this.falseLabel$ = this.configProps$.falseLabel; - - // timeout and detectChanges to avoid ExpressionChangedAfterItHasBeenCheckedError - setTimeout(() => { - if (this.configProps$.required != null) { - this.bRequired$ = this.utils.getBooleanValue(this.configProps$.required); + this.actionsApi = this.pConn$.getActionsApi(); + this.propName = (this.pConn$.getStateProps() as any).value; + + // multi case + this.selectionMode = this.configProps$.selectionMode; + if (this.selectionMode === 'multi') { + this.referenceList = this.configProps$.referenceList; + this.selectionList = this.configProps$.selectionList; + this.selectedvalues = this.configProps$.readonlyContextList; + this.primaryField = this.configProps$.primaryField; + + this.datasource = this.configProps$.datasource; + this.selectionKey = this.configProps$.selectionKey; + const listSourceItems = this.datasource?.source ?? []; + const dataField: any = this.selectionKey?.split?.('.')[1]; + const listToDisplay: any[] = []; + listSourceItems.forEach(element => { + element.selected = this.selectedvalues?.some?.(data => data[dataField] === element.key); + listToDisplay.push(element); + }); + this.listOfCheckboxes = listToDisplay; + } else { + if (this.configProps$.value != undefined) { + this.value$ = this.configProps$.value; } - this.cdRef.detectChanges(); - }); - - if (this.configProps$.visibility != null) { - this.bVisible$ = this.utils.getBooleanValue(this.configProps$.visibility); - } - // disabled - if (this.configProps$.disabled != undefined) { - this.bDisabled$ = this.utils.getBooleanValue(this.configProps$.disabled); - } + this.caption$ = this.configProps$.caption; + this.helperText = this.configProps$.helperText; + this.trueLabel$ = this.configProps$.trueLabel; + this.falseLabel$ = this.configProps$.falseLabel; + + // timeout and detectChanges to avoid ExpressionChangedAfterItHasBeenCheckedError + setTimeout(() => { + if (this.configProps$.required != null) { + this.bRequired$ = this.utils.getBooleanValue(this.configProps$.required); + } + this.cdRef.detectChanges(); + }); + + if (this.configProps$.visibility != null) { + this.bVisible$ = this.utils.getBooleanValue(this.configProps$.visibility); + } - if (this.bDisabled$) { - this.fieldControl.disable(); - } else { - this.fieldControl.enable(); - } + // disabled + if (this.configProps$.disabled != undefined) { + this.bDisabled$ = this.utils.getBooleanValue(this.configProps$.disabled); + } - if (this.configProps$.readOnly != null) { - this.bReadonly$ = this.utils.getBooleanValue(this.configProps$.readOnly); - } + if (this.bDisabled$) { + this.fieldControl.disable(); + } else { + this.fieldControl.enable(); + } - this.componentReference = (this.pConn$.getStateProps() as any).value; + if (this.configProps$.readOnly != null) { + this.bReadonly$ = this.utils.getBooleanValue(this.configProps$.readOnly); + this.fieldControl.disable(); + } - if (this.label$ != '') { - this.showLabel$ = true; - } + this.componentReference = (this.pConn$.getStateProps() as any).value; - // eslint-disable-next-line sonarjs/no-redundant-boolean - if (this.value$ === 'true' || this.value$ == true) { - this.isChecked$ = true; - } else { - this.isChecked$ = false; + // eslint-disable-next-line sonarjs/no-redundant-boolean + if (this.value$ === 'true' || this.value$ == true) { + this.isChecked$ = true; + } else { + this.isChecked$ = false; + } + // trigger display of error message with field control + if (this.angularPConnectData.validateMessage != null && this.angularPConnectData.validateMessage != '') { + const timer = interval(100).subscribe(() => { + this.fieldControl.setErrors({ message: true }); + this.fieldControl.markAsTouched(); + + timer.unsubscribe(); + }); + } } } fieldOnChange(event: any) { event.value = event.checked; - this.angularPConnectData.actions?.onChange(this, event); + handleEvent(this.actionsApi, 'changeNblur', this.propName, event.checked); } fieldOnBlur(event: any) { - event.value = event.checked; - this.angularPConnectData.actions?.onBlur(this, event); + if (this.selectionMode === 'multi') { + this.pConn$.getValidationApi().validate(this.selectedvalues, this.selectionList); + } else { + event.value = event.checked; + this.angularPConnectData.actions?.onBlur(this, event); + } + } + + handleChangeMultiMode(event, element) { + if (!element.selected) { + insertInstruction(this.pConn$, this.selectionList, this.selectionKey, this.primaryField, { + id: element.key, + primary: element.text ?? element.value + }); + } else { + deleteInstruction(this.pConn$, this.selectionList, this.selectionKey, { + id: element.key, + primary: element.text ?? element.value + }); + } + this.pConn$.clearErrorMessages({ + property: this.selectionList, + category: '', + context: '' + }); } getErrorMessage() { diff --git a/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.html b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.html new file mode 100644 index 00000000..26485f4e --- /dev/null +++ b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.html @@ -0,0 +1,34 @@ +
+ + {{ label$ }} + + + + {{ select.primary }} + + + + + + + + + {{ item.primary }} + + + + {{ getErrorMessage() }} + +
diff --git a/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.scss b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.scss new file mode 100644 index 00000000..f5d1b8cf --- /dev/null +++ b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.scss @@ -0,0 +1,7 @@ +.psdk-full-width { + width: 100%; +} + +::ng-deep .mat-mdc-form-field-infix { + padding-left: 10px; +} diff --git a/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.spec.ts b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.spec.ts new file mode 100644 index 00000000..16082e6c --- /dev/null +++ b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MultiselectComponent } from './multiselect.component'; + +describe('MultiselectComponent', () => { + let component: MultiselectComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [MultiselectComponent] + }); + fixture = TestBed.createComponent(MultiselectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.ts b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.ts new file mode 100644 index 00000000..ab972b22 --- /dev/null +++ b/packages/angular-sdk-components/src/lib/_components/field/multiselect/multiselect.component.ts @@ -0,0 +1,363 @@ +import { CommonModule } from '@angular/common'; +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule, MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatOptionModule } from '@angular/material/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { AngularPConnectData, AngularPConnectService } from '../../../_bridge/angular-pconnect'; +import { ComponentMapperComponent } from '../../../_bridge/component-mapper/component-mapper.component'; +import { Utils } from '../../../_helpers/utils'; +import { doSearch, getDisplayFieldsMetaData, getGroupDataForItemsTree, preProcessColumns } from './utils'; +import { deleteInstruction, insertInstruction } from '../../../_helpers/instructions-utils'; + +@Component({ + selector: 'app-multiselect', + templateUrl: './multiselect.component.html', + styleUrls: ['./multiselect.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatAutocompleteModule, + MatOptionModule, + MatCheckboxModule, + MatIconModule, + MatChipsModule, + forwardRef(() => ComponentMapperComponent) + ] +}) +export class MultiselectComponent implements OnInit, OnDestroy { + @Input() pConn$: typeof PConnect; + @Input() formGroup$: FormGroup; + + // Used with AngularPConnect + angularPConnectData: AngularPConnectData = {}; + + label$ = ''; + value$ = ''; + bRequired$ = false; + bDisabled$ = false; + bVisible$ = true; + controlName$: string; + bHasForm$ = true; + listType: string; + placeholder: string; + fieldControl = new FormControl('', null); + parameters: {}; + hideLabel: boolean; + configProps$: any; + + referenceList: any; + selectionKey: string; + primaryField: string; + initialCaseClass: any; + showSecondaryInSearchOnly = false; + isGroupData = false; + referenceType; + secondaryFields; + groupDataSource = []; + matchPosition = 'contains'; + maxResultsDisplay; + groupColumnsConfig = [{}]; + selectionList; + listActions: any; + selectedItems: any[] = []; + itemsTreeBaseData = []; + displayFieldMeta: any; + dataApiObj: any; + itemsTree: any[] = []; + trigger: any; + + constructor( + private angularPConnect: AngularPConnectService, + private utils: Utils + ) {} + + ngOnInit(): void { + // First thing in initialization is registering and subscribing to the AngularPConnect service + this.angularPConnectData = this.angularPConnect.registerAndSubscribeComponent(this, this.onStateChange); + this.controlName$ = this.angularPConnect.getComponentID(this); + + // Then, continue on with other initialization + this.checkAndUpdate(); + + if (this.formGroup$) { + // add control to formGroup + this.formGroup$.addControl(this.controlName$, this.fieldControl); + this.fieldControl.setValue(this.value$); + this.bHasForm$ = true; + } else { + this.bHasForm$ = false; + } + } + + ngOnDestroy(): void { + if (this.formGroup$) { + this.formGroup$.removeControl(this.controlName$); + } + + if (this.angularPConnectData.unsubscribeFn) { + this.angularPConnectData.unsubscribeFn(); + } + } + + // Callback passed when subscribing to store change + onStateChange() { + this.checkAndUpdate(); + } + + checkAndUpdate() { + // Should always check the bridge to see if the component should + // update itself (re-render) + const bUpdateSelf = this.angularPConnect.shouldComponentUpdate(this); + + // ONLY call updateSelf when the component should update + if (bUpdateSelf) { + this.updateSelf(); + } + } + + // updateSelf + updateSelf() { + this.configProps$ = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()); + + let { datasource = [], columns = [{}] } = this.configProps$; + this.setPropertyValuesFromProps(); + + if (this.referenceList.length > 0) { + datasource = this.referenceList; + columns = [ + { + value: this.primaryField, + display: 'true', + useForSearch: true, + primary: 'true' + }, + { + value: this.selectionKey, + setProperty: this.selectionKey, + key: 'true' + } + ]; + let secondaryColumns: any = []; + if (this.secondaryFields) { + secondaryColumns = this.secondaryFields.map(secondaryField => ({ + value: secondaryField, + display: 'true', + secondary: 'true', + useForSearch: 'true' + })); + } else { + secondaryColumns = [ + { + value: this.selectionKey, + display: 'true', + secondary: 'true', + useForSearch: 'true' + } + ]; + } + if (this.referenceType === 'Case') { + columns = [...columns, ...secondaryColumns]; + } + } + + this.value$ = this.value$ ? this.value$ : ''; + const contextName = this.pConn$.getContextName(); + + const dataConfig = { + dataSource: datasource, + groupDataSource: this.groupDataSource, + isGroupData: this.isGroupData, + showSecondaryInSearchOnly: this.showSecondaryInSearchOnly, + parameters: this.parameters, + matchPosition: this.matchPosition, + listType: this.listType, + maxResultsDisplay: this.maxResultsDisplay || '100', + columns: preProcessColumns(columns), + groupColumnsConfig: preProcessColumns(this.groupColumnsConfig) + }; + + const groupsDisplayFieldMeta = this.listType !== 'associated' ? getDisplayFieldsMetaData(dataConfig.groupColumnsConfig) : null; + + this.itemsTreeBaseData = getGroupDataForItemsTree(this.groupDataSource, groupsDisplayFieldMeta, this.showSecondaryInSearchOnly) || []; + + this.itemsTree = this.isGroupData ? getGroupDataForItemsTree(this.groupDataSource, groupsDisplayFieldMeta, this.showSecondaryInSearchOnly) : []; + + this.displayFieldMeta = this.listType !== 'associated' ? getDisplayFieldsMetaData(dataConfig.columns) : null; + + this.listActions = this.pConn$.getListActions(); + this.pConn$.setReferenceList(this.selectionList); + + if (this.configProps$.visibility != null) { + this.bVisible$ = this.utils.getBooleanValue(this.configProps$.visibility); + } + + // disabled + if (this.configProps$.disabled != undefined) { + this.bDisabled$ = this.utils.getBooleanValue(this.configProps$.disabled); + } + + if (this.bDisabled$) { + this.fieldControl.disable(); + } else { + this.fieldControl.enable(); + } + + if (this.listType !== 'associated') { + PCore.getDataApi() + ?.init(dataConfig, contextName) + .then(async dataObj => { + this.dataApiObj = dataObj; + if (!this.isGroupData) { + this.getCaseListBasedOnParams(this.value$ ?? '', '', [...this.selectedItems], [...this.itemsTree]); + } + }); + } + } + + setPropertyValuesFromProps() { + this.label$ = this.configProps$.label; + this.placeholder = this.configProps$.placeholder || ''; + this.listType = this.configProps$.listType ? this.configProps$.listType : ''; + this.hideLabel = this.configProps$.hideLabel; + this.parameters = this.configProps$?.parameters ? this.configProps$?.parameters : {}; + this.referenceList = this.configProps$?.referenceList; + this.selectionKey = this.configProps$?.selectionKey; + this.primaryField = this.configProps$?.primaryField; + this.initialCaseClass = this.configProps$?.initialCaseClass; + this.showSecondaryInSearchOnly = this.configProps$?.showSecondaryInSearchOnly ? this.configProps$?.showSecondaryInSearchOnly : false; + this.isGroupData = this.configProps$?.isGroupData ? this.configProps$.isGroupData : false; + this.referenceType = this.configProps$?.referenceType; + this.secondaryFields = this.configProps$?.secondaryFields; + this.groupDataSource = this.configProps$?.groupDataSource ? this.configProps$?.groupDataSource : []; + this.matchPosition = this.configProps$?.matchPosition ? this.configProps$?.matchPosition : 'contains'; + this.maxResultsDisplay = this.configProps$?.maxResultsDisplay; + this.groupColumnsConfig = this.configProps$?.groupColumnsConfig ? this.configProps$?.groupColumnsConfig : [{}]; + this.selectionList = this.configProps$?.selectionList; + this.value$ = this.configProps$?.value; + } + + // main search function trigger + getCaseListBasedOnParams(searchText, group, selectedRows, currentItemsTree, isTriggeredFromSearch = false) { + if (this.referenceList && this.referenceList.length > 0) { + this.listActions.getSelectedRows(true).then(result => { + selectedRows = + result.length > 0 + ? result.map(item => { + return { + id: item[this.selectionKey.startsWith('.') ? this.selectionKey.substring(1) : this.selectionKey], + primary: item[this.primaryField.startsWith('.') ? this.primaryField.substring(1) : this.primaryField] + }; + }) + : []; + this.selectedItems = selectedRows; + + const initalItemsTree = isTriggeredFromSearch || !currentItemsTree ? [...this.itemsTreeBaseData] : [...currentItemsTree]; + + doSearch( + searchText, + group, + this.initialCaseClass, + this.displayFieldMeta, + this.dataApiObj, + initalItemsTree, + this.isGroupData, + this.showSecondaryInSearchOnly, + selectedRows || [] + ).then(res => { + this.itemsTree = res || []; + if (this.trigger) { + this.trigger.openPanel(); + } + }); + }); + } + } + + fieldOnChange(event: Event) { + this.value$ = (event.target as HTMLInputElement).value; + this.getCaseListBasedOnParams(this.value$, '', [...this.selectedItems], [...this.itemsTree], true); + } + + optionChanged(event: MatAutocompleteSelectedEvent) { + this.angularPConnectData.actions?.onChange(this, event); + } + + optionClicked = (event: Event, data: any, trigger?: MatAutocompleteTrigger): void => { + event.stopPropagation(); + this.toggleSelection(data, trigger); + }; + + toggleSelection = (data: any, trigger?: MatAutocompleteTrigger): void => { + data.selected = !data.selected; + this.trigger = trigger; + this.itemsTree.map((ele: any) => { + if (ele.id === data.id) { + ele.selected = data.selected; + } + return ele; + }); + + if (data.selected === true) { + this.selectedItems.push(data); + } else { + const index = this.selectedItems.findIndex(value => value.id === data.id); + this.selectedItems.splice(index, 1); + } + + this.value$ = ''; + // if this is a referenceList case + if (this.referenceList) this.setSelectedItemsForReferenceList(data); + + this.getCaseListBasedOnParams(this.value$, '', [...this.selectedItems], [...this.itemsTree], true); + }; + + removeChip = (data: any): void => { + if (data) { + data = this.itemsTree.filter((ele: any) => { + return ele.id === data.id; + }); + this.toggleSelection(data[0]); + } + }; + + setSelectedItemsForReferenceList(data: any) { + // Clear error messages if any + const propName = (this.pConn$.getStateProps() as any).selectionList; + this.pConn$.clearErrorMessages({ + property: propName, + category: '', + context: '' + }); + const { selected } = data; + if (selected) { + insertInstruction(this.pConn$, this.selectionList, this.selectionKey, this.primaryField, data); + } else { + deleteInstruction(this.pConn$, this.selectionList, this.selectionKey, data); + } + } + + getErrorMessage() { + let errMessage = ''; + + // look for validation messages for json, pre-defined or just an error pushed from workitem (400) + if (this.fieldControl.hasError('message')) { + errMessage = this.angularPConnectData.validateMessage ?? ''; + return errMessage; + } + if (this.fieldControl.hasError('required')) { + errMessage = 'You must enter a value'; + } else if (this.fieldControl.errors) { + errMessage = this.fieldControl.errors.toString(); + } + + return errMessage; + } +} diff --git a/packages/angular-sdk-components/src/lib/_components/field/multiselect/utils.ts b/packages/angular-sdk-components/src/lib/_components/field/multiselect/utils.ts new file mode 100644 index 00000000..90324ca2 --- /dev/null +++ b/packages/angular-sdk-components/src/lib/_components/field/multiselect/utils.ts @@ -0,0 +1,209 @@ +import cloneDeep from 'lodash/cloneDeep'; + +export function setVisibilityForList(c11nEnv, visibility) { + const { selectionMode, selectionList, renderMode, referenceList } = c11nEnv.getComponentConfig(); + // usecase:multiselect, fieldgroup, editable table + if ((selectionMode === PCore.getConstants().LIST_SELECTION_MODE.MULTI && selectionList) || (renderMode === 'Editable' && referenceList)) { + c11nEnv.getListActions().setVisibility(visibility); + } +} + +function preProcessColumns(columns) { + return columns?.map(col => { + const tempColObj = { ...col }; + tempColObj.value = col.value && col.value.startsWith('.') ? col.value.substring(1) : col.value; + if (tempColObj.setProperty) { + tempColObj.setProperty = col.setProperty && col.setProperty.startsWith('.') ? col.setProperty.substring(1) : col.setProperty; + } + return tempColObj; + }); +} + +function getDisplayFieldsMetaData(columns) { + const displayColumns = columns?.filter(col => col.display === 'true'); + const metaDataObj: any = { + key: '', + primary: '', + secondary: [] + }; + const keyCol = columns?.filter(col => col.key === 'true'); + metaDataObj.key = keyCol?.length > 0 ? keyCol[0].value : 'auto'; + const itemsRecordsColumn = columns?.filter(col => col.itemsRecordsColumn === 'true'); + if (itemsRecordsColumn?.length > 0) { + metaDataObj.itemsRecordsColumn = itemsRecordsColumn[0].value; + } + const itemsGroupKeyColumn = columns?.filter(col => col.itemsGroupKeyColumn === 'true'); + if (itemsGroupKeyColumn?.length > 0) { + metaDataObj.itemsGroupKeyColumn = itemsGroupKeyColumn[0].value; + } + for (let index = 0; index < displayColumns?.length; index += 1) { + if (displayColumns[index].secondary === 'true') { + metaDataObj.secondary.push(displayColumns[index].value); + } else if (displayColumns[index].primary === 'true') { + metaDataObj.primary = displayColumns[index].value; + } + } + return metaDataObj; +} + +function createSingleTreeObejct(entry, displayFieldMeta, showSecondaryData, selected) { + const secondaryArr: any = []; + displayFieldMeta.secondary.forEach(col => { + secondaryArr.push(entry[col]); + }); + const isSelected = selected.some(item => item.id === entry[displayFieldMeta.key]); + + return { + id: entry[displayFieldMeta.key], + primary: entry[displayFieldMeta.primary], + secondary: showSecondaryData ? secondaryArr : [], + selected: isSelected + }; +} + +function putItemsDataInItemsTree(listObjData, displayFieldMeta, itemsTree, showSecondaryInSearchOnly, selected) { + let newTreeItems = itemsTree.slice(); + const showSecondaryData = !showSecondaryInSearchOnly; + for (const obj of listObjData) { + const items = obj[displayFieldMeta.itemsRecordsColumn].map(entry => createSingleTreeObejct(entry, displayFieldMeta, showSecondaryData, selected)); + + newTreeItems = newTreeItems.map(caseObject => { + if (caseObject.id === obj[displayFieldMeta.itemsGroupKeyColumn]) { + caseObject.items = [...items]; + } + return caseObject; + }); + } + return newTreeItems; +} + +function prepareSearchResults(listObjData, displayFieldMeta) { + const searchResults: any = []; + for (const obj of listObjData) { + searchResults.push(...obj[displayFieldMeta.itemsRecordsColumn]); + } + return searchResults; +} + +async function doSearch( + searchText, + clickedGroup, + initialCaseClass, + displayFieldMeta, + dataApiObj, // deep clone of the dataApiObj + itemsTree, + isGroupData, + showSecondaryInSearchOnly, + selected +) { + let searchTextForUngroupedData = ''; + if (dataApiObj) { + // creating dataApiObject in grouped data cases + if (isGroupData) { + dataApiObj = cloneDeep(dataApiObj); + dataApiObj.fetchedNQData = false; + dataApiObj.cache = {}; + + // if we have no search text and no group selected, return the original tree + if (searchText === '' && clickedGroup === '') { + return itemsTree; + } + + // setting the inital search text & search classes in ApiObject + dataApiObj.parameters[Object.keys(dataApiObj.parameters)[1]] = searchText; + dataApiObj.parameters[Object.keys(dataApiObj.parameters)[0]] = initialCaseClass; + + // if we have a selected group + if (clickedGroup) { + // check if the data for this group is already present and no search text + if (searchText === '') { + const containsData = itemsTree.find(item => item.id === clickedGroup); + // do not make API call when items of respective group are already fetched + if (containsData?.items?.length) return itemsTree; + } + + dataApiObj.parameters[Object.keys(dataApiObj.parameters)[0]] = JSON.stringify([clickedGroup]); + } + } else { + searchTextForUngroupedData = searchText; + } + + // search API call + const response = await dataApiObj.fetchData(searchTextForUngroupedData).catch(() => { + return itemsTree; + }); + + let listObjData = response.data; + let newItemsTree = []; + if (isGroupData) { + if (searchText) { + listObjData = prepareSearchResults(listObjData, displayFieldMeta); + } else { + newItemsTree = putItemsDataInItemsTree(listObjData, displayFieldMeta, itemsTree, showSecondaryInSearchOnly, selected); + return newItemsTree; + } + } + const showSecondaryData = showSecondaryInSearchOnly ? !!searchText : true; + if (listObjData !== undefined && listObjData.length > 0) { + newItemsTree = listObjData.map(entry => createSingleTreeObejct(entry, displayFieldMeta, showSecondaryData, selected)); + } + return newItemsTree; + } + + return itemsTree; +} + +function setValuesToPropertyList(searchText, assocProp, items, columns, actions, updatePropertyInRedux = true) { + const setPropertyList = columns + ?.filter(col => col.setProperty) + .map(col => { + return { + source: col.value, + target: col.setProperty, + key: col.key, + primary: col.primary + }; + }); + const valueToSet: any = []; + if (setPropertyList.length > 0) { + setPropertyList.forEach(prop => { + items.forEach(item => { + if (prop.key === 'true' && item) { + valueToSet.push(item.id); + } else if (prop.primary === 'true' || !item) { + valueToSet.push(searchText); + } + }); + + if (updatePropertyInRedux) { + // BUG-666851 setting options so that the store values are replaced and not merged + const options = { + isArrayDeepMerge: false + }; + if (prop.target === 'Associated property') { + actions.updateFieldValue(assocProp, valueToSet, options); + } else { + actions.updateFieldValue(`.${prop.target}`, valueToSet, options); + } + } + }); + } + return valueToSet; +} + +function getGroupDataForItemsTree(groupDataSource, groupsDisplayFieldMeta, showSecondaryInSearchOnly) { + return groupDataSource?.map(group => { + const secondaryArr: any = []; + groupsDisplayFieldMeta.secondary.forEach(col => { + secondaryArr.push(group[col]); + }); + return { + id: group[groupsDisplayFieldMeta.key], + primary: group[groupsDisplayFieldMeta.primary], + secondary: showSecondaryInSearchOnly ? [] : secondaryArr, + items: [] + }; + }); +} + +export { preProcessColumns, getDisplayFieldsMetaData, doSearch, setValuesToPropertyList, getGroupDataForItemsTree }; diff --git a/packages/angular-sdk-components/src/lib/_helpers/instructions-utils.ts b/packages/angular-sdk-components/src/lib/_helpers/instructions-utils.ts new file mode 100644 index 00000000..1cc9b7eb --- /dev/null +++ b/packages/angular-sdk-components/src/lib/_helpers/instructions-utils.ts @@ -0,0 +1,38 @@ +function isSelfReferencedProperty(param, referenceProp) { + const [, parentPropName] = param.split('.'); + const referencePropParent = referenceProp?.split('.').pop(); + return parentPropName === referencePropParent; +} + +function updateNewInstructions(c11nEnv, selectionList) { + const { datasource: { parameters = {} } = {} } = c11nEnv.getFieldMetadata(selectionList) || {}; + const compositeKeys: any = []; + Object.values(parameters).forEach((param: any) => { + if (isSelfReferencedProperty(param, selectionList)) compositeKeys.push(param.substring(param.lastIndexOf('.') + 1)); + }); + c11nEnv.getListActions().initDefaultPageInstructions(selectionList, compositeKeys); +} + +function insertInstruction(c11nEnv, selectionList, selectionKey, primaryField, item) { + const { id, primary } = item; + const actualProperty = selectionKey.startsWith('.') ? selectionKey.substring(1) : selectionKey; + const displayProperty = primaryField.startsWith('.') ? primaryField.substring(1) : primaryField; + const rows = c11nEnv.getValue(`${c11nEnv.getPageReference()}${selectionList}`) || []; + const startIndex = rows.length; + const content = { + [actualProperty]: id, + [displayProperty]: primary, + nonFormProperties: actualProperty !== displayProperty ? [displayProperty] : [] + }; + c11nEnv.getListActions().insert(content, startIndex); +} + +function deleteInstruction(c11nEnv, selectionList, selectionKey, item) { + const { id } = item; + const actualProperty = selectionKey.startsWith('.') ? selectionKey.substring(1) : selectionKey; + const rows = c11nEnv.getValue(`${c11nEnv.getPageReference()}${selectionList}`) || []; + const index = rows.findIndex(row => row[actualProperty] === id); + c11nEnv.getListActions().deleteEntry(index); +} + +export { updateNewInstructions, insertInstruction, deleteInstruction }; diff --git a/projects/angular-test-app/tests/e2e/DigV2/ComplexFields/DataReference.spec.js b/projects/angular-test-app/tests/e2e/DigV2/ComplexFields/DataReference.spec.js index 0275bb26..9d10ba5f 100644 --- a/projects/angular-test-app/tests/e2e/DigV2/ComplexFields/DataReference.spec.js +++ b/projects/angular-test-app/tests/e2e/DigV2/ComplexFields/DataReference.spec.js @@ -197,6 +197,67 @@ test.describe('E2E test', () => { await page.locator('button:has-text("Previous")').click(); + /** MultiSelect mode type test */ + selectedSubCategory = page.locator('mat-select[data-test-id="9463d5f18a8924b3200b56efaad63bda"]'); + await selectedSubCategory.click(); + await page.getByRole('option', { name: 'Mode' }).click(); + + selectedTestName = page.locator('mat-select[data-test-id="6f64b45d01d11d8efd1693dfcb63b735"]'); + await selectedTestName.click(); + await page.getByRole('option', { name: 'MultiSelect' }).click(); + + /** Combo-Box mode type test */ + let displayAs = page.locator('mat-select[data-test-id="4aa668349e0970901aa6b11528f95223"]'); + await displayAs.click(); + await page.getByRole('option', { name: 'Combo-Box' }).click(); + + const selectProducts = page.locator('div >> label:has-text("Select Products")'); + await selectProducts.click(); + await page.getByRole('option', { name: 'Mobile' }).click(); + await selectProducts.click(); + await page.getByRole('option', { name: 'Telivision' }).click(); + await expect(selectProducts).toBeVisible(); + + await page.locator('button:has-text("Next")').click(); + + assignment = page.locator('app-default-form'); + + await expect(assignment.locator('td >> text="Mobile"')).toBeVisible(); + await expect(assignment.locator('td >> text="Telivision"')).toBeVisible(); + + await page.locator('button:has-text("Previous")').click(); + + await expect(page.locator('mat-chip-row:has-text("Mobile")')).toBeVisible(); + await expect(page.locator('mat-chip-row:has-text("Telivision")')).toBeVisible(); + + let deleteProduct = await page.locator('mat-chip-row:has-text("Mobile")'); + await deleteProduct.locator('button:has-text("cancel")').click(); + + await page.locator('button:has-text("Next")').click(); + + await expect(assignment.locator('td >> text="Mobile"')).not.toBeVisible(); + + await page.locator('button:has-text("Previous")').click(); + + deleteProduct = await page.locator('mat-chip-row:has-text("Telivision")'); + await deleteProduct.locator('button:has-text("cancel")').click(); + + /** Checkbox group mode type test */ + displayAs = page.locator('mat-select[data-test-id="4aa668349e0970901aa6b11528f95223"]'); + await displayAs.click(); + await page.getByRole('option', { name: 'Checkbox group' }).click(); + + const checkbox = page.locator('app-check-box'); + await checkbox.getByRole('option', { name: 'Washing Machine' }).click(); + await checkbox.getByRole('option', { name: 'Mobile' }).click(); + + await page.locator('button:has-text("Next")').click(); + + await expect(assignment.locator('td >> text="Washing Machine"')).toBeVisible(); + await expect(assignment.locator('td >> text="Mobile"')).toBeVisible(); + + await page.locator('button:has-text("Previous")').click(); + /** Readonly mode type test */ selectedSubCategory = page.locator('mat-select[data-test-id="9463d5f18a8924b3200b56efaad63bda"]'); await selectedSubCategory.click();