mirror of
https://github.com/SuiteCRM/SuiteCRM-Core.git
synced 2025-08-29 11:00:40 +08:00
Add modal button group component
Component is meant to by used by the modals that have buttons Allows to implement a dynamic way to declare modal buttons. Each button onClick call back receives the activeModal. Allowing to close or dismiss the modal on the onClick callback - Re-use scrm-button-group - Extend button group and button interfaces -- Override the onClick handler to receiving the active modal -- Inject the active modal to the button callbacks - Allow for a configurable breakpoint using the config: -- ui.modal_buttons_collapse_breakpoint - Add karma/jasmine tests
This commit is contained in:
parent
b82d7097b7
commit
690162a2ce
7 changed files with 312 additions and 0 deletions
|
@ -1,3 +1,4 @@
|
||||||
parameters:
|
parameters:
|
||||||
ui:
|
ui:
|
||||||
alert_timeout: 3
|
alert_timeout: 3
|
||||||
|
modal_buttons_collapse_breakpoint: 4
|
||||||
|
|
29
core/app/src/app-common/components/modal/modal.model.ts
Normal file
29
core/app/src/app-common/components/modal/modal.model.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<scrm-button-group [config$]="buttonGroup$"></scrm-button-group>
|
|
@ -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: '<scrm-modal-button-group [config$]="config" [activeModal]="activeModal"></scrm-modal-button-group>'
|
||||||
|
})
|
||||||
|
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<ModalButtonGroupInterface> = 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<ModalButtonGroupTestHostComponent>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
|
@ -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<ModalButtonGroupInterface>;
|
||||||
|
@Input() activeModal: NgbActiveModal = null;
|
||||||
|
|
||||||
|
buttonGroup$: Observable<ButtonGroupInterface>;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -110,6 +110,7 @@ export const systemConfigMockData = {
|
||||||
value: null,
|
value: null,
|
||||||
items: {
|
items: {
|
||||||
alert_timeout: 3,
|
alert_timeout: 3,
|
||||||
|
modal_buttons_collapse_breakpoint: 4,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
date_format: {
|
date_format: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue