diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json index 50de5fb68f..7b78720b0d 100644 --- a/lib/core/src/lib/i18n/en.json +++ b/lib/core/src/lib/i18n/en.json @@ -51,6 +51,7 @@ "REST_API_FAILED": "The server `{{ hostname }}` is not reachable", "VARIABLE_DROPDOWN_OPTIONS_FAILED": "There was a problem loading dropdown elements. Please contact administrator.", "DATA_TABLE_LOAD_FAILED": "There was a problem loading table elements. Please contact administrator.", + "DATA_TABLE_EMPTY_CONTENT": "No data found", "EXTERNAL_PROPERTY_LOAD_FAILED": "There was a problem loading external property. Please contact administrator.", "FILE_NAME": "File Name", "TITLE": "Title", 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 f01b0897b4..ab36a34ddf 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 @@ -18,7 +18,7 @@ import { WidgetDataTableAdapter } from './data-table-adapter.widget'; import { mockEuropeCountriesData, - mockCountriesIncorrectData, + mockIncompleteCountriesData, mockInvalidSchemaDefinition, mockSchemaDefinition } from './mocks/data-table-widget.mock'; @@ -42,13 +42,13 @@ describe('WidgetDataTableAdapter', () => { ]); }); - it('should return an empty array if not all columns are linked to data', () => { - widgetDataTableAdapter = new WidgetDataTableAdapter(mockCountriesIncorrectData, mockSchemaDefinition); + it('should return rows if columns are partially linked to data', () => { + widgetDataTableAdapter = new WidgetDataTableAdapter(mockIncompleteCountriesData, mockSchemaDefinition); const rows = widgetDataTableAdapter.getRows(); const isDataSourceValid = widgetDataTableAdapter.isDataSourceValid(); - expect(rows).toEqual([]); - expect(isDataSourceValid).toBeFalse(); + expect(rows).toEqual([new ObjectDataRow({ id: 'IT' }), new ObjectDataRow({ id: 'PL' })]); + expect(isDataSourceValid).toBeTrue(); }); it('should return an empty array if columns have invalid structure', () => { 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 dc3e4c55db..cb0a2ca8c6 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 @@ -128,16 +128,10 @@ export class WidgetDataTableAdapter implements DataTableAdapter { } isDataSourceValid(): boolean { - return this.hasAllColumnsLinkedToData() && this.allMandatoryColumnPropertiesHaveValues(); + return this.allColumnsHaveKeys(); } - private allMandatoryColumnPropertiesHaveValues(): boolean { + private allColumnsHaveKeys(): boolean { return this.adapter.getColumns().every((column) => !!column.key); } - - private hasAllColumnsLinkedToData(): boolean { - const availableColumnKeys: string[] = this.adapter.getColumns().map((column) => column.key); - - return availableColumnKeys.every((columnKey) => this.adapter.getRows().some((row) => Object.keys(row.obj).includes(columnKey))); - } } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.html index d8e6571046..cbab69fd3b 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/data-table.widget.html @@ -9,11 +9,19 @@ - + + + + + + + + [required]="'FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate" /> 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 d55982e0c4..ec0d0b65de 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 @@ -26,7 +26,7 @@ import { mockAmericaCountriesData, mockInvalidSchemaDefinition, mockJsonFormVariable, - mockJsonFormVariableWithIncorrectData, + mockJsonFormVariableWithIncompleteData, mockJsonProcessVariables, mockSchemaDefinition, mockJsonResponseEuropeCountriesData, @@ -39,7 +39,8 @@ import { mockEuropeCountriesRows, mockAmericaCountriesRows, mockNestedCountryColumns, - mockNestedEuropeCountriesRows + mockNestedEuropeCountriesRows, + mockJsonFormVariableWithEmptyData } from './mocks/data-table-widget.mock'; describe('DataTableWidgetComponent', () => { @@ -238,7 +239,7 @@ describe('DataTableWidgetComponent', () => { describe('should NOT display error message if', () => { it('form is in preview state', () => { - widget.field = getDataVariable(mockVariableConfig, mockSchemaDefinition, [], mockJsonFormVariableWithIncorrectData); + widget.field = getDataVariable(mockVariableConfig, mockSchemaDefinition, [], mockJsonFormVariableWithIncompleteData); spyOn(formCloudService, 'getPreviewState').and.returnValue(true); fixture.detectChanges(); @@ -249,6 +250,16 @@ describe('DataTableWidgetComponent', () => { expect(previewDataTable).toBeTruthy(); }); + it('there are no rows', () => { + widget.field = getDataVariable(mockVariableConfig, mockSchemaDefinition, [], mockJsonFormVariableWithEmptyData); + fixture.detectChanges(); + + const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message')); + + assertData(mockCountryColumns, []); + expect(failedErrorMsgElement).toBeNull(); + }); + it('path points to single object with appropriate schema definition', () => { widget.field = getDataVariable({ ...mockVariableConfig, optionsPath: 'response.single-object' }, mockSchemaDefinition, [], []); widget.field.value = mockJsonNestedResponseEuropeCountriesData; @@ -262,16 +273,8 @@ describe('DataTableWidgetComponent', () => { }); describe('should display error message if', () => { - it('data source is NOT linked to every column', () => { - widget.field = getDataVariable(mockVariableConfig, mockSchemaDefinition, [], mockJsonFormVariableWithIncorrectData); - fixture.detectChanges(); - - checkDataTableErrorMessage(); - expect(widget.dataSource.getRows()).toEqual([]); - }); - it('data source has invalid column structure', () => { - widget.field = getDataVariable(mockVariableConfig, mockInvalidSchemaDefinition, [], mockJsonFormVariableWithIncorrectData); + widget.field = getDataVariable(mockVariableConfig, mockInvalidSchemaDefinition, [], mockJsonFormVariableWithIncompleteData); fixture.detectChanges(); checkDataTableErrorMessage(); @@ -283,12 +286,11 @@ describe('DataTableWidgetComponent', () => { { variableName: 'not-found-data-source' }, mockSchemaDefinition, [], - mockJsonFormVariableWithIncorrectData + mockJsonFormVariableWithIncompleteData ); fixture.detectChanges(); checkDataTableErrorMessage(); - expect(widget.dataSource).toBeUndefined(); }); it('path is incorrect', () => { @@ -301,7 +303,6 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); checkDataTableErrorMessage(); - expect(widget.dataSource).toBeUndefined(); }); it('provided data by path is NOT an array or object', () => { @@ -314,7 +315,6 @@ describe('DataTableWidgetComponent', () => { fixture.detectChanges(); checkDataTableErrorMessage(); - expect(widget.dataSource).toBeUndefined(); }); }); }); 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 f1e9e8f329..5310430107 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,8 +18,17 @@ /* eslint-disable @angular-eslint/component-selector */ import { Component, OnInit, ViewEncapsulation } from '@angular/core'; -import { WidgetComponent, FormService, FormBaseModule, DataRow, DataColumn, DataTableComponent } from '@alfresco/adf-core'; -import { CommonModule } from '@angular/common'; +import { + WidgetComponent, + FormService, + FormBaseModule, + DataRow, + DataColumn, + DataTableComponent, + NoContentTemplateDirective, + EmptyContentComponent +} from '@alfresco/adf-core'; +import { NgIf } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { FormCloudService } from '../../../services/form-cloud.service'; import { TaskVariableCloud } from '../../../models/task-variable-cloud.model'; @@ -28,7 +37,7 @@ import { DataTablePathParserHelper } from './helpers/data-table-path-parser.help @Component({ standalone: true, - imports: [CommonModule, TranslateModule, FormBaseModule, DataTableComponent], + imports: [NgIf, TranslateModule, FormBaseModule, DataTableComponent, NoContentTemplateDirective, EmptyContentComponent], selector: 'data-table', templateUrl: './data-table.widget.html', styleUrls: ['./data-table.widget.scss'], @@ -71,6 +80,8 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit } private init(): void { + this.dataTableLoadFailed = false; + this.setPreviewState(); this.getTableData(); this.initDataTable(); @@ -84,40 +95,50 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit } private initDataTable(): void { - this.dataTableLoadFailed = false; + this.dataSource = new WidgetDataTableAdapter(this.rowsData, this.columnsSchema); - if (this.rowsData?.length) { - this.dataSource = new WidgetDataTableAdapter(this.rowsData, this.columnsSchema); - - if (this.dataSource.isDataSourceValid()) { - this.field.updateForm(); - } else { - this.handleError('Data source has corrupted model or structure'); - } - } else { - this.handleError('Data source not found or it is not an array/object'); + if (!this.dataSource.isDataSourceValid()) { + this.handleError(); + return; } + + this.field.updateForm(); } private getRowsData(): void { const optionsPath = this.field?.variableConfig?.optionsPath ?? this.defaultResponseProperty; const fieldValue = this.field?.value; + const rowsData = fieldValue || this.getDataFromVariable(); - if (rowsData) { - const dataFromPath = this.pathParserHelper.retrieveDataFromPath(rowsData, optionsPath); - this.rowsData = (dataFromPath?.length ? dataFromPath : rowsData) as DataRow[]; + if (!rowsData) { + this.handleError(); + return; } + + this.rowsData = this.extractRowsData(rowsData, optionsPath); + + if (!this.rowsData) { + this.handleError(); + } + } + + private extractRowsData(rowsData: any, optionsPath: string): DataRow[] | undefined { + if (Array.isArray(rowsData)) { + return rowsData; + } + + return this.pathParserHelper.retrieveDataFromPath(rowsData, optionsPath); } private getDataFromVariable(): any { const processVariables = this.field?.form?.processVariables; const formVariables = this.field?.form?.variables; - const processVariableDropdownOptions = this.getVariableValueByName(processVariables, this.variableName); - const formVariableDropdownOptions = this.getVariableValueByName(formVariables, this.variableName); + const processVariableData = this.getVariableValueByName(processVariables, this.variableName); + const formVariableData = this.getVariableValueByName(formVariables, this.variableName); - return processVariableDropdownOptions ?? formVariableDropdownOptions; + return processVariableData ?? formVariableData; } private getVariableValueByName(variables: TaskVariableCloud[], variableName: string): any { @@ -129,10 +150,9 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit this.previewState = this.formCloudService.getPreviewState(); } - private handleError(error: any) { + private handleError(): void { if (!this.previewState) { this.dataTableLoadFailed = true; - this.widgetError.emit(error); } } } 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 ac2e55603f..d36d84bc56 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 @@ -23,7 +23,7 @@ interface DataTablePathParserTestCase { path?: string; data?: any; propertyName?: string; - expected?: unknown[]; + expected: unknown[]; } describe('DataTablePathParserHelper', () => { @@ -39,18 +39,18 @@ describe('DataTablePathParserHelper', () => { description: 'not existent', data: {}, path: 'nonexistent.path', - expected: [] + expected: undefined }, { description: 'not defined', data: {}, path: undefined, - expected: [] + expected: undefined }, { description: 'empty string', path: '', - expected: [] + expected: undefined }, { description: 'nested', @@ -76,77 +76,89 @@ describe('DataTablePathParserHelper', () => { { description: 'with nested brackets followed by an additional part of property name', propertyName: 'file.file[data]file', - path: 'response.[file.file[data]file]' + path: 'response.[file.file[data]file]', + expected: mockResultData }, { description: 'with nested brackets', propertyName: 'file.file[data]', - path: 'response.[file.file[data]]' + path: 'response.[file.file[data]]', + expected: mockResultData }, { description: 'with separator before nested brackets in property name', propertyName: 'file.[data]file', - path: 'response.[file.[data]file]' + path: 'response.[file.[data]file]', + expected: mockResultData }, { description: 'with separator before and no separator after nested brackets in property name', propertyName: 'file.[data]', - path: 'response.[file.[data]]' + path: 'response.[file.[data]]', + expected: mockResultData }, { description: 'with separator after nested brackets', propertyName: 'file[data].file', - path: 'response.[file[data].file]' + path: 'response.[file[data].file]', + expected: mockResultData }, { description: 'with multiple brackets in property name', propertyName: 'file.file[data]file[data]', - path: 'response.[file.file[data]file[data]]' + path: 'response.[file.file[data]file[data]]', + expected: mockResultData }, { description: 'with missing closing bracket in outermost square brackets', propertyName: 'file.file[data', - path: 'response.[file.file[data]' + path: 'response.[file.file[data]', + expected: mockResultData }, { description: 'with missing openning bracket in outermost square brackets', propertyName: 'file.filedata]', - path: 'response.[file.filedata]]' + path: 'response.[file.filedata]]', + expected: mockResultData }, { description: 'with special characters except separator (.) in brackets', propertyName: 'xyz:abc,xyz-abc,xyz_abc,abc+xyz', - path: 'response.[xyz:abc,xyz-abc,xyz_abc,abc+xyz]' + path: 'response.[xyz:abc,xyz-abc,xyz_abc,abc+xyz]', + expected: mockResultData }, { description: 'with special characters except separator (.) without brackets', propertyName: 'xyz:abc,xyz-abc,xyz_abc,abc+xyz', - path: 'response.xyz:abc,xyz-abc,xyz_abc,abc+xyz' + path: 'response.xyz:abc,xyz-abc,xyz_abc,abc+xyz', + expected: mockResultData }, { description: 'without separator in brackets', propertyName: 'my-data', - path: '[response].[my-data]' + path: '[response].[my-data]', + expected: mockResultData }, { description: 'with property followed by single index reference', propertyName: 'users', path: 'response.users[0].data', - data: mockResponseResultDataWithArrayInsideArray('users') + data: mockResponseResultDataWithArrayInsideArray('users'), + expected: mockResultData }, { description: 'with property followed by multiple index references', propertyName: 'users:Array', path: 'response.[users:Array][0][1][12].data', data: mockResponseResultDataWithArrayInsideArray('users:Array'), - expected: [] + expected: undefined }, { description: 'when path points to array in the middle (incorrect path)', propertyName: 'users', path: 'response.users.incorrectPath', data: mockResponseResultDataWithArrayInsideArray('users'), - expected: [] + expected: undefined }, { description: 'when path points to the particular element of the array', @@ -158,7 +170,7 @@ describe('DataTablePathParserHelper', () => { description: 'when path points to the particular element of the array which does NOT exist', propertyName: 'users', path: 'response.users[100]', - expected: [] + expected: undefined } ]; @@ -166,7 +178,7 @@ describe('DataTablePathParserHelper', () => { it(testCase.description, () => { const data = testCase.data ?? mockResponseResultData(testCase.propertyName); const result = helper.retrieveDataFromPath(data, testCase.path); - const expectedResult = testCase.expected ?? mockResultData; + const expectedResult = testCase.expected as any; expect(result).toEqual(expectedResult); }); }); 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 b3151b58b2..b579e7481e 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 @@ -15,13 +15,15 @@ * limitations under the License. */ +import { DataRow } from '@alfresco/adf-core'; + export class DataTablePathParserHelper { private readonly removeSquareBracketsRegEx = /^\[(.*)\]$/; private readonly indexReferencesRegEx = /(\[\d+\])+$/; - retrieveDataFromPath(data: any, path: string): any[] { + retrieveDataFromPath(data: any, path: string): DataRow[] | undefined { if (!path) { - return []; + return undefined; } const properties = this.splitPathIntoProperties(path); @@ -31,7 +33,7 @@ export class DataTablePathParserHelper { const isPropertyWithMultipleIndexReferences = propertyIndexReferences.length > 1; if (isPropertyWithMultipleIndexReferences || !this.isPropertyExistsInData(data, purePropertyName)) { - return []; + return undefined; } const isPropertyWithSingleIndexReference = propertyIndexReferences.length === 1; diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-widget.mock.ts b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-widget.mock.ts index 0cca8a91d0..73f49cf4ba 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-widget.mock.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/data-table/mocks/data-table-widget.mock.ts @@ -173,7 +173,7 @@ export const mockAmericaCountriesRows: DataRow[] = [ new ObjectDataRow({ id: 'US', name: 'United States' }) ]; -export const mockCountriesIncorrectData = [ +export const mockIncompleteCountriesData = [ { id: 'PL' }, @@ -182,8 +182,10 @@ export const mockCountriesIncorrectData = [ } ]; -export const mockJsonFormVariableWithIncorrectData = [ - new TaskVariableCloud({ name: 'json-form-variable', value: mockCountriesIncorrectData, type: 'json', id: 'fake-id-1' }) +export const mockJsonFormVariableWithEmptyData = [new TaskVariableCloud({ name: 'json-form-variable', value: [], type: 'json', id: 'fake-id-1' })]; + +export const mockJsonFormVariableWithIncompleteData = [ + new TaskVariableCloud({ name: 'json-form-variable', value: mockIncompleteCountriesData, type: 'json', id: 'fake-id-1' }) ]; export const mockJsonFormVariable = [