AAE-33007 Display empty data table without error (#10744)

* AAE-33007 Display empty data table without error

* update translation key
This commit is contained in:
Tomasz Gnyp 2025-03-26 16:44:49 +01:00 committed by GitHub
parent 9bbbc8193d
commit abaf7df9c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 118 additions and 79 deletions

View File

@ -51,6 +51,7 @@
"REST_API_FAILED": "The server `{{ hostname }}` is not reachable", "REST_API_FAILED": "The server `{{ hostname }}` is not reachable",
"VARIABLE_DROPDOWN_OPTIONS_FAILED": "There was a problem loading dropdown elements. Please contact administrator.", "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_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.", "EXTERNAL_PROPERTY_LOAD_FAILED": "There was a problem loading external property. Please contact administrator.",
"FILE_NAME": "File Name", "FILE_NAME": "File Name",
"TITLE": "Title", "TITLE": "Title",

View File

@ -18,7 +18,7 @@
import { WidgetDataTableAdapter } from './data-table-adapter.widget'; import { WidgetDataTableAdapter } from './data-table-adapter.widget';
import { import {
mockEuropeCountriesData, mockEuropeCountriesData,
mockCountriesIncorrectData, mockIncompleteCountriesData,
mockInvalidSchemaDefinition, mockInvalidSchemaDefinition,
mockSchemaDefinition mockSchemaDefinition
} from './mocks/data-table-widget.mock'; } 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', () => { it('should return rows if columns are partially linked to data', () => {
widgetDataTableAdapter = new WidgetDataTableAdapter(mockCountriesIncorrectData, mockSchemaDefinition); widgetDataTableAdapter = new WidgetDataTableAdapter(mockIncompleteCountriesData, mockSchemaDefinition);
const rows = widgetDataTableAdapter.getRows(); const rows = widgetDataTableAdapter.getRows();
const isDataSourceValid = widgetDataTableAdapter.isDataSourceValid(); const isDataSourceValid = widgetDataTableAdapter.isDataSourceValid();
expect(rows).toEqual([]); expect(rows).toEqual([new ObjectDataRow({ id: 'IT' }), new ObjectDataRow({ id: 'PL' })]);
expect(isDataSourceValid).toBeFalse(); expect(isDataSourceValid).toBeTrue();
}); });
it('should return an empty array if columns have invalid structure', () => { it('should return an empty array if columns have invalid structure', () => {

View File

@ -128,16 +128,10 @@ export class WidgetDataTableAdapter implements DataTableAdapter {
} }
isDataSourceValid(): boolean { isDataSourceValid(): boolean {
return this.hasAllColumnsLinkedToData() && this.allMandatoryColumnPropertiesHaveValues(); return this.allColumnsHaveKeys();
} }
private allMandatoryColumnPropertiesHaveValues(): boolean { private allColumnsHaveKeys(): boolean {
return this.adapter.getColumns().every((column) => !!column.key); 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)));
}
} }

View File

@ -9,11 +9,19 @@
</div> </div>
<ng-container *ngIf="!previewState; else previewTemplate"> <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" <error-widget *ngIf="dataTableLoadFailed"
class="adf-data-table-widget-failed-message" 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-container>
<ng-template #previewTemplate> <ng-template #previewTemplate>

View File

