From 2d7aa0a61e719985ecd02f181fdfd48b8affb1ac Mon Sep 17 00:00:00 2001 From: Wiktor Danielewski <63188869+wiktord2000@users.noreply.github.com> Date: Mon, 13 May 2024 10:11:06 +0200 Subject: [PATCH] AAE-22345 Support access to array elements by index (#9663) * AAE-22345 Update date table parser * AAE-22345 Update data table adapter * AAE-22345 Update * AAE-22345 Align with remarks * AAE-22345 Small update * AAE-22345 Update * AAE-22345 Unit tests fix --- .../data-table-adapter.widget.spec.ts | 32 +++++- .../data-table/data-table-adapter.widget.ts | 13 ++- .../data-table-path-parser.helper.spec.ts | 106 +++++++++++++++++- .../helpers/data-table-path-parser.helper.ts | 48 +++++++- .../mocks/data-table-adapter.mock.ts | 30 +++++ .../data-table-path-parser.helper.mock.ts | 10 ++ 6 files changed, 228 insertions(+), 11 deletions(-) diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table-adapter.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table-adapter.widget.spec.ts index b00692a492c..674dd59171e 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table-adapter.widget.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table-adapter.widget.spec.ts @@ -151,6 +151,21 @@ describe('WidgetDataTableAdapter', () => { type: 'json', key: 'person.phoneNumbers', title: 'Phone numbers' + }, + { + type: 'text', + key: 'person.phoneNumbers[0].phoneNumber', + title: 'Phone Home' + }, + { + type: 'text', + key: 'person.phoneNumbers[1].phoneNumber', + title: 'Phone Work' + }, + { + type: 'text', + key: 'person.cars[0].previousOwners[0].name', + title: 'Last Car Owner' } ]; @@ -165,7 +180,10 @@ describe('WidgetDataTableAdapter', () => { 'person.phoneNumbers': [ { type: 'home', phoneNumber: '123-456-7890' }, { type: 'work', phoneNumber: '098-765-4321' } - ] + ], + 'person.phoneNumbers[0].phoneNumber': '123-456-7890', + 'person.phoneNumbers[1].phoneNumber': '098-765-4321', + 'person.cars[0].previousOwners[0].name': 'Jane Smith' }); const expectedSecondRow = new ObjectDataRow({ 'person.personData.[address.[data]test].city': 'Westlake', @@ -174,20 +192,26 @@ describe('WidgetDataTableAdapter', () => { 'person.phoneNumbers': [ { type: 'home', phoneNumber: '123-456-7891' }, { type: 'work', phoneNumber: '321-654-1987' } - ] + ], + 'person.phoneNumbers[0].phoneNumber': '123-456-7891', + 'person.phoneNumbers[1].phoneNumber': '321-654-1987', + 'person.cars[0].previousOwners[0].name': 'Bob Johnson' }); const expectedColumns = [ new ObjectDataColumn({ key: 'person.name', type: 'text', title: 'Name' }), new ObjectDataColumn({ key: 'person.personData.[address.[data]test].city', type: 'text', title: 'City' }), new ObjectDataColumn({ key: 'person.personData.[address.[data]test].street', type: 'text', title: 'Street' }), - new ObjectDataColumn({ key: 'person.phoneNumbers', type: 'json', title: 'Phone numbers' }) + new ObjectDataColumn({ key: 'person.phoneNumbers', type: 'json', title: 'Phone numbers' }), + new ObjectDataColumn({ key: 'person.phoneNumbers[0].phoneNumber', type: 'text', title: 'Phone Home' }), + new ObjectDataColumn({ key: 'person.phoneNumbers[1].phoneNumber', type: 'text', title: 'Phone Work' }), + new ObjectDataColumn({ key: 'person.cars[0].previousOwners[0].name', type: 'text', title: 'Last Car Owner' }) ]; expect(rows.length).toBe(2); expect(rows[0]).toEqual(expectedFirstRow); expect(rows[1]).toEqual(expectedSecondRow); - expect(columns.length).toBe(4); + expect(columns.length).toBe(7); expect(columns).toEqual(expectedColumns); }); }); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table-adapter.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table-adapter.widget.ts index 3edc9f220f3..7fd94c926ef 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table-adapter.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table-adapter.widget.ts @@ -73,7 +73,18 @@ export class WidgetDataTableAdapter implements DataTableAdapter { } private extractPropertyValue(properties: string[], item: any): string { - return properties.reduce((acc, property) => (acc ? acc[this.helper.removeSquareBracketsFromProperty(property)] : undefined), item); + return properties.reduce((acc, property) => { + if (!acc) { + return undefined; + } + + const propertyIndexReferences = this.helper.getIndexReferencesFromProperty(property); + const isPropertyWithSingleIndexReference = propertyIndexReferences.length === 1; + + const purePropertyName = this.helper.extractPurePropertyName(property); + + return isPropertyWithSingleIndexReference ? acc[purePropertyName]?.[propertyIndexReferences[0]] : acc[purePropertyName]; + }, item); } getColumns(): Array { 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 index 9688a30605e..7cda0f4ed22 100644 --- 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 @@ -16,11 +16,11 @@ */ import { DataTablePathParserHelper } from './data-table-path-parser.helper'; -import { mockResponseResultData, mockResultData } from '../mocks/data-table-path-parser.helper.mock'; +import { mockResponseResultData, mockResponseResultDataWithNestedArray, mockResultData } from '../mocks/data-table-path-parser.helper.mock'; interface DataTablePathParserTestCase { description: string; - path: string; + path?: string; data?: any; propertyName?: string; expected?: unknown[]; @@ -47,6 +47,11 @@ describe('DataTablePathParserHelper', () => { path: undefined, expected: [] }, + { + description: 'empty string', + path: '', + expected: [] + }, { description: 'nested', data: { level1: { level2: { level3: { level4: ['parrot', 'fish'] } } } }, @@ -98,6 +103,16 @@ describe('DataTablePathParserHelper', () => { propertyName: 'file.file[data]file[data]', path: 'response.[file.file[data]file[data]]' }, + { + description: 'with missing closing bracket in outermost square brackets', + propertyName: 'file.file[data', + path: 'response.[file.file[data]' + }, + { + description: 'with missing openning bracket in outermost square brackets', + propertyName: 'file.filedata]', + path: 'response.[file.filedata]]' + }, { description: 'with special characters except separator (.) in brackets', propertyName: 'xyz:abc,xyz-abc,xyz_abc,abc+xyz', @@ -112,6 +127,26 @@ describe('DataTablePathParserHelper', () => { description: 'without separator in brackets', propertyName: 'my-data', path: '[response].[my-data]' + }, + { + description: 'with property followed by single index reference', + propertyName: 'users', + path: 'response.users[0].data', + data: mockResponseResultDataWithNestedArray('users') + }, + { + description: 'with property followed by multiple index references', + propertyName: 'users:Array', + path: 'response.[users:Array][0][1][12].data', + data: mockResponseResultDataWithNestedArray('users:Array'), + expected: [] + }, + { + description: 'when path does NOT point to array', + propertyName: 'users', + path: 'response.users[0]', + data: mockResponseResultDataWithNestedArray('users'), + expected: [] } ]; @@ -124,4 +159,71 @@ describe('DataTablePathParserHelper', () => { }); }); }); + + it('should split path to properties', () => { + const testCases: { path: string; expected: string[] }[] = [ + { path: 'response.0', expected: ['response', '0'] }, + { path: 'response', expected: ['response'] }, + { path: 'response.person.file', expected: ['response', 'person', 'file'] }, + { path: 'response.persons[0]', expected: ['response', 'persons[0]'] }, + { path: 'response.[persons:Array][0]', expected: ['response', '[persons:Array][0]'] }, + { path: 'response.persons[0][1]', expected: ['response', 'persons[0][1]'] }, + { path: 'response.persons[0].file.data[4]', expected: ['response', 'persons[0]', 'file', 'data[4]'] }, + { path: '', expected: [] }, + { path: null, expected: [] }, + { path: undefined, expected: [] } + ]; + + testCases.forEach((testCase) => { + const result = helper.splitPathIntoProperties(testCase.path); + expect(result).toEqual(testCase.expected); + }); + }); + + it('should extract pure property name', () => { + const testCases: { property: string; expected: string }[] = [ + { property: '[persons]', expected: 'persons' }, + { property: '[persons:data]', expected: 'persons:data' }, + { property: '[persons.data]', expected: 'persons.data' }, + { property: '[persons.data[1]]', expected: 'persons.data[1]' }, + { property: '[persons.data1]]', expected: 'persons.data1]' }, + { property: 'persons.data1]', expected: 'persons.data1]' }, + { property: 'persons.[data1]', expected: 'persons.[data1]' }, + { property: 'persons', expected: 'persons' }, + { property: 'persons[0]', expected: 'persons' }, + { property: '[persons:Array][0]', expected: 'persons:Array' }, + { property: 'persons[0][1]', expected: 'persons' }, + { property: '[persons[0].file.data][4]', expected: 'persons[0].file.data' }, + { property: '[persons[0].file.data][1][4]', expected: 'persons[0].file.data' }, + { property: '[persons.data1]][2][4][23]', expected: 'persons.data1]' }, + { property: '', expected: '' }, + { property: undefined, expected: '' }, + { property: null, expected: '' } + ]; + + testCases.forEach((testCase) => { + const result = helper.extractPurePropertyName(testCase.property); + expect(result).toEqual(testCase.expected); + }); + }); + + it('should return index references from property', () => { + const testCases: { property: string; expected: number[] }[] = [ + { property: 'persons[0]', expected: [0] }, + { property: '[persons:Array][0]', expected: [0] }, + { property: 'persons[0][1][7]', expected: [0, 1, 7] }, + { property: '[persons[0].file.data][4]', expected: [4] }, + { property: '[persons[0].file.data][1][4]', expected: [1, 4] }, + { property: '[persons[0].file.data]', expected: [] }, + { property: 'persons', expected: [] }, + { property: undefined, expected: [] }, + { property: null, expected: [] }, + { property: '', expected: [] } + ]; + + testCases.forEach((testCase) => { + const result = helper.getIndexReferencesFromProperty(testCase.property); + expect(result).toEqual(testCase.expected); + }); + }); }); 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 index 3fc6761fed4..05d5612a7ee 100644 --- 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 @@ -17,16 +17,25 @@ export class DataTablePathParserHelper { private readonly removeSquareBracketsRegEx = /^\[(.*)\]$/; + private readonly indexReferencesRegEx = /(\[\d+\])+$/; retrieveDataFromPath(data: any, path: string): any[] { + if (!path) { + return []; + } + const properties = this.splitPathIntoProperties(path); - const currentProperty = this.removeSquareBracketsFromProperty(properties.shift()); + const currentProperty = properties.shift(); + const propertyIndexReferences = this.getIndexReferencesFromProperty(currentProperty); + const purePropertyName = this.extractPurePropertyName(currentProperty); + const isPropertyWithMultipleIndexReferences = propertyIndexReferences.length > 1; - if (!this.isPropertyExistsInData(data, currentProperty)) { + if (isPropertyWithMultipleIndexReferences || !this.isPropertyExistsInData(data, purePropertyName)) { return []; } - const nestedData = data[currentProperty]; + const isPropertyWithSingleIndexReference = propertyIndexReferences.length === 1; + const nestedData = isPropertyWithSingleIndexReference ? data[purePropertyName]?.[propertyIndexReferences[0]] : data[purePropertyName]; if (Array.isArray(nestedData)) { return nestedData; @@ -80,7 +89,38 @@ export class DataTablePathParserHelper { return properties; } - removeSquareBracketsFromProperty(property: string): string { + getIndexReferencesFromProperty(property: string): number[] { + const match = this.indexReferencesRegEx.exec(property); + if (!match) { + return []; + } + + const indexReferencesString = match[0]; + const numbersFromBrackets = indexReferencesString.slice(1, -1).split('][').map(Number); + + return numbersFromBrackets; + } + + extractPurePropertyName(property: string): string { + const propertyIndexReferences = this.getIndexReferencesFromProperty(property); + const numberOfIndexReferences = propertyIndexReferences.length; + + if (property == null) { + return ''; + } else if (numberOfIndexReferences !== 0) { + return this.removeSquareBracketsAndIndexReferencesFromProperty(property); + } else { + return this.removeSquareBracketsFromProperty(property); + } + } + + private removeSquareBracketsAndIndexReferencesFromProperty(property: string): string { + const propertyWithoutIndexReferences = property?.replace(this.indexReferencesRegEx, ''); + + return this.removeSquareBracketsFromProperty(propertyWithoutIndexReferences); + } + + private removeSquareBracketsFromProperty(property: string): string { return property?.replace(this.removeSquareBracketsRegEx, '$1'); } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-adapter.mock.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-adapter.mock.ts index f6e494ebf05..860aa29bc97 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-adapter.mock.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-adapter.mock.ts @@ -34,6 +34,21 @@ export const mockPersonsData = [ type: 'work', phoneNumber: '098-765-4321' } + ], + cars: [ + { + make: 'Toyota', + model: 'Corolla', + year: 2019, + previousOwners: [ + { + name: 'Jane Smith' + }, + { + name: 'Jim Down' + } + ] + } ] } }, @@ -55,6 +70,21 @@ export const mockPersonsData = [ type: 'work', phoneNumber: '321-654-1987' } + ], + cars: [ + { + make: 'Honda', + model: 'Civic', + year: 2018, + previousOwners: [ + { + name: 'Bob Johnson' + }, + { + name: 'Tom Brown' + } + ] + } ] } } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-path-parser.helper.mock.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-path-parser.helper.mock.ts index 667d1f3028b..1c955e987a3 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-path-parser.helper.mock.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-path-parser.helper.mock.ts @@ -43,3 +43,13 @@ export const mockResponseResultData = (propertyName?: string) => ({ [propertyName]: mockResultData } }); + +export const mockResponseResultDataWithNestedArray = (propertyName?: string) => ({ + response: { + [propertyName]: [ + { + data: mockResultData + } + ] + } +});