From abaf7df9c64248b108cc4dac14be154f96f0ec7a Mon Sep 17 00:00:00 2001
From: Tomasz Gnyp <49343696+tomgny@users.noreply.github.com>
Date: Wed, 26 Mar 2025 16:44:49 +0100
Subject: [PATCH] AAE-33007 Display empty data table without error (#10744)

* AAE-33007 Display empty data table without error

* update translation key
---
 lib/core/src/lib/i18n/en.json                 |  1 +
 .../data-table-adapter.widget.spec.ts         | 10 +--
 .../data-table/data-table-adapter.widget.ts   | 10 +--
 .../widgets/data-table/data-table.widget.html | 12 +++-
 .../data-table/data-table.widget.spec.ts      | 32 +++++-----
 .../widgets/data-table/data-table.widget.ts   | 64 ++++++++++++-------
 .../data-table-path-parser.helper.spec.ts     | 52 +++++++++------
 .../helpers/data-table-path-parser.helper.ts  |  8 ++-
 .../mocks/data-table-widget.mock.ts           |  8 ++-
 9 files changed, 118 insertions(+), 79 deletions(-)

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 @@
     </div>
 
     <ng-container *ngIf="!previewState; else previewTemplate">
-        <adf-datatable data-automation-id="adf-data-table-widget" [data]="dataSource" />
+        <adf-datatable data-automation-id="adf-data-table-widget" [data]="dataSource">
+            <adf-no-content-template>
+                <ng-template>
+                    <adf-empty-content
+                        icon="border_all"
+                        [title]="'FORM.FIELD.DATA_TABLE_EMPTY_CONTENT' | translate" />
+                </ng-template>
+            </adf-no-content-template>
+        </adf-datatable>
 
         <error-widget *ngIf="dataTableLoadFailed"
             class="adf-data-table-widget-failed-message"
-            required="{{ 'FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate }}" />
+            [required]="'FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate" />
     </ng-container>
 
     <ng-template #previewTemplate>
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 = [