Skip to content

Commit

Permalink
AAE-21946 Support JSON paths with non-standard characters for data ta… (
Browse files Browse the repository at this point in the history
#9571)

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

* add unit test

* move path parse to extern helper class
  • Loading branch information
tomgny authored Apr 18, 2024
1 parent 4132517 commit caa2166
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -145,19 +146,15 @@ 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', () => {
widget.field = getDataVariable(mockVariableConfig, mockSchemaDefinition, [], mockJsonResponseFormVariable);
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', () => {
Expand All @@ -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', () => {
Expand All @@ -181,29 +176,23 @@ 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', () => {
widget.field = getDataVariable(mockVariableConfig, mockSchemaDefinition, [], mockJsonFormVariable);
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', () => {
widget.field = getDataVariable({ variableName: 'json-variable' }, mockSchemaDefinition, mockJsonProcessVariables);
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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -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[];
}
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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']);
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit caa2166

Please sign in to comment.