From caa2166151f097a4ba57c50283f5ceec76b83a74 Mon Sep 17 00:00:00 2001 From: Tomasz Gnyp <49343696+tomgny@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:28:37 +0200 Subject: [PATCH] =?UTF-8?q?AAE-21946=20Support=20JSON=20paths=20with=20non?= =?UTF-8?q?-standard=20characters=20for=20data=20ta=E2=80=A6=20(#9571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * AAE-21946 Support JSON paths with non-standard characters for data table widget * add unit test * move path parse to extern helper class --- .../data-table/data-table.widget.spec.ts | 37 +++----- .../widgets/data-table/data-table.widget.ts | 50 ++--------- .../data-table-path-parser.helper.spec.ts | 88 +++++++++++++++++++ .../helpers/data-table-path-parser.helper.ts | 50 +++++++++++ .../lib/form/mocks/data-table-widget.mock.ts | 22 +++++ 5 files changed, 182 insertions(+), 65 deletions(-) create mode 100644 lib/process-services-cloud/src/lib/form/components/widgets/data-table/helpers/data-table-path-parser.helper.spec.ts create mode 100644 lib/process-services-cloud/src/lib/form/components/widgets/data-table/helpers/data-table-path-parser.helper.ts diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.spec.ts index aca648f2a9b..6dd718bc6c7 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.spec.ts @@ -73,6 +73,11 @@ describe('DataTableWidgetComponent', () => { const getPreview = () => fixture.nativeElement.querySelector('[data-automation-id="adf-data-table-widget-preview"]'); + const assertDataRows = (expectedData: WidgetDataTableAdapter) => { + expectedData.getRows().forEach((row) => (row.cssClass = '')); + expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + }; + beforeEach(() => { TestBed.configureTestingModule({ imports: [ProcessServiceCloudTestingModule] @@ -123,9 +128,7 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); const expectedData = new WidgetDataTableAdapter(mockAmericaCountriesData, mockSchemaDefinition); - expectedData.getRows().forEach((row) => (row.cssClass = '')); - - expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + assertDataRows(expectedData); }); it('should properly initialize data source based on field value', () => { @@ -134,9 +137,7 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); const expectedData = new WidgetDataTableAdapter(mockAmericaCountriesData, mockSchemaDefinition); - expectedData.getRows().forEach((row) => (row.cssClass = '')); - - expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + assertDataRows(expectedData); }); it('should properly initialize default json response data source based on field value if path is NOT provided', () => { @@ -145,9 +146,7 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); const expectedData = new WidgetDataTableAdapter(mockEuropeCountriesData, mockSchemaDefinition); - expectedData.getRows().forEach((row) => (row.cssClass = '')); - - expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + assertDataRows(expectedData); }); it('should properly initialize default json response data source based on variable if path is NOT provided', () => { @@ -155,9 +154,7 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); const expectedData = new WidgetDataTableAdapter(mockEuropeCountriesData, mockSchemaDefinition); - expectedData.getRows().forEach((row) => (row.cssClass = '')); - - expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + assertDataRows(expectedData); }); it('should properly initialize json response data source based on field value if path is provided', () => { @@ -166,9 +163,7 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); const expectedData = new WidgetDataTableAdapter(mockEuropeCountriesData, mockSchemaDefinition); - expectedData.getRows().forEach((row) => (row.cssClass = '')); - - expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + assertDataRows(expectedData); }); it('should properly initialize json response data source based on variable if path is provided', () => { @@ -181,9 +176,7 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); const expectedData = new WidgetDataTableAdapter(mockEuropeCountriesData, mockSchemaDefinition); - expectedData.getRows().forEach((row) => (row.cssClass = '')); - - expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + assertDataRows(expectedData); }); it('should properly initialize data source based on form variable', () => { @@ -191,9 +184,7 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); const expectedData = new WidgetDataTableAdapter(mockEuropeCountriesData, mockSchemaDefinition); - expectedData.getRows().forEach((row) => (row.cssClass = '')); - - expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + assertDataRows(expectedData); }); it('should properly initialize data source based on process variable', () => { @@ -201,9 +192,7 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); const expectedData = new WidgetDataTableAdapter(mockEuropeCountriesData, mockSchemaDefinition); - expectedData.getRows().forEach((row) => (row.cssClass = '')); - - expect(widget.dataSource.getRows()).toEqual(expectedData.getRows()); + assertDataRows(expectedData); }); it('should NOT display error if form is in preview state', () => { diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.ts index f86b3ef0341..dadb811249b 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.ts @@ -18,29 +18,17 @@ /* eslint-disable @angular-eslint/component-selector */ import { Component, OnInit, ViewEncapsulation } from '@angular/core'; -import { - WidgetComponent, - FormService, - DataTableModule, - LogService, - FormBaseModule, - DataRow, - DataColumn -} from '@alfresco/adf-core'; +import { WidgetComponent, FormService, DataTableModule, LogService, FormBaseModule, DataRow, DataColumn } from '@alfresco/adf-core'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { FormCloudService } from '../../../services/form-cloud.service'; import { TaskVariableCloud } from '../../../models/task-variable-cloud.model'; import { WidgetDataTableAdapter } from './data-table-adapter.widget'; +import { DataTablePathParserHelper } from './helpers/data-table-path-parser.helper'; @Component({ standalone: true, - imports: [ - CommonModule, - TranslateModule, - DataTableModule, - FormBaseModule - ], + imports: [CommonModule, TranslateModule, DataTableModule, FormBaseModule], selector: 'data-table', templateUrl: './data-table.widget.html', styleUrls: ['./data-table.widget.scss'], @@ -58,7 +46,6 @@ import { WidgetDataTableAdapter } from './data-table-adapter.widget'; encapsulation: ViewEncapsulation.None }) export class DataTableWidgetComponent extends WidgetComponent implements OnInit { - dataSource: WidgetDataTableAdapter; dataTableLoadFailed = false; previewState = false; @@ -67,12 +54,9 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit private columnsSchema: DataColumn[]; private variableName: string; private defaultResponseProperty = 'data'; + private pathParserHelper = new DataTablePathParserHelper(); - constructor( - public formService: FormService, - private formCloudService: FormCloudService, - private logService: LogService - ) { + constructor(public formService: FormService, private formCloudService: FormCloudService, private logService: LogService) { super(formService); } @@ -109,8 +93,8 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit const rowsData = fieldValue || this.getDataFromVariable(); if (rowsData) { - const dataFromPath = this.getOptionsFromPath(rowsData, optionsPath); - this.rowsData = dataFromPath?.length ? dataFromPath : rowsData as DataRow[]; + const dataFromPath = this.pathParserHelper.retrieveDataFromPath(rowsData, optionsPath); + this.rowsData = (dataFromPath?.length ? dataFromPath : rowsData) as DataRow[]; } } @@ -124,25 +108,9 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit return processVariableDropdownOptions ?? formVariableDropdownOptions; } - private getOptionsFromPath(data: any, path: string): DataRow[] { - const properties = path.split('.'); - const currentProperty = properties.shift(); - - if (!Object.prototype.hasOwnProperty.call(data, currentProperty)) { - return []; - } - - const nestedData = data[currentProperty]; - - if (Array.isArray(nestedData)) { - return nestedData; - } - - return this.getOptionsFromPath(nestedData, properties.join('.')); - } - private getVariableValueByName(variables: TaskVariableCloud[], variableName: string): any { - return variables?.find((variable: TaskVariableCloud) => variable?.name === `variables.${variableName}` || variable?.name === variableName)?.value; + return variables?.find((variable: TaskVariableCloud) => variable?.name === `variables.${variableName}` || variable?.name === variableName) + ?.value; } private setPreviewState(): void { diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/helpers/data-table-path-parser.helper.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/helpers/data-table-path-parser.helper.spec.ts new file mode 100644 index 00000000000..46f6816eaf4 --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/helpers/data-table-path-parser.helper.spec.ts @@ -0,0 +1,88 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DataTablePathParserHelper } from './data-table-path-parser.helper'; +import { + mockEuropeCountriesData, + mockJsonNestedResponseEuropeCountriesDataWithSeparatorInPropertyName, + mockJsonNestedResponseEuropeCountriesDataWithMultipleSpecialCharacters, + mockJsonNestedResponseEuropeCountriesData +} from '../../../../mocks/data-table-widget.mock'; + +describe('DataTablePathParserHelper', () => { + let helper: DataTablePathParserHelper; + + beforeEach(() => { + helper = new DataTablePathParserHelper(); + }); + + it('should return the correct data for path with separator in nested brackets', () => { + const data = mockJsonNestedResponseEuropeCountriesDataWithSeparatorInPropertyName; + const path = 'response.[my.data].[country[data].country]'; + const result = helper.retrieveDataFromPath(data, path); + expect(result).toEqual(mockEuropeCountriesData); + }); + + it('should return the correct data for path with special characters except separator (.) in brackets', () => { + const data = mockJsonNestedResponseEuropeCountriesDataWithMultipleSpecialCharacters; + const path = 'response.[xyz:abc,xyz-abc,xyz_abc,abc+xyz]'; + const result = helper.retrieveDataFromPath(data, path); + expect(result).toEqual(mockEuropeCountriesData); + }); + + it('should return the correct data for path with special characters except separator (.) without brackets', () => { + const data = mockJsonNestedResponseEuropeCountriesDataWithMultipleSpecialCharacters; + const path = 'response.xyz:abc,xyz-abc,xyz_abc,abc+xyz'; + const result = helper.retrieveDataFromPath(data, path); + expect(result).toEqual(mockEuropeCountriesData); + }); + + it('should return the correct data for path without separator in brackets', () => { + const data = mockJsonNestedResponseEuropeCountriesData; + const path = '[response].[my-data]'; + const result = helper.retrieveDataFromPath(data, path); + expect(result).toEqual(mockEuropeCountriesData); + }); + + it('should return an empty array if the path does not exist in the data', () => { + const data = {}; + const path = 'nonexistent.path'; + const result = helper.retrieveDataFromPath(data, path); + expect(result).toEqual([]); + }); + + it('should return the correct data if the path is nested', () => { + const data = { level1: { level2: { level3: { level4: ['parrot', 'fish'] } } } }; + const path = 'level1.level2.level3.level4'; + const result = helper.retrieveDataFromPath(data, path); + expect(result).toEqual(['parrot', 'fish']); + }); + + it('should return the correct data if the path is NOT nested', () => { + const data = { pets: ['cat', 'dog'] }; + const path = 'pets'; + const result = helper.retrieveDataFromPath(data, path); + expect(result).toEqual(['cat', 'dog']); + }); + + it('should return the correct data if the path is NOT nested with separator (.) in property name', () => { + const data = { 'my.pets': ['cat', 'dog'] }; + const path = '[my.pets]'; + const result = helper.retrieveDataFromPath(data, path); + expect(result).toEqual(['cat', 'dog']); + }); +}); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/helpers/data-table-path-parser.helper.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/helpers/data-table-path-parser.helper.ts new file mode 100644 index 00000000000..4a945c41137 --- /dev/null +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/helpers/data-table-path-parser.helper.ts @@ -0,0 +1,50 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class DataTablePathParserHelper { + private readonly splitPathRegEx = /\.(?![^[]*\])/g; + private readonly removeSquareBracketsRegEx = /^\[(.*)\]$/; + + retrieveDataFromPath(data: any, path: string): any[] { + const properties = this.splitPathIntoProperties(path); + const currentProperty = this.removeSquareBracketsFromProperty(properties.shift()); + + if (!this.isPropertyExistsInData(data, currentProperty)) { + return []; + } + + const nestedData = data[currentProperty]; + + if (Array.isArray(nestedData)) { + return nestedData; + } + + return this.retrieveDataFromPath(nestedData, properties.join('.')); + } + + private splitPathIntoProperties(path: string): string[] { + return path.split(this.splitPathRegEx); + } + + private removeSquareBracketsFromProperty(property: string): string { + return property.replace(this.removeSquareBracketsRegEx, '$1'); + } + + private isPropertyExistsInData(data: any, property: string): boolean { + return Object.prototype.hasOwnProperty.call(data, property); + } +} diff --git a/lib/process-services-cloud/src/lib/form/mocks/data-table-widget.mock.ts b/lib/process-services-cloud/src/lib/form/mocks/data-table-widget.mock.ts index ba06c4696fd..bc0f80a4136 100644 --- a/lib/process-services-cloud/src/lib/form/mocks/data-table-widget.mock.ts +++ b/lib/process-services-cloud/src/lib/form/mocks/data-table-widget.mock.ts @@ -85,6 +85,28 @@ export const mockJsonNestedResponseEuropeCountriesData = { } }; +export const mockJsonNestedResponseEuropeCountriesDataWithSeparatorInPropertyName = { + response: { + empty: [], + 'my.data': { + 'country[data].country': mockEuropeCountriesData + }, + data: [ + { + id: 'HR', + name: 'Croatia' + } + ], + 'no-array': {} + } +}; + +export const mockJsonNestedResponseEuropeCountriesDataWithMultipleSpecialCharacters = { + response: { + 'xyz:abc,xyz-abc,xyz_abc,abc+xyz': mockEuropeCountriesData + } +}; + export const mockAmericaCountriesData = [ { id: 'CA',