[AAE-15815] Create Data Table widget (#8801)

* [AAE-15815] Create Data Table widget

* [AAE-15815] Add check for corectness of column schema

* fix mock name typo

* improve method name

* fix testing module config
This commit is contained in:
Tomasz Gnyp
2023-08-09 01:31:39 +02:00
committed by GitHub
parent 4f2b3bce3c
commit 1f96c3452c
11 changed files with 592 additions and 1 deletions

View File

@@ -26,6 +26,7 @@ import { PropertiesViewerWidgetComponent } from './widgets/properties-viewer/pro
import { RadioButtonsCloudWidgetComponent } from './widgets/radio-buttons/radio-buttons-cloud.widget';
import { FileViewerWidgetComponent } from './widgets/file-viewer/file-viewer.widget';
import { DisplayRichTextWidgetComponent } from './widgets/display-rich-text/display-rich-text.widget';
import { DataTableWidgetComponent } from './widgets/data-table/data-table.widget';
@Injectable({
providedIn: 'root'
@@ -43,7 +44,8 @@ export class CloudFormRenderingService extends FormRenderingService {
[FormFieldTypes.PROPERTIES_VIEWER]: () => PropertiesViewerWidgetComponent,
[FormFieldTypes.RADIO_BUTTONS]: () => RadioButtonsCloudWidgetComponent,
[FormFieldTypes.ALFRESCO_FILE_VIEWER]: () => FileViewerWidgetComponent,
[FormFieldTypes.DISPLAY_RICH_TEXT]: () => DisplayRichTextWidgetComponent
[FormFieldTypes.DISPLAY_RICH_TEXT]: () => DisplayRichTextWidgetComponent,
[FormFieldTypes.DATA_TABLE]: () => DataTableWidgetComponent
}, true);
}
}

View File

