diff --git a/docs/core/components/data-column.component.md b/docs/core/components/data-column.component.md index a9ade82ed6..770c8369d4 100644 --- a/docs/core/components/data-column.component.md +++ b/docs/core/components/data-column.component.md @@ -59,9 +59,25 @@ Defines column properties for DataTable, Tasklist, Document List and other compo | sortingKey | `string` | | When using server side sorting the column used by the api call where the sorting will be performed | | srTitle | `string` | | Title to be used for screen readers. | | title | `string` | "" | Display title of the column, typically used for column headers. You can use the i18n resource key to get it translated automatically. | -| type | `string` | "text" | Value type for the column. Possible settings are 'text', 'image', 'date', 'fileSize', 'location', and 'json'. | +| type | `string` | "text" | Value type for the column. Possible settings are 'text', 'boolean', 'icon', 'image', 'date', 'fileSize', 'location', and 'json'. | | order | `number` | | Sets position of column. | +## Properties configuration + +### `type` Input + +The `type` input allows us to specify the type of hosted values for a given column. The complete array of possible values is outlined below: + +- `text` - The given values are represented as a strings (default option). +- `boolean` - The column expects true / false (boolean values) and in addition accepts two strings - 'false' and 'true'. Other values are not recognized by the column, and the cell remains empty. +- `icon` - TODO +- `image` - TODO +- `date` - TODO +- `fileSize` - TODO +- `location` - TODO +- `json` - TODO +- `currency` - This column is responsible for displaying currencies. It expects numerals represented by a string or a number. This type comes with `currencyConfig` (TODO: Add ref). The default setup use **USD** `code` with a **symbol** as a `display` option. Example output: $10.26 (for number 10.26) + ## Details ### Conditional visibility diff --git a/docs/core/pipes/boolean.pipe.md b/docs/core/pipes/boolean.pipe.md new file mode 100644 index 0000000000..5b33d37183 --- /dev/null +++ b/docs/core/pipes/boolean.pipe.md @@ -0,0 +1,26 @@ +--- +Title: Boolean pipe +Added: v6.4.0 +Status: Active +Last reviewed: 2023-10-12 +--- + +# [Boolean pipe](../../../lib/core/src/lib/pipes/boolean.pipe.ts "Defined in boolean.pipe.ts") + +Converts the received values to one of the possible strings: 'true', 'false' or ""(empty string). + +## Basic Usage + + + +```HTML +
+ Is available: {{ isAvailable | adfBoolean }} +
+``` + + + +## Details + +This pipe is prepared for any input values. The value `'true'` will be returned if true (boolean) or 'true' (exact string) appears on the input. The situation is identical for the value `'false'` - it will be returned in the case of false(boolean) or 'false'. In other cases, we can expect an `empty string('')`. diff --git a/lib/core/src/lib/datatable/components/boolean-cell/boolean-cell.component.spec.ts b/lib/core/src/lib/datatable/components/boolean-cell/boolean-cell.component.spec.ts new file mode 100644 index 0000000000..9e49cca0f5 --- /dev/null +++ b/lib/core/src/lib/datatable/components/boolean-cell/boolean-cell.component.spec.ts @@ -0,0 +1,131 @@ +/*! + * @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 { BooleanCellComponent } from './boolean-cell.component'; +import { ObjectDataTableAdapter } from '../../data/object-datatable-adapter'; +import { ObjectDataColumn } from '../../data/object-datacolumn.model'; + +describe('BooleanCellComponent', () => { + let component: BooleanCellComponent; + let fixture: ComponentFixture; + const getBooleanCell = () => fixture.debugElement.nativeElement.querySelector('span'); + const renderAndCheckResult = (value: any, expectedOccurrence: boolean, expectedLabel?: string) => { + component.value$.next(value); + fixture.detectChanges(); + + const booleanCell = getBooleanCell(); + + expectedOccurrence ? expect(booleanCell).toBeTruthy() : expect(booleanCell).toBeFalsy(); + if (expectedLabel) { + expect(booleanCell.textContent.trim()).toBe(expectedLabel); + } + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [BooleanCellComponent] + }); + fixture = TestBed.createComponent(BooleanCellComponent); + component = fixture.componentInstance; + }); + + describe('Initialization', () => { + let rowData: any; + let columnData: any; + let dataTableAdapter: ObjectDataTableAdapter; + let nextSpy: jasmine.Spy; + + beforeEach(() => { + rowData = { + id: '1', + value: false + }; + columnData = { type: 'boolean', key: 'value' }; + dataTableAdapter = new ObjectDataTableAdapter([rowData], [new ObjectDataColumn(columnData)]); + nextSpy = spyOn(component.value$, 'next'); + }); + + it('should setup inital value', () => { + component.column = dataTableAdapter.getColumns()[0]; + component.row = dataTableAdapter.getRows()[0]; + component.data = dataTableAdapter; + + fixture.detectChanges(); + + expect(nextSpy).toHaveBeenCalledOnceWith(rowData.value); + }); + + it('should NOT setup inital value', () => { + fixture.detectChanges(); + + expect(nextSpy).not.toHaveBeenCalled(); + }); + }); + + describe('UI', () => { + describe('should render "true" inside cell when', () => { + it('boolean value is true', () => { + renderAndCheckResult(true, true, 'true'); + }); + + it('exact string is provided', () => { + renderAndCheckResult('true', true, 'true'); + }); + }); + + describe('should render "false" inside cell when', () => { + it('boolean value is false', () => { + renderAndCheckResult(false, true, 'false'); + }); + + it('exact string is provided', () => { + renderAndCheckResult('false', true, 'false'); + }); + }); + + describe('should NOT render value inside cell in case of', () => { + it('invalid string', () => { + renderAndCheckResult('tru', false); + }); + + it('number', () => { + renderAndCheckResult(0, false); + }); + + it('object', () => { + renderAndCheckResult({}, false); + }); + + it('null', () => { + renderAndCheckResult(null, false); + }); + + it('undefined', () => { + renderAndCheckResult(undefined, false); + }); + + it('empty string', () => { + renderAndCheckResult('', false); + }); + + it('NaN', () => { + renderAndCheckResult(NaN, false); + }); + }); + }); +}); diff --git a/lib/core/src/lib/datatable/components/boolean-cell/boolean-cell.component.ts b/lib/core/src/lib/datatable/components/boolean-cell/boolean-cell.component.ts new file mode 100644 index 0000000000..f9ba601fd5 --- /dev/null +++ b/lib/core/src/lib/datatable/components/boolean-cell/boolean-cell.component.ts @@ -0,0 +1,49 @@ +/*! + * @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 { ChangeDetectionStrategy, Component, OnInit, Optional, ViewEncapsulation } from '@angular/core'; +import { DataTableCellComponent } from '../datatable-cell/datatable-cell.component'; +import { CommonModule } from '@angular/common'; +import { DataTableService } from '../../services/datatable.service'; +import { BooleanPipe } from '../../../pipes/boolean.pipe'; + +@Component({ + standalone: true, + imports: [CommonModule, BooleanPipe], + selector: 'adf-boolean-cell', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + {{ value }} + + + `, + encapsulation: ViewEncapsulation.None, + host: { class: 'adf-datatable-content-cell' } +}) +export class BooleanCellComponent extends DataTableCellComponent implements OnInit { + constructor(@Optional() dataTableService: DataTableService) { + super(dataTableService); + } + + ngOnInit() { + if (this.column?.key && this.row && this.data) { + this.value$.next(this.data.getValue(this.row, this.column, this.resolverFn)); + } + } +} diff --git a/lib/core/src/lib/datatable/components/datatable/datatable.component.html b/lib/core/src/lib/datatable/components/datatable/datatable.component.html index 5475c04140..910908dc8b 100644 --- a/lib/core/src/lib/datatable/components/datatable/datatable.component.html +++ b/lib/core/src/lib/datatable/components/datatable/datatable.component.html @@ -287,6 +287,16 @@ [tooltip]="getCellTooltip(row, col)"> +
+ + +
` }) class CustomColumnTemplateComponent { @@ -1195,6 +1196,17 @@ describe('DataTable', () => { expect(rows3[0].isSelected).toBeFalsy(); expect(rows3[1].isSelected).toBeTruthy(); }); + + it('should be able to display column of type boolean', () => { + dataTable.data = new ObjectDataTableAdapter(mockCarsData, mockCarsSchemaDefinition); + + fixture.detectChanges(); + const rows = dataTable.data.getRows(); + + expect(rows[0].getValue('is_available')).toBe('false'); + expect(rows[1].getValue('is_available')).toBe('true'); + expect(rows[2].getValue('is_available')).toBe('true'); + }); }); describe('Accesibility', () => { diff --git a/lib/core/src/lib/datatable/components/mocks/datatable.mock.ts b/lib/core/src/lib/datatable/components/mocks/datatable.mock.ts new file mode 100644 index 0000000000..c7f1e747f1 --- /dev/null +++ b/lib/core/src/lib/datatable/components/mocks/datatable.mock.ts @@ -0,0 +1,115 @@ +/*! + * @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. + */ + +export const mockCarsData: any = [ + { + car_id: 1, + car_name: 'Fiat 126p (Process)', + car_price: 599.00, + is_available: 'false', + production_start: '1972-04-23', + description: 'The Fiat 126 (Type 126) is a four-passenger, rear-engine, city car manufactured and marketed by Fiat over a twenty-eight year production run from 1972 until 2000, over a single generation.', + icon: 'airport_shuttle', + wikipedia_link: 'https://en.wikipedia.org/wiki/Fiat_126' + }, + { + car_id: 2, + car_name: 'Citroen Xsara Picasso (Process)', + car_price: 10000, + is_available: 'true', + production_start: '2004-02-10T12:25:43.511Z', + description: 'The Citroën Xsara Picasso is a car produced by Citroën from 1999 to 2012. It has a five-seater five-door compact MPV design.', + icon: 'local_shipping', + wikipedia_link: 'https://en.wikipedia.org/wiki/Citro%C3%ABn_Xsara_Picasso' + }, + { + car_id: 3, + car_name: 'Audi A3 (Process)', + car_price: 15000.12345, + is_available: 'true', + production_start: '1998-06-25T12:25:20', + description: 'The Audi A3 is a subcompact executive/small family car (C-segment) manufactured and marketed by the German automaker Audi AG since September 1996, currently in its fourth generation.', + icon: 'directions_car', + wikipedia_link: 'https://en.wikipedia.org/wiki/Audi_A3' + } +]; + +export const mockCarsSchemaDefinition: any[] = [ + { + type: 'icon', + key: 'icon', + title: '', + sortable: true, + draggable: true + }, + { + type: 'text', + key: 'car_id', + title: 'Car ID', + sortable: true, + draggable: true + }, + { + type: 'text', + key: 'car_name', + title: 'Car Name', + sortable: true, + draggable: true + }, + { + type: 'amount', + key: 'car_price', + title: 'Car Price', + sortable: true, + draggable: true, + currencyConfig: { + code: 'USA', + display: 'code', + digitsInfo: '1.0-2', + locale: 'en-US' + } + }, + { + type: 'boolean', + key: 'is_available', + title: 'Available?', + sortable: true, + draggable: true + }, + { + type: 'date', + key: 'production_start', + title: 'Production Start', + sortable: true, + draggable: true + }, + { + type: 'json', + key: 'description', + title: 'Description', + sortable: true, + draggable: true + }, + { + type: 'location', + format: '/somewhere', + key: 'wikipedia_link', + title: 'Wikipedia', + sortable: true, + draggable: true + } +]; diff --git a/lib/core/src/lib/datatable/data-column/data-column.component.ts b/lib/core/src/lib/datatable/data-column/data-column.component.ts index 3b242358bf..bb6d08a2b0 100644 --- a/lib/core/src/lib/datatable/data-column/data-column.component.ts +++ b/lib/core/src/lib/datatable/data-column/data-column.component.ts @@ -18,6 +18,7 @@ /* eslint-disable @angular-eslint/component-selector, @angular-eslint/no-input-rename */ import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core'; +import { DataColumnType } from '../public-api'; @Component({ selector: 'data-column', @@ -41,11 +42,11 @@ export class DataColumnComponent implements OnInit { customData: any; /** - * Value type for the column. Possible settings are 'text', 'image', - * 'date', 'fileSize', 'location', and 'json'. + * Value type for the column. Possible settings are defined via DataColumnType. + * Example types: `text`, `date`, `image`, `icon`, `boolean`. */ @Input() - type: string = 'text'; + type: DataColumnType = 'text'; /** Value format (if supported by the parent component), for example format of the date. */ @Input() diff --git a/lib/core/src/lib/datatable/data/data-column.model.ts b/lib/core/src/lib/datatable/data/data-column.model.ts index dce5919aa3..2602f77909 100644 --- a/lib/core/src/lib/datatable/data/data-column.model.ts +++ b/lib/core/src/lib/datatable/data/data-column.model.ts @@ -25,6 +25,8 @@ export interface DataColumnTypes { icon: string; fileSize: string; location: string; + // eslint-disable-next-line id-blacklist + boolean: string; } export type DataColumnType = keyof DataColumnTypes; diff --git a/lib/core/src/lib/datatable/datatable.module.ts b/lib/core/src/lib/datatable/datatable.module.ts index 12212895ca..5e73768cef 100644 --- a/lib/core/src/lib/datatable/datatable.module.ts +++ b/lib/core/src/lib/datatable/datatable.module.ts @@ -53,6 +53,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { DataColumnComponent, DataColumnListComponent, DateColumnHeaderComponent } from './data-column'; import { ResizableModule } from './directives/resizable/resizable.module'; import { DataColumnModule } from './data-column/data-column.module'; +import { BooleanCellComponent } from './components/boolean-cell/boolean-cell.component'; @NgModule({ imports: [ @@ -69,7 +70,8 @@ import { DataColumnModule } from './data-column/data-column.module'; FormsModule, ReactiveFormsModule, ResizableModule, - DataColumnModule + DataColumnModule, + BooleanCellComponent ], declarations: [ DataTableComponent, diff --git a/lib/core/src/lib/pipes/boolean.pipe.spec.ts b/lib/core/src/lib/pipes/boolean.pipe.spec.ts new file mode 100644 index 0000000000..438e027352 --- /dev/null +++ b/lib/core/src/lib/pipes/boolean.pipe.spec.ts @@ -0,0 +1,85 @@ +/*! + * @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 { BooleanPipe } from './boolean.pipe'; + +describe('BooleanPipe', () => { + let pipe: BooleanPipe; + + beforeEach(() => { + pipe = new BooleanPipe(); + }); + + describe('should return "true" when', () => { + it('boolean value is true', () => { + expect(pipe.transform(true)).toBe('true'); + }); + + it('exact string is provided', () => { + expect(pipe.transform('true')).toBe('true'); + }); + }); + + describe('should return "false" when', () => { + it('boolean value is false', () => { + expect(pipe.transform(false)).toBe('false'); + }); + + it('exact string is provided', () => { + expect(pipe.transform('false')).toBe('false'); + }); + }); + + describe('should return empty string in case of', () => { + it('invalid string', () => { + expect(pipe.transform('fal')).toBe(''); + expect(pipe.transform('truee')).toBe(''); + expect(pipe.transform('0')).toBe(''); + expect(pipe.transform('1')).toBe(''); + expect(pipe.transform('122')).toBe(''); + expect(pipe.transform('TRUE')).toBe(''); + expect(pipe.transform('FALSE')).toBe(''); + expect(pipe.transform(' false')).toBe(''); + expect(pipe.transform(' true ')).toBe(''); + }); + + it('falsy value (excluding false)', () => { + expect(pipe.transform(null)).toBe(''); + expect(pipe.transform(undefined)).toBe(''); + expect(pipe.transform(false)).not.toBe(''); + expect(pipe.transform(NaN)).toBe(''); + expect(pipe.transform(0)).toBe(''); + expect(pipe.transform(-0)).toBe(''); + expect(pipe.transform(BigInt(0))).toBe(''); + expect(pipe.transform('')).toBe(''); + }); + + it('number', () => { + expect(pipe.transform(-20.5)).toBe(''); + expect(pipe.transform(-1)).toBe(''); + expect(pipe.transform(0)).toBe(''); + expect(pipe.transform(1)).toBe(''); + expect(pipe.transform(100)).toBe(''); + expect(pipe.transform(100.5)).toBe(''); + }); + + it('object', () => { + expect(pipe.transform({})).toBe(''); + expect(pipe.transform({ value: 'true'})).toBe(''); + }); + }); +}); diff --git a/lib/core/src/lib/pipes/boolean.pipe.ts b/lib/core/src/lib/pipes/boolean.pipe.ts new file mode 100644 index 0000000000..459987d52f --- /dev/null +++ b/lib/core/src/lib/pipes/boolean.pipe.ts @@ -0,0 +1,35 @@ +/*! + * @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 { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'adfBoolean', + standalone: true +}) +export class BooleanPipe implements PipeTransform { + transform(value: any): string { + if (typeof value === 'boolean') { + return value ? 'true' : 'false'; + } + return this.isRecognizedAsBoolean(value) ? value : ''; + } + + private isRecognizedAsBoolean(value: any): boolean { + return value === 'true' || value === 'false'; + } +} diff --git a/lib/core/src/lib/pipes/public-api.ts b/lib/core/src/lib/pipes/public-api.ts index 8424f55653..ddb4ef4e67 100644 --- a/lib/core/src/lib/pipes/public-api.ts +++ b/lib/core/src/lib/pipes/public-api.ts @@ -33,3 +33,4 @@ export * from './moment-datetime.pipe'; export * from './date-time.pipe'; export * from './filter-string.pipe'; export * from './filter-out-every-object-by-prop.pipe'; +export * from './boolean.pipe'; diff --git a/lib/process-services-cloud/src/lib/services/variable-mapper.sevice.spec.ts b/lib/process-services-cloud/src/lib/services/variable-mapper.sevice.spec.ts index 06a7db5ad3..d1887f15b3 100644 --- a/lib/process-services-cloud/src/lib/services/variable-mapper.sevice.spec.ts +++ b/lib/process-services-cloud/src/lib/services/variable-mapper.sevice.spec.ts @@ -20,27 +20,31 @@ import { getProcessInstanceVariableMock } from '../mock/process-instance-variabl import { ProcessListDataColumnCustomData } from '../models/data-column-custom-data'; import { ProcessInstanceVariable } from '../models/process-instance-variable.model'; import { VariableMapperService } from './variable-mapper.sevice'; -import { getDataColumnMock } from '@alfresco/adf-core'; +import { DataColumn, getDataColumnMock } from '@alfresco/adf-core'; describe('VariableMapperService', () => { let service: VariableMapperService; + let variable: ProcessInstanceVariable; + let column: DataColumn; + let objectWithVariables: { variables: ProcessInstanceVariable[] }; + const checkTypeMapping = (processVariableType: string, expectedColumnType: string) => { + variable.type = processVariableType; + + const viewModel = service.mapVariablesByColumnTitle([objectWithVariables], [column]); + + expect(viewModel[0].variablesMap[column.title].type).toEqual(expectedColumnType); + }; beforeEach(() => { service = new VariableMapperService(); - }); - it('should map variables by column title', () => { - const variable: ProcessInstanceVariable = getProcessInstanceVariableMock({ + variable = getProcessInstanceVariableMock({ processDefinitionKey: 'processKey', name: 'variableName' }); - const objectWithVariables = { - variables: [variable] - }; - - const column = getDataColumnMock({ - title: 'column name', + column = getDataColumnMock({ + title: 'Column Name', key: '', customData: { variableDefinitionsPayload: ['processKey/variableName'], @@ -49,8 +53,12 @@ describe('VariableMapperService', () => { } }); - const viewModel = service.mapVariablesByColumnTitle([objectWithVariables], [column]); + objectWithVariables = { + variables: [variable] + }; + }); + it('should map variables by column title', () => { const expectedObjectWithVariableMap = { ...objectWithVariables, variablesMap: { @@ -58,6 +66,34 @@ describe('VariableMapperService', () => { } }; + const viewModel = service.mapVariablesByColumnTitle([objectWithVariables], [column]); + expect(viewModel).toEqual([expectedObjectWithVariableMap]); }); + + describe('should map correct column type according to process variable type in case of', () => { + it('date type', () => { + checkTypeMapping('boolean', 'boolean'); + }); + + it('integer type', () => { + checkTypeMapping('integer', 'text'); + }); + + it('string type', () => { + checkTypeMapping('string', 'text'); + }); + + it('date type', () => { + checkTypeMapping('date', 'date'); + }); + + it('datetime type', () => { + checkTypeMapping('datetime', 'date'); + }); + + it('other types', () => { + checkTypeMapping('custom', 'text'); + }); + }); }); diff --git a/lib/process-services-cloud/src/lib/services/variable-mapper.sevice.ts b/lib/process-services-cloud/src/lib/services/variable-mapper.sevice.ts index 1f3efc5bd5..d3e1ad4392 100644 --- a/lib/process-services-cloud/src/lib/services/variable-mapper.sevice.ts +++ b/lib/process-services-cloud/src/lib/services/variable-mapper.sevice.ts @@ -78,6 +78,7 @@ export class VariableMapperService { private mapProcessVariableTypes(variableType: string): DataColumnType { switch (variableType) { case 'boolean': + return 'boolean'; case 'integer': case 'string': return 'text';