</cd-table>
<legend i18n>Snapshots</legend>
- <cd-table [sorts]="snapshot.sortProperties"
- [data]="selectedDir.snapshots"
- [columns]="snapshot.columns">
+ <cd-table [data]="selectedDir.snapshots"
+ [columns]="snapshot.columns"
+ identifier="name"
+ forceIdentifier="true"
+ selectionType="multiClick"
+ (updateSelection)="snapshot.updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="snapshot.selection"
+ [tableActions]="snapshot.tableActions">
+ </cd-table-actions>
</cd-table>
</div>
</div>
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
import { NodeEvent, Tree, TreeModel, TreeModule } from 'ng2-tree';
+import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal';
+import { ToastrModule } from 'ngx-toastr';
import { of } from 'rxjs';
-import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import {
+ configureTestBed,
+ i18nProviders,
+ modalServiceShow,
+ PermissionHelper
+} from '../../../../testing/unit-test-helper';
import { CephfsService } from '../../../shared/api/cephfs.service';
import {
CephfsDir,
describe('CephfsDirectoriesComponent', () => {
let component: CephfsDirectoriesComponent;
let fixture: ComponentFixture<CephfsDirectoriesComponent>;
+ let cephfsService: CephfsService;
let lsDirSpy;
let originalDate;
+ let modal;
// Get's private attributes or functions
const get = {
let mockData: {
nodes: TreeModel[];
parent: Tree;
+ createdSnaps: CephfsSnapshot[] | any[];
+ deletedSnaps: CephfsSnapshot[] | any[];
};
// Object contains mock functions
const name = 'someSnapshot';
const snapshots = [];
for (let i = 0; i < howMany; i++) {
- const path = `${dirPath}/.snap/${name}${i}`;
+ const snapName = `${name}${i + 1}`;
+ const path = `${dirPath}/.snap/${snapName}`;
const created = new Date(
+new Date() - 3600 * 24 * 1000 * howMany * (howMany - i)
).toString();
- snapshots.push({ name, path, created });
+ snapshots.push({ name: snapName, path, created });
}
return snapshots;
},
dir: (path: string, name: string, modifier: number): CephfsDir => {
const dirPath = `${path === '/' ? '' : path}/${name}`;
+ let snapshots = mockLib.snapshots(path, modifier);
+ const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath);
+ if (extraSnapshots.length > 0) {
+ snapshots = snapshots.concat(extraSnapshots);
+ }
+ const deletedSnapshots = mockData.deletedSnaps
+ .filter((s) => s.path === dirPath)
+ .map((s) => s.name);
+ if (deletedSnapshots.length > 0) {
+ snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name));
+ }
return {
name,
path: dirPath,
parent: path,
quotas: mockLib.quotas(1024 * modifier, 10 * modifier),
- snapshots: mockLib.snapshots(path, modifier)
+ snapshots: snapshots
};
},
// Only used inside other mocks
});
return of(data);
},
+ mkSnapshot: (_id, path, name) => {
+ mockData.createdSnaps.push({
+ name,
+ path,
+ created: new Date().toString()
+ });
+ return of(name);
+ },
+ rmSnapshot: (_id, path, name) => {
+ mockData.deletedSnaps.push({
+ name,
+ path,
+ created: new Date().toString()
+ });
+ return of(name);
+ },
+ modalShow: (comp, init) => {
+ modal = modalServiceShow(comp, init);
+ return modal.ref;
+ },
date: (arg) => (arg ? new originalDate(arg) : new Date('2022-02-22T00:00:00')),
getControllerByPath: (path: string) => {
return {
},
// Only used inside other mocks to mock "tree.expand" of every node
expand: (path: string) => {
- component.updateDirectory(path, (nodes) => (mockData.nodes = mockData.nodes.concat(nodes)));
+ component.updateDirectory(path, (nodes) => {
+ mockData.nodes = mockData.nodes.concat(nodes);
+ });
},
getNodeEvent: (path: string): NodeEvent => {
const tree = mockData.nodes.find((n) => n.id === path) as Tree;
id: dir.path,
value: name
});
+ },
+ createSnapshotThroughModal: (name: string) => {
+ component.createSnapshot();
+ modal.component.onSubmitForm({ name });
+ },
+ deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
+ component.snapshot.selection.selected = snapshots;
+ component.deleteSnapshotModal();
+ modal.component.callSubmitAction();
}
};
nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n),
lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
+ snapshotsByName: (snaps: string[]) =>
+ expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
quotaSettings: (
fileValue: number | string,
fileOrigin: string,
};
configureTestBed({
- imports: [HttpClientTestingModule, SharedModule, TreeModule],
+ imports: [
+ HttpClientTestingModule,
+ SharedModule,
+ RouterTestingModule,
+ TreeModule,
+ NgBootstrapFormValidationModule.forRoot(),
+ ToastrModule.forRoot(),
+ ModalModule.forRoot()
+ ],
declarations: [CephfsDirectoriesComponent],
- providers: [i18nProviders]
+ providers: [i18nProviders, BsModalRef]
});
beforeEach(() => {
mockData = {
nodes: undefined,
- parent: undefined
+ parent: undefined,
+ createdSnaps: [],
+ deletedSnaps: []
};
originalDate = Date;
spyOn(global, 'Date').and.callFake(mockLib.date);
- lsDirSpy = spyOn(TestBed.get(CephfsService), 'lsDir').and.callFake(mockLib.lsDir);
+ cephfsService = TestBed.get(CephfsService);
+ lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
+ spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
+ spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
+
+ spyOn(TestBed.get(BsModalService), 'show').and.callFake(mockLib.modalShow);
fixture = TestBed.createComponent(CephfsDirectoriesComponent);
component = fixture.componentInstance;
expect(component).toBeTruthy();
});
+ describe('mock self test', () => {
+ it('tests snapshots mock', () => {
+ expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([
+ {
+ name: 'someSnapshot1',
+ path: '/a/.snap/someSnapshot1'
+ }
+ ]);
+ expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
+ {
+ name: 'someSnapshot1',
+ path: '/a/b/.snap/someSnapshot1'
+ },
+ {
+ name: 'someSnapshot2',
+ path: '/a/b/.snap/someSnapshot2'
+ },
+ {
+ name: 'someSnapshot3',
+ path: '/a/b/.snap/someSnapshot3'
+ }
+ ]);
+ });
+
+ it('tests dir mock', () => {
+ const path = '/a/b/c';
+ mockData.createdSnaps = [{ path, name: 's1' }, { path, name: 's2' }];
+ mockData.deletedSnaps = [{ path, name: 'someSnapshot2' }, { path, name: 's2' }];
+ const dir = mockLib.dir('/a/b', 'c', 2);
+ expect(dir.path).toBe('/a/b/c');
+ expect(dir.parent).toBe('/a/b');
+ expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 });
+ expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']);
+ });
+
+ it('tests lsdir mock', () => {
+ let dirs: CephfsDir[] = [];
+ mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x));
+ expect(dirs.map((d) => d.path)).toEqual([
+ '/a/c',
+ '/a/a',
+ '/a/b',
+ '/a/c/c',
+ '/a/c/a',
+ '/a/c/b',
+ '/a/a/c',
+ '/a/a/a',
+ '/a/a/b'
+ ]);
+ });
+ });
+
it('calls lsDir only if an id exits', () => {
component.ngOnChanges();
assert.lsDirCalledTimes(0);
});
});
});
+
+ describe('snapshots', () => {
+ beforeEach(() => {
+ mockLib.changeId(1);
+ mockLib.selectNode('/a');
+ });
+
+ it('should create a snapshot', () => {
+ mockLib.createSnapshotThroughModal('newSnap');
+ expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap');
+ assert.snapshotsByName(['someSnapshot1', 'newSnap']);
+ });
+
+ it('should delete a snapshot', () => {
+ mockLib.createSnapshotThroughModal('deleteMe');
+ mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]);
+ assert.snapshotsByName(['someSnapshot1']);
+ });
+
+ it('should delete all snapshots', () => {
+ mockLib.createSnapshotThroughModal('deleteAll');
+ mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
+ assert.snapshotsByName([]);
+ });
+
+ afterEach(() => {
+ // Makes sure the directory is updated correctly
+ expect(component.selectedDir).toEqual(get.nodeIds()[component.selectedDir.path]);
+ });
+ });
+
+ it('should test all snapshot table actions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions = permissionHelper.setPermissionsAndGetActions(
+ component.snapshot.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ update: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
});
import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
-import { SortDirection, SortPropDir } from '@swimlane/ngx-datatable';
import * as _ from 'lodash';
+import * as moment from 'moment';
import { NodeEvent, Tree, TreeComponent, TreeModel } from 'ng2-tree';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { CephfsService } from '../../../shared/api/cephfs.service';
+import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
+import { Icons } from '../../../shared/enum/icons.enum';
+import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdTableAction } from '../../../shared/models/cd-table-action';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CephfsDir } from '../../../shared/models/cephfs-directory-models';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { CephfsDir, CephfsSnapshot } from '../../../shared/models/cephfs-directory-models';
+import { Permission } from '../../../shared/models/permissions';
import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { NotificationService } from '../../../shared/services/notification.service';
@Component({
selector: 'cd-cephfs-directories',
@Input()
id: number;
+ private modalRef: BsModalRef;
private dirs: CephfsDir[];
private nodeIds: { [path: string]: CephfsDir };
private requestedPaths: string[];
+ private selectedNode: Tree;
+ permission: Permission;
selectedDir: CephfsDir;
- tree: TreeModel;
settings: {
name: string;
value: number | string;
origin: string;
}[];
-
settingsColumns: CdTableColumn[];
- snapshot: { columns: CdTableColumn[]; sortProperties: SortPropDir[] };
+ snapshot: {
+ columns: CdTableColumn[];
+ selection: CdTableSelection;
+ tableActions: CdTableAction[];
+ updateSelection: Function;
+ };
+ tree: TreeModel;
constructor(
+ private authStorageService: AuthStorageService,
+ private modalService: BsModalService,
private cephfsService: CephfsService,
private cdDatePipe: CdDatePipe,
private i18n: I18n,
+ private notificationService: NotificationService,
private dimlessBinaryPipe: DimlessBinaryPipe
) {}
ngOnInit() {
+ this.permission = this.authStorageService.getPermissions().cephfs;
this.settingsColumns = [
{
prop: 'name',
pipe: this.cdDatePipe
}
],
- sortProperties: [
+ selection: new CdTableSelection(),
+ updateSelection: (selection: CdTableSelection) => {
+ this.snapshot.selection = selection;
+ },
+ tableActions: [
+ {
+ name: this.i18n('Create'),
+ icon: Icons.add,
+ permission: 'create',
+ canBePrimary: (selection) => !selection.hasSelection,
+ click: () => this.createSnapshot()
+ },
{
- dir: SortDirection.desc,
- prop: 'created'
+ name: this.i18n('Delete'),
+ icon: Icons.destroy,
+ permission: 'delete',
+ click: () => this.deleteSnapshotModal(),
+ canBePrimary: (selection) => selection.hasSelection,
+ disable: (selection) => !selection.hasSelection
}
]
};
private loadDirectory(data: CephfsDir[], path: string, callback: (x: any[]) => void) {
if (path !== '/') {
- // Removes duplicate directories
+ // As always to levels are loaded all sub-directories of the current called path are
+ // already loaded, that's why they are filtered out.
data = data.filter((dir) => dir.parent !== path);
}
- const dirs = this.dirs.concat(data);
- this.dirs = dirs;
+ this.dirs = this.dirs.concat(data);
this.getChildren(path, callback);
}
this.treeComponent.getControllerByNodeId(node.id).expand();
this.setSettings(node);
this.selectedDir = this.getDirectory(node);
+ this.selectedNode = node;
}
private setSettings(node: Tree) {
const path = node.id as string;
return this.nodeIds[path];
}
+
+ createSnapshot() {
+ // Create a snapshot. Auto-generate a snapshot name by default.
+ const path = this.selectedDir.path;
+ this.modalService.show(FormModalComponent, {
+ initialState: {
+ titleText: this.i18n('Create Snapshot'),
+ message: this.i18n('Please enter the name of the snapshot.'),
+ fields: [
+ {
+ type: 'inputText',
+ name: 'name',
+ value: `${moment().toISOString(true)}`,
+ required: true
+ }
+ ],
+ submitButtonText: this.i18n('Create Snapshot'),
+ onSubmit: (values) => {
+ this.cephfsService.mkSnapshot(this.id, path, values.name).subscribe((name) => {
+ this.notificationService.show(
+ NotificationType.success,
+ this.i18n('Created snapshot "{{name}}" for "{{path}}"', {
+ name: name,
+ path: path
+ })
+ );
+ this.forceDirRefresh();
+ });
+ }
+ }
+ });
+ }
+
+ /**
+ * Forces an update of the current selected directory
+ *
+ * As all nodes point by their path on an directory object, the easiest way is to update
+ * the objects by merge with their latest change.
+ */
+ private forceDirRefresh() {
+ const path = this.selectedNode.parent.id as string;
+ this.cephfsService.lsDir(this.id, path).subscribe((data) =>
+ data.forEach((d) => {
+ Object.assign(this.dirs.find((sub) => sub.path === d.path), d);
+ })
+ );
+ }
+
+ deleteSnapshotModal() {
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ initialState: {
+ itemDescription: 'CephFs Snapshot',
+ itemNames: this.snapshot.selection.selected.map(
+ (snapshot: CephfsSnapshot) => snapshot.name
+ ),
+ submitAction: () => this.deleteSnapshot()
+ }
+ });
+ }
+
+ deleteSnapshot() {
+ const path = this.selectedDir.path;
+ this.snapshot.selection.selected.forEach((snapshot: CephfsSnapshot) => {
+ const name = snapshot.name;
+ this.cephfsService.rmSnapshot(this.id, path, name).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ this.i18n('Deleted snapshot "{{name}}" for "{{path}}"', {
+ name: name,
+ path: path
+ })
+ );
+ });
+ });
+ this.modalRef.hide();
+ this.forceDirRefresh();
+ }
}
const req = httpTesting.expectOne('api/cephfs/1/ls_dir?depth=2');
expect(req.request.method).toBe('GET');
service.lsDir(2, '/some/path').subscribe();
- httpTesting.expectOne('api/cephfs/2/ls_dir?depth=2&path=%2Fsome%2Fpath');
+ httpTesting.expectOne('api/cephfs/2/ls_dir?depth=2&path=%252Fsome%252Fpath');
+ });
+
+ it('should call mkSnapshot', () => {
+ service.mkSnapshot(3, '/some/path').subscribe();
+ const req = httpTesting.expectOne('api/cephfs/3/mk_snapshot?path=%252Fsome%252Fpath');
+ expect(req.request.method).toBe('POST');
+
+ service.mkSnapshot(4, '/some/other/path', 'snap').subscribe();
+ httpTesting.expectOne('api/cephfs/4/mk_snapshot?path=%252Fsome%252Fother%252Fpath&name=snap');
+ });
+
+ it('should call rmSnapshot', () => {
+ service.rmSnapshot(1, '/some/path', 'snap').subscribe();
+ const req = httpTesting.expectOne('api/cephfs/1/rm_snapshot?path=%252Fsome%252Fpath&name=snap');
+ expect(req.request.method).toBe('POST');
});
});
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import * as _ from 'lodash';
import { Observable } from 'rxjs';
+import { cdEncode } from '../decorators/cd-encode';
import { CephfsDir } from '../models/cephfs-directory-models';
import { ApiModule } from './api.module';
+@cdEncode
@Injectable({
providedIn: ApiModule
})
getMdsCounters(id) {
return this.http.get(`${this.baseURL}/${id}/mds_counters`);
}
+
+ mkSnapshot(id, path, name?) {
+ let params = new HttpParams();
+ params = params.append('path', path);
+ if (!_.isUndefined(name)) {
+ params = params.append('name', name);
+ }
+ return this.http.post(`${this.baseURL}/${id}/mk_snapshot`, null, { params: params });
+ }
+
+ rmSnapshot(id, path, name) {
+ let params = new HttpParams();
+ params = params.append('path', path);
+ params = params.append('name', name);
+ return this.http.post(`${this.baseURL}/${id}/rm_snapshot`, null, { params: params });
+ }
}
import { ConfigOptionComponent } from './config-option/config-option.component';
import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component';
import { CriticalConfirmationModalComponent } from './critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from './form-modal/form-modal.component';
import { GrafanaComponent } from './grafana/grafana.component';
import { HelperComponent } from './helper/helper.component';
import { LanguageSelectorComponent } from './language-selector/language-selector.component';
BackButtonComponent,
RefreshSelectorComponent,
ConfigOptionComponent,
- AlertPanelComponent
+ AlertPanelComponent,
+ FormModalComponent
],
providers: [],
exports: [
ConfigOptionComponent,
AlertPanelComponent
],
- entryComponents: [ModalComponent, CriticalConfirmationModalComponent, ConfirmationModalComponent]
+ entryComponents: [
+ ModalComponent,
+ CriticalConfirmationModalComponent,
+ ConfirmationModalComponent,
+ FormModalComponent
+ ]
})
export class ComponentsModule {}
--- /dev/null
+<cd-modal [modalRef]="bsModalRef">
+ <ng-container *ngIf="titleText"
+ class="modal-title">
+ {{ titleText }}
+ </ng-container>
+ <ng-container class="modal-content">
+ <form [formGroup]="formGroup"
+ novalidate>
+ <div class="modal-body">
+ <p *ngIf="message">{{ message }}</p>
+ <ng-container *ngFor="let field of fields">
+ <div class="form-group row">
+ <ng-container [ngSwitch]="field.type">
+ <ng-template [ngSwitchCase]="'inputText'">
+ <label *ngIf="field.label"
+ class="col-form-label col-sm-3"
+ [for]="field.name">
+ {{ field.label }}
+ </label>
+ <div [ngClass]="{'col-sm-9': field.label, 'col-sm-12': !field.label}">
+ <input type="text"
+ class="form-control"
+ [id]="field.name"
+ [name]="field.name"
+ [formControlName]="field.name">
+ <span *ngIf="formGroup.hasError('required', field.name)"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+ </ng-template>
+ </ng-container>
+ </div>
+ </ng-container>
+ </div>
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-submit-button [form]="formGroup"
+ (submitAction)="onSubmitForm(formGroup.value)">
+ {{ submitButtonText }}
+ </cd-submit-button>
+ <cd-back-button [back]="bsModalRef.hide"></cd-back-button>
+ </div>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
+import { BsModalRef, ModalModule } from 'ngx-bootstrap/modal';
+
+import {
+ configureTestBed,
+ FixtureHelper,
+ i18nProviders
+} from '../../../../testing/unit-test-helper';
+import { SharedModule } from '../../shared.module';
+import { FormModalComponent } from './form-modal.component';
+
+describe('InputModalComponent', () => {
+ let component: FormModalComponent;
+ let fixture: ComponentFixture<FormModalComponent>;
+ let fh: FixtureHelper;
+ let submitted;
+
+ const initialState = {
+ titleText: 'Some title',
+ message: 'Some description',
+ fields: [
+ {
+ type: 'inputText',
+ name: 'requiredField',
+ value: 'some-value',
+ required: true
+ },
+ {
+ type: 'inputText',
+ name: 'optionalField',
+ label: 'Optional'
+ }
+ ],
+ submitButtonText: 'Submit button name',
+ onSubmit: (values) => (submitted = values)
+ };
+
+ configureTestBed({
+ imports: [
+ ModalModule.forRoot(),
+ NgBootstrapFormValidationModule.forRoot(),
+ RouterTestingModule,
+ ReactiveFormsModule,
+ SharedModule
+ ],
+ providers: [i18nProviders, BsModalRef]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FormModalComponent);
+ component = fixture.componentInstance;
+ Object.assign(component, initialState);
+ fixture.detectChanges();
+ fh = new FixtureHelper(fixture);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('has the defined title', () => {
+ fh.expectTextToBe('.modal-title', 'Some title');
+ });
+
+ it('has the defined description', () => {
+ fh.expectTextToBe('.modal-body > p', 'Some description');
+ });
+
+ it('should display both inputs', () => {
+ fh.expectElementVisible('#requiredField', true);
+ fh.expectElementVisible('#optionalField', true);
+ });
+
+ it('has one defined label field', () => {
+ fh.expectTextToBe('.col-form-label', 'Optional');
+ });
+
+ it('has a predefined values for requiredField', () => {
+ fh.expectFormFieldToBe('#requiredField', 'some-value');
+ });
+
+ it('gives back all form values on submit', () => {
+ component.onSubmitForm(component.formGroup.value);
+ expect(submitted).toEqual({
+ requiredField: 'some-value',
+ optionalField: null
+ });
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+
+import * as _ from 'lodash';
+import { BsModalRef } from 'ngx-bootstrap/modal';
+
+import { CdFormBuilder } from '../../forms/cd-form-builder';
+
+interface CdFormFieldConfig {
+ type: 'textInput';
+ name: string;
+ label?: string;
+ value?: any;
+ required?: boolean;
+}
+
+@Component({
+ selector: 'cd-form-modal',
+ templateUrl: './form-modal.component.html',
+ styleUrls: ['./form-modal.component.scss']
+})
+export class FormModalComponent implements OnInit {
+ // Input
+ titleText: string;
+ message: string;
+ fields: CdFormFieldConfig[];
+ submitButtonText: string;
+ onSubmit: Function;
+
+ // Internal
+ formGroup: FormGroup;
+
+ constructor(public bsModalRef: BsModalRef, private formBuilder: CdFormBuilder) {}
+
+ createForm() {
+ const controlsConfig = {};
+ this.fields.forEach((field) => {
+ const validators = [];
+ if (_.isBoolean(field.required) && field.required) {
+ validators.push(Validators.required);
+ }
+ controlsConfig[field.name] = new FormControl(_.defaultTo(field.value, null), { validators });
+ });
+ this.formGroup = this.formBuilder.group(controlsConfig);
+ }
+
+ ngOnInit() {
+ this.createForm();
+ }
+
+ onSubmitForm(values) {
+ this.bsModalRef.hide();
+ if (_.isFunction(this.onSubmit)) {
+ this.onSubmit(values);
+ }
+ }
+}
expect(props['value'] || props['checked'].toString()).toBe(value);
}
+ expectTextToBe(css: string, value: string) {
+ expect(this.getText(css)).toBe(value);
+ }
+
clickElement(css: string) {
this.getElementByCss(css).triggerEventHandler('click', null);
this.fixture.detectChanges();