@@ -0,0 +1,76 @@
/*!
* @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 { WidgetDataTableAdapter } from './data-table-adapter.widget';
import {
mockCountriesData,
mockCountriesIncorrectData,
mockInvalidSchemaDefinition,
mockSchemaDefinition,
mockSchemaDefinitionWithDifferentTypes
} from '../../../mocks/data-table-widget.mock';
import { ObjectDataRow } from '@alfresco/adf-core';
describe('WidgetDataTableAdapter', () => {
let widgetDataTableAdapter: WidgetDataTableAdapter;
beforeEach(() => {
widgetDataTableAdapter = new WidgetDataTableAdapter(mockCountriesData, mockSchemaDefinition);
});
it('should set columns type to "text" during initialization', () => {
widgetDataTableAdapter = new WidgetDataTableAdapter(mockCountriesData, mockSchemaDefinitionWithDifferentTypes);
widgetDataTableAdapter.getColumns().forEach(column =>
expect(column.type).toBe('text')
);
});
it('should return rows if all columns are linked to data', () => {
const rows = widgetDataTableAdapter.getRows();
expect(rows).toEqual([
new ObjectDataRow({ id: 'IT', name: 'Italy' }),
new ObjectDataRow({ id: 'PL', name: 'Poland' }),
new ObjectDataRow({ id: 'UK', name: 'United Kingdom' })
]);
});
it('should return an empty array if not all columns are linked to data', () => {
widgetDataTableAdapter = new WidgetDataTableAdapter(mockCountriesIncorrectData, mockSchemaDefinition);
const rows = widgetDataTableAdapter.getRows();
const isDataSourceValid = widgetDataTableAdapter.isDataSourceValid();
expect(rows).toEqual([]);
expect(isDataSourceValid).toBeFalse();
});
it('should return an empty array if columns have invalid structure', () => {
widgetDataTableAdapter = new WidgetDataTableAdapter(mockCountriesData, mockInvalidSchemaDefinition);
const rows = widgetDataTableAdapter.getRows();
const isDataSourceValid = widgetDataTableAdapter.isDataSourceValid();
expect(rows).toEqual([]);
expect(isDataSourceValid).toBeFalse();
});
it('should return true for isDataSourceValid() if rows have data and valid columns schema', () => {
const isValid = widgetDataTableAdapter.isDataSourceValid();
expect(isValid).toBeTrue();
});
});

View File

@@ -0,0 +1,62 @@
/*!
* @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 {
ObjectDataTableAdapter,
DataColumn,
DataRow
} from '@alfresco/adf-core';
export class WidgetDataTableAdapter extends ObjectDataTableAdapter {
private rows: DataRow[];
private columns: DataColumn[];
constructor(data?: any[], schema?: DataColumn[]) {
super(data, schema);
this.rows = super.getRows();
this.columns = super.getColumns();
this.setColumnsTypeToText();
}
getRows(): DataRow[] {
if (this.isDataSourceValid()) {
return this.rows;
}
return [];
}
isDataSourceValid(): boolean {
return this.hasAllColumnsLinkedToData() && this.hasAllMandatoryColumnPropertiesHaveValues();
}
private hasAllMandatoryColumnPropertiesHaveValues(): boolean {
return this.columns.every(column => !!column.key);
}
private hasAllColumnsLinkedToData(): boolean {
const availableColumnKeys: string[] = this.columns.map(column => column.key);
return availableColumnKeys.every(columnKey => this.rows.some(row => Object.keys(row.obj).includes(columnKey)));
}
private setColumnsTypeToText(): void {
super.setColumns(this.columns.map(column => ({ ...column, type: 'text' })));
}
}

View File

@@ -0,0 +1,22 @@
<div class="adf-data-table-widget-container">
<div class="adf-data-table-widget-label">
<label
class="adf-label"
[class.adf-left-label]="field.leftLabels"
[attr.for]="field.id">
{{field.name | translate }}
</label>
</div>
<ng-container *ngIf="!previewState; else previewTemplate">
<adf-datatable data-automation-id="adf-data-table-widget" [data]="dataSource"></adf-datatable>
<error-widget *ngIf="dataTableLoadFailed"
class="adf-data-table-widget-failed-message"
required="{{ 'FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate }}"></error-widget>
</ng-container>
<ng-template #previewTemplate>
<adf-datatable data-automation-id="adf-data-table-widget-preview"></adf-datatable>
</ng-template>
</div>

View File

@@ -0,0 +1,3 @@
.adf-data-table-widget-failed-message {
margin: 10px;
}

View File

@@ -0,0 +1,186 @@
/*!
* @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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { DataColumn, FormFieldModel, FormFieldTypes, FormModel, LogService } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { DataTableWidgetComponent } from './data-table.widget';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
import { FormCloudService } from '../../../services/form-cloud.service';
import { WidgetDataTableAdapter } from './data-table-adapter.widget';
import {
mockCountriesData,
mockInvalidSchemaDefinition,
mockJsonFormVariable,
mockJsonFormVariableWithIncorrectData,
mockJsonProcessVariables,
mockSchemaDefinition
} from '../../../mocks/data-table-widget.mock';
describe('DataTableWidgetComponent', () => {
let widget: DataTableWidgetComponent;
let fixture: ComponentFixture<DataTableWidgetComponent>;
let formCloudService: FormCloudService;
let logService: LogService;
let logServiceSpy: jasmine.Spy;
const errorIcon: string = 'error_outline';
const getDataVariable = (
variableName: string,
schemaDefinition: DataColumn[],
processVariables?: TaskVariableCloud[],
variables?: TaskVariableCloud[]
) => new FormFieldModel(
new FormModel({ taskId: 'fake-task-id', processVariables, variables }), {
id: 'fake-datatable-id',
name: 'Data Table',
type: FormFieldTypes.DATA_TABLE,
optionType: 'variable',
schemaDefinition,
variableConfig: {
variableName
}
});
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
ProcessServiceCloudTestingModule
]
});
fixture = TestBed.createComponent(DataTableWidgetComponent);
widget = fixture.componentInstance;
formCloudService = TestBed.inject(FormCloudService);
logService = TestBed.inject(LogService);
widget.field = new FormFieldModel( new FormModel({ taskId: 'fake-task-id' }), {
type: FormFieldTypes.DATA_TABLE,
name: 'Data Table'
});
logServiceSpy = spyOn(logService, 'error');
});
it('should display label', () => {
fixture.detectChanges();
const widgetLabel: HTMLElement = fixture.nativeElement.querySelector('.adf-label');
expect(widgetLabel).toBeTruthy();
expect(widgetLabel.textContent.trim()).toBe('Data Table');
});
it('should display only data table with data source in NOT preview mode', () => {
fixture.detectChanges();
const dataTable = fixture.nativeElement.querySelector('[data-automation-id="adf-data-table-widget"]');
const dataTablePreview = fixture.nativeElement.querySelector('[data-automation-id="adf-data-table-widget-preview"]');
expect(dataTable).toBeTruthy();
expect(dataTablePreview).toBeNull();
});
it('should properly initialize column schema', () => {
widget.field = getDataVariable('json-form-variable', mockSchemaDefinition, [], mockJsonFormVariable);
fixture.detectChanges();
widget.dataSource.getColumns().forEach((column, index) =>
expect(column.key).toEqual(mockSchemaDefinition[index].key
));
});
it('should properly initialize data source based on form variable', () => {
widget.field = getDataVariable('json-form-variable', mockSchemaDefinition, [], mockJsonFormVariable);
fixture.detectChanges();
const expectedData = new WidgetDataTableAdapter(mockCountriesData, mockSchemaDefinition);
expectedData.getRows().forEach(row => row.cssClass = '');
expect(widget.dataSource.getRows()).toEqual(expectedData.getRows());
});
it('should properly initialize data source based on process variable', () => {
widget.field = getDataVariable('json-variable', mockSchemaDefinition, mockJsonProcessVariables);
fixture.detectChanges();
const expectedData = new WidgetDataTableAdapter(mockCountriesData, mockSchemaDefinition);
expectedData.getRows().forEach(row => row.cssClass = '');
expect(widget.dataSource.getRows()).toEqual(expectedData.getRows());
});
it('should NOT display error if form is in preview state', () => {
widget.field = getDataVariable('json-form-variable', mockSchemaDefinition, [], mockJsonFormVariableWithIncorrectData);
spyOn(formCloudService, 'getPreviewState').and.returnValue(true);
fixture.detectChanges();
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message'));
const previewDataTable = fixture.nativeElement.querySelector('[data-automation-id="adf-data-table-widget-preview"]');
expect(failedErrorMsgElement).toBeNull();
expect(previewDataTable).toBeTruthy();
});
it('should NOT display data table with data source if form is in preview state', () => {
widget.field = getDataVariable('json-form-variable', mockSchemaDefinition, [], mockJsonFormVariable);
spyOn(formCloudService, 'getPreviewState').and.returnValue(true);
fixture.detectChanges();
const previewDataTable = fixture.nativeElement.querySelector('[data-automation-id="adf-data-table-widget-preview"]');
const dataTable = fixture.nativeElement.querySelector('[data-automation-id="adf-data-table-widget"]');
expect(previewDataTable).toBeTruthy();
expect(dataTable).toBeNull();
});
it('should be able to display and log error if data source is not linked to every column', () => {
widget.field = getDataVariable('json-form-variable', mockSchemaDefinition, [], mockJsonFormVariableWithIncorrectData);
fixture.detectChanges();
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message'));
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(errorIcon.concat('FORM.FIELD.DATA_TABLE_LOAD_FAILED'));
expect(logServiceSpy).toHaveBeenCalledWith('Data source has corrupted model or structure');
expect(widget.dataSource.getRows()).toEqual([]);
});
it('should be able to display and log error if data source has invalid column structure', () => {
widget.field = getDataVariable('json-form-variable', mockInvalidSchemaDefinition, [], mockJsonFormVariableWithIncorrectData);
fixture.detectChanges();
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message'));
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(errorIcon.concat('FORM.FIELD.DATA_TABLE_LOAD_FAILED'));
expect(logServiceSpy).toHaveBeenCalledWith('Data source has corrupted model or structure');
expect(widget.dataSource.getRows()).toEqual([]);
});
it('should be able to display and log error if variable is not found', () => {
const notFoundVariable = 'not-found-json-variable';
widget.field = getDataVariable(notFoundVariable, mockSchemaDefinition, [], mockJsonFormVariableWithIncorrectData);
fixture.detectChanges();
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message'));
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(errorIcon.concat('FORM.FIELD.DATA_TABLE_LOAD_FAILED'));
expect(logServiceSpy).toHaveBeenCalledWith(`${notFoundVariable} not found`);
expect(widget.dataSource).toBeUndefined();
});
});

View File

@@ -0,0 +1,128 @@
/*!
* @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.
*/
/* 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 { 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';
@Component({
standalone: true,
imports: [
CommonModule,
TranslateModule,
DataTableModule,
FormBaseModule
],
selector: 'data-table',
templateUrl: './data-table.widget.html',
styleUrls: ['./data-table.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class DataTableWidgetComponent extends WidgetComponent implements OnInit {
dataSource: WidgetDataTableAdapter;
dataTableLoadFailed = false;
previewState = false;
private rowsData: DataRow[];
private columnsSchema: DataColumn[];
private variableName: string;
constructor(
public formService: FormService,
private formCloudService: FormCloudService,
private logService: LogService
) {
super(formService);
}
ngOnInit(): void {
this.setPreviewState();
this.getTableData();
this.initDataTable();
}
private getTableData(): void {
const processVariables = this.field?.form?.processVariables;
const formVariables = this.field?.form?.variables;
this.variableName = this.field?.variableConfig?.variableName;
this.columnsSchema = this.field?.schemaDefinition;
this.rowsData = this.getDataFromVariable(processVariables, formVariables);
}
private initDataTable(): void {
if (this.rowsData) {
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(`${this.variableName} not found`);
}
}
private getDataFromVariable(processVariables: TaskVariableCloud[], formVariables: TaskVariableCloud[]): any {
const processVariableDropdownOptions = this.getVariableValueByName(processVariables, this.variableName);
const formVariableDropdownOptions = this.getVariableValueByName(formVariables, this.variableName);
return processVariableDropdownOptions ?? formVariableDropdownOptions;
}
private getVariableValueByName(variables: TaskVariableCloud[], variableName: string): any {
return variables?.find((variable: TaskVariableCloud) => variable?.name === `variables.${variableName}` || variable?.name === variableName)?.value;
}
private setPreviewState(): void {
this.previewState = this.formCloudService.getPreviewState();
}
private handleError(error: any) {
if (!this.previewState) {
this.dataTableLoadFailed = true;
this.logService.error(error);
}
}
}

View File

@@ -0,0 +1,107 @@
/*!
* @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 { DataColumn } from '@alfresco/adf-core';
import { TaskVariableCloud } from '../models/task-variable-cloud.model';
export const mockSchemaDefinition: DataColumn[] = [
{
type: 'text',
key: 'id',
title: 'Country ID',
sortable: true,
draggable: true
},
{
type: 'text',
key: 'name',
title: 'Country Name',
sortable: true,
draggable: true
}
];
export const mockSchemaDefinitionWithDifferentTypes: DataColumn[] = [
{
type: 'json',
key: 'id',
title: 'Country ID',
sortable: true,
draggable: true
},
{
type: 'date',
key: 'name',
title: 'Country Name',
sortable: true,
draggable: true
}
];
export const mockInvalidSchemaDefinition: DataColumn[] = [
{
type: 'text',
key: '',
title: 'Country ID',
sortable: true,
draggable: true
},
{
type: 'text',
key: undefined,
title: 'Country Name',
sortable: true,
draggable: true
}
];
export const mockCountriesData = [
{
id: 'PL',
name: 'Poland'
},
{
id: 'IT',
name: 'Italy'
},
{
id: 'UK',
name: 'United Kingdom'
}
];
export const mockCountriesIncorrectData = [
{
id: 'PL'
},
{
id: 'IT'
}
];
export const mockJsonFormVariableWithIncorrectData = [
new TaskVariableCloud({ name: 'json-form-variable', value: mockCountriesIncorrectData, type: 'json', id: 'fake-id-1' })
];
export const mockJsonFormVariable = [
new TaskVariableCloud({ name: 'json-form-variable', value: mockCountriesData, type: 'json', id: 'fake-id-1' })
];
export const mockJsonProcessVariables = [
new TaskVariableCloud({ name: 'variables.json-variable', value: mockCountriesData, type: 'json', id: 'fake-id-1' }),
new TaskVariableCloud({ name: 'variables.different-variable', value: 'fake-value', type: 'json', id: 'fake-id-2' })
];