AAE-21946 Support JSON paths with non-standard characters for data ta… (#9571)

* AAE-21946 Support JSON paths with non-standard characters for data table widget

* add unit test

* move path parse to extern helper class
This commit is contained in:
Tomasz Gnyp
2024-04-18 17:28:37 +02:00
committed by GitHub
parent 4132517ba7
commit caa2166151
5 changed files with 182 additions and 65 deletions

View File

@@ -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', () => {

View File

@@ -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 {

View File

@@ -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']);
});
});

View File

@@ -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);
}
}

View File

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