From bc30475e201416f35ad3e44481f9d7fafb3f6d26 Mon Sep 17 00:00:00 2001 From: Bastian Jakobi <55296998+bastianjakobi@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:27:39 +0100 Subject: [PATCH] feat: add ability to dynamically hide/disable action buttons (#169) * feat: add ability to dynamically hide/disable table action buttons * feat: add ability to dynamically hide/disable list action buttons * refactor: remove whitespace difference * feat: add ability to dynamically hide/disable grid action buttons * test: add tests for data table * test: add tests for data list grid * test: add tests for hidden action buttons in data table * test: add smoke tests to parent components --- .../data-list-grid.component.html | 15 +- .../data-list-grid.component.spec.ts | 861 ++++++++++++------ .../data-list-grid.component.stories.ts | 153 ++++ .../data-list-grid.component.ts | 41 +- .../data-table/data-table.component.html | 12 +- .../data-table/data-table.component.spec.ts | 253 ++++- .../data-table.component.stories.ts | 75 +- .../data-table/data-table.component.ts | 11 + .../data-view/data-view.component.html | 12 + .../data-view/data-view.component.spec.ts | 161 ++++ .../data-view/data-view.component.ts | 6 + .../interactive-data-view.component.html | 6 + .../interactive-data-view.component.spec.ts | 141 +++ .../interactive-data-view.component.ts | 6 + .../testing/data-list-grid.harness.ts | 35 +- .../testing/data-table.harness.ts | 28 +- 16 files changed, 1481 insertions(+), 335 deletions(-) create mode 100644 libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.stories.ts diff --git a/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.html b/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.html index ec90279a..a0922f2a 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.html +++ b/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.html @@ -56,10 +56,11 @@ @@ -80,7 +81,7 @@ {{ resolveFieldData(item, titleLineId) || '' }}
- + - + - + diff --git a/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.spec.ts b/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.spec.ts index b2a3dac8..8f12db67 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.spec.ts +++ b/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.spec.ts @@ -1,296 +1,607 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { TranslateModule, TranslateService } from '@ngx-translate/core' import { DataListGridComponent } from './data-list-grid.component' -import { PrimeNgModule } from '../../primeng.module'; -import { TranslateTestingModule } from 'ngx-translate-testing'; -import { ColumnType } from '../../../model/column-type.model'; -import { PortalCoreModule } from '../../portal-core.module'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { DataListGridHarness, DataTableHarness } from '../../../../../../../libs/portal-integration-angular/testing'; -import { MockAuthModule } from '../../../mock-auth/mock-auth.module'; -import { ActivatedRoute, RouterModule } from '@angular/router'; +import { PrimeNgModule } from '../../primeng.module' +import { TranslateTestingModule } from 'ngx-translate-testing' +import { ColumnType } from '../../../model/column-type.model' +import { PortalCoreModule } from '../../portal-core.module' +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' +import { DataListGridHarness, DataTableHarness } from '../../../../../../../libs/portal-integration-angular/testing' +import { MockAuthModule } from '../../../mock-auth/mock-auth.module' +import { ActivatedRoute, RouterModule } from '@angular/router' +import { UserService } from '../../../services/user.service' +import { MockUserService } from '../../../../../mocks/mock-user-service' describe('DataListGridComponent', () => { - let fixture: ComponentFixture - let component: DataListGridComponent - let translateService: TranslateService - - const ENGLISH_LANGUAGE = 'en'; - const ENGLISH_TRANSLATIONS = { - OCX_DATA_TABLE: { - SHOWING: "{{first}} - {{last}} of {{totalRecords}}", - SHOWING_WITH_TOTAL_ON_SERVER: "{{first}} - {{last}} of {{totalRecords}} ({{totalRecordsOnServer}})", - ALL: "All" - } - }; - - const GERMAN_LANGUAGE = 'de'; - const GERMAN_TRANSLATIONS = { - OCX_DATA_TABLE: { - SHOWING: "{{first}} - {{last}} von {{totalRecords}}", - SHOWING_WITH_TOTAL_ON_SERVER: "{{first}} - {{last}} von {{totalRecords}} ({{totalRecordsOnServer}})", - ALL: "Alle" - } - }; + let fixture: ComponentFixture + let component: DataListGridComponent + let translateService: TranslateService + let listGrid: DataListGridHarness - const TRANSLATIONS = { - [ENGLISH_LANGUAGE]: ENGLISH_TRANSLATIONS, - [GERMAN_LANGUAGE]: GERMAN_TRANSLATIONS - }; + const ENGLISH_LANGUAGE = 'en' + const ENGLISH_TRANSLATIONS = { + OCX_DATA_TABLE: { + SHOWING: '{{first}} - {{last}} of {{totalRecords}}', + SHOWING_WITH_TOTAL_ON_SERVER: '{{first}} - {{last}} of {{totalRecords}} ({{totalRecordsOnServer}})', + ALL: 'All', + }, + } - const mockData = [ - { - version: 0, - creationDate: '2023-09-12T09:34:11.997048Z', - creationUser: 'creation user', - modificationDate: '2023-09-12T09:34:11.997048Z', - modificationUser: '', - id: '195ee34e-41c6-47b7-8fc4-3f245dee7651', - name: 'some name', - description: '', - status: 'some status', - responsible: 'someone responsible', - endDate: '2023-09-14T09:34:09Z', - startDate: '2023-09-13T09:34:05Z', - imagePath: '/path/to/image', - testNumber: '1', - }, - { - version: 0, - creationDate: '2023-09-12T09:33:58.544494Z', - creationUser: '', - modificationDate: '2023-09-12T09:33:58.544494Z', - modificationUser: '', - id: '5f8bb05b-d089-485e-a234-0bb6ff25234e', - name: 'example', - description: 'example description', - status: 'status example', - responsible: '', - endDate: '2023-09-13T09:33:55Z', - startDate: '2023-09-12T09:33:53Z', - imagePath: '', - testNumber: '3.141', - }, - { - version: 0, - creationDate: '2023-09-12T09:34:27.184086Z', - creationUser: '', - modificationDate: '2023-09-12T09:34:27.184086Z', - modificationUser: '', - id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064', - name: 'name 1', - description: '', - status: 'status name 1', - responsible: '', - endDate: '2023-09-15T09:34:24Z', - startDate: '2023-09-14T09:34:22Z', - imagePath: '', - testNumber: '123456789', - }, - { - version: 0, - creationDate: '2023-09-12T09:34:27.184086Z', - creationUser: '', - modificationDate: '2023-09-12T09:34:27.184086Z', - modificationUser: '', - id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064', - name: 'name 2', - description: '', - status: 'status name 2', - responsible: '', - endDate: '2023-09-15T09:34:24Z', - startDate: '2023-09-14T09:34:22Z', - imagePath: '', - testNumber: '12345.6789', - }, + const GERMAN_LANGUAGE = 'de' + const GERMAN_TRANSLATIONS = { + OCX_DATA_TABLE: { + SHOWING: '{{first}} - {{last}} von {{totalRecords}}', + SHOWING_WITH_TOTAL_ON_SERVER: '{{first}} - {{last}} von {{totalRecords}} ({{totalRecordsOnServer}})', + ALL: 'Alle', + }, + } + + const TRANSLATIONS = { + [ENGLISH_LANGUAGE]: ENGLISH_TRANSLATIONS, + [GERMAN_LANGUAGE]: GERMAN_TRANSLATIONS, + } + + const mockData = [ + { + version: 0, + creationDate: '2023-09-12T09:34:11.997048Z', + creationUser: 'creation user', + modificationDate: '2023-09-12T09:34:11.997048Z', + modificationUser: '', + id: '195ee34e-41c6-47b7-8fc4-3f245dee7651', + name: 'some name', + description: '', + status: 'some status', + responsible: 'someone responsible', + endDate: '2023-09-14T09:34:09Z', + startDate: '2023-09-13T09:34:05Z', + imagePath: '/path/to/image', + testNumber: '1', + }, + { + version: 0, + creationDate: '2023-09-12T09:33:58.544494Z', + creationUser: '', + modificationDate: '2023-09-12T09:33:58.544494Z', + modificationUser: '', + id: '5f8bb05b-d089-485e-a234-0bb6ff25234e', + name: 'example', + description: 'example description', + status: 'status example', + responsible: '', + endDate: '2023-09-13T09:33:55Z', + startDate: '2023-09-12T09:33:53Z', + imagePath: '', + testNumber: '3.141', + }, + { + version: 0, + creationDate: '2023-09-12T09:34:27.184086Z', + creationUser: '', + modificationDate: '2023-09-12T09:34:27.184086Z', + modificationUser: '', + id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064', + name: 'name 1', + description: '', + status: 'status name 1', + responsible: '', + endDate: '2023-09-15T09:34:24Z', + startDate: '2023-09-14T09:34:22Z', + imagePath: '', + testNumber: '123456789', + }, + { + version: 0, + creationDate: '2023-09-12T09:34:27.184086Z', + creationUser: '', + modificationDate: '2023-09-12T09:34:27.184086Z', + modificationUser: '', + id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064', + name: 'name 2', + description: '', + status: 'status name 2', + responsible: '', + endDate: '2023-09-15T09:34:24Z', + startDate: '2023-09-14T09:34:22Z', + imagePath: '', + testNumber: '12345.6789', + }, + { + version: 0, + creationDate: '2023-09-12T09:34:27.184086Z', + creationUser: '', + modificationDate: '2023-09-12T09:34:27.184086Z', + modificationUser: '', + id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064', + name: 'name 3', + description: '', + status: 'status name 3', + responsible: '', + endDate: '2023-09-15T09:34:24Z', + startDate: '2023-09-14T09:34:22Z', + imagePath: '', + testNumber: '7.1', + }, + ] + const mockColumns = [ + { + columnType: ColumnType.STRING, + id: 'name', + nameKey: 'COLUMN_HEADER_NAME.NAME', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], + }, + { + columnType: ColumnType.STRING, + id: 'description', + nameKey: 'COLUMN_HEADER_NAME.DESCRIPTION', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], + }, + { + columnType: ColumnType.DATE, + id: 'startDate', + nameKey: 'COLUMN_HEADER_NAME.START_DATE', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], + }, + { + columnType: ColumnType.DATE, + id: 'endDate', + nameKey: 'COLUMN_HEADER_NAME.END_DATE', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], + }, + { + columnType: ColumnType.TRANSLATION_KEY, + id: 'status', + nameKey: 'COLUMN_HEADER_NAME.STATUS', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], + }, + { + columnType: ColumnType.STRING, + id: 'responsible', + nameKey: 'COLUMN_HEADER_NAME.RESPONSIBLE', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], + }, + { + columnType: ColumnType.RELATIVE_DATE, + id: 'modificationDate', + nameKey: 'COLUMN_HEADER_NAME.MODIFICATION_DATE', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.FULL'], + }, + { + columnType: ColumnType.STRING, + id: 'creationUser', + nameKey: 'COLUMN_HEADER_NAME.CREATION_USER', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.FULL'], + }, + { + columnType: ColumnType.NUMBER, + id: 'testNumber', + nameKey: 'COLUMN_HEADER_NAME.TEST_NUMBER', + filterable: true, + sortable: true, + predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], + }, + ] + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DataListGridComponent], + imports: [ + PrimeNgModule, + TranslateModule.forRoot(), + TranslateTestingModule.withTranslations(TRANSLATIONS), + PortalCoreModule, + MockAuthModule, + RouterModule, + NoopAnimationsModule, + ], + providers: [ { - version: 0, - creationDate: '2023-09-12T09:34:27.184086Z', - creationUser: '', - modificationDate: '2023-09-12T09:34:27.184086Z', - modificationUser: '', - id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064', - name: 'name 3', - description: '', - status: 'status name 3', - responsible: '', - endDate: '2023-09-15T09:34:24Z', - startDate: '2023-09-14T09:34:22Z', - imagePath: '', - testNumber: '7.1', + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: { + get: () => '1', + }, + }, + }, }, + { provide: UserService, useClass: MockUserService }, + ], + }).compileComponents() + + fixture = TestBed.createComponent(DataListGridComponent) + component = fixture.componentInstance + component.data = mockData + component.columns = mockColumns + component.paginator = true + translateService = TestBed.inject(TranslateService) + translateService.use('en') + fixture.detectChanges() + listGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) + }) + + it('should create the data list grid component', () => { + expect(component).toBeTruthy() + }) + + it('loads dataListGrid', async () => { + const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) + expect(dataListGrid).toBeTruthy() + }) + + describe('should display the paginator currentPageReport -', () => { + it('de', async () => { + translateService.use('de') + const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) + const paginator = await dataListGrid.getPaginator() + const currentPageReport = await paginator.getCurrentPageReportText() + expect(currentPageReport).toEqual('1 - 5 von 5') + }) + + it('en', async () => { + translateService.use('en') + const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) + const paginator = await dataListGrid.getPaginator() + const currentPageReport = await paginator.getCurrentPageReportText() + expect(currentPageReport).toEqual('1 - 5 of 5') + }) + }) + + describe('should display the paginator currentPageReport with totalRecordsOnServer -', () => { + it('de', async () => { + component.totalRecordsOnServer = 10 + translateService.use('de') + const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) + const paginator = await dataListGrid.getPaginator() + const currentPageReport = await paginator.getCurrentPageReportText() + expect(currentPageReport).toEqual('1 - 5 von 5 (10)') + }) + + it('en', async () => { + component.totalRecordsOnServer = 10 + translateService.use('en') + const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) + const paginator = await dataListGrid.getPaginator() + const currentPageReport = await paginator.getCurrentPageReportText() + expect(currentPageReport).toEqual('1 - 5 of 5 (10)') + }) + }) + + describe('should display the paginator rowsPerPageOptions -', () => { + it('de', async () => { + window.HTMLElement.prototype.scrollIntoView = jest.fn() + translateService.use('de') + const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataTableHarness) + const paginator = await dataListGrid.getPaginator() + const rowsPerPageOptions = await paginator.getRowsPerPageOptions() + const rowsPerPageOptionsText = await rowsPerPageOptions.selectedDropdownItemText(3) + expect(rowsPerPageOptionsText).toEqual('Alle') + }) + + it('en', async () => { + window.HTMLElement.prototype.scrollIntoView = jest.fn() + translateService.use('en') + const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataTableHarness) + const paginator = await dataListGrid.getPaginator() + const rowsPerPageOptions = await paginator.getRowsPerPageOptions() + const rowsPerPageOptionsText = await rowsPerPageOptions.selectedDropdownItemText(3) + expect(rowsPerPageOptionsText).toEqual('All') + }) + }) + + const setUpListActionButtonMockData = () => { + component.columns = [ + ...mockColumns, + { + columnType: ColumnType.STRING, + id: 'ready', + nameKey: 'Ready', + }, ] - const mockColumns = [ - { - columnType: ColumnType.STRING, - id: 'name', - nameKey: 'COLUMN_HEADER_NAME.NAME', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], - }, - { - columnType: ColumnType.STRING, - id: 'description', - nameKey: 'COLUMN_HEADER_NAME.DESCRIPTION', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], - }, - { - columnType: ColumnType.DATE, - id: 'startDate', - nameKey: 'COLUMN_HEADER_NAME.START_DATE', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], - }, - { - columnType: ColumnType.DATE, - id: 'endDate', - nameKey: 'COLUMN_HEADER_NAME.END_DATE', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], - }, - { - columnType: ColumnType.TRANSLATION_KEY, - id: 'status', - nameKey: 'COLUMN_HEADER_NAME.STATUS', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], - }, - { - columnType: ColumnType.STRING, - id: 'responsible', - nameKey: 'COLUMN_HEADER_NAME.RESPONSIBLE', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.DEFAULT', 'PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], - }, - { - columnType: ColumnType.RELATIVE_DATE, - id: 'modificationDate', - nameKey: 'COLUMN_HEADER_NAME.MODIFICATION_DATE', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.FULL'], - }, - { - columnType: ColumnType.STRING, - id: 'creationUser', - nameKey: 'COLUMN_HEADER_NAME.CREATION_USER', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.FULL'], - }, - { - columnType: ColumnType.NUMBER, - id: 'testNumber', - nameKey: 'COLUMN_HEADER_NAME.TEST_NUMBER', - filterable: true, - sortable: true, - predefinedGroupKeys: ['PREDEFINED_GROUP.EXTENDED', 'PREDEFINED_GROUP.FULL'], - }, + + component.data = [ + { + version: 0, + creationDate: '2023-09-12T09:34:27.184086Z', + creationUser: '', + modificationDate: '2023-09-12T09:34:27.184086Z', + modificationUser: '', + id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064', + name: 'name 3', + description: '', + status: 'status name 3', + responsible: '', + endDate: '2023-09-15T09:34:24Z', + startDate: '2023-09-14T09:34:22Z', + imagePath: '', + testNumber: '7.1', + ready: false, + }, ] - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DataListGridComponent], - imports: [PrimeNgModule, BrowserAnimationsModule, TranslateModule.forRoot(), TranslateTestingModule.withTranslations(TRANSLATIONS), - PortalCoreModule, MockAuthModule, RouterModule], - providers: [ - { - provide: ActivatedRoute, - useValue: { - snapshot: { - paramMap: { - get: () => '1', - }, - }, - }, - }, - ], - }).compileComponents() - - fixture = TestBed.createComponent(DataListGridComponent) - component = fixture.componentInstance - component.data = mockData - component.columns = mockColumns - component.paginator = true - translateService = TestBed.inject(TranslateService) - translateService.use('en') - fixture.detectChanges() + component.viewItem.subscribe(() => console.log()) + component.editItem.subscribe(() => console.log()) + component.deleteItem.subscribe(() => console.log()) + component.viewPermission = 'VIEW' + component.editPermission = 'EDIT' + component.deletePermission = 'DELETE' + } + describe('Disable list action buttons based on field path', () => { + it('should not disable any list action button by default', async () => { + component.layout = 'list' + + expect(component.viewItemObserved).toBe(false) + expect(component.editItemObserved).toBe(false) + expect(component.deleteItemObserved).toBe(false) + + setUpListActionButtonMockData() + + expect(component.viewItemObserved).toBe(true) + expect(component.editItemObserved).toBe(true) + expect(component.deleteItemObserved).toBe(true) + + const listActions = await listGrid.getActionButtons('list') + expect(listActions.length).toBe(3) + const expectedIcons = ['pi pi-eye', 'pi pi-trash', 'pi pi-pencil'] + + for (const action of listActions) { + expect(await listGrid.actionButtonIsDisabled(action, 'list')).toBe(false) + const icon = await action.getAttribute('icon') + if (icon) { + const index = expectedIcons.indexOf(icon) + expect(index).toBeGreaterThanOrEqual(0) + expectedIcons.splice(index, 1) + } + } + + expect(expectedIcons.length).toBe(0) + }) + + it('should dynamically enable/disable an action button based on the contents of a specified field', async () => { + component.layout = 'list' + setUpListActionButtonMockData() + component.viewActionEnabledField = 'ready' + + let listActions = await listGrid.getActionButtons('list') + expect(listActions.length).toBe(3) + + for (const action of listActions) { + const icon = await action.getAttribute('icon') + const isDisabled = await listGrid.actionButtonIsDisabled(action, 'list') + if (icon === 'pi pi-eye') { + expect(isDisabled).toBe(true) + } else { + expect(isDisabled).toBe(false) + } + } + + const tempData = [...component.data] + + tempData[0]['ready'] = true + + component.data = [...tempData] + + listActions = await listGrid.getActionButtons('list') + + for (const action of listActions) { + expect(await listGrid.actionButtonIsDisabled(action, 'list')).toBe(false) + } + }) + }) + + describe('Hide list action buttons based on field path', () => { + it('should not hide any list action button by default', async () => { + component.layout = 'list' + + expect(component.viewItemObserved).toBe(false) + expect(component.editItemObserved).toBe(false) + expect(component.deleteItemObserved).toBe(false) + + setUpListActionButtonMockData() + + expect(component.viewItemObserved).toBe(true) + expect(component.editItemObserved).toBe(true) + expect(component.deleteItemObserved).toBe(true) + + const listActions = await listGrid.getActionButtons('list') + expect(listActions.length).toBe(3) + const expectedIcons = ['pi pi-eye', 'pi pi-trash', 'pi pi-pencil'] + + for (const action of listActions) { + const icon = await action.getAttribute('icon') + if (icon) { + const index = expectedIcons.indexOf(icon) + expect(index).toBeGreaterThanOrEqual(0) + expectedIcons.splice(index, 1) + } + } + + expect(expectedIcons.length).toBe(0) }) - it('should create the data list grid component', () => { - expect(component).toBeTruthy() + it('should dynamically hide/show an action button based on the contents of a specified field', async () => { + component.layout = 'list' + setUpListActionButtonMockData() + component.viewActionVisibleField = 'ready' + + let listActions = await listGrid.getActionButtons('list') + expect(listActions.length).toBe(2) + + for (const action of listActions) { + const icon = await action.getAttribute('icon') + expect(icon === 'pi pi-eye').toBe(false) + } + + const tempData = [...component.data] + + tempData[0]['ready'] = true + + component.data = [...tempData] + + listActions = await listGrid.getActionButtons('list') + + expect(listActions.length).toBe(3) }) + }) + const setUpGridActionButtonMockData = () => { + component.columns = [ + ...mockColumns, + { + columnType: ColumnType.STRING, + id: 'ready', + nameKey: 'Ready', + }, + ] + component.data = [ + { + id: 'Test', + imagePath: + 'https://images.unsplash.com/photo-1682686581427-7c80ab60e3f3?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', + property1: 'Card 1', + ready: false, + }, + ] + component.titleLineId = 'property1' + component.viewItem.subscribe(() => console.log()) + component.editItem.subscribe(() => console.log()) + component.deleteItem.subscribe(() => console.log()) + component.viewPermission = 'VIEW' + component.editPermission = 'EDIT' + component.deletePermission = 'DELETE' + } + describe('Disable grid action buttons based on field path', () => { + it('should not disable any grid action button by default', async () => { + component.layout = 'grid' + expect(component.viewItemObserved).toBe(false) + expect(component.editItemObserved).toBe(false) + expect(component.deleteItemObserved).toBe(false) + + setUpGridActionButtonMockData() + + expect(component.viewItemObserved).toBe(true) + expect(component.editItemObserved).toBe(true) + expect(component.deleteItemObserved).toBe(true) - it('loads dataListGrid', async () => { - const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) - expect(dataListGrid).toBeTruthy() + const gridMenuButton = await listGrid.getMenuButton() + + await gridMenuButton.click() + + const gridActions = await listGrid.getActionButtons('grid') + expect(gridActions.length).toBe(3) + + for (const action of gridActions) { + expect(await listGrid.actionButtonIsDisabled(action, 'grid')).toBe(false) + } }) - describe('should display the paginator currentPageReport -', () => { - it('de', async () => { - translateService.use('de') - const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) - const paginator = await dataListGrid.getPaginator() - const currentPageReport = await paginator.getCurrentPageReportText() - expect(currentPageReport).toEqual('1 - 5 von 5') - }) - - it('en', async () => { - translateService.use('en') - const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) - const paginator = await dataListGrid.getPaginator() - const currentPageReport = await paginator.getCurrentPageReportText() - expect(currentPageReport).toEqual('1 - 5 of 5') - }) + it('should dynamically enable/disable an action button based on the contents of a specified field', async () => { + component.layout = 'grid' + setUpGridActionButtonMockData() + component.viewActionEnabledField = 'ready' + const gridMenuButton = await listGrid.getMenuButton() + + await gridMenuButton.click() + + let gridActions = await listGrid.getActionButtons('grid') + expect(gridActions.length).toBe(3) + + for (const action of gridActions) { + const isDisabled = await listGrid.actionButtonIsDisabled(action, 'grid') + const text = await action.text() + if (gridActions.indexOf(action) === 0) { + expect(text).toBe('OCX_DATA_LIST_GRID.MENU.VIEW') + expect(isDisabled).toBe(true) + } else { + expect(text === 'OCX_DATA_LIST_GRID.MENU.VIEW').toBe(false) + expect(isDisabled).toBe(false) + } + } + + const tempData = [...component.data] + + tempData[0]['ready'] = true + + component.data = [...tempData] + + await gridMenuButton.click() + await gridMenuButton.click() + + gridActions = await listGrid.getActionButtons('grid') + + for (const action of gridActions) { + expect(await listGrid.actionButtonIsDisabled(action, 'grid')).toBe(false) + } }) - - describe('should display the paginator currentPageReport with totalRecordsOnServer -', () => { - it('de', async () => { - component.totalRecordsOnServer = 10 - translateService.use('de') - const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) - const paginator = await dataListGrid.getPaginator() - const currentPageReport = await paginator.getCurrentPageReportText() - expect(currentPageReport).toEqual('1 - 5 von 5 (10)') - }) - - it('en', async () => { - component.totalRecordsOnServer = 10 - translateService.use('en') - const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataListGridHarness) - const paginator = await dataListGrid.getPaginator() - const currentPageReport = await paginator.getCurrentPageReportText() - expect(currentPageReport).toEqual('1 - 5 of 5 (10)') - }) + }) + + describe('Hide grid action buttons based on field path', () => { + it('should not hide any grid action button by default', async () => { + component.layout = 'grid' + expect(component.viewItemObserved).toBe(false) + expect(component.editItemObserved).toBe(false) + expect(component.deleteItemObserved).toBe(false) + + setUpGridActionButtonMockData() + + expect(component.viewItemObserved).toBe(true) + expect(component.editItemObserved).toBe(true) + expect(component.deleteItemObserved).toBe(true) + + const gridMenuButton = await listGrid.getMenuButton() + + await gridMenuButton.click() + + const gridActions = await listGrid.getActionButtons('grid') + expect(gridActions.length).toBe(3) }) - - describe('should display the paginator rowsPerPageOptions -', () => { - it('de', async () => { - window.HTMLElement.prototype.scrollIntoView = jest.fn() - translateService.use('de') - const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataTableHarness) - const paginator = await dataListGrid.getPaginator() - const rowsPerPageOptions = await paginator.getRowsPerPageOptions() - const rowsPerPageOptionsText = await rowsPerPageOptions.selectedDropdownItemText(3) - expect(rowsPerPageOptionsText).toEqual('Alle') - }) - - it('en', async () => { - window.HTMLElement.prototype.scrollIntoView = jest.fn() - translateService.use('en') - const dataListGrid = await TestbedHarnessEnvironment.harnessForFixture(fixture, DataTableHarness) - const paginator = await dataListGrid.getPaginator() - const rowsPerPageOptions = await paginator.getRowsPerPageOptions() - const rowsPerPageOptionsText = await rowsPerPageOptions.selectedDropdownItemText(3) - expect(rowsPerPageOptionsText).toEqual('All') - }) + + it('should dynamically hide/show an action button based on the contents of a specified field', async () => { + component.layout = 'grid' + setUpGridActionButtonMockData() + component.viewActionVisibleField = 'ready' + const gridMenuButton = await listGrid.getMenuButton() + + await gridMenuButton.click() + + let gridActions = await listGrid.getActionButtons('grid') + expect(gridActions.length).toBe(2) + + let hiddenGridActions = await listGrid.getActionButtons('grid-hidden') + expect(hiddenGridActions.length).toBe(1) + expect(await hiddenGridActions[0].text()).toBe('OCX_DATA_LIST_GRID.MENU.VIEW') + + for (const action of gridActions) { + const text = await action.text() + expect(text === 'OCX_DATA_LIST_GRID.MENU.VIEW').toBe(false) + } + + const tempData = [...component.data] + + tempData[0]['ready'] = true + + component.data = [...tempData] + + await gridMenuButton.click() + await gridMenuButton.click() + gridActions = await listGrid.getActionButtons('grid') + expect(gridActions.length).toBe(3) + hiddenGridActions = await listGrid.getActionButtons('grid-hidden') + expect(hiddenGridActions.length).toBe(0) }) - + }) }) diff --git a/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.stories.ts b/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.stories.ts new file mode 100644 index 00000000..b9b0b70c --- /dev/null +++ b/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.stories.ts @@ -0,0 +1,153 @@ +import { StorybookTranslateModule } from './../../storybook-translate.module'; +import { Meta, moduleMetadata, applicationConfig, StoryFn } from '@storybook/angular'; +import { DataListGridComponent } from './data-list-grid.component' +import { ButtonModule } from 'primeng/button'; +import { MultiSelectModule } from 'primeng/multiselect'; +import { importProvidersFrom } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MockAuthModule } from '../../../mock-auth/mock-auth.module'; +import { IfPermissionDirective } from '../../directives/if-permission.directive'; +import { UserService } from '../../../services/user.service'; +import { MockUserService } from '../../../../../mocks/mock-user-service' +import { DataViewModule } from 'primeng/dataview'; +import { MenuModule } from 'primeng/menu'; +import { RouterModule } from '@angular/router'; + +const DataListGridComponentSBConfig: Meta = { + title: 'DataListGridComponent', + component: DataListGridComponent, + decorators: [ + applicationConfig({ + providers: [ + importProvidersFrom(BrowserModule), + importProvidersFrom(BrowserAnimationsModule), + { provide: UserService, useClass: MockUserService }, + importProvidersFrom(RouterModule.forRoot([], { useHash: true })), + ], + }), + moduleMetadata({ + declarations: [DataListGridComponent, IfPermissionDirective], + imports: [ + DataViewModule, + MenuModule, + ButtonModule, + MultiSelectModule, + StorybookTranslateModule, + MockAuthModule + ], + }) + ] +} +const Template: StoryFn = (args) => ({ + props: args, +}) + +const defaultComponentArgs = { + data: [ + { + id: "Test", + imagePath: "https://images.unsplash.com/photo-1682686581427-7c80ab60e3f3?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + property1: "Card 1", + available: true + }, + { + id: "Test2", + imagePath: "https://images.unsplash.com/photo-1710092662335-065cdbfb9781?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + property1: "Card 2", + available: false + }, + ], + emptyResultsMessage: "No results", + titleLineId: "property1", + layout: "list", + deleteItem: ($event: any) => console.log("Delete table row ", $event), + editItem: ($event: any) => console.log("Edit table row ", $event), + viewItem: ($event: any) => console.log("View table row ", $event), + deletePermission: 'TEST_MGMT#TEST_DELETE', + editPermission: 'TEST_MGMT#TEST_EDIT', + viewPermission: 'TEST_MGMT#TEST_VIEW', +} +const defaultArgTypes = { + deleteItem: {action: 'deleteItem'}, + editItem: {action: 'deleteItem'}, + viewItem: {action: 'deleteItem'} +} + +export const ListWithMockData = { + render: Template, + argTypes: defaultArgTypes, + args: defaultComponentArgs, +} + +export const ListWithNoData = { + render: Template, + argTypes: defaultArgTypes, + args: { + ...defaultComponentArgs, + data: [], + } +} + +export const ListWithConditionallyDisabledActionButtons = { + argTypes: defaultArgTypes, + render: Template, + args: { + ...defaultComponentArgs, + deleteActionEnabledField: "available", + editActionEnabledField: "available", + }, +} + +export const ListWithConditionallyHiddenActionButtons = { + argTypes: defaultArgTypes, + render: Template, + args: { + ...defaultComponentArgs, + deleteActionVisibleField: "available", + editActionVisibleField: "available", + }, +} + +export const GridWithMockData = { + render: Template, + argTypes: defaultArgTypes, + args: { + ...defaultComponentArgs, + layout: "grid" + }, +} + +export const GridWithNoData = { + render: Template, + argTypes: defaultArgTypes, + args: { + ...defaultComponentArgs, + data: [], + layout: "grid" + } +} + +export const GridWithConditionallyDisabledActionButtons = { + argTypes: defaultArgTypes, + render: Template, + args: { + ...defaultComponentArgs, + deleteActionEnabledField: "available", + editActionEnabledField: "available", + layout: "grid" + }, +} + +export const GridWithConditionallyHiddenActionButtons = { + argTypes: defaultArgTypes, + render: Template, + args: { + ...defaultComponentArgs, + deleteActionVisibleField: "available", + editActionVisibleField: "available", + layout: "grid" + }, +} + +export default DataListGridComponentSBConfig; \ No newline at end of file diff --git a/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.ts b/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.ts index c2c162b7..ade8a260 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.ts +++ b/libs/portal-integration-angular/src/lib/core/components/data-list-grid/data-list-grid.component.ts @@ -56,6 +56,12 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe @Input() viewPermission: string | undefined @Input() editPermission: string | undefined @Input() deletePermission: string | undefined + @Input() deleteActionVisibleField: string | undefined + @Input() deleteActionEnabledField: string | undefined + @Input() viewActionVisibleField: string | undefined + @Input() viewActionEnabledField: string | undefined + @Input() editActionVisibleField: string | undefined + @Input() editActionEnabledField: string | undefined @Input() viewMenuItemKey: string | undefined @Input() editMenuItemKey: string | undefined @Input() deleteMenuItemKey: string | undefined @@ -246,7 +252,25 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe : `./onecx-portal-lib/assets/images/${this.fallbackImage}` } - updateGridMenuItems(): void { + updateGridMenuItems(useSelectedItem = false): void { + let deleteDisabled = false; + let editDisabled = false; + let viewDisabled = false; + + let deleteVisible = true; + let editVisible = true; + let viewVisible = true; + + if(useSelectedItem && this.selectedItem) { + viewDisabled = !!this.viewActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.viewActionEnabledField); + editDisabled = !!this.editActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.editActionEnabledField); + deleteDisabled = !!this.deleteActionEnabledField && !this.fieldIsTruthy(this.selectedItem, this.deleteActionEnabledField); + + viewVisible = (!this.viewActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.viewActionVisibleField)) + editVisible = (!this.editActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.editActionVisibleField)) + deleteVisible = (!this.deleteActionVisibleField || this.fieldIsTruthy(this.selectedItem, this.deleteActionVisibleField)) + } + this.translateService .get([ this.viewMenuItemKey || 'OCX_DATA_LIST_GRID.MENU.VIEW', @@ -256,11 +280,16 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe ]) .subscribe((translations) => { let menuItems: MenuItem[] = [] + const automationId = 'data-grid-action-button' + const automationIdHidden = 'data-grid-action-button-hidden' if (this.viewItem.observed && this.userService.hasPermission(this.viewPermission || '')) { menuItems.push({ label: translations[this.viewMenuItemKey || 'OCX_DATA_LIST_GRID.MENU.VIEW'], icon: PrimeIcons.EYE, command: () => this.viewItem.emit(this.selectedItem), + disabled: viewDisabled, + visible: viewVisible, + automationId: viewVisible ? automationId : automationIdHidden }) } if (this.editItem.observed && this.userService.hasPermission(this.editPermission || '')) { @@ -268,6 +297,9 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe label: translations[this.editMenuItemKey || 'OCX_DATA_LIST_GRID.MENU.EDIT'], icon: PrimeIcons.PENCIL, command: () => this.editItem.emit(this.selectedItem), + disabled: editDisabled, + visible: editVisible, + automationId: editVisible ? automationId : automationIdHidden }) } if (this.deleteItem.observed && this.userService.hasPermission(this.deletePermission || '')) { @@ -275,6 +307,9 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe label: translations[this.deleteMenuItemKey || 'OCX_DATA_LIST_GRID.MENU.DELETE'], icon: PrimeIcons.TRASH, command: () => this.deleteItem.emit(this.selectedItem), + disabled: deleteDisabled, + visible: deleteVisible, + automationId: deleteVisible ? automationId : automationIdHidden }) } menuItems = menuItems.concat( @@ -305,4 +340,8 @@ export class DataListGridComponent extends DataSortBase implements OnInit, DoChe this.page = page this.pageChanged.emit(page) } + + fieldIsTruthy(object: any, key: any) { + return !!this.resolveFieldData(object, key) + } } diff --git a/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.html b/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.html index 1f47d1dd..2cb95ca3 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.html +++ b/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.html @@ -9,37 +9,43 @@ [ngClass]="(frozenActionColumn && actionColumnPosition === 'left') ? 'border-right-1' : (frozenActionColumn && actionColumnPosition === 'right') ? 'border-left-1' : ''" >
- + - + - + diff --git a/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.spec.ts b/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.spec.ts index e2732a4b..22119012 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.spec.ts +++ b/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.spec.ts @@ -8,6 +8,9 @@ import { ColumnType } from '../../../model/column-type.model' import { PortalCoreModule } from '../../portal-core.module' import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' import { DataTableHarness, PTableCheckboxHarness } from '../../../../../testing' +import { UserService } from '../../../services/user.service' +import { MockUserService } from '../../../../../mocks/mock-user-service' +import { MockAuthModule } from '../../../mock-auth/mock-auth.module' describe('DataTableComponent', () => { let fixture: ComponentFixture @@ -205,7 +208,9 @@ describe('DataTableComponent', () => { TranslateModule.forRoot(), TranslateTestingModule.withTranslations(TRANSLATIONS), PortalCoreModule, + MockAuthModule, ], + providers: [{ provide: UserService, useClass: MockUserService }], }).compileComponents() fixture = TestBed.createComponent(DataTableComponent) @@ -314,57 +319,223 @@ describe('DataTableComponent', () => { expect(selectedCheckBoxes.length).toBe(2) expect(unselectedCheckBoxes.length).toBe(3) }) + + it('should emit all selected elements when checkbox is clicked', async () => { + let selectionChangedEvent: Row[] | undefined + + component.selectionChanged.subscribe((event) => (selectionChangedEvent = event)) + unselectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('unchecked') + selectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('checked') + expect(unselectedCheckBoxes.length).toBe(5) + expect(selectedCheckBoxes.length).toBe(0) + expect(selectionChangedEvent).toBeUndefined() + + const firstRowCheckBox = unselectedCheckBoxes[0] + await firstRowCheckBox.checkBox() + unselectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('unchecked') + selectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('checked') + expect(unselectedCheckBoxes.length).toBe(4) + expect(selectedCheckBoxes.length).toBe(1) + expect(selectionChangedEvent).toEqual([mockData[0]]) + }) }) - it('should emit all selected elements when checkbox is clicked', async () => { - let selectionChangedEvent: Row[] | undefined - - component.selectionChanged.subscribe((event) => (selectionChangedEvent = event)) - unselectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('unchecked') - selectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('checked') - expect(unselectedCheckBoxes.length).toBe(5) - expect(selectedCheckBoxes.length).toBe(0) - expect(selectionChangedEvent).toBeUndefined() - - const firstRowCheckBox = unselectedCheckBoxes[0] - await firstRowCheckBox.checkBox() - unselectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('unchecked') - selectedCheckBoxes = await dataTable.getHarnessesForCheckboxes('checked') - expect(unselectedCheckBoxes.length).toBe(4) - expect(selectedCheckBoxes.length).toBe(1) - expect(selectionChangedEvent).toEqual([mockData[0]]) + describe('Frozen action column', () => { + it('should render an unpinnend action column on the right side of the table by default', async () => { + component.viewTableRow.subscribe((event) => console.log(event)) + + expect(component.frozenActionColumn).toBe(false) + expect(component.actionColumnPosition).toBe('right') + expect(await dataTable.getActionColumnHeader('left')).toBe(null) + expect(await dataTable.getActionColumn('left')).toBe(null) + + const rightActionColumnHeader = await dataTable.getActionColumnHeader('right') + const rightActionColumn = await dataTable.getActionColumn('right') + expect(rightActionColumnHeader).toBeTruthy() + expect(rightActionColumn).toBeTruthy() + expect(await dataTable.columnIsFrozen(rightActionColumnHeader)).toBe(false) + expect(await dataTable.columnIsFrozen(rightActionColumn)).toBe(false) + }) + + it('should render an pinned action column on the specified side of the table', async () => { + component.viewTableRow.subscribe((event) => console.log(event)) + + component.frozenActionColumn = true + component.actionColumnPosition = 'left' + + expect(await dataTable.getActionColumnHeader('right')).toBe(null) + expect(await dataTable.getActionColumn('right')).toBe(null) + + const leftActionColumnHeader = await dataTable.getActionColumnHeader('left') + const leftActionColumn = await dataTable.getActionColumn('left') + expect(leftActionColumnHeader).toBeTruthy() + expect(leftActionColumn).toBeTruthy() + expect(await dataTable.columnIsFrozen(leftActionColumnHeader)).toBe(true) + expect(await dataTable.columnIsFrozen(leftActionColumn)).toBe(true) + }) }) - it('should render an unpinnend action column on the right side of the table by default', async () => { - component.viewTableRow.subscribe((event) => console.log(event)) + const setUpActionButtonMockData = () => { + component.columns = [ + ...mockColumns, + { + columnType: ColumnType.STRING, + id: 'ready', + nameKey: 'Ready', + }, + ] + + component.rows = [ + { + version: 0, + creationDate: '2023-09-12T09:34:27.184086Z', + creationUser: '', + modificationDate: '2023-09-12T09:34:27.184086Z', + modificationUser: '', + id: 'cf9e7d6b-5362-46af-91f8-62f7ef5c6064', + name: 'name 3', + description: '', + status: 'status name 3', + responsible: '', + endDate: '2023-09-15T09:34:24Z', + startDate: '2023-09-14T09:34:22Z', + imagePath: '', + testNumber: '7.1', + ready: false, + }, + ] + component.viewTableRow.subscribe(() => console.log()) + component.editTableRow.subscribe(() => console.log()) + component.deleteTableRow.subscribe(() => console.log()) + component.viewPermission = 'VIEW' + component.editPermission = 'EDIT' + component.deletePermission = 'DELETE' + } + + describe('Disable action buttons based on field path', () => { + it('should not disable any action button by default', async () => { + expect(component.viewTableRowObserved).toBe(false) + expect(component.editTableRowObserved).toBe(false) + expect(component.deleteTableRowObserved).toBe(false) + + setUpActionButtonMockData() + + expect(component.viewTableRowObserved).toBe(true) + expect(component.editTableRowObserved).toBe(true) + expect(component.deleteTableRowObserved).toBe(true) + + const tableActions = await dataTable.getActionButtons() + expect(tableActions.length).toBe(3) + const expectedIcons = ['pi pi-eye', 'pi pi-trash', 'pi pi-pencil'] - expect(component.frozenActionColumn).toBe(false) - expect(component.actionColumnPosition).toBe('right') - expect(await dataTable.getActionColumnHeader('left')).toBe(null) - expect(await dataTable.getActionColumn('left')).toBe(null) + for (const action of tableActions) { + expect(await action.matchesSelector('.p-button:disabled')).toBe(false) + const icon = await action.getAttribute('icon') + if (icon) { + const index = expectedIcons.indexOf(icon) + expect(index).toBeGreaterThanOrEqual(0) + expectedIcons.splice(index, 1) + } + } - const rightActionColumnHeader = await dataTable.getActionColumnHeader('right') - const rightActionColumn = await dataTable.getActionColumn('right') - expect(rightActionColumnHeader).toBeTruthy() - expect(rightActionColumn).toBeTruthy() - expect(await dataTable.columnIsFrozen(rightActionColumnHeader)).toBe(false) - expect(await dataTable.columnIsFrozen(rightActionColumn)).toBe(false) + expect(expectedIcons.length).toBe(0) + }) + + it('should dynamically enable/disable an action button based on the contents of a specified column', async () => { + setUpActionButtonMockData() + component.viewActionEnabledField = 'ready' + + let tableActions = await dataTable.getActionButtons() + expect(tableActions.length).toBe(3) + + for (const action of tableActions) { + const icon = await action.getAttribute('icon') + const isDisabled = await dataTable.actionButtonIsDisabled(action) + if (icon === 'pi pi-eye') { + expect(isDisabled).toBe(true) + } else { + expect(isDisabled).toBe(false) + } + } + + const tempRows = [...component.rows] + + tempRows[0]['ready'] = true + + component.rows = [ + ...tempRows + ] + + tableActions = await dataTable.getActionButtons() + + for (const action of tableActions) { + expect(await dataTable.actionButtonIsDisabled(action)).toBe(false) + } + }) }) - it('should render an pinned action column on the specified side of the table', async () => { - component.viewTableRow.subscribe((event) => console.log(event)) + describe('Hide action buttons based on field path', () => { + it('should not hide any action button by default', async () => { + expect(component.viewTableRowObserved).toBe(false) + expect(component.editTableRowObserved).toBe(false) + expect(component.deleteTableRowObserved).toBe(false) + + setUpActionButtonMockData() + + expect(component.viewTableRowObserved).toBe(true) + expect(component.editTableRowObserved).toBe(true) + expect(component.deleteTableRowObserved).toBe(true) - component.frozenActionColumn = true - component.actionColumnPosition = 'left' + const tableActions = await dataTable.getActionButtons() + expect(tableActions.length).toBe(3) + const expectedIcons = ['pi pi-eye', 'pi pi-trash', 'pi pi-pencil'] - expect(await dataTable.getActionColumnHeader('right')).toBe(null) - expect(await dataTable.getActionColumn('right')).toBe(null) + for (const action of tableActions) { + const icon = await action.getAttribute('icon') + if (icon) { + const index = expectedIcons.indexOf(icon) + expect(index).toBeGreaterThanOrEqual(0) + expectedIcons.splice(index, 1) + } + } - const leftActionColumnHeader = await dataTable.getActionColumnHeader('left') - const leftActionColumn = await dataTable.getActionColumn('left') - expect(leftActionColumnHeader).toBeTruthy() - expect(leftActionColumn).toBeTruthy() - expect(await dataTable.columnIsFrozen(leftActionColumnHeader)).toBe(true) - expect(await dataTable.columnIsFrozen(leftActionColumn)).toBe(true) + expect(expectedIcons.length).toBe(0) + }) + + it('should dynamically hide/show an action button based on the contents of a specified column', async () => { + setUpActionButtonMockData() + component.viewActionVisibleField = 'ready' + + let tableActions = await dataTable.getActionButtons() + expect(tableActions.length).toBe(2) + + for (const action of tableActions) { + const icon = await action.getAttribute('icon') + expect(icon === 'pi pi-eye').toBe(false) + } + + const tempRows = [...component.rows] + + tempRows[0]['ready'] = true + + component.rows = [ + ...tempRows + ] + + tableActions = await dataTable.getActionButtons() + expect(tableActions.length).toBe(3) + const expectedIcons = ['pi pi-eye', 'pi pi-trash', 'pi pi-pencil'] + + for (const action of tableActions) { + const icon = await action.getAttribute('icon') + if (icon) { + const index = expectedIcons.indexOf(icon) + expect(index).toBeGreaterThanOrEqual(0) + expectedIcons.splice(index, 1) + } + } + + expect(expectedIcons.length).toBe(0) + }) }) }) diff --git a/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.stories.ts b/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.stories.ts index fc5e27c1..0445d59a 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.stories.ts +++ b/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.stories.ts @@ -8,6 +8,11 @@ import { importProvidersFrom } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ColumnType } from '../../../model/column-type.model'; +import { MockAuthModule } from '../../../mock-auth/mock-auth.module'; +import { IfPermissionDirective } from '../../directives/if-permission.directive'; +import { UserService } from '../../../services/user.service'; +import { MockUserService } from '../../../../../mocks/mock-user-service' + type DataTableInputTypes = Pick const DataTableComponentSBConfig: Meta = { title: 'DataTableComponent', @@ -16,16 +21,18 @@ const DataTableComponentSBConfig: Meta = { applicationConfig({ providers: [ importProvidersFrom(BrowserModule), - importProvidersFrom(BrowserAnimationsModule) + importProvidersFrom(BrowserAnimationsModule), + { provide: UserService, useClass: MockUserService }, ], }), moduleMetadata({ - declarations: [DataTableComponent], + declarations: [DataTableComponent, IfPermissionDirective], imports: [ TableModule, ButtonModule, MultiSelectModule, StorybookTranslateModule, + MockAuthModule ], }) ] @@ -47,23 +54,32 @@ const defaultComponentArgs: DataTableInputTypes = { columnType: ColumnType.NUMBER, nameKey: "Amount", sortable: true + }, + { + id: "available", + columnType: ColumnType.STRING, + nameKey: "Available", + sortable: false } ], rows: [ { id: 1, product: "Apples", - amount: 2 + amount: 2, + available: false }, { id: 2, product: "Bananas", - amount: 10 + amount: 10, + available: true, }, { id: 3, product: "Strawberries", - amount: 5 + amount: 5, + available: false } ], emptyResultsMessage: "No results", @@ -201,15 +217,62 @@ export const ResponsiveWithScroll = { export const ResponsiveWithScrollAndFrozenActionsColumn = { argTypes: { - deleteTableRow: {action: 'deleteTableRow'} + deleteTableRow: {action: 'deleteTableRow'}, + editTableRow: {action: 'deleteTableRow'}, + viewTableRow: {action: 'deleteTableRow'} }, render: Template, args: { ...extendedComponentArgs, deleteTableRow: ($event: any) => console.log("Delete table row ", $event), + editTableRow: ($event: any) => console.log("Edit table row ", $event), + viewTableRow: ($event: any) => console.log("View table row ", $event), + deletePermission: 'TEST_MGMT#TEST_DELETE', + editPermission: 'TEST_MGMT#TEST_EDIT', + viewPermission: 'TEST_MGMT#TEST_VIEW', frozenActionColumn: true, actionColumnPosition: 'left' }, } +export const WithConditionallyDisabledActionButtons = { + argTypes: { + deleteTableRow: {action: 'deleteTableRow'}, + editTableRow: {action: 'deleteTableRow'}, + viewTableRow: {action: 'deleteTableRow'} + }, + render: Template, + args: { + ...defaultComponentArgs, + deleteTableRow: ($event: any) => console.log("Delete table row ", $event), + editTableRow: ($event: any) => console.log("Edit table row ", $event), + viewTableRow: ($event: any) => console.log("View table row ", $event), + deleteActionEnabledField: "available", + editActionEnabledField: "available", + deletePermission: 'TEST_MGMT#TEST_DELETE', + editPermission: 'TEST_MGMT#TEST_EDIT', + viewPermission: 'TEST_MGMT#TEST_VIEW' + }, +} + +export const WithConditionallyHiddenActionButtons = { + argTypes: { + deleteTableRow: {action: 'deleteTableRow'}, + editTableRow: {action: 'deleteTableRow'}, + viewTableRow: {action: 'deleteTableRow'} + }, + render: Template, + args: { + ...defaultComponentArgs, + deleteTableRow: ($event: any) => console.log("Delete table row ", $event), + editTableRow: ($event: any) => console.log("Edit table row ", $event), + viewTableRow: ($event: any) => console.log("View table row ", $event), + deleteActionVisibleField: "available", + editActionVisibleField: "available", + deletePermission: 'TEST_MGMT#TEST_DELETE', + editPermission: 'TEST_MGMT#TEST_EDIT', + viewPermission: 'TEST_MGMT#TEST_VIEW' + }, +} + export default DataTableComponentSBConfig; \ No newline at end of file diff --git a/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.ts b/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.ts index e81deccf..f856cbef 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.ts +++ b/libs/portal-integration-angular/src/lib/core/components/data-table/data-table.component.ts @@ -19,6 +19,7 @@ import { DataAction } from '../../../model/data-action' import { DataSortDirection } from '../../../model/data-sort-direction' import { DataTableColumn } from '../../../model/data-table-column.model' import { DataSortBase } from '../data-sort-base/data-sort-base' +import { ObjectUtils } from '../../utils/objectutils' type Primitive = number | string | boolean | bigint | Date export type Row = { @@ -86,6 +87,12 @@ export class DataTableComponent extends DataSortBase implements OnInit { @Input() deletePermission: string | undefined @Input() viewPermission: string | undefined @Input() editPermission: string | undefined + @Input() deleteActionVisibleField: string | undefined + @Input() deleteActionEnabledField: string | undefined + @Input() viewActionVisibleField: string | undefined + @Input() viewActionEnabledField: string | undefined + @Input() editActionVisibleField: string | undefined + @Input() editActionEnabledField: string | undefined @Input() paginator = true @Input() page = 0 @Input() @@ -332,4 +339,8 @@ export class DataTableComponent extends DataSortBase implements OnInit { this.page = page this.pageChanged.emit(page) } + + fieldIsTruthy(object: any, key: any) { + return !!(ObjectUtils.resolveFieldData(object, key)) + } } diff --git a/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.html b/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.html index 496948c0..20934a1c 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.html +++ b/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.html @@ -21,6 +21,12 @@ [deletePermission]="deletePermission" [editPermission]="editPermission" [viewPermission]="viewPermission" + [deleteActionEnabledField]="deleteActionEnabledField" + [deleteActionVisibleField]="deleteActionVisibleField" + [editActionEnabledField]="editActionEnabledField" + [editActionVisibleField]="editActionVisibleField" + [viewActionEnabledField]="viewActionEnabledField" + [viewActionVisibleField]="viewActionVisibleField" [additionalActions]="additionalActions" [gridItemSubtitleLinesTemplate]="_gridItemSubtitleLines ? gridItemSubtitleLines : undefined" [listItemSubtitleLinesTemplate]="_listItemSubtitleLines ? listItemSubtitleLines : undefined" @@ -79,6 +85,12 @@ [deletePermission]="deletePermission" [editPermission]="editPermission" [viewPermission]="viewPermission" + [deleteActionEnabledField]="deleteActionEnabledField" + [deleteActionVisibleField]="deleteActionVisibleField" + [editActionEnabledField]="editActionEnabledField" + [editActionVisibleField]="editActionVisibleField" + [viewActionEnabledField]="viewActionEnabledField" + [viewActionVisibleField]="viewActionVisibleField" [additionalActions]="additionalActions" [stringCellTemplate]="_stringTableCell ? stringCell : undefined" [numberCellTemplate]="_numberTableCell ? numberCell : undefined" diff --git a/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.spec.ts b/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.spec.ts index e43600de..9e8b137e 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.spec.ts +++ b/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.spec.ts @@ -11,6 +11,10 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' import { DataListGridHarness, DataTableHarness, DataViewHarness } from '../../../../../testing' import { ColumnType } from '../../../model/column-type.model' import { PortalCoreModule } from '../../portal-core.module' +import { UserService } from '../../../services/user.service' +import { MockUserService } from '../../../../../mocks/mock-user-service' +import { ActivatedRoute, RouterModule } from '@angular/router' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' describe('DataViewComponent', () => { let component: DataViewComponent @@ -196,7 +200,22 @@ describe('DataViewComponent', () => { TranslateTestingModule.withTranslations(TRANSLATIONS), HttpClientTestingModule, PortalCoreModule, + RouterModule, + NoopAnimationsModule ], + providers: [ + { provide: UserService, useClass: MockUserService }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: { + get: () => '1', + }, + }, + }, + }, + ] }).compileComponents() fixture = TestBed.createComponent(DataViewComponent) @@ -309,4 +328,146 @@ describe('DataViewComponent', () => { const dataTableRaport = await dataTablePaginator.getCurrentPageReportText() expect(dataTableRaport).toEqual('11 - 11 of 11') }) + + describe('Dynamically disable/hide based on field path in data view', () => { + const setUpMockData = (viewType: 'grid' | 'list' | 'table') => { + component.viewItem.subscribe(() => console.log()) + component.editItem.subscribe(() => console.log()) + component.deleteItem.subscribe(() => console.log()) + component.viewPermission = 'VIEW' + component.editPermission = 'EDIT' + component.deletePermission = 'DELETE' + component.layout = viewType + component.columns = [ + { + columnType: ColumnType.STRING, + id: 'name', + nameKey: 'COLUMN_HEADER_NAME.NAME', + }, + { + columnType: ColumnType.STRING, + id: 'ready', + nameKey: 'Ready', + }, + ] + component.data = [ + { + id: 'Test', + imagePath: + 'https://images.unsplash.com/photo-1682686581427-7c80ab60e3f3?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', + name: 'Card 1', + ready: false, + }, + ] + component.titleLineId = 'name' + } + + describe('Disable list action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('list') + const dataView = await dataViewHarness.getDataListGrid() + expect(await dataView.hasAmountOfActionButtons('list', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('list', 0)).toBe(true) + }) + + it('should disable a button based on a given field path', async() => { + setUpMockData('list') + component.viewActionEnabledField = 'ready' + const dataView = await dataViewHarness.getDataListGrid() + expect(await dataView.hasAmountOfActionButtons('list', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('list', 1)).toBe(true) + }) + }) + + describe('Disable grid action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('grid') + const dataView = await dataViewHarness.getDataListGrid() + await (await dataView.getMenuButton()).click() + expect(await dataView.hasAmountOfActionButtons('grid', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('grid', 0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('grid') + component.viewActionEnabledField = 'ready' + const dataView = await dataViewHarness.getDataListGrid() + await (await dataView.getMenuButton()).click() + expect(await dataView.hasAmountOfActionButtons('grid', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('grid', 1)).toBe(true) + }) + }) + + describe('Disable table action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('table') + const dataTable = await dataViewHarness.getDataTable() + expect(await dataTable.hasAmountOfActionButtons(3)).toBe(true) + expect(await dataTable.hasAmountOfDisabledActionButtons(0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('table') + component.viewActionEnabledField = 'ready' + const dataTable = await dataViewHarness.getDataTable() + expect(await dataTable.hasAmountOfActionButtons(3)).toBe(true) + expect(await dataTable.hasAmountOfDisabledActionButtons(1)).toBe(true) + }) + }) + + describe('Hide list action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('list') + const dataView = await dataViewHarness.getDataListGrid() + expect(await dataView.hasAmountOfActionButtons('list', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('list', 0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('list') + component.viewActionVisibleField = 'ready' + const dataView = await dataViewHarness.getDataListGrid() + expect(await dataView.hasAmountOfActionButtons('list', 2)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('list', 0)).toBe(true) + }) + }) + + describe('Hide grid action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('grid') + const dataView = await dataViewHarness.getDataListGrid() + await (await dataView.getMenuButton()).click() + expect(await dataView.hasAmountOfActionButtons('grid', 3)).toBe(true) + expect(await dataView.hasAmountOfActionButtons('grid-hidden', 0)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('grid', 0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('grid') + component.viewActionVisibleField = 'ready' + const dataView = await dataViewHarness.getDataListGrid() + await (await dataView.getMenuButton()).click() + expect(await dataView.hasAmountOfActionButtons('grid', 2)).toBe(true) + expect(await dataView.hasAmountOfActionButtons('grid-hidden', 1)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('grid', 0)).toBe(true) + }) + }) + + describe('Hide table action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('table') + const dataTable = await dataViewHarness.getDataTable() + expect(await dataTable.hasAmountOfActionButtons(3)).toBe(true) + expect(await dataTable.hasAmountOfDisabledActionButtons(0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('table') + component.viewActionVisibleField = 'ready' + const dataTable = await dataViewHarness.getDataTable() + expect(await dataTable.hasAmountOfActionButtons(2)).toBe(true) + expect(await dataTable.hasAmountOfDisabledActionButtons(0)).toBe(true) + }) + }) + }) }) diff --git a/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.ts b/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.ts index 6abf645d..83550401 100644 --- a/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.ts +++ b/libs/portal-integration-angular/src/lib/core/components/data-view/data-view.component.ts @@ -44,6 +44,12 @@ export class DataViewComponent implements DoCheck, OnInit { @Input() deletePermission: string | undefined @Input() editPermission: string | undefined @Input() viewPermission: string | undefined + @Input() deleteActionVisibleField: string | undefined + @Input() deleteActionEnabledField: string | undefined + @Input() viewActionVisibleField: string | undefined + @Input() viewActionEnabledField: string | undefined + @Input() editActionVisibleField: string | undefined + @Input() editActionEnabledField: string | undefined @Input() data: RowListGridData[] = [] @Input() name = 'Data table' @Input() titleLineId: string | undefined diff --git a/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.html b/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.html index 7fc37039..5df5084e 100644 --- a/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.html +++ b/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.html @@ -58,6 +58,12 @@ [deletePermission]="deletePermission" [editPermission]="editPermission" [viewPermission]="viewPermission" + [deleteActionEnabledField]="deleteActionEnabledField" + [deleteActionVisibleField]="deleteActionVisibleField" + [editActionEnabledField]="editActionEnabledField" + [editActionVisibleField]="editActionVisibleField" + [viewActionEnabledField]="viewActionEnabledField" + [viewActionVisibleField]="viewActionVisibleField" [additionalActions]="additionalActions" [listGridPaginator]="listGridPaginator" [tablePaginator]="tablePaginator" diff --git a/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.spec.ts b/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.spec.ts index 7ab94e9c..b6c62bef 100644 --- a/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.spec.ts +++ b/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.spec.ts @@ -1539,4 +1539,145 @@ describe('InteractiveDataViewComponent', () => { expect(listItemsData).toEqual(expectedFilteredListItemsData) }) }) + describe('Dynamically disable/hide based on field path in interactive data view', () => { + const setUpMockData = (viewType: 'grid' | 'list' | 'table') => { + component.viewItem.subscribe(() => console.log()) + component.editItem.subscribe(() => console.log()) + component.deleteItem.subscribe(() => console.log()) + component.viewPermission = 'VIEW' + component.editPermission = 'EDIT' + component.deletePermission = 'DELETE' + component.layout = viewType + component.columns = [ + { + columnType: ColumnType.STRING, + id: 'name', + nameKey: 'COLUMN_HEADER_NAME.NAME', + }, + { + columnType: ColumnType.STRING, + id: 'ready', + nameKey: 'Ready', + }, + ] + component.data = [ + { + id: 'Test', + imagePath: + 'https://images.unsplash.com/photo-1682686581427-7c80ab60e3f3?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', + name: 'Card 1', + ready: false, + }, + ] + component.titleLineId = 'name' + } + + describe('Disable list action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('list') + const dataView = await (await interactiveDataViewHarness.getDataView()).getDataListGrid() + expect(await dataView.hasAmountOfActionButtons('list', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('list', 0)).toBe(true) + }) + + it('should disable a button based on a given field path', async() => { + setUpMockData('list') + component.viewActionEnabledField = 'ready' + const dataView = await (await interactiveDataViewHarness.getDataView()).getDataListGrid() + expect(await dataView.hasAmountOfActionButtons('list', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('list', 1)).toBe(true) + }) + }) + + describe('Disable grid action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('grid') + const dataView = await (await interactiveDataViewHarness.getDataView()).getDataListGrid() + await (await dataView.getMenuButton()).click() + expect(await dataView.hasAmountOfActionButtons('grid', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('grid', 0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('grid') + component.viewActionEnabledField = 'ready' + const dataView = await (await interactiveDataViewHarness.getDataView()).getDataListGrid() + await (await dataView.getMenuButton()).click() + expect(await dataView.hasAmountOfActionButtons('grid', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('grid', 1)).toBe(true) + }) + }) + + describe('Disable table action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('table') + const dataTable = await (await interactiveDataViewHarness.getDataView()).getDataTable() + expect(await dataTable.hasAmountOfActionButtons(3)).toBe(true) + expect(await dataTable.hasAmountOfDisabledActionButtons(0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('table') + component.viewActionEnabledField = 'ready' + const dataTable = await (await interactiveDataViewHarness.getDataView()).getDataTable() + expect(await dataTable.hasAmountOfActionButtons(3)).toBe(true) + expect(await dataTable.hasAmountOfDisabledActionButtons(1)).toBe(true) + }) + }) + + describe('Hide list action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('list') + const dataView = await (await interactiveDataViewHarness.getDataView()).getDataListGrid() + expect(await dataView.hasAmountOfActionButtons('list', 3)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('list', 0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('list') + component.viewActionVisibleField = 'ready' + const dataView = await (await interactiveDataViewHarness.getDataView()).getDataListGrid() + expect(await dataView.hasAmountOfActionButtons('list', 2)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('list', 0)).toBe(true) + }) + }) + + describe('Hide grid action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('grid') + const dataView = await (await interactiveDataViewHarness.getDataView()).getDataListGrid() + await (await dataView.getMenuButton()).click() + expect(await dataView.hasAmountOfActionButtons('grid', 3)).toBe(true) + expect(await dataView.hasAmountOfActionButtons('grid-hidden', 0)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('grid', 0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('grid') + component.viewActionVisibleField = 'ready' + const dataView = await (await interactiveDataViewHarness.getDataView()).getDataListGrid() + await (await dataView.getMenuButton()).click() + expect(await dataView.hasAmountOfActionButtons('grid', 2)).toBe(true) + expect(await dataView.hasAmountOfActionButtons('grid-hidden', 1)).toBe(true) + expect(await dataView.hasAmountOfDisabledActionButtons('grid', 0)).toBe(true) + }) + }) + + describe('Hide table action buttons based on field path', () => { + it('should not disable any buttons initially', async () => { + setUpMockData('table') + const dataTable = await (await interactiveDataViewHarness.getDataView()).getDataTable() + expect(await dataTable.hasAmountOfActionButtons(3)).toBe(true) + expect(await dataTable.hasAmountOfDisabledActionButtons(0)).toBe(true) + }) + + it('should disable a button based on a given field path', async () => { + setUpMockData('table') + component.viewActionVisibleField = 'ready' + const dataTable = await (await interactiveDataViewHarness.getDataView()).getDataTable() + expect(await dataTable.hasAmountOfActionButtons(2)).toBe(true) + expect(await dataTable.hasAmountOfDisabledActionButtons(0)).toBe(true) + }) + }) + }) }) diff --git a/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.ts b/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.ts index 65cbb6e8..9d0571d0 100644 --- a/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.ts +++ b/libs/portal-integration-angular/src/lib/core/components/interactive-data-view/interactive-data-view.component.ts @@ -29,6 +29,12 @@ export class InteractiveDataViewComponent implements OnInit { @Input() deletePermission: string | undefined @Input() editPermission: string | undefined @Input() viewPermission: string | undefined + @Input() deleteActionVisibleField: string | undefined + @Input() deleteActionEnabledField: string | undefined + @Input() viewActionVisibleField: string | undefined + @Input() viewActionEnabledField: string | undefined + @Input() editActionVisibleField: string | undefined + @Input() editActionEnabledField: string | undefined @Input() name = 'Data' @Input() titleLineId: string | undefined @Input() subtitleLineIds: string[] = [] diff --git a/libs/portal-integration-angular/testing/data-list-grid.harness.ts b/libs/portal-integration-angular/testing/data-list-grid.harness.ts index 86e0be0e..c5d1fda4 100644 --- a/libs/portal-integration-angular/testing/data-list-grid.harness.ts +++ b/libs/portal-integration-angular/testing/data-list-grid.harness.ts @@ -1,4 +1,4 @@ -import { ContentContainerComponentHarness } from '@angular/cdk/testing' +import { ContentContainerComponentHarness, TestElement } from '@angular/cdk/testing' import { DefaultGridItemHarness } from './default-grid-item.harness' import { DefaultListItemHarness } from './default-list-item.harness' import { PPaginatorHarness } from './primeng/p-paginator.harness' @@ -9,4 +9,37 @@ export class DataListGridHarness extends ContentContainerComponentHarness { getDefaultGridItems = this.locatorForAll(DefaultGridItemHarness) getDefaultListItems = this.locatorForAll(DefaultListItemHarness) getPaginator = this.locatorFor(PPaginatorHarness) + getMenuButton = this.locatorFor(`[name="data-grid-item-menu-button"]`) + + async getActionButtons(actionButtonType: 'list' | 'grid' | 'grid-hidden') { + if(actionButtonType === 'list') { + return await this.locatorForAll(`[name="data-list-action-button"]`)() + } else if (actionButtonType === 'grid-hidden') { + return await this.documentRootLocatorFactory().locatorForAll(`[data-automationid="data-grid-action-button-hidden"]`)() + } else { + return await this.documentRootLocatorFactory().locatorForAll(`[data-automationid="data-grid-action-button"]`)() + } + } + + async actionButtonIsDisabled(actionButton: TestElement, viewType: 'list' | 'grid') { + if(viewType === 'list') { + return await actionButton.getProperty("disabled") + } else { + return await actionButton.hasClass("p-disabled") + } + } + + async hasAmountOfActionButtons(actionButtonType: 'list' | 'grid' | 'grid-hidden', amount: number) { + return (await this.getActionButtons(actionButtonType)).length === amount + } + + async hasAmountOfDisabledActionButtons(viewType: 'list' | 'grid', amount: number) { + let disabledActionButtons = []; + if(viewType === 'list') { + disabledActionButtons = await this.documentRootLocatorFactory().locatorForAll(`[name="data-list-action-button"]:disabled`)() + } else { + disabledActionButtons = await this.documentRootLocatorFactory().locatorForAll(`li.p-menuitem>a.p-menuitem-link.p-disabled`)() + } + return disabledActionButtons.length === amount + } } diff --git a/libs/portal-integration-angular/testing/data-table.harness.ts b/libs/portal-integration-angular/testing/data-table.harness.ts index dbf9b196..b2f302c5 100644 --- a/libs/portal-integration-angular/testing/data-table.harness.ts +++ b/libs/portal-integration-angular/testing/data-table.harness.ts @@ -19,11 +19,11 @@ export class DataTableHarness extends ContentContainerComponentHarness { async getHarnessesForCheckboxes(type: 'all' | 'checked' | 'unchecked'): Promise { let checkBoxHarnesses: PTableCheckboxHarness[] if (type === 'checked') { - checkBoxHarnesses = await this.getAllHarnesses(PTableCheckboxHarness.with({isSelected: true})) + checkBoxHarnesses = await this.getAllHarnesses(PTableCheckboxHarness.with({ isSelected: true })) return checkBoxHarnesses } if (type === 'unchecked') { - checkBoxHarnesses = await this.getAllHarnesses(PTableCheckboxHarness.with({isSelected: false})) + checkBoxHarnesses = await this.getAllHarnesses(PTableCheckboxHarness.with({ isSelected: false })) return checkBoxHarnesses } else { checkBoxHarnesses = await this.getAllHarnesses(PTableCheckboxHarness) @@ -32,15 +32,35 @@ export class DataTableHarness extends ContentContainerComponentHarness { } async getActionColumnHeader(position: 'left' | 'right') { - return await this.locatorForOptional(`[name="action-column-header-${position}"]`)() + return await this.locatorForOptional(`[name="action-column-header-${position}"]`)() } async getActionColumn(position: 'left' | 'right') { return await this.locatorForOptional(`[name="action-column-${position}"]`)() } + async getActionButtons() { + return await this.locatorForAll(`[name="data-table-action-button"]`)() + } + + async actionButtonIsDisabled(actionButton: TestElement) { + const isDisabled = await actionButton.getProperty('disabled') + return isDisabled + } + + async hasAmountOfActionButtons(amount: number) { + return (await this.getActionButtons()).length === amount + } + + async hasAmountOfDisabledActionButtons(amount: number) { + const disabledActionButtons = await this.documentRootLocatorFactory().locatorForAll( + `[name="data-table-action-button"]:disabled` + )() + return disabledActionButtons.length === amount + } + async columnIsFrozen(column: TestElement | null) { - if(column == null) { + if (column == null) { throw new Error('Given column is null') } return await column.hasClass('p-frozen-column')