@ -26,7 +26,7 @@ import {
mockAmericaCountriesData, mockAmericaCountriesData,
mockInvalidSchemaDefinition, mockInvalidSchemaDefinition,
mockJsonFormVariable, mockJsonFormVariable,
mockJsonFormVariableWithIncorrectData, mockJsonFormVariableWithIncompleteData,
mockJsonProcessVariables, mockJsonProcessVariables,
mockSchemaDefinition, mockSchemaDefinition,
mockJsonResponseEuropeCountriesData, mockJsonResponseEuropeCountriesData,
@ -39,7 +39,8 @@ import {
mockEuropeCountriesRows, mockEuropeCountriesRows,
mockAmericaCountriesRows, mockAmericaCountriesRows,
mockNestedCountryColumns, mockNestedCountryColumns,
mockNestedEuropeCountriesRows mockNestedEuropeCountriesRows,
mockJsonFormVariableWithEmptyData
} from './mocks/data-table-widget.mock'; } from './mocks/data-table-widget.mock';
describe('DataTableWidgetComponent', () => { describe('DataTableWidgetComponent', () => {
@ -238,7 +239,7 @@ describe('DataTableWidgetComponent', () => {
describe('should NOT display error message if', () => { describe('should NOT display error message if', () => {
it('form is in preview state', () => { it('form is in preview state', () => {
widget.field = getDataVariable(mockVariableConfig, mockSchemaDefinition, [], mockJsonFormVariableWithIncorrectData); widget.field = getDataVariable(mockVariableConfig, mockSchemaDefinition, [], mockJsonFormVariableWithIncompleteData);
spyOn(formCloudService, 'getPreviewState').and.returnValue(true); spyOn(formCloudService, 'getPreviewState').and.returnValue(true);
fixture.detectChanges(); fixture.detectChanges();
@ -249,6 +250,16 @@ describe('DataTableWidgetComponent', () => {
expect(previewDataTable).toBeTruthy(); 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', () => { it('path points to single object with appropriate schema definition', () => {
widget.field = getDataVariable({ ...mockVariableConfig, optionsPath: 'response.single-object' }, mockSchemaDefinition, [], []); widget.field = getDataVariable({ ...mockVariableConfig, optionsPath: 'response.single-object' }, mockSchemaDefinition, [], []);
widget.field.value = mockJsonNestedResponseEuropeCountriesData; widget.field.value = mockJsonNestedResponseEuropeCountriesData;
@ -262,16 +273,8 @@ describe('DataTableWidgetComponent', () => {
}); });
describe('should display error message if', () => { 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', () => { it('data source has invalid column structure', () => {
widget.field = getDataVariable(mockVariableConfig, mockInvalidSchemaDefinition, [], mockJsonFormVariableWithIncorrectData); widget.field = getDataVariable(mockVariableConfig, mockInvalidSchemaDefinition, [], mockJsonFormVariableWithIncompleteData);
fixture.detectChanges(); fixture.detectChanges();
checkDataTableErrorMessage(); checkDataTableErrorMessage();
@ -283,12 +286,11 @@ describe('DataTableWidgetComponent', () => {
{ variableName: 'not-found-data-source' }, { variableName: 'not-found-data-source' },
mockSchemaDefinition, mockSchemaDefinition,
[], [],
mockJsonFormVariableWithIncorrectData mockJsonFormVariableWithIncompleteData
); );
fixture.detectChanges(); fixture.detectChanges();
checkDataTableErrorMessage(); checkDataTableErrorMessage();
expect(widget.dataSource).toBeUndefined();
}); });
it('path is incorrect', () => { it('path is incorrect', () => {
@ -301,7 +303,6 @@ describe('DataTableWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
checkDataTableErrorMessage(); checkDataTableErrorMessage();
expect(widget.dataSource).toBeUndefined();
}); });
it('provided data by path is NOT an array or object', () => { it('provided data by path is NOT an array or object', () => {
@ -314,7 +315,6 @@ describe('DataTableWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
checkDataTableErrorMessage(); checkDataTableErrorMessage();
expect(widget.dataSource).toBeUndefined();
}); });
}); });
}); });

View File

@ -18,8 +18,17 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { WidgetComponent, FormService, FormBaseModule, DataRow, DataColumn, DataTableComponent } from '@alfresco/adf-core'; import {
import { CommonModule } from '@angular/common'; WidgetComponent,
FormService,
FormBaseModule,
DataRow,
DataColumn,
DataTableComponent,
NoContentTemplateDirective,
EmptyContentComponent
} from '@alfresco/adf-core';
import { NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { FormCloudService } from '../../../services/form-cloud.service'; import { FormCloudService } from '../../../services/form-cloud.service';
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model'; import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
@ -28,7 +37,7 @@ import { DataTablePathParserHelper } from './helpers/data-table-path-parser.help
@Component({ @Component({
standalone: true, standalone: true,
imports: [CommonModule, TranslateModule, FormBaseModule, DataTableComponent], imports: [NgIf, TranslateModule, FormBaseModule, DataTableComponent, NoContentTemplateDirective, EmptyContentComponent],
selector: 'data-table', selector: 'data-table',
templateUrl: './data-table.widget.html', templateUrl: './data-table.widget.html',
styleUrls: ['./data-table.widget.scss'], styleUrls: ['./data-table.widget.scss'],
@ -71,6 +80,8 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit
} }
private init(): void { private init(): void {
this.dataTableLoadFailed = false;
this.setPreviewState(); this.setPreviewState();
this.getTableData(); this.getTableData();
this.initDataTable(); this.initDataTable();
@ -84,40 +95,50 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit
} }
private initDataTable(): void { private initDataTable(): void {
this.dataTableLoadFailed = false;
if (this.rowsData?.length) {
this.dataSource = new WidgetDataTableAdapter(this.rowsData, this.columnsSchema); this.dataSource = new WidgetDataTableAdapter(this.rowsData, this.columnsSchema);
if (this.dataSource.isDataSourceValid()) { if (!this.dataSource.isDataSourceValid()) {
this.handleError();
return;
}
this.field.updateForm(); 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');
}
} }
private getRowsData(): void { private getRowsData(): void {
const optionsPath = this.field?.variableConfig?.optionsPath ?? this.defaultResponseProperty; const optionsPath = this.field?.variableConfig?.optionsPath ?? this.defaultResponseProperty;
const fieldValue = this.field?.value; const fieldValue = this.field?.value;
const rowsData = fieldValue || this.getDataFromVariable(); const rowsData = fieldValue || this.getDataFromVariable();
if (rowsData) { if (!rowsData) {
const dataFromPath = this.pathParserHelper.retrieveDataFromPath(rowsData, optionsPath); this.handleError();
this.rowsData = (dataFromPath?.length ? dataFromPath : rowsData) as DataRow[]; 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 { private getDataFromVariable(): any {
const processVariables = this.field?.form?.processVariables; const processVariables = this.field?.form?.processVariables;
const formVariables = this.field?.form?.variables; const formVariables = this.field?.form?.variables;
const processVariableDropdownOptions = this.getVariableValueByName(processVariables, this.variableName); const processVariableData = this.getVariableValueByName(processVariables, this.variableName);
const formVariableDropdownOptions = this.getVariableValueByName(formVariables, this.variableName); const formVariableData = this.getVariableValueByName(formVariables, this.variableName);
return processVariableDropdownOptions ?? formVariableDropdownOptions; return processVariableData ?? formVariableData;
} }
private getVariableValueByName(variables: TaskVariableCloud[], variableName: string): any { private getVariableValueByName(variables: TaskVariableCloud[], variableName: string): any {
@ -129,10 +150,9 @@ export class DataTableWidgetComponent extends WidgetComponent implements OnInit
this.previewState = this.formCloudService.getPreviewState(); this.previewState = this.formCloudService.getPreviewState();
} }
private handleError(error: any) { private handleError(): void {
if (!this.previewState) { if (!this.previewState) {
this.dataTableLoadFailed = true; this.dataTableLoadFailed = true;
this.widgetError.emit(error);
} }
} }
} }

View File

@ -23,7 +23,7 @@ interface DataTablePathParserTestCase {
path?: string; path?: string;
data?: any; data?: any;
propertyName?: string; propertyName?: string;
expected?: unknown[]; expected: unknown[];
} }
describe('DataTablePathParserHelper', () => { describe('DataTablePathParserHelper', () => {
@ -39,18 +39,18 @@ describe('DataTablePathParserHelper', () => {
description: 'not existent', description: 'not existent',
data: {}, data: {},
path: 'nonexistent.path', path: 'nonexistent.path',
expected: [] expected: undefined
}, },
{ {
description: 'not defined', description: 'not defined',
data: {}, data: {},
path: undefined, path: undefined,
expected: [] expected: undefined
}, },
{ {
description: 'empty string', description: 'empty string',
path: '', path: '',
expected: [] expected: undefined
}, },
{ {
description: 'nested', description: 'nested',
@ -76,77 +76,89 @@ describe('DataTablePathParserHelper', () => {
{ {
description: 'with nested brackets followed by an additional part of property name', description: 'with nested brackets followed by an additional part of property name',
propertyName: 'file.file[data]file', propertyName: 'file.file[data]file',
path: 'response.[file.file[data]file]' path: 'response.[file.file[data]file]',
expected: mockResultData
}, },
{ {
description: 'with nested brackets', description: 'with nested brackets',
propertyName: 'file.file[data]', 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', description: 'with separator before nested brackets in property name',
propertyName: 'file.[data]file', 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', description: 'with separator before and no separator after nested brackets in property name',
propertyName: 'file.[data]', propertyName: 'file.[data]',
path: 'response.[file.[data]]' path: 'response.[file.[data]]',
expected: mockResultData
}, },
{ {
description: 'with separator after nested brackets', description: 'with separator after nested brackets',
propertyName: 'file[data].file', propertyName: 'file[data].file',
path: 'response.[file[data].file]' path: 'response.[file[data].file]',
expected: mockResultData
}, },
{ {
description: 'with multiple brackets in property name', description: 'with multiple brackets in property name',
propertyName: 'file.file[data]file[data]', 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', description: 'with missing closing bracket in outermost square brackets',
propertyName: 'file.file[data', 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', description: 'with missing openning bracket in outermost square brackets',
propertyName: 'file.filedata]', propertyName: 'file.filedata]',
path: 'response.[file.filedata]]' path: 'response.[file.filedata]]',
expected: mockResultData
}, },
{ {
description: 'with special characters except separator (.) in brackets', description: 'with special characters except separator (.) in brackets',
propertyName: 'xyz:abc,xyz-abc,xyz_abc,abc+xyz', 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', description: 'with special characters except separator (.) without brackets',
propertyName: 'xyz:abc,xyz-abc,xyz_abc,abc+xyz', 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', description: 'without separator in brackets',
propertyName: 'my-data', propertyName: 'my-data',
path: '[response].[my-data]' path: '[response].[my-data]',
expected: mockResultData
}, },
{ {
description: 'with property followed by single index reference', description: 'with property followed by single index reference',
propertyName: 'users', propertyName: 'users',
path: 'response.users[0].data', path: 'response.users[0].data',
data: mockResponseResultDataWithArrayInsideArray('users') data: mockResponseResultDataWithArrayInsideArray('users'),
expected: mockResultData
}, },
{ {
description: 'with property followed by multiple index references', description: 'with property followed by multiple index references',
propertyName: 'users:Array', propertyName: 'users:Array',
path: 'response.[users:Array][0][1][12].data', path: 'response.[users:Array][0][1][12].data',
data: mockResponseResultDataWithArrayInsideArray('users:Array'), data: mockResponseResultDataWithArrayInsideArray('users:Array'),
expected: [] expected: undefined
}, },
{ {
description: 'when path points to array in the middle (incorrect path)', description: 'when path points to array in the middle (incorrect path)',
propertyName: 'users', propertyName: 'users',
path: 'response.users.incorrectPath', path: 'response.users.incorrectPath',
data: mockResponseResultDataWithArrayInsideArray('users'), data: mockResponseResultDataWithArrayInsideArray('users'),
expected: [] expected: undefined
}, },
{ {
description: 'when path points to the particular element of the array', 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', description: 'when path points to the particular element of the array which does NOT exist',
propertyName: 'users', propertyName: 'users',
path: 'response.users[100]', path: 'response.users[100]',
expected: [] expected: undefined
} }
]; ];
@ -166,7 +178,7 @@ describe('DataTablePathParserHelper', () => {
it(testCase.description, () => { it(testCase.description, () => {
const data = testCase.data ?? mockResponseResultData(testCase.propertyName); const data = testCase.data ?? mockResponseResultData(testCase.propertyName);
const result = helper.retrieveDataFromPath(data, testCase.path); const result = helper.retrieveDataFromPath(data, testCase.path);
const expectedResult = testCase.expected ?? mockResultData; const expectedResult = testCase.expected as any;
expect(result).toEqual(expectedResult); expect(result).toEqual(expectedResult);
}); });
}); });

View File

@ -15,13 +15,15 @@
* limitations under the License. * limitations under the License.
*/ */
import { DataRow } from '@alfresco/adf-core';
export class DataTablePathParserHelper { export class DataTablePathParserHelper {
private readonly removeSquareBracketsRegEx = /^\[(.*)\]$/; private readonly removeSquareBracketsRegEx = /^\[(.*)\]$/;
private readonly indexReferencesRegEx = /(\[\d+\])+$/; private readonly indexReferencesRegEx = /(\[\d+\])+$/;
retrieveDataFromPath(data: any, path: string): any[] { retrieveDataFromPath(data: any, path: string): DataRow[] | undefined {
if (!path) { if (!path) {
return []; return undefined;
} }
const properties = this.splitPathIntoProperties(path); const properties = this.splitPathIntoProperties(path);
@ -31,7 +33,7 @@ export class DataTablePathParserHelper {
const isPropertyWithMultipleIndexReferences = propertyIndexReferences.length > 1; const isPropertyWithMultipleIndexReferences = propertyIndexReferences.length > 1;
if (isPropertyWithMultipleIndexReferences || !this.isPropertyExistsInData(data, purePropertyName)) { if (isPropertyWithMultipleIndexReferences || !this.isPropertyExistsInData(data, purePropertyName)) {
return []; return undefined;
} }
const isPropertyWithSingleIndexReference = propertyIndexReferences.length === 1; const isPropertyWithSingleIndexReference = propertyIndexReferences.length === 1;

View File

@ -173,7 +173,7 @@ export const mockAmericaCountriesRows: DataRow[] = [
new ObjectDataRow({ id: 'US', name: 'United States' }) new ObjectDataRow({ id: 'US', name: 'United States' })
]; ];
export const mockCountriesIncorrectData = [ export const mockIncompleteCountriesData = [
{ {
id: 'PL' id: 'PL'
}, },
@ -182,8 +182,10 @@ export const mockCountriesIncorrectData = [
} }
]; ];
export const mockJsonFormVariableWithIncorrectData = [ export const mockJsonFormVariableWithEmptyData = [new TaskVariableCloud({ name: 'json-form-variable', value: [], type: 'json', id: 'fake-id-1' })];
new TaskVariableCloud({ name: 'json-form-variable', value: mockCountriesIncorrectData, 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 = [ export const mockJsonFormVariable = [