Add message modal component

- Add re-usable message modal component
-- Allow for passing a message labelKey
-- Allow for configuring an array of buttons
- Declare component as an entry component
- Add karma/jasmine tests
This commit is contained in:
Clemente Raposo 2021-01-11 17:31:07 +00:00 committed by Dillon-Brown
parent 690162a2ce
commit b6bbbf26ed
6 changed files with 258 additions and 2 deletions

View file

@ -40,6 +40,8 @@ import {ColumnChooserComponent} from '@components/columnchooser/columnchooser.co
import {AppInit} from '@app/app-initializer';
import {AuthService} from '@services/auth/auth.service';
import {GraphQLError} from 'graphql';
import {MessageModalComponent} from '@components/modal/components/message-modal/message-modal.component';
import {MessageModalModule} from '@components/modal/components/message-modal/message-modal.module';
export const initializeApp = (appInitService: AppInit) => (): Promise<any> => appInitService.init();
@ -67,7 +69,8 @@ export const initializeApp = (appInitService: AppInit) => (): Promise<any> => ap
ImageModule,
BrowserAnimationsModule,
NgbModule,
FullPageSpinnerModule
FullPageSpinnerModule,
MessageModalModule
],
providers: [
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
@ -82,7 +85,7 @@ export const initializeApp = (appInitService: AppInit) => (): Promise<any> => ap
}
],
bootstrap: [AppComponent],
entryComponents: [ColumnChooserComponent]
entryComponents: [ColumnChooserComponent, MessageModalComponent]
})
export class AppModule {
constructor(apollo: Apollo, httpLink: HttpLink, protected auth: AuthService) {

View file

@ -0,0 +1,15 @@
<scrm-modal [closable]="true"
[close]="closeButton"
[title]="titleKey"
klass="message-modal">
<div class="p-3" modal-body>
<span><scrm-label [labelKey]="textKey"></scrm-label></span>
</div>
<div modal-footer>
<scrm-modal-button-group [activeModal]="activeModal"
[config$]="buttonGroup$">
</scrm-modal-button-group>
</div>
</scrm-modal>

View file

@ -0,0 +1,159 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MessageModalComponent} from './message-modal.component';
import {MessageModalModule} from '@components/modal/components/message-modal/message-modal.module';
import {LanguageStore} from '@store/language/language.store';
import {languageStoreMock} from '@store/language/language.store.spec.mock';
import {SystemConfigStore} from '@store/system-config/system-config.store';
import {systemConfigStoreMock} from '@store/system-config/system-config.store.spec.mock';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {Component, OnInit} from '@angular/core';
import {ModalButtonInterface} from '@app-common/components/modal/modal.model';
import {NgbModalRef} from '@ng-bootstrap/ng-bootstrap/modal/modal-ref';
@Component({
selector: 'messages-modal-test-host-component',
template: '<div></div>'
})
class MessageModalTestHostComponent implements OnInit {
modal: NgbModalRef;
cancelClicked = 0;
okClicked = 0;
constructor(public modalService: NgbModal) {
}
ngOnInit(): void {
this.modal = this.modalService.open(MessageModalComponent);
this.modal.componentInstance.textKey = 'WARN_UNSAVED_CHANGES';
this.modal.componentInstance.buttons = [
{
labelKey: 'LBL_CANCEL',
klass: ['btn-secondary'],
onClick: activeModal => {
this.cancelClicked++;
activeModal.dismiss();
}
} as ModalButtonInterface,
{
labelKey: 'LBL_OK',
klass: ['btn-main'],
onClick: activeModal => {
this.okClicked++;
activeModal.close();
}
} as ModalButtonInterface,
];
}
}
describe('MessageModalComponent', () => {
let component: MessageModalTestHostComponent;
let fixture: ComponentFixture<MessageModalTestHostComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MessageModalTestHostComponent],
imports: [
MessageModalModule,
],
providers: [
{provide: LanguageStore, useValue: languageStoreMock},
{provide: SystemConfigStore, useValue: systemConfigStoreMock}
],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MessageModalTestHostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should have modal', () => {
expect(component).toBeTruthy();
const modal = document.getElementsByClassName('message-modal');
expect(modal).toBeTruthy();
expect(modal.length).toEqual(1);
component.modal.close();
});
it('should have message', () => {
expect(component).toBeTruthy();
const modal = document.getElementsByClassName('message-modal');
expect(modal).toBeTruthy();
expect(modal.length).toEqual(1);
const body = modal.item(0).getElementsByClassName('modal-body');
expect(body).toBeTruthy();
expect(body.length).toEqual(1);
expect(body.item(0).textContent).toContain('Are you sure you want to navigate away from this record?');
component.modal.close();
});
it('should have a clickable cancel button', () => {
expect(component).toBeTruthy();
const modal = document.getElementsByClassName('message-modal');
expect(modal).toBeTruthy();
expect(modal.length).toEqual(1);
const footer = modal.item(0).getElementsByClassName('modal-footer');
expect(footer).toBeTruthy();
expect(footer.length).toEqual(1);
const buttons = footer.item(0).getElementsByTagName('button');
expect(buttons).toBeTruthy();
expect(buttons.length).toEqual(2);
const cancelButton = buttons.item(0);
expect(cancelButton.textContent).toContain('Cancel');
expect(cancelButton.className).toContain('btn-secondary');
cancelButton.click();
expect(component.cancelClicked).toEqual(1);
});
it('should have a clickable ok button', () => {
expect(component).toBeTruthy();
const modal = document.getElementsByClassName('message-modal');
expect(modal).toBeTruthy();
expect(modal.length).toEqual(1);
const footer = modal.item(0).getElementsByClassName('modal-footer');
expect(footer).toBeTruthy();
expect(footer.length).toEqual(1);
const buttons = footer.item(0).getElementsByTagName('button');
expect(buttons).toBeTruthy();
expect(buttons.length).toEqual(2);
const okButton = buttons.item(1);
expect(okButton.textContent).toContain('OK');
expect(okButton.className).toContain('btn-main');
okButton.click();
expect(component.okClicked).toEqual(1);
});
});

