diff --git a/config/services/ui/ui.yaml b/config/services/ui/ui.yaml index 20d3b389e..ff459fa09 100644 --- a/config/services/ui/ui.yaml +++ b/config/services/ui/ui.yaml @@ -1,3 +1,4 @@ parameters: ui: alert_timeout: 3 + modal_buttons_collapse_breakpoint: 4 diff --git a/core/app/src/app-common/components/modal/modal.model.ts b/core/app/src/app-common/components/modal/modal.model.ts new file mode 100644 index 000000000..93cb88fe7 --- /dev/null +++ b/core/app/src/app-common/components/modal/modal.model.ts @@ -0,0 +1,29 @@ +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {ButtonInterface} from '@app-common/components/button/button.model'; +import {DropdownButtonInterface} from '@app-common/components/button/dropdown-button.model'; +import {DropdownOptions} from '@app-common/components/button/button-group.model'; + +export declare type ModalButtonCallback = (activeModal: NgbActiveModal) => void; + +export type AnyModalButtonInterface = ModalButtonInterface | ModalDropdownButtonInterface; + +export interface ModalButtonInterface extends ButtonInterface { + onClick?: ModalButtonCallback; +} + +export interface ModalDropdownButtonInterface extends DropdownButtonInterface { + items?: AnyModalButtonInterface[]; +} + +export interface ModalCloseFeedBack { + type: string; +} + +export interface ModalButtonGroupInterface { + wrapperKlass?: string[]; + buttonKlass?: string[]; + buttons?: AnyModalButtonInterface[]; + dropdownLabel?: string; + dropdownOptions?: DropdownOptions; + breakpoint?: number; +} diff --git a/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.html b/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.html new file mode 100644 index 000000000..fa58f6ed7 --- /dev/null +++ b/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.html @@ -0,0 +1 @@ + diff --git a/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.spec.ts b/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.spec.ts new file mode 100644 index 000000000..834c0afa1 --- /dev/null +++ b/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.spec.ts @@ -0,0 +1,195 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {Component} from '@angular/core'; +import {Observable, of} from 'rxjs'; +import {shareReplay} from 'rxjs/operators'; +import {ModalButtonGroupInterface} from '@app-common/components/modal/modal.model'; +import {ModalButtonGroupModule} from '@components/modal/components/modal-button-group/modal-button-group.module'; +import {LanguageStore} from '@store/language/language.store'; +import {languageStoreMock} from '@store/language/language.store.spec.mock'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {SystemConfigStore} from '@store/system-config/system-config.store'; +import {systemConfigStoreMock} from '@store/system-config/system-config.store.spec.mock'; + +@Component({ + selector: 'modal-button-group-test-host-component', + template: '' +}) +class ModalButtonGroupTestHostComponent { + closeClicked = 0; + dismissClicked = 0; + activeModal = { + close: (): void => { + this.closeClicked++; + }, + dismiss: (): void => { + this.dismissClicked++; + } + } as NgbActiveModal; + clicked = 0; + clickedItem1 = 0; + clickedItem2 = 0; + clickedItem3 = 0; + clickedItem4 = 0; + clickedItem5 = 0; + clickedItem6 = 0; + config: Observable = of({ + wrapperKlass: ['some-class'], + dropdownLabel: 'Test Dropdown Label', + buttons: [ + { + label: 'Item 1', + onClick: (activeModal: NgbActiveModal): void => { + if (activeModal && activeModal.close) { + activeModal.close(); + } + this.clickedItem1++; + } + }, + { + label: 'Item 2', + onClick: (activeModal: NgbActiveModal): void => { + if (activeModal && activeModal.dismiss) { + activeModal.dismiss(); + } + this.clickedItem2++; + } + }, + { + label: 'Item 3', + onClick: (): void => { + this.clickedItem3++; + } + }, + { + label: 'Item 4', + onClick: (): void => { + this.clickedItem4++; + } + }, + { + label: 'Item 5', + onClick: (): void => { + this.clickedItem5++; + } + }, + { + label: 'Item 6', + onClick: (): void => { + this.clickedItem6++; + } + }, + ] + }).pipe(shareReplay(1)); +} + +describe('ModalButtonGroupComponent', () => { + + let testHostComponent: ModalButtonGroupTestHostComponent; + let testHostFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ModalButtonGroupTestHostComponent, + ], + imports: [ + ModalButtonGroupModule + ], + providers: [ + {provide: LanguageStore, useValue: languageStoreMock}, + {provide: SystemConfigStore, useValue: systemConfigStoreMock} + ], + }).compileComponents(); + + testHostFixture = TestBed.createComponent(ModalButtonGroupTestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + })); + + it('should create', () => { + expect(testHostComponent).toBeTruthy(); + }); + + it('buttons should be clickable', async(() => { + expect(testHostComponent).toBeTruthy(); + const buttons = testHostFixture.nativeElement.getElementsByClassName('button-group-button'); + + expect(buttons).toBeTruthy(); + expect(buttons.length).toEqual(5); + + buttons.item(2).click(); + buttons.item(3).click(); + + testHostFixture.detectChanges(); + testHostFixture.whenStable().then(() => { + + expect(testHostComponent.clickedItem3).toEqual(1); + expect(testHostComponent.clickedItem4).toEqual(1); + }); + })); + + it('buttons should be clickable and call close modal', async(() => { + expect(testHostComponent).toBeTruthy(); + const buttons = testHostFixture.nativeElement.getElementsByClassName('button-group-button'); + + expect(buttons).toBeTruthy(); + expect(buttons.length).toEqual(5); + + buttons.item(0).click(); + + testHostFixture.detectChanges(); + testHostFixture.whenStable().then(() => { + + expect(testHostComponent.clickedItem1).toEqual(1); + expect(testHostComponent.closeClicked).toEqual(1); + }); + })); + + it('buttons should be clickable and call dismiss modal', async(() => { + expect(testHostComponent).toBeTruthy(); + const buttons = testHostFixture.nativeElement.getElementsByClassName('button-group-button'); + + expect(buttons).toBeTruthy(); + expect(buttons.length).toEqual(5); + + buttons.item(1).click(); + + testHostFixture.detectChanges(); + testHostFixture.whenStable().then(() => { + + expect(testHostComponent.clickedItem2).toEqual(1); + expect(testHostComponent.dismissClicked).toEqual(1); + }); + })); + + it('dropdown items should be clickable', async(() => { + expect(testHostComponent).toBeTruthy(); + + const element = testHostFixture.nativeElement; + const buttonWrapper = element.getElementsByClassName('button-group-dropdown').item(0); + const button = buttonWrapper.getElementsByClassName('dropdown-toggle button-group-button').item(0); + const divElement = element.querySelector('scrm-dropdown-button'); + + testHostComponent.clicked = 0; + + expect(button).toBeTruthy(); + + button.click(); + testHostFixture.detectChanges(); + testHostFixture.whenStable().then(() => { + + const links = divElement.getElementsByClassName('dropdown-item'); + + expect(links.length).toEqual(2); + links.item(0).click(); + links.item(1).click(); + + testHostFixture.detectChanges(); + testHostFixture.whenStable().then(() => { + expect(testHostComponent.clickedItem5).toEqual(1); + expect(testHostComponent.clickedItem6).toEqual(1); + }); + }); + })); +}); diff --git a/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.ts b/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.ts new file mode 100644 index 000000000..eda1fca90 --- /dev/null +++ b/core/app/src/components/modal/components/modal-button-group/modal-button-group.component.ts @@ -0,0 +1,68 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Observable} from 'rxjs'; +import {ButtonGroupInterface} from '@app-common/components/button/button-group.model'; +import {ModalButtonGroupInterface} from '@app-common/components/modal/modal.model'; +import {deepClone} from '@app-common/utils/object-utils'; +import {map} from 'rxjs/operators'; +import {ButtonUtils} from '@components/button/button.utils'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import * as _ from 'lodash'; +import {SystemConfigStore} from '@store/system-config/system-config.store'; + +@Component({ + selector: 'scrm-modal-button-group', + templateUrl: './modal-button-group.component.html', + styleUrls: [] +}) +export class ModalButtonGroupComponent implements OnInit { + + @Input() config$: Observable; + @Input() activeModal: NgbActiveModal = null; + + buttonGroup$: Observable; + protected defaultButtonGroup: ButtonGroupInterface = { + breakpoint: 4, + wrapperKlass: ['modal-buttons'], + buttonKlass: ['modal-button', 'btn', 'btn-sm'], + buttons: [] + }; + + constructor( + protected buttonUtils: ButtonUtils, + protected config: SystemConfigStore, + ) { + const ui = this.config.getConfigValue('ui'); + if (ui && ui.modal_button_group_breakpoint) { + this.defaultButtonGroup.breakpoint = ui.modal_buttons_collapse_breakpoint; + } + } + + ngOnInit(): void { + + if (this.config$) { + this.buttonGroup$ = this.config$.pipe( + map((config: ButtonGroupInterface) => this.mapButtonGroup(config)) + ); + } + } + + protected mapButtonGroup(config: ButtonGroupInterface): ButtonGroupInterface { + const group = _.defaults({...config}, deepClone(this.defaultButtonGroup)); + + this.mapButtons(group); + + return group; + } + + protected mapButtons(group: ButtonGroupInterface): void { + const buttons = group.buttons || []; + group.buttons = []; + + if (buttons.length > 0) { + buttons.forEach(modalButton => { + const button = this.buttonUtils.addOnClickPartial(modalButton, this.activeModal); + group.buttons.push(button); + }); + } + } +} diff --git a/core/app/src/components/modal/components/modal-button-group/modal-button-group.module.ts b/core/app/src/components/modal/components/modal-button-group/modal-button-group.module.ts new file mode 100644 index 000000000..c0ae32358 --- /dev/null +++ b/core/app/src/components/modal/components/modal-button-group/modal-button-group.module.ts @@ -0,0 +1,17 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ModalButtonGroupComponent} from './modal-button-group.component'; +import {ButtonGroupModule} from '@components/button-group/button-group.module'; + +@NgModule({ + declarations: [ModalButtonGroupComponent], + exports: [ + ModalButtonGroupComponent + ], + imports: [ + CommonModule, + ButtonGroupModule + ] +}) +export class ModalButtonGroupModule { +} diff --git a/core/app/src/store/system-config/system-config.store.spec.mock.ts b/core/app/src/store/system-config/system-config.store.spec.mock.ts index f98990b92..46aa405a3 100644 --- a/core/app/src/store/system-config/system-config.store.spec.mock.ts +++ b/core/app/src/store/system-config/system-config.store.spec.mock.ts @@ -110,6 +110,7 @@ export const systemConfigMockData = { value: null, items: { alert_timeout: 3, + modal_buttons_collapse_breakpoint: 4, } }, date_format: {