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: {