View file

@ -0,0 +1,53 @@
import {Component, Input, OnInit} from '@angular/core';
import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
import {animate, transition, trigger} from '@angular/animations';
import {ButtonInterface} from '@app-common/components/button/button.model';
import {
AnyModalButtonInterface,
ModalButtonGroupInterface,
ModalCloseFeedBack
} from '@app-common/components/modal/modal.model';
import {Observable, of} from 'rxjs';
@Component({
selector: 'scrm-message-modal',
templateUrl: './message-modal.component.html',
styleUrls: [],
animations: [
trigger('modalFade', [
transition('void <=> *', [
animate('800ms')
]),
]),
]
})
export class MessageModalComponent implements OnInit {
@Input() titleKey: string;
@Input() textKey: string;
@Input() buttons: AnyModalButtonInterface[] = [];
buttonGroup$: Observable<ModalButtonGroupInterface>;
closeButton: ButtonInterface;
constructor(public activeModal: NgbActiveModal) {
}
ngOnInit(): void {
this.buttonGroup$ = of({
buttons: this.buttons
} as ModalButtonGroupInterface);
this.closeButton = {
klass: ['btn', 'btn-outline-light', 'btn-sm'],
onClick: (): void => {
this.activeModal.close({
type: 'close-button'
} as ModalCloseFeedBack);
}
} as ButtonInterface;
}
}

View file

@ -0,0 +1,24 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MessageModalComponent} from './message-modal.component';
import {ModalModule} from '@components/modal/components/modal/modal.module';
import {ButtonGroupModule} from '@components/button-group/button-group.module';
import {LabelModule} from '@components/label/label.module';
import {ModalButtonGroupModule} from '@components/modal/components/modal-button-group/modal-button-group.module';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
@NgModule({
declarations: [MessageModalComponent],
exports: [MessageModalComponent],
imports: [
CommonModule,
ModalModule,
ButtonGroupModule,
LabelModule,
ModalButtonGroupModule,
NgbModule
]
})
export class MessageModalModule {
}

View file

@ -11,6 +11,7 @@ export const languageMockData = {
LBL_HISTORY: 'History',
LBL_SAVE_BUTTON_LABEL: 'Save',
LBL_CANCEL: 'Cancel',
LBL_OK: 'OK',
LBL_SEARCH_REAULTS_TITLE: 'Results',
ERR_SEARCH_INVALID_QUERY: 'An error has occurred while performing the search. Your query syntax might not be valid.',
ERR_SEARCH_NO_RESULTS: 'No results matching your search criteria. Try broadening your search.',
@ -45,6 +46,7 @@ export const languageMockData = {
LBL_OPPORTUNITIES_TOTAL: 'Total Opportunity Value',
LBL_INSIGHTS: 'Insights',
LBL_NO_DATA: 'No Data',
WARN_UNSAVED_CHANGES: 'Are you sure you want to navigate away from this record?',
},
appListStrings: {
// eslint-disable-next-line camelcase,@typescript-eslint/camelcase