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:
|
||||
ui:
|
||||
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,
|
||||
items: {
|
||||
alert_timeout: 3,
|
||||
modal_buttons_collapse_breakpoint: 4,
|
||||
}
|
||||
},
|
||||
date_format: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue