From 0d77113704c5584a68afe0614317b7983e63c6d0 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Fri, 19 Jun 2020 08:43:22 +0200 Subject: [PATCH 1/7] first version of sample.component finished --- package-lock.json | 16 +- package.json | 9 +- src/app/api.service.spec.ts | 53 --- src/app/api.service.ts | 30 -- src/app/app-routing.module.ts | 8 +- src/app/app.component.html | 15 + src/app/app.component.ts | 8 +- src/app/app.module.ts | 24 +- src/app/error/error.component.html | 3 + src/app/error/error.component.scss | 0 src/app/error/error.component.spec.ts | 45 +++ src/app/error/error.component.ts | 17 + src/app/login.service.spec.ts | 81 ----- src/app/login/login.component.html | 13 +- src/app/login/login.component.scss | 2 +- src/app/login/login.component.spec.ts | 8 +- src/app/login/login.component.ts | 36 +- src/app/models/custom-fields.model.spec.ts | 7 + src/app/models/custom-fields.model.ts | 13 + src/app/models/deserializable.model.spec.ts | 5 + src/app/models/deserializable.model.ts | 3 + src/app/models/id.model.spec.ts | 5 + src/app/models/id.model.ts | 1 + src/app/models/material.model.spec.ts | 7 + src/app/models/material.model.ts | 33 ++ src/app/models/measurement.model.spec.ts | 7 + src/app/models/measurement.model.ts | 29 ++ src/app/models/sample.model.spec.ts | 7 + src/app/models/sample.model.ts | 37 ++ src/app/models/sendformat.model.spec.ts | 5 + src/app/models/sendformat.model.ts | 3 + src/app/models/template.model.spec.ts | 7 + src/app/models/template.model.ts | 14 + src/app/sample/sample.component.html | 157 ++++++++ src/app/sample/sample.component.scss | 17 + src/app/sample/sample.component.spec.ts | 25 ++ src/app/sample/sample.component.ts | 341 ++++++++++++++++++ src/app/samples/samples.component.html | 16 +- src/app/samples/samples.component.scss | 11 + src/app/samples/samples.component.ts | 21 +- src/app/services/api.service.spec.ts | 53 +++ src/app/services/api.service.ts | 55 +++ src/app/services/autocomplete.service.spec.ts | 16 + src/app/services/autocomplete.service.ts | 20 + src/app/services/login.service.spec.ts | 81 +++++ src/app/{ => services}/login.service.ts | 32 +- .../{ => services}/validation.service.spec.ts | 0 src/app/services/validation.service.ts | 124 +++++++ src/app/validate.directive.spec.ts | 8 + src/app/validate.directive.ts | 28 ++ src/app/validation.service.ts | 36 -- tsconfig.json | 3 +- tslint.json | 16 +- 53 files changed, 1336 insertions(+), 275 deletions(-) delete mode 100644 src/app/api.service.spec.ts delete mode 100644 src/app/api.service.ts create mode 100644 src/app/error/error.component.html create mode 100644 src/app/error/error.component.scss create mode 100644 src/app/error/error.component.spec.ts create mode 100644 src/app/error/error.component.ts delete mode 100644 src/app/login.service.spec.ts create mode 100644 src/app/models/custom-fields.model.spec.ts create mode 100644 src/app/models/custom-fields.model.ts create mode 100644 src/app/models/deserializable.model.spec.ts create mode 100644 src/app/models/deserializable.model.ts create mode 100644 src/app/models/id.model.spec.ts create mode 100644 src/app/models/id.model.ts create mode 100644 src/app/models/material.model.spec.ts create mode 100644 src/app/models/material.model.ts create mode 100644 src/app/models/measurement.model.spec.ts create mode 100644 src/app/models/measurement.model.ts create mode 100644 src/app/models/sample.model.spec.ts create mode 100644 src/app/models/sample.model.ts create mode 100644 src/app/models/sendformat.model.spec.ts create mode 100644 src/app/models/sendformat.model.ts create mode 100644 src/app/models/template.model.spec.ts create mode 100644 src/app/models/template.model.ts create mode 100644 src/app/sample/sample.component.html create mode 100644 src/app/sample/sample.component.scss create mode 100644 src/app/sample/sample.component.spec.ts create mode 100644 src/app/sample/sample.component.ts create mode 100644 src/app/services/api.service.spec.ts create mode 100644 src/app/services/api.service.ts create mode 100644 src/app/services/autocomplete.service.spec.ts create mode 100644 src/app/services/autocomplete.service.ts create mode 100644 src/app/services/login.service.spec.ts rename src/app/{ => services}/login.service.ts (66%) rename src/app/{ => services}/validation.service.spec.ts (100%) create mode 100644 src/app/services/validation.service.ts create mode 100644 src/app/validate.directive.spec.ts create mode 100644 src/app/validate.directive.ts delete mode 100644 src/app/validation.service.ts diff --git a/package-lock.json b/package-lock.json index a2bffa1..bc55c27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1887,11 +1887,11 @@ } }, "@inst-iot/bosch-angular-ui-components": { - "version": "0.5.30", - "resolved": "https://rb-artifactory.bosch.com:443/artifactory/api/npm/iot-insights-release-local/@inst-iot/bosch-angular-ui-components/-/@inst-iot/bosch-angular-ui-components-0.5.30.tgz", - "integrity": "sha1-s7Xl3h1BCr4MQPi118S5otrA4Cc=", + "version": "0.6.0", + "resolved": "https://rb-artifactory.bosch.com:443/artifactory/api/npm/iot-insights-release-local/@inst-iot/bosch-angular-ui-components/-/@inst-iot/bosch-angular-ui-components-0.6.0.tgz", + "integrity": "sha1-+yjXwe/qCeBHYL+WoG7mOarPXIQ=", "requires": { - "tslib": "^1.9.0" + "tslib": "^1.10.0" } }, "@istanbuljs/schema": { @@ -8061,8 +8061,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.clonedeep": { "version": "4.5.0", @@ -10831,6 +10830,11 @@ "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", "dev": true }, + "quick-score": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/quick-score/-/quick-score-0.0.8.tgz", + "integrity": "sha512-nCWx9FPiVvNeO8aUkrikrVL/v0XIGQSMQMBLLTXa/d64Wb/w8v8odSiuQAMPez20HdOngWPB8LcB8SfnIoE8bA==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/package.json b/package.json index 7ec9656..d77ae2a 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,13 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve -o", + "start": "ng serve", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", - "coverage": "ng test --no-watch --code-coverage" + "coverage": "ng test --no-watch --code-coverage", + "api": "cd C:\\Users\\vle2fe\\Documents\\Code\\API && node dist\\index.js" }, "private": true, "dependencies": { @@ -21,9 +22,11 @@ "@angular/platform-browser-dynamic": "~9.1.7", "@angular/router": "~9.1.7", "@hapi/joi": "^17.1.1", - "@inst-iot/bosch-angular-ui-components": "^0.5.30", + "@inst-iot/bosch-angular-ui-components": "^0.6.0", "angular-2-local-storage": "^3.0.2", "flatpickr": "^4.6.3", + "lodash": "^4.17.15", + "quick-score": "0.0.8", "rxjs": "~6.5.5", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/src/app/api.service.spec.ts b/src/app/api.service.spec.ts deleted file mode 100644 index 615a2fc..0000000 --- a/src/app/api.service.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { ApiService } from './api.service'; -import {HttpClient} from '@angular/common/http'; -import {LocalStorageService} from 'angular-2-local-storage'; -import {Observable} from 'rxjs'; - -let apiService: ApiService; -let httpClientSpy: jasmine.SpyObj; -let localStorageServiceSpy: jasmine.SpyObj; - -describe('ApiService', () => { - beforeEach(() => { - const httpSpy = jasmine.createSpyObj('HttpClient', ['get']); - const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['get']); - - TestBed.configureTestingModule({ - providers: [ - ApiService, - {provide: HttpClient, useValue: httpSpy}, - {provide: LocalStorageService, useValue: localStorageSpy} - ] - }); - - apiService = TestBed.inject(ApiService); - httpClientSpy = TestBed.inject(HttpClient) as jasmine.SpyObj; - localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj; - }); - - it('should be created', () => { - expect(apiService).toBeTruthy(); - }); - - it('should do get requests without auth if not available', () => { - const getReturn = new Observable(); - httpClientSpy.get.and.returnValue(getReturn); - localStorageServiceSpy.get.and.returnValue(undefined); - - const result = apiService.get('/testurl'); - expect(result).toBe(getReturn); - expect(httpClientSpy.get).toHaveBeenCalledWith('/testurl', {}); - expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); - }); - it('should do get requests with basic auth if available', () => { - const getReturn = new Observable(); - httpClientSpy.get.and.returnValue(getReturn); - localStorageServiceSpy.get.and.returnValue('basicAuth'); - - const result = apiService.get('/testurl'); - expect(result).toBe(getReturn); - expect(httpClientSpy.get).toHaveBeenCalledWith('/testurl', jasmine.any(Object)); // could not test http headers better - expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); - }); -}); diff --git a/src/app/api.service.ts b/src/app/api.service.ts deleted file mode 100644 index 6183512..0000000 --- a/src/app/api.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@angular/core'; -import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {LocalStorageService} from 'angular-2-local-storage'; - -@Injectable({ - providedIn: 'root' -}) -export class ApiService { - - private host = '/api'; - - constructor( - private http: HttpClient, - private storage: LocalStorageService - ) { } - - get(url) { - return this.http.get(this.host + url, this.authOptions()); - } - - private authOptions() { - const auth = this.storage.get('basicAuth'); - if (auth) { - return {headers: new HttpHeaders({Authorization: 'Basic ' + auth})}; - } - else { - return {}; - } - } -} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 56965a3..e11eb05 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,15 +1,17 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import {HomeComponent} from './home/home.component'; -import {LoginService} from './login.service'; +import {LoginService} from './services/login.service'; +import {SampleComponent} from './sample/sample.component'; import {SamplesComponent} from './samples/samples.component'; const routes: Routes = [ {path: '', component: HomeComponent}, {path: 'home', component: HomeComponent}, - {path: 'samples', component: SamplesComponent}, - {path: 'replace-me', component: HomeComponent, canActivate: [LoginService]}, + {path: 'samples', component: SamplesComponent, canActivate: [LoginService]}, + {path: 'samples/new', component: SampleComponent, canActivate: [LoginService]}, + {path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]}, // if not authenticated { path: '**', redirectTo: '' } diff --git a/src/app/app.component.html b/src/app/app.component.html index e31308e..140a69a 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -3,6 +3,21 @@ Home Samples + + + +
+

+ Some user specific information +

+ + Logout +
+
+
Digital Fingerprint of Plastics
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b997e1a..1f033c4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,11 @@ import { Component } from '@angular/core'; -import {LoginService} from './login.service'; +import {LoginService} from './services/login.service'; + +// TODO: add multiple samples at once +// TODO: guess properties from material name +// TODO: validation: VZ, Humidity: min/max value, DPT: filename +// TODO: filter by not completely filled/no measurements +// TODO: account @Component({ selector: 'app-root', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bed1712..0bfe9db 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -3,21 +3,28 @@ import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; -import { LoginComponent } from './login/login.component'; +import {FormFieldsModule, ModalService, RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; +import {LoginComponent} from './login/login.component'; import { HomeComponent } from './home/home.component'; -import {FormsModule} from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {LocalStorageModule} from 'angular-2-local-storage'; import {HttpClientModule} from '@angular/common/http'; import { SamplesComponent } from './samples/samples.component'; import {RbTableModule} from './rb-table/rb-table.module'; +import { SampleComponent } from './sample/sample.component'; +import { ValidateDirective } from './validate.directive'; +import {CommonModule} from '@angular/common'; +import { ErrorComponent } from './error/error.component'; @NgModule({ declarations: [ AppComponent, LoginComponent, HomeComponent, - SamplesComponent + SamplesComponent, + SampleComponent, + ValidateDirective, + ErrorComponent ], imports: [ LocalStorageModule.forRoot({ @@ -29,9 +36,14 @@ import {RbTableModule} from './rb-table/rb-table.module'; RbUiComponentsModule, FormsModule, HttpClientModule, - RbTableModule + RbTableModule, + ReactiveFormsModule, + FormFieldsModule, + CommonModule + ], + providers: [ + ModalService ], - providers: [], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/app/error/error.component.html b/src/app/error/error.component.html new file mode 100644 index 0000000..5a16414 --- /dev/null +++ b/src/app/error/error.component.html @@ -0,0 +1,3 @@ + + {{message}} + diff --git a/src/app/error/error.component.scss b/src/app/error/error.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/error/error.component.spec.ts b/src/app/error/error.component.spec.ts new file mode 100644 index 0000000..34f1cc0 --- /dev/null +++ b/src/app/error/error.component.spec.ts @@ -0,0 +1,45 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ErrorComponent } from './error.component'; +import {ModalService, RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; +import {By} from '@angular/platform-browser'; + +describe('ErrorComponent', () => { + let component: ErrorComponent; + let fixture: ComponentFixture; + let css; // get native element by css selector + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ErrorComponent ], + imports: [ + RbUiComponentsModule, + ], + providers: [ + ModalService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ErrorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + css = (selector) => fixture.debugElement.query(By.css(selector)).nativeElement; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show the alert', () => { + expect(css('rb-alert')).toBeTruthy(); + }); + + it('should have the right message', () => { + component.message = 'test'; + fixture.detectChanges(); + expect(css('.dialog-text').innerText).toBe('test'); + }); +}); diff --git a/src/app/error/error.component.ts b/src/app/error/error.component.ts new file mode 100644 index 0000000..fe0940d --- /dev/null +++ b/src/app/error/error.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-error', + templateUrl: './error.component.html', + styleUrls: ['./error.component.scss'] +}) +export class ErrorComponent implements OnInit { + + message = ''; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/app/login.service.spec.ts b/src/app/login.service.spec.ts deleted file mode 100644 index ededdbe..0000000 --- a/src/app/login.service.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { LoginService } from './login.service'; -import {LocalStorageService} from 'angular-2-local-storage'; -import {ApiService} from './api.service'; -import {Observable} from 'rxjs'; - -let loginService: LoginService; -let apiServiceSpy: jasmine.SpyObj; -let localStorageServiceSpy: jasmine.SpyObj; - -describe('LoginService', () => { - beforeEach(() => { - const apiSpy = jasmine.createSpyObj('ApiService', ['get']); - const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['set', 'remove']); - - TestBed.configureTestingModule({ - providers: [ - LoginService, - {provide: ApiService, useValue: apiSpy}, - {provide: LocalStorageService, useValue: localStorageSpy} - ] - }); - loginService = TestBed.inject(LoginService); - apiServiceSpy = TestBed.inject(ApiService) as jasmine.SpyObj; - localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj; - }); - - it('should be created', () => { - expect(loginService).toBeTruthy(); - }); - - describe('login', () => { - it('should store the basic auth', () => { - localStorageServiceSpy.set.and.returnValue(true); - apiServiceSpy.get.and.returnValue(new Observable()); - loginService.login('username', 'password'); - expect(localStorageServiceSpy.set).toHaveBeenCalledWith('basicAuth', 'dXNlcm5hbWU6cGFzc3dvcmQ='); - }); - - it('should remove the basic auth if login fails', () => { - localStorageServiceSpy.set.and.returnValue(true); - localStorageServiceSpy.remove.and.returnValue(true); - apiServiceSpy.get.and.returnValue(new Observable(o => o.error())); - loginService.login('username', 'password'); - expect(localStorageServiceSpy.remove.calls.count()).toBe(1); - expect(localStorageServiceSpy.remove).toHaveBeenCalledWith('basicAuth'); - }); - - it('should resolve true when login succeeds', async () => { - localStorageServiceSpy.set.and.returnValue(true); - apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'Authorization successful', method: 'basic'}))); - expect(await loginService.login('username', 'password')).toBeTruthy(); - }); - - it('should resolve false when a wrong result comes in', async () => { - localStorageServiceSpy.set.and.returnValue(true); - apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'xxx', method: 'basic'}))); - expect(await loginService.login('username', 'password')).toBeFalsy(); - }); - - it('should resolve false on an error', async () => { - localStorageServiceSpy.set.and.returnValue(true); - apiServiceSpy.get.and.returnValue(new Observable(o => o.error())); - expect(await loginService.login('username', 'password')).toBeFalsy(); - }); - }); - - describe('canActivate', () => { - it('should return false at first', () => { - expect(loginService.canActivate(null, null)).toBeFalsy(); - }); - - it('returns true if login was successful', async () => { - localStorageServiceSpy.set.and.returnValue(true); - apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'Authorization successful', method: 'basic'}))); - await loginService.login('username', 'password'); - expect(loginService.canActivate(null, null)).toBeTruthy(); - }); - }); -}); diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index 593bffc..e765fc9 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -1,10 +1,15 @@ diff --git a/src/app/login/login.component.scss b/src/app/login/login.component.scss index f5a95d1..52566ab 100644 --- a/src/app/login/login.component.scss +++ b/src/app/login/login.component.scss @@ -8,5 +8,5 @@ } .login-button { - display: block; + margin-right: 10px; } diff --git a/src/app/login/login.component.spec.ts b/src/app/login/login.component.spec.ts index 220c313..359167d 100644 --- a/src/app/login/login.component.spec.ts +++ b/src/app/login/login.component.spec.ts @@ -1,7 +1,7 @@ import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import { LoginComponent } from './login.component'; -import {LoginService} from '../login.service'; -import {ValidationService} from '../validation.service'; +import {LoginService} from '../services/login.service'; +import {ValidationService} from '../services/validation.service'; import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; import {FormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; @@ -71,7 +71,7 @@ describe('LoginComponent', () => { cssd('.login-button').triggerEventHandler('click', null); fixture.detectChanges(); - expect(css('.message').innerText).toBe('username must only contain a-z0-9-_.'); + expect(css('.error-messages > div').innerText).toBe('username must only contain a-z0-9-_.'); }); it('should display a message when a wrong password was entered', () => { @@ -102,6 +102,6 @@ describe('LoginComponent', () => { expect(loginServiceSpy.login.calls.count()).toBe(1); tick(); fixture.detectChanges(); - expect(css('.message').innerText).toBe('Wrong credentials! Try again.'); + expect(css('.message').innerText).toBe('Wrong credentials!'); })); }); diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 2bc6ad0..a1b6f0e 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -1,6 +1,8 @@ -import { Component, OnInit } from '@angular/core'; -import {ValidationService} from '../validation.service'; -import {LoginService} from '../login.service'; +import {Component, OnInit} from '@angular/core'; +import {ValidationService} from '../services/validation.service'; +import {LoginService} from '../services/login.service'; + +// TODO: catch up with testing @Component({ selector: 'app-login', @@ -9,33 +11,27 @@ import {LoginService} from '../login.service'; }) export class LoginComponent implements OnInit { - message = ''; // message below login fields username = ''; // credentials password = ''; - validCredentials = false; // true if entered credentials are valid + message = ''; // message below login fields + constructor( private validate: ValidationService, - private loginService: LoginService + private loginService: LoginService, ) { } ngOnInit() { } login() { - const {ok: userOk, error: userError} = this.validate.username(this.username); - const {ok: passwordOk, error: passwordError} = this.validate.password(this.password); - this.message = userError + (userError !== '' && passwordError !== '' ? '\n' : '') + passwordError; // display errors - if (userOk && passwordOk) { - this.loginService.login(this.username, this.password).then(ok => { - if (ok) { - this.message = 'Login successful'; // TODO: think about following action - } - else { - this.message = 'Wrong credentials! Try again.'; - } - }); - } + this.loginService.login(this.username, this.password).then(ok => { + if (ok) { + this.message = 'Login successful'; // TODO: think about following action + } + else { + this.message = 'Wrong credentials!'; + } + }); } - } diff --git a/src/app/models/custom-fields.model.spec.ts b/src/app/models/custom-fields.model.spec.ts new file mode 100644 index 0000000..4b2d5d2 --- /dev/null +++ b/src/app/models/custom-fields.model.spec.ts @@ -0,0 +1,7 @@ +import { CustomFieldsModel } from './custom-fields.model'; + +describe('CustomFieldsModel', () => { + it('should create an instance', () => { + expect(new CustomFieldsModel()).toBeTruthy(); + }); +}); diff --git a/src/app/models/custom-fields.model.ts b/src/app/models/custom-fields.model.ts new file mode 100644 index 0000000..2edfc40 --- /dev/null +++ b/src/app/models/custom-fields.model.ts @@ -0,0 +1,13 @@ +import {Deserializable} from './deserializable.model'; + +// TODO: put all deserialize methods in one place + +export class CustomFieldsModel implements Deserializable{ + name = ''; + qty = 0; + + deserialize(input: any): this { + Object.assign(this, input); + return this; + } +} diff --git a/src/app/models/deserializable.model.spec.ts b/src/app/models/deserializable.model.spec.ts new file mode 100644 index 0000000..0e265c6 --- /dev/null +++ b/src/app/models/deserializable.model.spec.ts @@ -0,0 +1,5 @@ +// import { DeserializableModel } from './deserializable.model'; +// +// describe('DeserializableModel', () => { +// +// }); diff --git a/src/app/models/deserializable.model.ts b/src/app/models/deserializable.model.ts new file mode 100644 index 0000000..55b3ec4 --- /dev/null +++ b/src/app/models/deserializable.model.ts @@ -0,0 +1,3 @@ +export interface Deserializable { + deserialize(input: any): this; +} diff --git a/src/app/models/id.model.spec.ts b/src/app/models/id.model.spec.ts new file mode 100644 index 0000000..90dda80 --- /dev/null +++ b/src/app/models/id.model.spec.ts @@ -0,0 +1,5 @@ +import { IdModel } from './id.model'; + +describe('IdModel', () => { + +}); diff --git a/src/app/models/id.model.ts b/src/app/models/id.model.ts new file mode 100644 index 0000000..a945ecc --- /dev/null +++ b/src/app/models/id.model.ts @@ -0,0 +1 @@ +export type IdModel = string | null; diff --git a/src/app/models/material.model.spec.ts b/src/app/models/material.model.spec.ts new file mode 100644 index 0000000..6be3590 --- /dev/null +++ b/src/app/models/material.model.spec.ts @@ -0,0 +1,7 @@ +import { MaterialModel } from './material.model'; + +describe('MaterialModel', () => { + it('should create an instance', () => { + expect(new MaterialModel()).toBeTruthy(); + }); +}); diff --git a/src/app/models/material.model.ts b/src/app/models/material.model.ts new file mode 100644 index 0000000..8c88210 --- /dev/null +++ b/src/app/models/material.model.ts @@ -0,0 +1,33 @@ +import _ from 'lodash'; +import {Deserializable} from './deserializable.model'; +import {IdModel} from './id.model'; +import {SendFormat} from './sendformat.model'; + +export class MaterialModel implements Deserializable, SendFormat { + _id: IdModel = null; + name = ''; + supplier = ''; + group = ''; + mineral = 0; + glass_fiber = 0; + carbon_fiber = 0; + private numberTemplate = {color: '', number: ''}; + numbers: {color: string, number: string}[] = [_.cloneDeep(this.numberTemplate)]; + + deserialize(input: any): this { + Object.assign(this, input); + return this; + } + + sendFormat() { + return _.pick(this, ['name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers']); + } + + addNumber() { + this.numbers.push(_.cloneDeep(this.numberTemplate)); + } + + popNumber() { + this.numbers.pop(); + } +} diff --git a/src/app/models/measurement.model.spec.ts b/src/app/models/measurement.model.spec.ts new file mode 100644 index 0000000..2a96f8f --- /dev/null +++ b/src/app/models/measurement.model.spec.ts @@ -0,0 +1,7 @@ +import { MeasurementModel } from './measurement.model'; + +describe('MeasurementModel', () => { + it('should create an instance', () => { + expect(new MeasurementModel()).toBeTruthy(); + }); +}); diff --git a/src/app/models/measurement.model.ts b/src/app/models/measurement.model.ts new file mode 100644 index 0000000..a3120f4 --- /dev/null +++ b/src/app/models/measurement.model.ts @@ -0,0 +1,29 @@ +import _ from 'lodash'; +import {IdModel} from './id.model'; +import {SendFormat} from './sendformat.model'; +import {Deserializable} from './deserializable.model'; + +export class MeasurementModel implements Deserializable, SendFormat{ + _id: IdModel = null; + sample_id: IdModel = null; + measurement_template: IdModel; + values: {[prop: string]: any} = {}; + + constructor(measurementTemplate: IdModel = null) { + this.measurement_template = measurementTemplate; + } + + deserialize(input: any): this { + Object.assign(this, input); + Object.keys(this.values).forEach(key => { + if (this.values[key] === null) { + this.values[key] = ''; + } + }); + return this; + } + + sendFormat(omit = []) { + return _.omit(_.pick(this, ['sample_id', 'measurement_template', 'values']), omit); + } +} diff --git a/src/app/models/sample.model.spec.ts b/src/app/models/sample.model.spec.ts new file mode 100644 index 0000000..2959c9a --- /dev/null +++ b/src/app/models/sample.model.spec.ts @@ -0,0 +1,7 @@ +import { SampleModel } from './sample.model'; + +describe('SampleModel', () => { + it('should create an instance', () => { + expect(new SampleModel()).toBeTruthy(); + }); +}); diff --git a/src/app/models/sample.model.ts b/src/app/models/sample.model.ts new file mode 100644 index 0000000..bd0d5d3 --- /dev/null +++ b/src/app/models/sample.model.ts @@ -0,0 +1,37 @@ +import _ from 'lodash'; +import {Deserializable} from './deserializable.model'; +import {IdModel} from './id.model'; +import {SendFormat} from './sendformat.model'; +import {MaterialModel} from './material.model'; +import {MeasurementModel} from './measurement.model'; + +export class SampleModel implements Deserializable, SendFormat { + _id: IdModel = null; + color = ''; + number = ''; + type = ''; + batch = ''; + condition: {condition_template: string, [prop: string]: string} | {} = {}; + material_id: IdModel = null; + material: MaterialModel; + measurements: MeasurementModel[]; + note_id: IdModel = null; + user_id: IdModel = null; + notes: {comment: string, sample_references: {sample_id: IdModel, relation: string}[], custom_fields: {[prop: string]: string}} = {comment: '', sample_references: [], custom_fields: {}}; + + deserialize(input: any): this { + Object.assign(this, input); + if (input.hasOwnProperty('material')) { + this.material = new MaterialModel().deserialize(input.material); + this.material_id = input.material._id; + } + if (input.hasOwnProperty('measurements')) { + this.measurements = input.measurements.map(e => new MeasurementModel().deserialize(e)); + } + return this; + } + + sendFormat() { + return _.pick(this, ['color', 'type', 'batch', 'condition', 'material_id', 'notes']); + } +} diff --git a/src/app/models/sendformat.model.spec.ts b/src/app/models/sendformat.model.spec.ts new file mode 100644 index 0000000..3f6f470 --- /dev/null +++ b/src/app/models/sendformat.model.spec.ts @@ -0,0 +1,5 @@ +// import { SendformatModel } from './sendformat.model'; +// +// describe('SendformatModel', () => { +// +// }); diff --git a/src/app/models/sendformat.model.ts b/src/app/models/sendformat.model.ts new file mode 100644 index 0000000..9eea07e --- /dev/null +++ b/src/app/models/sendformat.model.ts @@ -0,0 +1,3 @@ +export interface SendFormat { + sendFormat(omit?: string[]): {[prop: string]: any}; +} diff --git a/src/app/models/template.model.spec.ts b/src/app/models/template.model.spec.ts new file mode 100644 index 0000000..39913ae --- /dev/null +++ b/src/app/models/template.model.spec.ts @@ -0,0 +1,7 @@ +import { TemplateModel } from './template.model'; + +describe('TemplateModel', () => { + it('should create an instance', () => { + expect(new TemplateModel()).toBeTruthy(); + }); +}); diff --git a/src/app/models/template.model.ts b/src/app/models/template.model.ts new file mode 100644 index 0000000..0c4081a --- /dev/null +++ b/src/app/models/template.model.ts @@ -0,0 +1,14 @@ +import {Deserializable} from './deserializable.model'; +import {IdModel} from './id.model'; + +export class TemplateModel implements Deserializable{ + _id: IdModel = null; + name = ''; + version = 1; + parameters: {name: string, range: {[prop: string]: any}}[] = []; + + deserialize(input: any): this { + Object.assign(this, input); + return this; + } +} diff --git a/src/app/sample/sample.component.html b/src/app/sample/sample.component.html new file mode 100644 index 0000000..1274b1b --- /dev/null +++ b/src/app/sample/sample.component.html @@ -0,0 +1,157 @@ +

{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}

+ + + +
+ +
+
+ + Cannot be empty + Unknown material, add properties for new material + + +
+ +
+

Material properties

+ + {{supplierInput.errors.failure}} + + + {{groupInput.errors.failure}} + + + Invalid value + Minimum value is 0 + Maximum value is 100 + + + Invalid value + Minimum value is 0 + Maximum value is 100 + + + Invalid value + Minimum value is 0 + Maximum value is 100 + + +
+
+ + + + +
+
+
+ +   + +
+ + {{typeInput.errors.failure}} + Cannot be empty + + + {{colorInput.errors.failure}} + Cannot be empty + + + {{batchInput.errors.failure}} + +
+
+ +
+ + {{commentInput.errors.failure}} + +
Additional properties
+
+
+ + {{keyInput.errors.failure}} + +
+ + Cannot be empty + +
+ +
+ +   + +
+

+ Condition + +

+
+ + + + + + {{parameterInput.errors.failure}} + Cannot be empty + +
+
+ +   + +
+

Measurements

+
+ + + + +
+ + {{parameterInput.errors.failure}} + Cannot be empty + + + Cannot be empty + +
+ + +
+ +   + +
+ +
+
+ +   + +
+ +
+
+ +
+

Successfully added sample:

+ + Sample number{{responseData.number}} + Type{{responseData.type}} + color{{responseData.color}} + Batch{{responseData.batch}} + Material{{material.name}} + + +   + + +
diff --git a/src/app/sample/sample.component.scss b/src/app/sample/sample.component.scss new file mode 100644 index 0000000..c65b876 --- /dev/null +++ b/src/app/sample/sample.component.scss @@ -0,0 +1,17 @@ +::ng-deep rb-table#response-data > table { + width: auto !important; +} + +td:first-child { + font-weight: bold; +} + +.condition-set { + float: right; +} + +.two-col { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: 10px; +} diff --git a/src/app/sample/sample.component.spec.ts b/src/app/sample/sample.component.spec.ts new file mode 100644 index 0000000..a2698cb --- /dev/null +++ b/src/app/sample/sample.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SampleComponent } from './sample.component'; + +describe('SampleComponent', () => { + let component: SampleComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SampleComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SampleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/sample/sample.component.ts b/src/app/sample/sample.component.ts new file mode 100644 index 0000000..6f2919b --- /dev/null +++ b/src/app/sample/sample.component.ts @@ -0,0 +1,341 @@ +import _ from 'lodash'; +import { + AfterContentChecked, + Component, + OnInit, + ViewChild +} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; +import {AutocompleteService} from '../services/autocomplete.service'; +import {ApiService} from '../services/api.service'; +import {MaterialModel} from '../models/material.model'; +import {SampleModel} from '../models/sample.model'; +import {NgForm, Validators} from '@angular/forms'; +import {ValidationService} from '../services/validation.service'; +import {TemplateModel} from '../models/template.model'; +import {MeasurementModel} from '../models/measurement.model'; + +// TODO: tests +// TODO: confirmation for new group/supplier +// TODO: DPT preview +// TODO: work on better recognition for file input + + + +@Component({ + selector: 'app-sample', + templateUrl: './sample.component.html', + styleUrls: ['./sample.component.scss'] +}) +export class SampleComponent implements OnInit, AfterContentChecked { + + @ViewChild('sampleForm') sampleForm: NgForm; + + new; // true if new sample should be created + newMaterial = false; // true if new material should be created + materials: MaterialModel[] = []; // all materials + suppliers: string[] = []; // all suppliers + groups: string[] = []; // all groups + conditionTemplates: TemplateModel[]; // all conditions + condition: TemplateModel | null = null; // selected condition + materialNames = []; // names of all materials + material = new MaterialModel(); // object of current selected material + sample = new SampleModel(); + customFields: [string, string][] = [['', '']]; + availableCustomFields: string[] = []; + responseData: SampleModel; // gets filled with response data after saving the sample + measurementTemplates: TemplateModel[]; + loading = 0; // number of currently loading instances + checkFormAfterInit = false; + + constructor( + private router: Router, + private route: ActivatedRoute, + private api: ApiService, + private validation: ValidationService, + public autocomplete: AutocompleteService + ) { } + + ngOnInit(): void { + this.new = this.router.url === '/samples/new'; + this.loading = 6; + this.api.get('/materials?status=all', (data: any) => { + this.materials = data.map(e => new MaterialModel().deserialize(e)); + this.materialNames = data.map(e => e.name); + this.loading--; + }); + this.api.get('/material/suppliers', (data: any) => { + this.suppliers = data; + this.loading--; + }); + this.api.get('/material/groups', (data: any) => { + this.groups = data; + this.loading--; + }); + this.api.get('/template/conditions', data => { + this.conditionTemplates = data.map(e => new TemplateModel().deserialize(e)); + this.loading--; + }); + this.api.get('/template/measurements', data => { + this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e)); + this.loading--; + }); + this.api.get('/sample/notes/fields', data => { + this.availableCustomFields = data.map(e => e.name); + this.loading--; + }); + if (!this.new) { + this.loading++; + this.api.get('/sample/' + this.route.snapshot.paramMap.get('id'), sData => { + this.sample.deserialize(sData); + this.material = sData.material; + this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ? Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']]; + if ('condition_template' in this.sample.condition) { + this.selectCondition(this.sample.condition.condition_template); + } + console.log('data loaded'); + this.loading--; + this.checkFormAfterInit = true; + }); + } + } + + ngAfterContentChecked() { + // attach validators to dynamic condition fields when all values are available and template was fully created + if (this.condition && this.condition.hasOwnProperty('parameters') && this.condition.parameters.length > 0 && this.condition.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('conditionParameter0')) { + for (const i in this.condition.parameters) { + if (this.condition.parameters[i]) { + this.attachValidator('conditionParameter' + i, this.condition.parameters[i].range, true); + } + } + } + + if (this.sampleForm && this.sampleForm.form.get('measurementParameter0-0')) { + this.sample.measurements.forEach((measurement, mIndex) => { + const template = this.getMeasurementTemplate(measurement.measurement_template); + for (const i in template.parameters) { + if (template.parameters[i]) { + this.attachValidator('measurementParameter' + mIndex + '-' + i, template.parameters[i].range, false); + } + } + }); + if (this.checkFormAfterInit) { + this.checkFormAfterInit = false; + this.initialValidate(); + } + } + } + + initialValidate() { + console.log('initVal'); + Object.keys(this.sampleForm.form.controls).forEach(field => { + this.sampleForm.form.get(field).updateValueAndValidity(); + }); + } + + attachValidator(name: string, range: {[prop: string]: any}, required: boolean) { + if (this.sampleForm.form.get(name)) { + const validators = []; + if (required) { + validators.push(Validators.required); + } + if (range.hasOwnProperty('values')) { + validators.push(this.validation.generate('stringOf', [range.values])); + } + else if (range.hasOwnProperty('min') && range.hasOwnProperty('max')) { + validators.push(this.validation.generate('minMax', [range.min, range.max])); + } + else if (range.hasOwnProperty('min')) { + validators.push(this.validation.generate('min', [range.min])); + } + else if (range.hasOwnProperty('max')) { + validators.push(this.validation.generate('max', [range.max])); + } + this.sampleForm.form.get(name).setValidators(validators); + } + } + + saveSample() { + new Promise(resolve => { + if (this.newMaterial) { // save material first if new one exists + for (const i in this.material.numbers) { // remove empty numbers fields + if (this.material.numbers[i].color === '') { + this.material.numbers.splice(i as any as number, 1); + } + } + this.api.post('/material/new', this.material.sendFormat(), data => { + this.materials.push(data); // add material to data + this.material = data; + this.sample.material_id = data._id; // add new material id to sample data + resolve(); + }); + } + else { + resolve(); + } + }).then(() => { // save sample + this.sample.notes.custom_fields = {}; + this.customFields.forEach(element => { + if (element[0] !== '') { + this.sample.notes.custom_fields[element[0]] = element[1]; + } + }); + new Promise(resolve => { + if (this.new) { + this.api.post('/sample/new', this.sample.sendFormat(), resolve); + } + else { + this.api.put('/sample/' + this.sample._id, this.sample.sendFormat(), resolve); + } + }).then( data => { + this.responseData = new SampleModel().deserialize(data); + this.material = this.materials.find(e => e._id === this.responseData.material_id); + this.sample.measurements.forEach(measurement => { + if (Object.keys(measurement.values).map(e => measurement.values[e]).join('') !== '') { + Object.keys(measurement.values).forEach(key => { + measurement.values[key] = measurement.values[key] === '' ? null : measurement.values[key]; + }); + if (measurement._id === null) { // new measurement + measurement.sample_id = data._id; + this.api.post('/measurement/new', measurement.sendFormat()); + } + else { // update measurement + this.api.put('/measurement/' + measurement._id, measurement.sendFormat(['sample_id', 'measurement_template'])); + } + } + else if (measurement._id !== null) { // existing measurement was left empty to delete + this.api.delete('/measurement/' + measurement._id); + } + }); + }); + }); + } + + findMaterial(name) { + const res = this.materials.find(e => e.name === name); // search for match + if (res) { + this.material = _.cloneDeep(res); + this.sample.material_id = this.material._id; + } + else { + this.sample.material_id = null; + } + this.setNewMaterial(); + } + + preventSubmit(event) { + if (event.key === 'Enter') { + event.preventDefault(); + } + } + + getColors(material) { + return material ? material.numbers.map(e => e.color) : []; + } + + // TODO: rework later + setNewMaterial(value = null) { + if (value === null) { + this.newMaterial = !this.sample.material_id; + } + else { + this.newMaterial = value; + } + if (this.newMaterial) { + this.sampleForm.form.get('materialname').setValidators([Validators.required]); + } + else { + this.sampleForm.form.get('materialname').setValidators([Validators.required, this.validation.generate('stringOf', [this.materialNames])]); + } + this.sampleForm.form.get('materialname').updateValueAndValidity(); + } + + handleMaterialNumbers() { + const fieldNo = this.material.numbers.length; + let filledFields = 0; + this.material.numbers.forEach(mNumber => { + if (mNumber.color !== '') { + filledFields ++; + } + }); + // append new field + if (filledFields === fieldNo) { + this.material.addNumber(); + } + // remove if two end fields are empty + if (fieldNo > 1 && this.material.numbers[fieldNo - 1].color === '' && this.material.numbers[fieldNo - 2].color === '') { + this.material.popNumber(); + } + } + + selectCondition(id) { + this.condition = this.conditionTemplates.find(e => e._id === id); + console.log(this.condition); + console.log(this.sample); + if ('condition_template' in this.sample.condition) { + this.sample.condition.condition_template = id; + } + } + + getMeasurementTemplate(id): TemplateModel { + return this.measurementTemplates && id ? this.measurementTemplates.find(e => e._id === id) : new TemplateModel(); + } + + addMeasurement() { + this.sample.measurements.push(new MeasurementModel(this.measurementTemplates[0]._id)); + } + + removeMeasurement(index) { + if (this.sample.measurements[index]._id !== null) { + this.api.delete('/measurement/' + this.sample.measurements[index]._id); + } + this.sample.measurements.splice(index, 1); + } + + fileToArray(event, mIndex, parameter) { + const fileReader = new FileReader(); + fileReader.onload = () => { + this.sample.measurements[mIndex].values[parameter] = fileReader.result.toString().split('\r\n').map(e => e.split(',')); + }; + fileReader.readAsText(event.target.files[0]); + } + + toggleCondition() { + if (this.condition) { + this.condition = null; + } + else { + this.sample.condition = {condition_template: null}; + this.selectCondition(this.conditionTemplates[0]._id); + } + } + + adjustCustomFields(value, index) { + this.customFields[index][0] = value; + const fieldNo = this.customFields.length; + let filledFields = 0; + this.customFields.forEach(field => { + if (field[0] !== '') { + filledFields ++; + } + }); + // append new field + if (filledFields === fieldNo) { + this.customFields.push(['', '']); + } + // remove if two end fields are empty + if (fieldNo > 1 && this.customFields[fieldNo - 1][0] === '' && this.customFields[fieldNo - 2][0] === '') { + this.customFields.pop(); + } + } + + uniqueCfValues(index) { // returns all names until index for unique check + return this.customFields.slice(0, index).map(e => e[0]); + } +} + + + +// 1. ngAfterViewInit wird ja jedes mal nach einem ngOnChanges aufgerufen, also zB wenn sich dein ngFor aufbaut. Du könntest also in der Methode prüfen, ob die Daten schon da sind und dann dementsprechend handeln. Das wäre die Eleganteste Variante +// 2. Der state "dirty" soll eigentlich anzeigen, wenn ein Form-Field vom User geändert wurde; damit missbrauchst du es hier etwas +// 3. Die Dirty-Variante: Pack in deine ngFor ein {{ onFirstLoad(data) }} rein, das einfach ausgeführt wird. müsstest dann natürlich abfangen, dass das nicht nach jedem view-cycle neu getriggert wird. Schön ist das nicht, aber besser als mit Timeouts^^ diff --git a/src/app/samples/samples.component.html b/src/app/samples/samples.component.html index 00c2768..8e7f199 100644 --- a/src/app/samples/samples.component.html +++ b/src/app/samples/samples.component.html @@ -1,12 +1,22 @@

Samples

- + + +
  Filter - Not implemented (yet) +
+ + + + + + + +
@@ -23,6 +33,7 @@ type Color Batch + @@ -37,5 +48,6 @@ {{sample.type}} {{sample.color}} {{sample.batch}} + diff --git a/src/app/samples/samples.component.scss b/src/app/samples/samples.component.scss index 4d9fd1d..e2d94b5 100644 --- a/src/app/samples/samples.component.scss +++ b/src/app/samples/samples.component.scss @@ -1,3 +1,5 @@ +@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; + .header-addnew { margin-bottom: 40px; @@ -11,3 +13,12 @@ } } +.rb-ic.rb-ic-edit { + font-size: 1.1rem; + color: $color-gray-silver-sand; + cursor: pointer; + + &:hover { + color: #000; + } +} diff --git a/src/app/samples/samples.component.ts b/src/app/samples/samples.component.ts index df3327e..a3bdb39 100644 --- a/src/app/samples/samples.component.ts +++ b/src/app/samples/samples.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import {ApiService} from '../api.service'; +import {ApiService} from '../services/api.service'; @Component({ selector: 'app-samples', @@ -10,24 +10,27 @@ export class SamplesComponent implements OnInit { // TODO: implement paging materials = {}; samples = []; + filters = {status: 'validated'}; constructor( private api: ApiService ) { } ngOnInit(): void { - this.api.get('/materials').subscribe((mData: any) => { + this.api.get('/materials?status=all', (mData: any) => { this.materials = {}; mData.forEach(material => { this.materials[material._id] = material; }); - console.log(this.materials); - this.api.get('/samples').subscribe(sData => { - console.log(sData); - this.samples = sData as any; - this.samples.forEach(sample => { - sample.material_number = this.materials[sample.material_id].numbers.find(e => sample.color === e.color).number; - }); + this.loadSamples(); + }); + } + + loadSamples() { + this.api.get(`/samples?status=${this.filters.status}`, sData => { + this.samples = sData as any; + this.samples.forEach(sample => { + sample.material_number = this.materials[sample.material_id].numbers.find(e => sample.color === e.color).number; }); }); } diff --git a/src/app/services/api.service.spec.ts b/src/app/services/api.service.spec.ts new file mode 100644 index 0000000..7927549 --- /dev/null +++ b/src/app/services/api.service.spec.ts @@ -0,0 +1,53 @@ +// import { TestBed } from '@angular/core/testing'; +// import { ApiService } from './api.service'; +// import {HttpClient} from '@angular/common/http'; +// import {LocalStorageService} from 'angular-2-local-storage'; +// import {Observable} from 'rxjs'; +// +// let apiService: ApiService; +// let httpClientSpy: jasmine.SpyObj; +// let localStorageServiceSpy: jasmine.SpyObj; +// +// describe('ApiService', () => { +// beforeEach(() => { +// const httpSpy = jasmine.createSpyObj('HttpClient', ['get']); +// const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['get']); +// +// TestBed.configureTestingModule({ +// providers: [ +// ApiService, +// {provide: HttpClient, useValue: httpSpy}, +// {provide: LocalStorageService, useValue: localStorageSpy} +// ] +// }); +// +// apiService = TestBed.inject(ApiService); +// httpClientSpy = TestBed.inject(HttpClient) as jasmine.SpyObj; +// localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj; +// }); +// +// it('should be created', () => { +// expect(apiService).toBeTruthy(); +// }); +// +// it('should do get requests without auth if not available', () => { +// const getReturn = new Observable(); +// httpClientSpy.get.and.returnValue(getReturn); +// localStorageServiceSpy.get.and.returnValue(undefined); +// +// const result = apiService.get('/testurl'); +// expect(result).toBe(getReturn); +// expect(httpClientSpy.get).toHaveBeenCalledWith('/testurl', {}); +// expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); +// }); +// it('should do get requests with basic auth if available', () => { +// const getReturn = new Observable(); +// httpClientSpy.get.and.returnValue(getReturn); +// localStorageServiceSpy.get.and.returnValue('basicAuth'); +// +// const result = apiService.get('/testurl'); +// expect(result).toBe(getReturn); +// expect(httpClientSpy.get).toHaveBeenCalledWith('/testurl', jasmine.any(Object)); // could not test http headers better +// expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); +// }); +// }); diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts new file mode 100644 index 0000000..3296ea8 --- /dev/null +++ b/src/app/services/api.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {LocalStorageService} from 'angular-2-local-storage'; +import {Observable} from 'rxjs'; +import {ErrorComponent} from '../error/error.component'; +import {ModalService} from '@inst-iot/bosch-angular-ui-components'; + +@Injectable({ + providedIn: 'root' +}) +export class ApiService { + + private host = '/api'; + + constructor( + private http: HttpClient, + private storage: LocalStorageService, + private modalService: ModalService + ) { } + + get(url, f: (data?: T, err?) => void = () => {}) { + this.requestErrorHandler(this.http.get(this.host + url, this.authOptions()), f); + } + + post(url, data = null, f: (data?: T, err?) => void = () => {}) { + this.requestErrorHandler(this.http.post(this.host + url, data, this.authOptions()), f); + } + + put(url, data = null, f: (data?: T, err?) => void = () => {}) { + this.requestErrorHandler(this.http.put(this.host + url, data, this.authOptions()), f); + } + + delete(url, f: (data?: T, err?) => void = () => {}) { + this.requestErrorHandler(this.http.delete(this.host + url, this.authOptions()), f); + } + + private requestErrorHandler(observable: Observable, f: (data?: T, err?) => void) { + observable.subscribe(data => { + f(data, undefined); + }, () => { + const modalRef = this.modalService.openComponent(ErrorComponent); + modalRef.instance.message = 'Network request failed!'; + }); + } + + private authOptions() { + const auth = this.storage.get('basicAuth'); + if (auth) { + return {headers: new HttpHeaders({Authorization: 'Basic ' + auth})}; + } + else { + return {}; + } + } +} diff --git a/src/app/services/autocomplete.service.spec.ts b/src/app/services/autocomplete.service.spec.ts new file mode 100644 index 0000000..790b9d3 --- /dev/null +++ b/src/app/services/autocomplete.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AutocompleteService } from './autocomplete.service'; + +describe('AutocompleteService', () => { + let service: AutocompleteService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AutocompleteService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/autocomplete.service.ts b/src/app/services/autocomplete.service.ts new file mode 100644 index 0000000..15080ea --- /dev/null +++ b/src/app/services/autocomplete.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import {QuickScore} from 'quick-score'; +import {of} from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class AutocompleteService { + + constructor() { } + + bind(ref, list) { + return this.search.bind(ref, list); + } + + search(arr, str) { + const qs = new QuickScore(arr); + return of(str === '' ? [] : qs.search(str).map(e => e.item)); + } +} diff --git a/src/app/services/login.service.spec.ts b/src/app/services/login.service.spec.ts new file mode 100644 index 0000000..da0899c --- /dev/null +++ b/src/app/services/login.service.spec.ts @@ -0,0 +1,81 @@ +// import { TestBed } from '@angular/core/testing'; +// +// import { LoginService } from './login.service'; +// import {LocalStorageService} from 'angular-2-local-storage'; +// import {ApiService} from './api.service'; +// import {Observable} from 'rxjs'; +// +// let loginService: LoginService; +// let apiServiceSpy: jasmine.SpyObj; +// let localStorageServiceSpy: jasmine.SpyObj; +// +// describe('LoginService', () => { +// beforeEach(() => { +// const apiSpy = jasmine.createSpyObj('ApiService', ['get']); +// const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['set', 'remove']); +// +// TestBed.configureTestingModule({ +// providers: [ +// LoginService, +// {provide: ApiService, useValue: apiSpy}, +// {provide: LocalStorageService, useValue: localStorageSpy} +// ] +// }); +// loginService = TestBed.inject(LoginService); +// apiServiceSpy = TestBed.inject(ApiService) as jasmine.SpyObj; +// localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj; +// }); +// +// it('should be created', () => { +// expect(loginService).toBeTruthy(); +// }); +// +// describe('login', () => { +// it('should store the basic auth', () => { +// localStorageServiceSpy.set.and.returnValue(true); +// apiServiceSpy.get.and.returnValue(new Observable()); +// loginService.login('username', 'password'); +// expect(localStorageServiceSpy.set).toHaveBeenCalledWith('basicAuth', 'dXNlcm5hbWU6cGFzc3dvcmQ='); +// }); +// +// it('should remove the basic auth if login fails', () => { +// localStorageServiceSpy.set.and.returnValue(true); +// localStorageServiceSpy.remove.and.returnValue(true); +// apiServiceSpy.get.and.returnValue(new Observable(o => o.error())); +// loginService.login('username', 'password'); +// expect(localStorageServiceSpy.remove.calls.count()).toBe(1); +// expect(localStorageServiceSpy.remove).toHaveBeenCalledWith('basicAuth'); +// }); +// +// it('should resolve true when login succeeds', async () => { +// localStorageServiceSpy.set.and.returnValue(true); +// apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'Authorization successful', method: 'basic'}))); +// expect(await loginService.login('username', 'password')).toBeTruthy(); +// }); +// +// it('should resolve false when a wrong result comes in', async () => { +// localStorageServiceSpy.set.and.returnValue(true); +// apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'xxx', method: 'basic'}))); +// expect(await loginService.login('username', 'password')).toBeFalsy(); +// }); +// +// it('should resolve false on an error', async () => { +// localStorageServiceSpy.set.and.returnValue(true); +// apiServiceSpy.get.and.returnValue(new Observable(o => o.error())); +// expect(await loginService.login('username', 'password')).toBeFalsy(); +// }); +// }); +// +// describe('canActivate', () => { +// it('should return false at first', () => { +// expect(loginService.canActivate(null, null)).toBeFalsy(); +// }); +// +// it('returns true if login was successful', async () => { +// localStorageServiceSpy.set.and.returnValue(true); +// apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'Authorization successful', method: 'basic'}))); +// await loginService.login('username', 'password'); +// expect(loginService.canActivate(null, null)).toBeTruthy(); +// }); +// }); +// }); diff --git a/src/app/login.service.ts b/src/app/services/login.service.ts similarity index 66% rename from src/app/login.service.ts rename to src/app/services/login.service.ts index cfa1308..bb578b6 100644 --- a/src/app/login.service.ts +++ b/src/app/services/login.service.ts @@ -2,19 +2,20 @@ import { Injectable } from '@angular/core'; import {ApiService} from './api.service'; import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router'; import {LocalStorageService} from 'angular-2-local-storage'; +import {Observable} from 'rxjs'; @Injectable({ providedIn: 'root' }) export class LoginService implements CanActivate { - private loggedIn = false; + private loggedIn; constructor( private api: ApiService, private storage: LocalStorageService ) { - this.login(); + } login(username = '', password = '') { @@ -22,26 +23,37 @@ export class LoginService implements CanActivate { if (username !== '') { this.storage.set('basicAuth', btoa(username + ':' + password)); } - this.api.get('/authorized').subscribe((data: any) => { + this.api.get('/authorized', (data: any, error) => { + if (!error) { if (data.status === 'Authorization successful') { this.loggedIn = true; resolve(true); - } - else { + } else { this.loggedIn = false; this.storage.remove('basicAuth'); resolve(false); } - }, - () => { + } else { this.loggedIn = false; this.storage.remove('basicAuth'); resolve(false); - }); + } + }); }); } - canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null) { - return this.loggedIn; + canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable { + return new Observable(observer => { + if (this.loggedIn === undefined) { + this.login().then(res => { + observer.next(res as any); + observer.complete(); + }); + } + else { + observer.next(this.loggedIn); + observer.complete(); + } + }); } } diff --git a/src/app/validation.service.spec.ts b/src/app/services/validation.service.spec.ts similarity index 100% rename from src/app/validation.service.spec.ts rename to src/app/services/validation.service.spec.ts diff --git a/src/app/services/validation.service.ts b/src/app/services/validation.service.ts new file mode 100644 index 0000000..4fd69bc --- /dev/null +++ b/src/app/services/validation.service.ts @@ -0,0 +1,124 @@ +import { Injectable } from '@angular/core'; +import Joi from '@hapi/joi'; +import {AbstractControl} from '@angular/forms'; + +@Injectable({ + providedIn: 'root' +}) +export class ValidationService { + + private vUsername = Joi.string() + .lowercase() + .pattern(new RegExp('^[a-z0-9-_.]+$')) + .min(1) + .max(128); + + private vPassword = Joi.string() + .pattern(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&'()*+,-.\/:;<=>?@[\]^_`{|}~])(?=\S+$)[a-zA-Z0-9!"#%&'()*+,\-.\/:;<=>?@[\]^_`{|}~]{8,}$/) + .max(128); + + constructor() { } + + generate(method, args) { // generate a Validator function + return (control: AbstractControl): {[key: string]: any} | null => { + let ok; + let error; + if (args) { + ({ok, error} = this[method](control.value, ...args)); + } + else { + ({ok, error} = this[method](control.value)); + } + return ok ? null : { failure: error }; + }; + } + + username(data) { + const {ignore, error} = this.vUsername.validate(data); + if (error) { + return {ok: false, error: 'username must only contain a-z0-9-_.'}; + } + return {ok: true, error: ''}; + } + + password(data) { + const {ignore, error} = this.vPassword.validate(data); + if (error) { + if (Joi.string().min(8).validate(data).error) { + return {ok: false, error: 'password must have at least 8 characters'}; + } + else if (Joi.string().pattern(/[a-z]+/).validate(data).error) { + return {ok: false, error: 'password must have at least one lowercase character'}; + } + else if (Joi.string().pattern(/[A-Z]+/).validate(data).error) { + return {ok: false, error: 'password must have at least one uppercase character'}; + } + else if (Joi.string().pattern(/[0-9]+/).validate(data).error) { + return {ok: false, error: 'password must have at least one number'}; + } + else if (Joi.string().pattern(/[!"#%&'()*+,-.\/:;<=>?@[\]^_`{|}~]+/).validate(data).error) { + return {ok: false, error: 'password must have at least one of the following characters !"#%&\'()*+,-.\\/:;<=>?@[]^_`{|}~'}; + } + else { + return {ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'}; + } + } + return {ok: true, error: ''}; + } + + string(data) { + const {ignore, error} = Joi.string().max(128).allow('').validate(data); + if (error) { + return {ok: false, error: 'must contain max 128 characters'}; + } + return {ok: true, error: ''}; + } + + stringOf(data, list) { + const {ignore, error} = Joi.string().allow('').valid(...list.map(e => e.toString())).validate(data); + if (error) { + return {ok: false, error: 'must be one of ' + list.join(', ')}; + } + return {ok: true, error: ''}; + } + + stringLength(data, length) { + const {ignore, error} = Joi.string().max(length).allow('').validate(data); + if (error) { + return {ok: false, error: 'must contain max ' + length + ' characters'}; + } + return {ok: true, error: ''}; + } + + minMax(data, min, max) { + const {ignore, error} = Joi.number().allow('').min(min).max(max).validate(data); + if (error) { + return {ok: false, error: `must be between ${min} and ${max}`}; + } + return {ok: true, error: ''}; + } + + min(data, min) { + const {ignore, error} = Joi.number().allow('').min(min).validate(data); + if (error) { + return {ok: false, error: `must not be below ${min}`}; + } + return {ok: true, error: ''}; + } + + max(data, max) { + const {ignore, error} = Joi.number().allow('').min(max).validate(data); + if (error) { + return {ok: false, error: `must not be above ${max}`}; + } + return {ok: true, error: ''}; + } + + unique(data, list) { + const {ignore, error} = Joi.string().allow('').invalid(...list.map(e => e.toString())).validate(data); + if (error) { + return {ok: false, error: `values must be unique`}; + } + return {ok: true, error: ''}; + } +} diff --git a/src/app/validate.directive.spec.ts b/src/app/validate.directive.spec.ts new file mode 100644 index 0000000..9489d0d --- /dev/null +++ b/src/app/validate.directive.spec.ts @@ -0,0 +1,8 @@ +// import { ValidateDirective } from './validate.directive'; +// +// describe('ValidateDirective', () => { +// it('should create an instance', () => { +// const directive = new ValidateDirective(); +// expect(directive).toBeTruthy(); +// }); +// }); diff --git a/src/app/validate.directive.ts b/src/app/validate.directive.ts new file mode 100644 index 0000000..7b8a2ec --- /dev/null +++ b/src/app/validate.directive.ts @@ -0,0 +1,28 @@ +import {Directive, Input} from '@angular/core'; +import {AbstractControl, NG_VALIDATORS} from '@angular/forms'; +import {ValidationService} from './services/validation.service'; + +@Directive({ + selector: '[appValidate]', + providers: [{provide: NG_VALIDATORS, useExisting: ValidateDirective, multi: true}] +}) +export class ValidateDirective { + @Input('appValidate') method: string; + @Input('appValidateArgs') args: Array; + + constructor( + private validation: ValidationService + ) { } + + validate(control: AbstractControl): {[key: string]: any} | null { + let ok; + let error; + if (this.args) { + ({ok, error} = this.validation[this.method](control.value, ...this.args)); + } + else { + ({ok, error} = this.validation[this.method](control.value)); + } + return ok ? null : { failure: error }; + } +} diff --git a/src/app/validation.service.ts b/src/app/validation.service.ts deleted file mode 100644 index bc2d41b..0000000 --- a/src/app/validation.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Injectable } from '@angular/core'; -import Joi from '@hapi/joi'; - -@Injectable({ - providedIn: 'root' -}) -export class ValidationService { - - private vUsername = Joi.string() - .lowercase() - .pattern(new RegExp('^[a-z0-9-_.]+$')) - .min(1) - .max(128); - - private vPassword = Joi.string() - .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')) - .max(128); - - constructor() { } - - username(data) { - const {ignore, error} = this.vUsername.validate(data); - if (error) { - return {ok: false, error: 'username must only contain a-z0-9-_.'}; - } - return {ok: true, error: ''}; - } - - password(data) { - const {ignore, error} = this.vPassword.validate(data); - if (error) { - return {ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'}; - } - return {ok: true, error: ''}; - } -} diff --git a/tsconfig.json b/tsconfig.json index 30956ae..08610e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, - "strictInjectionParameters": true + "strictInjectionParameters": true, + "debug": true } } diff --git a/tslint.json b/tslint.json index d97b386..a1e1fa5 100644 --- a/tslint.json +++ b/tslint.json @@ -34,10 +34,7 @@ ], "interface-name": false, "max-classes-per-file": false, - "max-line-length": [ - true, - 140 - ], + "max-line-length": false, "member-access": false, "member-ordering": [ true, @@ -73,6 +70,7 @@ "as-needed" ], "object-literal-sort-keys": false, + "one-line": false, "ordered-imports": false, "quotemark": [ true, @@ -90,7 +88,15 @@ "template-banana-in-box": true, "template-no-negated-async": true, "use-lifecycle-interface": true, - "use-pipe-transform-interface": true + "use-pipe-transform-interface": true, + "variable-name": { + "options": [ + "allow-leading-underscore", + "allow-pascal-case", + "allow-snake-case", + "check-format" + ] + } }, "rulesDirectory": [ "codelyzer" From 09598c4ba760b89450c4e3b2aff7c2aa614c6a9a Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Mon, 22 Jun 2020 10:22:45 +0200 Subject: [PATCH 2/7] tests done except for sample and samples component --- src/app/app.component.spec.ts | 9 + src/app/login/login.component.spec.ts | 60 ++++-- src/app/login/login.component.ts | 4 +- src/app/models/base.model.spec.ts | 7 + src/app/models/base.model.ts | 10 + src/app/models/custom-fields.model.ts | 10 +- src/app/models/deserializable.model.spec.ts | 5 - src/app/models/deserializable.model.ts | 3 - src/app/models/id.model.spec.ts | 5 - src/app/models/material.model.ts | 10 +- src/app/models/measurement.model.ts | 6 +- src/app/models/sample.model.ts | 5 +- src/app/models/sendformat.model.spec.ts | 5 - src/app/models/sendformat.model.ts | 3 - src/app/models/template.model.ts | 9 +- .../rb-table/rb-table/rb-table.component.html | 1 + src/app/sample/sample.component.spec.ts | 52 ++--- src/app/samples/samples.component.spec.ts | 52 ++--- src/app/services/api.service.spec.ts | 189 +++++++++++++----- src/app/services/api.service.ts | 12 +- src/app/services/autocomplete.service.spec.ts | 27 ++- src/app/services/autocomplete.service.ts | 4 +- src/app/services/login.service.spec.ts | 168 ++++++++-------- src/app/services/validation.service.spec.ts | 83 +++++++- src/app/services/validation.service.ts | 2 +- 25 files changed, 474 insertions(+), 267 deletions(-) create mode 100644 src/app/models/base.model.spec.ts create mode 100644 src/app/models/base.model.ts delete mode 100644 src/app/models/deserializable.model.spec.ts delete mode 100644 src/app/models/deserializable.model.ts delete mode 100644 src/app/models/id.model.spec.ts delete mode 100644 src/app/models/sendformat.model.spec.ts delete mode 100644 src/app/models/sendformat.model.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 037d7a8..8105dfb 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -3,6 +3,9 @@ import { AppComponent } from './app.component'; import {By} from '@angular/platform-browser'; import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; import {RouterTestingModule} from '@angular/router/testing'; +import {LoginService} from './services/login.service'; + +let loginServiceSpy: jasmine.SpyObj; describe('AppComponent', () => { let component: AppComponent; @@ -10,13 +13,19 @@ describe('AppComponent', () => { let css; // get native element by css selector beforeEach(async(() => { + const loginSpy = jasmine.createSpyObj('LoginService', ['login', 'canActivate']); + TestBed.configureTestingModule({ declarations: [ AppComponent ], imports: [ RbUiComponentsModule, RouterTestingModule + ], + providers: [ + {provide: LoginService, useValue: loginSpy} ] }).compileComponents(); + loginServiceSpy = TestBed.inject(LoginService) as jasmine.SpyObj; })); beforeEach(() => { diff --git a/src/app/login/login.component.spec.ts b/src/app/login/login.component.spec.ts index 359167d..5922456 100644 --- a/src/app/login/login.component.spec.ts +++ b/src/app/login/login.component.spec.ts @@ -2,9 +2,10 @@ import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/t import { LoginComponent } from './login.component'; import {LoginService} from '../services/login.service'; import {ValidationService} from '../services/validation.service'; -import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; import {FormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; +import {ValidateDirective} from '../validate.directive'; +import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components'; let validationServiceSpy: jasmine.SpyObj; let loginServiceSpy: jasmine.SpyObj; @@ -20,7 +21,7 @@ describe('LoginComponent', () => { const loginSpy = jasmine.createSpyObj('LoginService', ['login']); TestBed.configureTestingModule({ - declarations: [ LoginComponent ], + declarations: [ LoginComponent, ValidateDirective ], imports: [ RbUiComponentsModule, FormsModule @@ -39,12 +40,15 @@ describe('LoginComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; + component.ngOnInit(); fixture.detectChanges(); cssd = (selector) => fixture.debugElement.query(By.css(selector)); css = (selector) => fixture.debugElement.query(By.css(selector)).nativeElement; }); it('should create', () => { + validationServiceSpy.username.and.returnValue({ok: true, error: ''}); + validationServiceSpy.password.and.returnValue({ok: true, error: ''}); expect(component).toBeTruthy(); }); @@ -61,27 +65,45 @@ describe('LoginComponent', () => { expect(css('.message').innerText).toBe(''); }); - it('should have a login button', () => { - expect(css('.login-button')).toBeTruthy(); - }); - - it('should display a message when a wrong username was entered', () => { - validationServiceSpy.username.and.returnValue({ok: false, error: 'username must only contain a-z0-9-_.'}); - validationServiceSpy.password.and.returnValue({ok: true, error: ''}); - - cssd('.login-button').triggerEventHandler('click', null); - fixture.detectChanges(); - expect(css('.error-messages > div').innerText).toBe('username must only contain a-z0-9-_.'); - }); - - it('should display a message when a wrong password was entered', () => { + it('should have a login button', async () => { validationServiceSpy.username.and.returnValue({ok: true, error: ''}); + validationServiceSpy.password.and.returnValue({ok: true, error: ''}); + await fixture.whenStable(); + fixture.detectChanges(); + expect(css('.login-button')).toBeTruthy(); + expect(css('.login-button').disabled).toBeTruthy(); + }); + + it('should reject a wrong username', async () => { + validationServiceSpy.username.and.returnValue({ok: false, error: 'username must only contain a-z0-9-_.'}); + component.username = 'ab#'; + fixture.detectChanges(); + await fixture.whenRenderingDone(); + expect(component.loginForm.controls.username.valid).toBeFalsy(); + expect(validationServiceSpy.username).toHaveBeenCalledWith('ab#'); + }); + + it('should reject a wrong password', async () => { validationServiceSpy.password.and.returnValue({ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'}); + component.password = 'abc'; + + fixture.detectChanges(); + await fixture.whenRenderingDone(); + expect(component.loginForm.controls.password.valid).toBeFalsy(); + expect(validationServiceSpy.password).toHaveBeenCalledWith('abc'); + }); + + it('should enable the login button with valid credentials', async () => { + validationServiceSpy.username.and.returnValue({ok: true, error: ''}); + validationServiceSpy.password.and.returnValue({ok: true, error: ''}); + loginServiceSpy.login.and.returnValue(new Promise(r => r(true))); + + fixture.detectChanges(); + await fixture.whenRenderingDone(); cssd('.login-button').triggerEventHandler('click', null); - fixture.detectChanges(); - expect(css('.message').innerText).toBe('password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'); - expect(loginServiceSpy.login).not.toHaveBeenCalled(); + expect(css('.login-button').disabled).toBeFalsy(); + expect(loginServiceSpy.login.calls.count()).toBe(1); }); it('should call the LoginService with valid credentials', () => { diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index a1b6f0e..bc5612e 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -1,8 +1,7 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, OnInit, ViewChild} from '@angular/core'; import {ValidationService} from '../services/validation.service'; import {LoginService} from '../services/login.service'; -// TODO: catch up with testing @Component({ selector: 'app-login', @@ -14,6 +13,7 @@ export class LoginComponent implements OnInit { username = ''; // credentials password = ''; message = ''; // message below login fields + @ViewChild('loginForm') loginForm; constructor( diff --git a/src/app/models/base.model.spec.ts b/src/app/models/base.model.spec.ts new file mode 100644 index 0000000..4bbedf0 --- /dev/null +++ b/src/app/models/base.model.spec.ts @@ -0,0 +1,7 @@ +import { BaseModel } from './base.model'; + +describe('BaseModel', () => { + it('should create an instance', () => { + expect(new BaseModel()).toBeTruthy(); + }); +}); diff --git a/src/app/models/base.model.ts b/src/app/models/base.model.ts new file mode 100644 index 0000000..587913d --- /dev/null +++ b/src/app/models/base.model.ts @@ -0,0 +1,10 @@ +export class BaseModel { + deserialize(input: any): this { + Object.assign(this, input); + return this; + } + + sendFormat(): this { + return this; + } +} diff --git a/src/app/models/custom-fields.model.ts b/src/app/models/custom-fields.model.ts index 2edfc40..7f20193 100644 --- a/src/app/models/custom-fields.model.ts +++ b/src/app/models/custom-fields.model.ts @@ -1,13 +1,7 @@ -import {Deserializable} from './deserializable.model'; +import {BaseModel} from './base.model'; -// TODO: put all deserialize methods in one place -export class CustomFieldsModel implements Deserializable{ +export class CustomFieldsModel extends BaseModel { name = ''; qty = 0; - - deserialize(input: any): this { - Object.assign(this, input); - return this; - } } diff --git a/src/app/models/deserializable.model.spec.ts b/src/app/models/deserializable.model.spec.ts deleted file mode 100644 index 0e265c6..0000000 --- a/src/app/models/deserializable.model.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -// import { DeserializableModel } from './deserializable.model'; -// -// describe('DeserializableModel', () => { -// -// }); diff --git a/src/app/models/deserializable.model.ts b/src/app/models/deserializable.model.ts deleted file mode 100644 index 55b3ec4..0000000 --- a/src/app/models/deserializable.model.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Deserializable { - deserialize(input: any): this; -} diff --git a/src/app/models/id.model.spec.ts b/src/app/models/id.model.spec.ts deleted file mode 100644 index 90dda80..0000000 --- a/src/app/models/id.model.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IdModel } from './id.model'; - -describe('IdModel', () => { - -}); diff --git a/src/app/models/material.model.ts b/src/app/models/material.model.ts index 8c88210..e0e8821 100644 --- a/src/app/models/material.model.ts +++ b/src/app/models/material.model.ts @@ -1,9 +1,8 @@ import _ from 'lodash'; -import {Deserializable} from './deserializable.model'; import {IdModel} from './id.model'; -import {SendFormat} from './sendformat.model'; +import {BaseModel} from './base.model'; -export class MaterialModel implements Deserializable, SendFormat { +export class MaterialModel extends BaseModel { _id: IdModel = null; name = ''; supplier = ''; @@ -14,11 +13,6 @@ export class MaterialModel implements Deserializable, SendFormat { private numberTemplate = {color: '', number: ''}; numbers: {color: string, number: string}[] = [_.cloneDeep(this.numberTemplate)]; - deserialize(input: any): this { - Object.assign(this, input); - return this; - } - sendFormat() { return _.pick(this, ['name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers']); } diff --git a/src/app/models/measurement.model.ts b/src/app/models/measurement.model.ts index a3120f4..4539430 100644 --- a/src/app/models/measurement.model.ts +++ b/src/app/models/measurement.model.ts @@ -1,15 +1,15 @@ import _ from 'lodash'; import {IdModel} from './id.model'; -import {SendFormat} from './sendformat.model'; -import {Deserializable} from './deserializable.model'; +import {BaseModel} from './base.model'; -export class MeasurementModel implements Deserializable, SendFormat{ +export class MeasurementModel extends BaseModel { _id: IdModel = null; sample_id: IdModel = null; measurement_template: IdModel; values: {[prop: string]: any} = {}; constructor(measurementTemplate: IdModel = null) { + super(); this.measurement_template = measurementTemplate; } diff --git a/src/app/models/sample.model.ts b/src/app/models/sample.model.ts index bd0d5d3..c73acd0 100644 --- a/src/app/models/sample.model.ts +++ b/src/app/models/sample.model.ts @@ -1,11 +1,10 @@ import _ from 'lodash'; -import {Deserializable} from './deserializable.model'; import {IdModel} from './id.model'; -import {SendFormat} from './sendformat.model'; import {MaterialModel} from './material.model'; import {MeasurementModel} from './measurement.model'; +import {BaseModel} from './base.model'; -export class SampleModel implements Deserializable, SendFormat { +export class SampleModel extends BaseModel { _id: IdModel = null; color = ''; number = ''; diff --git a/src/app/models/sendformat.model.spec.ts b/src/app/models/sendformat.model.spec.ts deleted file mode 100644 index 3f6f470..0000000 --- a/src/app/models/sendformat.model.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -// import { SendformatModel } from './sendformat.model'; -// -// describe('SendformatModel', () => { -// -// }); diff --git a/src/app/models/sendformat.model.ts b/src/app/models/sendformat.model.ts deleted file mode 100644 index 9eea07e..0000000 --- a/src/app/models/sendformat.model.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface SendFormat { - sendFormat(omit?: string[]): {[prop: string]: any}; -} diff --git a/src/app/models/template.model.ts b/src/app/models/template.model.ts index 0c4081a..c6d7099 100644 --- a/src/app/models/template.model.ts +++ b/src/app/models/template.model.ts @@ -1,14 +1,9 @@ -import {Deserializable} from './deserializable.model'; import {IdModel} from './id.model'; +import {BaseModel} from './base.model'; -export class TemplateModel implements Deserializable{ +export class TemplateModel extends BaseModel { _id: IdModel = null; name = ''; version = 1; parameters: {name: string, range: {[prop: string]: any}}[] = []; - - deserialize(input: any): this { - Object.assign(this, input); - return this; - } } diff --git a/src/app/rb-table/rb-table/rb-table.component.html b/src/app/rb-table/rb-table/rb-table.component.html index 9ced8a1..49cef91 100644 --- a/src/app/rb-table/rb-table/rb-table.component.html +++ b/src/app/rb-table/rb-table/rb-table.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/sample/sample.component.spec.ts b/src/app/sample/sample.component.spec.ts index a2698cb..c32c4c1 100644 --- a/src/app/sample/sample.component.spec.ts +++ b/src/app/sample/sample.component.spec.ts @@ -1,25 +1,27 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SampleComponent } from './sample.component'; - -describe('SampleComponent', () => { - let component: SampleComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ SampleComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(SampleComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); +// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +// +// import { SampleComponent } from './sample.component'; +// +// // TODO +// +// describe('SampleComponent', () => { +// let component: SampleComponent; +// let fixture: ComponentFixture; +// +// beforeEach(async(() => { +// TestBed.configureTestingModule({ +// declarations: [ SampleComponent ] +// }) +// .compileComponents(); +// })); +// +// beforeEach(() => { +// fixture = TestBed.createComponent(SampleComponent); +// component = fixture.componentInstance; +// fixture.detectChanges(); +// }); +// +// it('should create', () => { +// expect(component).toBeTruthy(); +// }); +// }); diff --git a/src/app/samples/samples.component.spec.ts b/src/app/samples/samples.component.spec.ts index 2f3cdc5..1964449 100644 --- a/src/app/samples/samples.component.spec.ts +++ b/src/app/samples/samples.component.spec.ts @@ -1,25 +1,27 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SamplesComponent } from './samples.component'; - -describe('SamplesComponent', () => { - let component: SamplesComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ SamplesComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(SamplesComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); +// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +// +// import { SamplesComponent } from './samples.component'; +// +// // TODO +// +// describe('SamplesComponent', () => { +// let component: SamplesComponent; +// let fixture: ComponentFixture; +// +// beforeEach(async(() => { +// TestBed.configureTestingModule({ +// declarations: [ SamplesComponent ] +// }) +// .compileComponents(); +// })); +// +// beforeEach(() => { +// fixture = TestBed.createComponent(SamplesComponent); +// component = fixture.componentInstance; +// fixture.detectChanges(); +// }); +// +// it('should create', () => { +// expect(component).toBeTruthy(); +// }); +// }); diff --git a/src/app/services/api.service.spec.ts b/src/app/services/api.service.spec.ts index 7927549..58212b6 100644 --- a/src/app/services/api.service.spec.ts +++ b/src/app/services/api.service.spec.ts @@ -1,53 +1,136 @@ -// import { TestBed } from '@angular/core/testing'; -// import { ApiService } from './api.service'; -// import {HttpClient} from '@angular/common/http'; -// import {LocalStorageService} from 'angular-2-local-storage'; -// import {Observable} from 'rxjs'; -// -// let apiService: ApiService; -// let httpClientSpy: jasmine.SpyObj; -// let localStorageServiceSpy: jasmine.SpyObj; -// -// describe('ApiService', () => { -// beforeEach(() => { -// const httpSpy = jasmine.createSpyObj('HttpClient', ['get']); -// const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['get']); -// -// TestBed.configureTestingModule({ -// providers: [ -// ApiService, -// {provide: HttpClient, useValue: httpSpy}, -// {provide: LocalStorageService, useValue: localStorageSpy} -// ] -// }); -// -// apiService = TestBed.inject(ApiService); -// httpClientSpy = TestBed.inject(HttpClient) as jasmine.SpyObj; -// localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj; -// }); -// -// it('should be created', () => { -// expect(apiService).toBeTruthy(); -// }); -// -// it('should do get requests without auth if not available', () => { -// const getReturn = new Observable(); -// httpClientSpy.get.and.returnValue(getReturn); -// localStorageServiceSpy.get.and.returnValue(undefined); -// -// const result = apiService.get('/testurl'); -// expect(result).toBe(getReturn); -// expect(httpClientSpy.get).toHaveBeenCalledWith('/testurl', {}); -// expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); -// }); -// it('should do get requests with basic auth if available', () => { -// const getReturn = new Observable(); -// httpClientSpy.get.and.returnValue(getReturn); -// localStorageServiceSpy.get.and.returnValue('basicAuth'); -// -// const result = apiService.get('/testurl'); -// expect(result).toBe(getReturn); -// expect(httpClientSpy.get).toHaveBeenCalledWith('/testurl', jasmine.any(Object)); // could not test http headers better -// expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); -// }); -// }); +import {async, TestBed} from '@angular/core/testing'; +import { ApiService } from './api.service'; +import {HttpClient} from '@angular/common/http'; +import {LocalStorageService} from 'angular-2-local-storage'; +import {Observable} from 'rxjs'; +import {ModalService} from '@inst-iot/bosch-angular-ui-components'; + +let apiService: ApiService; +let httpClientSpy: jasmine.SpyObj; +let localStorageServiceSpy: jasmine.SpyObj; +let modalServiceSpy: jasmine.SpyObj; + +// TODO: test options + +describe('ApiService', () => { + beforeEach(() => { + const httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']); + const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['get']); + const modalSpy = jasmine.createSpyObj('ModalService', ['openComponent']); + + TestBed.configureTestingModule({ + providers: [ + ApiService, + {provide: HttpClient, useValue: httpSpy}, + {provide: LocalStorageService, useValue: localStorageSpy}, + {provide: ModalService, useValue: modalSpy} + ] + }); + + apiService = TestBed.inject(ApiService); + httpClientSpy = TestBed.inject(HttpClient) as jasmine.SpyObj; + localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj; + modalServiceSpy = TestBed.inject(ModalService) as jasmine.SpyObj; + }); + + it('should be created', () => { + expect(apiService).toBeTruthy(); + }); + + it('shows an error message when the request fails', () => { + const getReturn = new Observable(observer => { + observer.error('error'); + }); + httpClientSpy.get.and.returnValue(getReturn); + localStorageServiceSpy.get.and.returnValue(undefined); + modalServiceSpy.openComponent.and.returnValue({instance: {message: ''}} as any); + + apiService.get('/testurl'); + expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', {}); + expect(modalServiceSpy.openComponent.calls.count()).toBe(1); + }); + + it('returns the error message if the callback function had an error parameter', () => { + const getReturn = new Observable(observer => { + observer.error('error'); + }); + httpClientSpy.get.and.returnValue(getReturn); + localStorageServiceSpy.get.and.returnValue(undefined); + modalServiceSpy.openComponent.and.returnValue({instance: {message: ''}} as any); + + apiService.get('/testurl', (data, error) => { + expect(modalServiceSpy.openComponent.calls.count()).toBe(0); + expect(error).toBe('error'); + }); + }); + + it('should do get requests without auth if not available', async(() => { + const getReturn = new Observable(observer => { + observer.next('data'); + }); + httpClientSpy.get.and.returnValue(getReturn); + localStorageServiceSpy.get.and.returnValue(undefined); + + apiService.get('/testurl', res => { + expect(res).toBe('data'); + expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', {}); + expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); + }); + })); + + it('should do get requests with basic auth if available', async(() => { + const getReturn = new Observable(observer => { + observer.next('data'); + }); + httpClientSpy.get.and.returnValue(getReturn); + localStorageServiceSpy.get.and.returnValue('basicAuth'); + + apiService.get('/testurl', res => { + expect(res).toBe('data'); + expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object)); // could not test http headers better + expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); + }); + })); + + it('should do post requests', async(() => { + const resReturn = new Observable(observer => { + observer.next('data'); + }); + httpClientSpy.post.and.returnValue(resReturn); + localStorageServiceSpy.get.and.returnValue('basicAuth'); + + apiService.post('/testurl', 'reqData', res => { + expect(res).toBe('data'); + expect(httpClientSpy.post).toHaveBeenCalledWith('/api/testurl', 'reqData', jasmine.any(Object)); + expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); + }); + })); + + it('should do put requests', async(() => { + const resReturn = new Observable(observer => { + observer.next('data'); + }); + httpClientSpy.put.and.returnValue(resReturn); + localStorageServiceSpy.get.and.returnValue('basicAuth'); + + apiService.put('/testurl', 'reqData', res => { + expect(res).toBe('data'); + expect(httpClientSpy.put).toHaveBeenCalledWith('/api/testurl', 'reqData', jasmine.any(Object)); + expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); + }); + })); + + it('should do delete requests', async(() => { + const resReturn = new Observable(observer => { + observer.next('data'); + }); + httpClientSpy.delete.and.returnValue(resReturn); + localStorageServiceSpy.get.and.returnValue('basicAuth'); + + apiService.delete('/testurl', res => { + expect(res).toBe('data'); + expect(httpClientSpy.delete).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object)); + expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); + }); + })); +}); diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 3296ea8..86f74ec 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -5,6 +5,7 @@ import {Observable} from 'rxjs'; import {ErrorComponent} from '../error/error.component'; import {ModalService} from '@inst-iot/bosch-angular-ui-components'; + @Injectable({ providedIn: 'root' }) @@ -37,9 +38,14 @@ export class ApiService { private requestErrorHandler(observable: Observable, f: (data?: T, err?) => void) { observable.subscribe(data => { f(data, undefined); - }, () => { - const modalRef = this.modalService.openComponent(ErrorComponent); - modalRef.instance.message = 'Network request failed!'; + }, err => { + if (f.length === 2) { + f(undefined, err); + } + else { + const modalRef = this.modalService.openComponent(ErrorComponent); + modalRef.instance.message = 'Network request failed!'; + } }); } diff --git a/src/app/services/autocomplete.service.spec.ts b/src/app/services/autocomplete.service.spec.ts index 790b9d3..ef9aae6 100644 --- a/src/app/services/autocomplete.service.spec.ts +++ b/src/app/services/autocomplete.service.spec.ts @@ -2,15 +2,34 @@ import { TestBed } from '@angular/core/testing'; import { AutocompleteService } from './autocomplete.service'; +let autocompleteService: AutocompleteService; + describe('AutocompleteService', () => { - let service: AutocompleteService; beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(AutocompleteService); + TestBed.configureTestingModule({ + providers: [AutocompleteService] + }); + autocompleteService = TestBed.inject(AutocompleteService); }); it('should be created', () => { - expect(service).toBeTruthy(); + expect(autocompleteService).toBeTruthy(); + }); + + it('should should return a bind function', () => { + expect(autocompleteService.bind('a', ['b'])).toBeTruthy(); + }); + + it('should return search results', () => { + autocompleteService.search(['aa', 'ab', 'bb'], 'a').subscribe(res => { + expect(res).toEqual(['aa', 'ab']); + }); + }); + + it('should return an empty array if no result was found', () => { + autocompleteService.search(['aa', 'ab', 'bb'], 'c').subscribe(res => { + expect(res).toEqual([]); + }); }); }); diff --git a/src/app/services/autocomplete.service.ts b/src/app/services/autocomplete.service.ts index 15080ea..4000b67 100644 --- a/src/app/services/autocomplete.service.ts +++ b/src/app/services/autocomplete.service.ts @@ -9,11 +9,11 @@ export class AutocompleteService { constructor() { } - bind(ref, list) { + bind(ref, list: string[]) { return this.search.bind(ref, list); } - search(arr, str) { + search(arr: string[], str: string) { const qs = new QuickScore(arr); return of(str === '' ? [] : qs.search(str).map(e => e.item)); } diff --git a/src/app/services/login.service.spec.ts b/src/app/services/login.service.spec.ts index da0899c..d7891fe 100644 --- a/src/app/services/login.service.spec.ts +++ b/src/app/services/login.service.spec.ts @@ -1,81 +1,87 @@ -// import { TestBed } from '@angular/core/testing'; -// -// import { LoginService } from './login.service'; -// import {LocalStorageService} from 'angular-2-local-storage'; -// import {ApiService} from './api.service'; -// import {Observable} from 'rxjs'; -// -// let loginService: LoginService; -// let apiServiceSpy: jasmine.SpyObj; -// let localStorageServiceSpy: jasmine.SpyObj; -// -// describe('LoginService', () => { -// beforeEach(() => { -// const apiSpy = jasmine.createSpyObj('ApiService', ['get']); -// const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['set', 'remove']); -// -// TestBed.configureTestingModule({ -// providers: [ -// LoginService, -// {provide: ApiService, useValue: apiSpy}, -// {provide: LocalStorageService, useValue: localStorageSpy} -// ] -// }); -// loginService = TestBed.inject(LoginService); -// apiServiceSpy = TestBed.inject(ApiService) as jasmine.SpyObj; -// localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj; -// }); -// -// it('should be created', () => { -// expect(loginService).toBeTruthy(); -// }); -// -// describe('login', () => { -// it('should store the basic auth', () => { -// localStorageServiceSpy.set.and.returnValue(true); -// apiServiceSpy.get.and.returnValue(new Observable()); -// loginService.login('username', 'password'); -// expect(localStorageServiceSpy.set).toHaveBeenCalledWith('basicAuth', 'dXNlcm5hbWU6cGFzc3dvcmQ='); -// }); -// -// it('should remove the basic auth if login fails', () => { -// localStorageServiceSpy.set.and.returnValue(true); -// localStorageServiceSpy.remove.and.returnValue(true); -// apiServiceSpy.get.and.returnValue(new Observable(o => o.error())); -// loginService.login('username', 'password'); -// expect(localStorageServiceSpy.remove.calls.count()).toBe(1); -// expect(localStorageServiceSpy.remove).toHaveBeenCalledWith('basicAuth'); -// }); -// -// it('should resolve true when login succeeds', async () => { -// localStorageServiceSpy.set.and.returnValue(true); -// apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'Authorization successful', method: 'basic'}))); -// expect(await loginService.login('username', 'password')).toBeTruthy(); -// }); -// -// it('should resolve false when a wrong result comes in', async () => { -// localStorageServiceSpy.set.and.returnValue(true); -// apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'xxx', method: 'basic'}))); -// expect(await loginService.login('username', 'password')).toBeFalsy(); -// }); -// -// it('should resolve false on an error', async () => { -// localStorageServiceSpy.set.and.returnValue(true); -// apiServiceSpy.get.and.returnValue(new Observable(o => o.error())); -// expect(await loginService.login('username', 'password')).toBeFalsy(); -// }); -// }); -// -// describe('canActivate', () => { -// it('should return false at first', () => { -// expect(loginService.canActivate(null, null)).toBeFalsy(); -// }); -// -// it('returns true if login was successful', async () => { -// localStorageServiceSpy.set.and.returnValue(true); -// apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'Authorization successful', method: 'basic'}))); -// await loginService.login('username', 'password'); -// expect(loginService.canActivate(null, null)).toBeTruthy(); -// }); -// }); -// }); +import { TestBed } from '@angular/core/testing'; + +import { LoginService } from './login.service'; +import {LocalStorageService} from 'angular-2-local-storage'; +import {ApiService} from './api.service'; + +let loginService: LoginService; +let apiServiceSpy: jasmine.SpyObj; +let localStorageServiceSpy: jasmine.SpyObj; + +describe('LoginService', () => { + beforeEach(() => { + const apiSpy = jasmine.createSpyObj('ApiService', ['get']); + const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['set', 'remove']); + + TestBed.configureTestingModule({ + providers: [ + LoginService, + {provide: ApiService, useValue: apiSpy}, + {provide: LocalStorageService, useValue: localStorageSpy} + ] + }); + loginService = TestBed.inject(LoginService); + apiServiceSpy = TestBed.inject(ApiService) as jasmine.SpyObj; + localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj; + }); + + it('should be created', () => { + expect(loginService).toBeTruthy(); + }); + + describe('login', () => { + it('should store the basic auth', () => { + localStorageServiceSpy.set.and.returnValue(true); + apiServiceSpy.get.and.callFake(() => {}); + loginService.login('username', 'password'); + expect(localStorageServiceSpy.set).toHaveBeenCalledWith('basicAuth', 'dXNlcm5hbWU6cGFzc3dvcmQ='); + }); + + it('should remove the basic auth if login fails', () => { + localStorageServiceSpy.set.and.returnValue(true); + localStorageServiceSpy.remove.and.returnValue(true); + apiServiceSpy.get.and.callFake((a, b) => {b(undefined, 'error'); }); + loginService.login('username', 'password'); + expect(localStorageServiceSpy.remove.calls.count()).toBe(1); + expect(localStorageServiceSpy.remove).toHaveBeenCalledWith('basicAuth'); + }); + + it('should resolve true when login succeeds', async () => { + localStorageServiceSpy.set.and.returnValue(true); + apiServiceSpy.get.and.callFake((a, b) => {b({status: 'Authorization successful', method: 'basic'} as any, undefined); }); + expect(await loginService.login('username', 'password')).toBeTruthy(); + }); + + it('should resolve false when a wrong result comes in', async () => { + localStorageServiceSpy.set.and.returnValue(true); + apiServiceSpy.get.and.callFake((a, b) => {b({status: 'xxx', method: 'basic'} as any, undefined); }); + expect(await loginService.login('username', 'password')).toBeFalsy(); + }); + + it('should resolve false on an error', async () => { + localStorageServiceSpy.set.and.returnValue(true); + apiServiceSpy.get.and.callFake((a, b) => {b(undefined, 'error'); }); + expect(await loginService.login('username', 'password')).toBeFalsy(); + }); + }); + + describe('canActivate', () => { + it('should return false at first', done => { + apiServiceSpy.get.and.callFake((a, b) => {b(undefined, 'error'); }); + loginService.canActivate(null, null).subscribe(res => { + expect(res).toBeFalsy(); + done(); + }); + }); + + it('returns true if login was successful', async done => { + localStorageServiceSpy.set.and.returnValue(true); + apiServiceSpy.get.and.callFake((a, b) => {b({status: 'Authorization successful', method: 'basic'} as any, undefined); }); + await loginService.login('username', 'password'); + loginService.canActivate(null, null).subscribe(res => { + expect(res).toBeTruthy(); + done(); + }); + }); + }); +}); diff --git a/src/app/services/validation.service.spec.ts b/src/app/services/validation.service.spec.ts index e931a6f..67f8108 100644 --- a/src/app/services/validation.service.spec.ts +++ b/src/app/services/validation.service.spec.ts @@ -28,8 +28,87 @@ describe('ValidationService', () => { expect(validationService.password('Abc123!#')).toEqual({ok: true, error: ''}); }); - it('should return an error on an incorrect password', () => { - expect(validationService.password('Abc123')).toEqual({ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'}); + it('should return an error on a password too short', () => { + expect(validationService.password('Abc123')).toEqual({ok: false, error: 'password must have at least 8 characters'}); }); + it('should return an error on a password without a lowercase letter', () => { + expect(validationService.password('ABC123!#')).toEqual({ok: false, error: 'password must have at least one lowercase character'}); + }); + + it('should return an error on a password without an uppercase letter', () => { + expect(validationService.password('abc123!#')).toEqual({ok: false, error: 'password must have at least one uppercase character'}); + }); + + it('should return an error on a password without a number', () => { + expect(validationService.password('Abcabc!#')).toEqual({ok: false, error: 'password must have at least one number'}); + }); + + it('should return an error on a password without a special character', () => { + expect(validationService.password('Abc12345')).toEqual({ok: false, error: 'password must have at least one of the following characters !"#%&\'()*+,-.\\/:;<=>?@[]^_`{|}~'}); + }); + + it('should return an error on a password with a character not allowed', () => { + expect(validationService.password('Abc123!€')).toEqual({ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'}); + }); + + it('should return true on a correct string', () => { + expect(validationService.string('Abc')).toEqual({ok: true, error: ''}); + }); + + it('should return an error on a string too long', () => { + expect(validationService.string('abcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbacab')).toEqual({ok: false, error: 'must contain max 128 characters'}); + }); + + it('should return true on a string in the list', () => { + expect(validationService.stringOf('Abc', ['Abc', 'Def'])).toEqual({ok: true, error: ''}); + }); + + it('should return an error on a string not in the list', () => { + expect(validationService.stringOf('abc', ['Abc', 'Def'])).toEqual({ok: false, error: 'must be one of Abc, Def'}); + }); + + it('should return true on a string of correct length', () => { + expect(validationService.stringLength('Abc', 5)).toEqual({ok: true, error: ''}); + }); + + it('should return an error on a string longer than specified', () => { + expect(validationService.stringLength('Abc', 2)).toEqual({ok: false, error: 'must contain max 2 characters'}); + }); + + it('should return true on a number in the range', () => { + expect(validationService.minMax(2, -2, 2)).toEqual({ok: true, error: ''}); + }); + + it('should return an error on a number below the range', () => { + expect(validationService.minMax(0, 1, 3)).toEqual({ok: false, error: 'must be between 1 and 3'}); + }); + + it('should return an error on a number above the range', () => { + expect(validationService.minMax(3.1, 1, 3)).toEqual({ok: false, error: 'must be between 1 and 3'}); + }); + + it('should return true on a number above min', () => { + expect(validationService.min(2, -2)).toEqual({ok: true, error: ''}); + }); + + it('should return an error on a number below min', () => { + expect(validationService.min(0, 1)).toEqual({ok: false, error: 'must not be below 1'}); + }); + + it('should return true on a number below max', () => { + expect(validationService.max(2, 2)).toEqual({ok: true, error: ''}); + }); + + it('should return an error on a number above max', () => { + expect(validationService.max(2, 1)).toEqual({ok: false, error: 'must not be above 1'}); + }); + + it('should return true on a string not in the list', () => { + expect(validationService.unique('Abc', ['Def', 'Ghi'])).toEqual({ok: true, error: ''}); + }); + + it('should return an error on a string from the list', () => { + expect(validationService.unique('Abc', ['Abc', 'Def'])).toEqual({ok: false, error: 'values must be unique'}); + }); }); diff --git a/src/app/services/validation.service.ts b/src/app/services/validation.service.ts index 4fd69bc..229c967 100644 --- a/src/app/services/validation.service.ts +++ b/src/app/services/validation.service.ts @@ -107,7 +107,7 @@ export class ValidationService { } max(data, max) { - const {ignore, error} = Joi.number().allow('').min(max).validate(data); + const {ignore, error} = Joi.number().allow('').max(max).validate(data); if (error) { return {ok: false, error: `must not be above ${max}`}; } From 81621e77cc9d31ea4903e476bd1cd86a28f4fb09 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Fri, 26 Jun 2020 11:09:59 +0200 Subject: [PATCH 3/7] sorting implemented --- src/app/models/sample.model.ts | 2 +- src/app/sample/sample.component.ts | 3 + src/app/samples/samples.component.html | 75 ++++++++++++++++------- src/app/samples/samples.component.scss | 80 ++++++++++++++++++++++++ src/app/samples/samples.component.ts | 84 ++++++++++++++++++++++++-- 5 files changed, 216 insertions(+), 28 deletions(-) diff --git a/src/app/models/sample.model.ts b/src/app/models/sample.model.ts index c73acd0..319af04 100644 --- a/src/app/models/sample.model.ts +++ b/src/app/models/sample.model.ts @@ -13,7 +13,7 @@ export class SampleModel extends BaseModel { condition: {condition_template: string, [prop: string]: string} | {} = {}; material_id: IdModel = null; material: MaterialModel; - measurements: MeasurementModel[]; + measurements: MeasurementModel[] = []; note_id: IdModel = null; user_id: IdModel = null; notes: {comment: string, sample_references: {sample_id: IdModel, relation: string}[], custom_fields: {[prop: string]: string}} = {comment: '', sample_references: [], custom_fields: {}}; diff --git a/src/app/sample/sample.component.ts b/src/app/sample/sample.component.ts index 6f2919b..137e986 100644 --- a/src/app/sample/sample.component.ts +++ b/src/app/sample/sample.component.ts @@ -19,6 +19,9 @@ import {MeasurementModel} from '../models/measurement.model'; // TODO: confirmation for new group/supplier // TODO: DPT preview // TODO: work on better recognition for file input +// TODO: only show condition (if not set) and measurements in edit sample dialog at first +// TODO: multiple spectra, drag and drop +// TODO: multiple samples for base data, extend multiple measurements, conditions diff --git a/src/app/samples/samples.component.html b/src/app/samples/samples.component.html index 8e7f199..02f0a6a 100644 --- a/src/app/samples/samples.component.html +++ b/src/app/samples/samples.component.html @@ -6,48 +6,77 @@ -   Filter +   Filter -
- - - - + +
+ + + validated + + + new + +
+ + + + + + + - +
+ + - Number - Material number - Material name - Supplier - Material - GF - CF - M - type - Color - Batch + +
+ {{key.name}} + + +
+ {{sample.number}} - {{sample.material_number}} + {{materials[sample.material_id].name}} {{materials[sample.material_id].supplier}} - {{materials[sample.material_id].group}} - {{materials[sample.material_id].glass_fiber}} - {{materials[sample.material_id].carbon_fiber}} - {{materials[sample.material_id].mineral}} + + + + {{sample.type}} {{sample.color}} {{sample.batch}} + {{sample.added | date}}
+ + + + +
+ + + + of {{pages()}} + + +
+
+ diff --git a/src/app/samples/samples.component.scss b/src/app/samples/samples.component.scss index e2d94b5..c3fb393 100644 --- a/src/app/samples/samples.component.scss +++ b/src/app/samples/samples.component.scss @@ -22,3 +22,83 @@ color: #000; } } + +form { + overflow: hidden; +} + +.status-selection { + overflow: hidden; + margin-bottom: 10px; + float: left; + margin-right: 15px; + + label { + display: block; + font-weight: 700; + font-size: 10px; + } + + rb-form-checkbox { + float: left; + margin-right: 10px; + margin-top: -10px; + } +} + +.page-size-selection { + max-width: 125px; + float: left; +} + +.paging { + rb-form-input { + max-width: 50px; + } + + > * { + float: left; + } + + > button { + margin-top: 18px; + } + + > span { + margin-top: 20px; + margin-left: 5px; + } +} + +.sort-header { + display: inline-grid; + grid-template-columns: 1fr auto; + grid-column-gap: 5px; + width: 100%; + + :first-child { + grid-row: span 2; + } + + :nth-child(2) { + margin-bottom: -3px; + cursor: pointer; + } + + :nth-child(3) { + margin-top: -3px; + cursor: pointer; + } +} + +.sort-active-asc { + color: $color-bosch-dark-blue; + background: linear-gradient(to bottom, #FFF 17%, $color-bosch-light-blue-w50 17%);; + border-radius: 0 0 8px 8px; +} + +.sort-active-desc { + color: $color-bosch-dark-blue; + background: linear-gradient(to top, #FFF 17%, $color-bosch-light-blue-w50 17%);; + border-radius: 8px 8px 0 0; +} diff --git a/src/app/samples/samples.component.ts b/src/app/samples/samples.component.ts index a3bdb39..2384cbc 100644 --- a/src/app/samples/samples.component.ts +++ b/src/app/samples/samples.component.ts @@ -1,16 +1,46 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit, ViewChild} from '@angular/core'; import {ApiService} from '../services/api.service'; + + +interface LoadSamplesOptions { + toPage?: number; + event?: Event; + firstPage?: boolean; +} + @Component({ selector: 'app-samples', templateUrl: './samples.component.html', styleUrls: ['./samples.component.scss'] }) + +// TODO: always show first page on sort change + export class SamplesComponent implements OnInit { // TODO: implement paging + @ViewChild('pageSizeSelection') pageSizeSelection: HTMLElement; + materials = {}; samples = []; - filters = {status: 'validated'}; + totalSamples = 0; // total number of samples + filters = {status: {new: true, validated: true}, pageSize: 25, toPage: 0, sort: 'added-asc'}; + page = 1; + loadSamplesQueue = []; // arguments of queued up loadSamples() calls + activeKeys = [ + {name: 'Number', key: 'number'}, + // {name: 'Material number', key: ''}, + {name: 'Material name', key: ''}, + {name: 'Supplier', key: ''}, + // {name: 'Material', key: ''}, + // {name: 'GF', key: ''}, + // {name: 'CF', key: ''}, + // {name: 'M', key: ''}, + {name: 'Type', key: 'type'}, + {name: 'Color', key: 'color'}, + {name: 'Batch', key: 'batch'}, + {name: 'Added', key: 'added'}, + ]; constructor( private api: ApiService @@ -24,15 +54,61 @@ export class SamplesComponent implements OnInit { // TODO: implement paging }); this.loadSamples(); }); + this.api.get('/samples/count', (data: {count: number}) => { + this.totalSamples = data.count; + }); } - loadSamples() { - this.api.get(`/samples?status=${this.filters.status}`, sData => { + loadSamples(options: LoadSamplesOptions = {}) { // set toPage to null to reload first page, queues calls + this.loadSamplesQueue.push(options); + if (this.loadSamplesQueue.length <= 1) { // nothing queued up + this.sampleLoader(this.loadSamplesQueue[0]); + } + } + + private sampleLoader(options: LoadSamplesOptions) { // actual loading of the sample, do not call directly + const query: string[] = []; + query.push('status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated'))); + if (this.samples[0]) { // do not include from-id when page size was changed + if (!options.firstPage && (!options.event || ((options.event.target as HTMLElement).id.indexOf(this.pageSizeSelection.id) < 0))) { + query.push('from-id=' + this.samples[0]._id); + } + else { + this.page = 1; + } + } + if (options.toPage) { + query.push('to-page=' + options.toPage); + } + query.push('page-size=' + this.filters.pageSize); + query.push('sort=' + this.filters.sort); + + this.api.get('/samples?' + query.join('&'), sData => { this.samples = sData as any; this.samples.forEach(sample => { sample.material_number = this.materials[sample.material_id].numbers.find(e => sample.color === e.color).number; }); + this.loadSamplesQueue.shift(); + if (this.loadSamplesQueue.length > 0) { // execute next queue item + this.sampleLoader(this.loadSamplesQueue[0]); + } }); } + loadPage(delta) { + if (!/[0-9]+/.test(delta) || (this.page <= 1 && delta < 0)) { // invalid delta + return; + } + this.page += delta; + this.loadSamples({toPage: delta}); + } + + pages() { + return Math.ceil(this.totalSamples / this.filters.pageSize); + } + + setSort(string) { + this.filters.sort = string; + this.loadSamples({firstPage: true}); + } } From b28be4b7924ee11baa839b67676e83fd69fdc0d3 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Mon, 13 Jul 2020 10:52:10 +0200 Subject: [PATCH 4/7] finished filters --- angular.json | 3 +- manifest.yml | 8 + package.json | 3 +- src/Staticfile | 2 + src/app/app.component.html | 4 +- src/app/app.component.scss | 5 + src/app/app.component.ts | 7 +- src/app/app.module.ts | 4 +- src/app/login/login.component.ts | 3 + src/app/object.pipe.spec.ts | 8 + src/app/object.pipe.ts | 12 ++ src/app/sample/sample.component.ts | 1 - src/app/samples/samples.component.html | 97 ++++++++--- src/app/samples/samples.component.scss | 79 ++++++++- src/app/samples/samples.component.ts | 229 ++++++++++++++++++++----- src/app/services/api.service.ts | 38 ++-- src/app/services/login.service.ts | 4 + 17 files changed, 419 insertions(+), 88 deletions(-) create mode 100644 manifest.yml create mode 100644 src/Staticfile create mode 100644 src/app/object.pipe.spec.ts create mode 100644 src/app/object.pipe.ts diff --git a/angular.json b/angular.json index 126fb2c..066b70a 100644 --- a/angular.json +++ b/angular.json @@ -26,7 +26,8 @@ "assets": [ "src/favicon.ico", "src/assets", - { "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" } + { "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" }, + "src/Staticfile" ], "styles": [ "src/styles.scss" diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 0000000..852c5cd --- /dev/null +++ b/manifest.yml @@ -0,0 +1,8 @@ +--- +applications: + - name: definma + path: dist/UI + buildpack: staticfile_buildpack + memory: 128M + instances: 1 + stack: cflinuxfs3 diff --git a/package.json b/package.json index d77ae2a..b188818 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "ng build", + "build": "ng build --prod --aot", + "build-push": "ng build --prod --aot && cf push", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", diff --git a/src/Staticfile b/src/Staticfile new file mode 100644 index 0000000..03b776c --- /dev/null +++ b/src/Staticfile @@ -0,0 +1,2 @@ +pushstate: enabled +force_https: true diff --git a/src/app/app.component.html b/src/app/app.component.html index 140a69a..0546de3 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,7 +1,7 @@ - - -
-

- Some user specific information -

+ + + +
+

+ +

- Logout -
-
+ +
+
+
DEVELOPMENTDigital Fingerprint of Plastics
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8b6391c..fc074c0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component, isDevMode} from '@angular/core'; import {LoginService} from './services/login.service'; +import {Router} from '@angular/router'; // TODO: add multiple samples at once // TODO: guess properties from material name @@ -16,11 +17,17 @@ import {LoginService} from './services/login.service'; export class AppComponent { constructor( - public loginService: LoginService + public loginService: LoginService, + private router: Router ) { } get devMode() { return isDevMode(); } + + logout() { + this.loginService.logout(); + this.router.navigate(['/']); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 60c0738..663e8e9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,5 +1,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { ChartsModule } from 'ng2-charts'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -16,6 +17,7 @@ import { ValidateDirective } from './validate.directive'; import {CommonModule} from '@angular/common'; import { ErrorComponent } from './error/error.component'; import { ObjectPipe } from './object.pipe'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; @NgModule({ declarations: [ @@ -34,6 +36,7 @@ import { ObjectPipe } from './object.pipe'; storageType: 'localStorage' }), BrowserModule, + BrowserAnimationsModule, AppRoutingModule, RbUiComponentsModule, FormsModule, @@ -41,7 +44,8 @@ import { ObjectPipe } from './object.pipe'; RbTableModule, ReactiveFormsModule, FormFieldsModule, - CommonModule + CommonModule, + ChartsModule ], providers: [ ModalService diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 765d641..6c28dcc 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -29,7 +29,7 @@ export class LoginComponent implements OnInit { login() { this.loginService.login(this.username, this.password).then(ok => { if (ok) { - this.message = 'Login successful'; // TODO: think about following action + this.message = 'Login successful'; this.router.navigate(['/samples']); } else { diff --git a/src/app/sample/sample.component.html b/src/app/sample/sample.component.html index 1274b1b..138e0e9 100644 --- a/src/app/sample/sample.component.html +++ b/src/app/sample/sample.component.html @@ -6,14 +6,14 @@
- + Cannot be empty Unknown material, add properties for new material
-
+

Material properties

{{supplierInput.errors.failure}} @@ -38,7 +38,7 @@
-
+
@@ -69,7 +69,7 @@ {{commentInput.errors.failure}}
Additional properties
-
+
{{keyInput.errors.failure}} @@ -89,7 +89,7 @@ Condition -
+
@@ -105,8 +105,8 @@

Measurements

-
- +
+ @@ -115,9 +115,16 @@ {{parameterInput.errors.failure}} Cannot be empty - + Cannot be empty + +
@@ -130,7 +137,6 @@
-  
diff --git a/src/app/sample/sample.component.scss b/src/app/sample/sample.component.scss index c65b876..8b4e416 100644 --- a/src/app/sample/sample.component.scss +++ b/src/app/sample/sample.component.scss @@ -15,3 +15,7 @@ td:first-child { grid-template-columns: 1fr 1fr; grid-column-gap: 10px; } + +.dpt-chart { + max-width: 400px; +} diff --git a/src/app/sample/sample.component.ts b/src/app/sample/sample.component.ts index 96d57c3..ba13d26 100644 --- a/src/app/sample/sample.component.ts +++ b/src/app/sample/sample.component.ts @@ -14,20 +14,35 @@ import {NgForm, Validators} from '@angular/forms'; import {ValidationService} from '../services/validation.service'; import {TemplateModel} from '../models/template.model'; import {MeasurementModel} from '../models/measurement.model'; +import { ChartOptions } from 'chart.js'; +import {animate, style, transition, trigger} from '@angular/animations'; // TODO: tests // TODO: confirmation for new group/supplier -// TODO: DPT preview // TODO: work on better recognition for file input // TODO: only show condition (if not set) and measurements in edit sample dialog at first -// TODO: multiple spectra, drag and drop +// TODO: multiple spectra // TODO: multiple samples for base data, extend multiple measurements, conditions @Component({ selector: 'app-sample', templateUrl: './sample.component.html', - styleUrls: ['./sample.component.scss'] + styleUrls: ['./sample.component.scss'], + animations: [ + trigger( + 'inOut', [ + transition(':enter', [ + style({height: 0, opacity: 0}), + animate('0.5s ease-out', style({height: '*', opacity: 1})) + ]), + transition(':leave', [ + style({height: '*', opacity: 1}), + animate('0.5s ease-in', style({height: 0, opacity: 0})) + ]) + ] + ) + ] }) export class SampleComponent implements OnInit, AfterContentChecked { @@ -49,6 +64,27 @@ export class SampleComponent implements OnInit, AfterContentChecked { measurementTemplates: TemplateModel[]; loading = 0; // number of currently loading instances checkFormAfterInit = false; + charts = []; // chart data for spectrums + readonly chartInit = [{ + data: [], + label: 'Spectrum', + showLine: true, + fill: false, + pointRadius: 0, + borderColor: '#00a8b0', + borderWidth: 2 + }]; + readonly chartOptions: ChartOptions = { + scales: { + xAxes: [{ticks: {min: 400, max: 4000, stepSize: 400, reverse: true}}], + yAxes: [{ticks: {min: 0, max: 1}}] + }, + responsive: true, + tooltips: {enabled: false}, + hover: {mode: null}, + maintainAspectRatio: true, + plugins: {datalabels: {display: false}} + }; constructor( private router: Router, @@ -90,6 +126,19 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.loading++; this.api.get('/sample/' + this.route.snapshot.paramMap.get('id'), sData => { this.sample.deserialize(sData); + this.charts = []; + const spectrumTemplate = this.measurementTemplates.find(e => e.name === 'spectrum')._id; + let spectrumCounter = 0; + this.sample.measurements.forEach((measurement, i) => { + this.charts.push(_.cloneDeep(this.chartInit)); + if (measurement.measurement_template === spectrumTemplate) { + setTimeout(() => { + this.generateChart(measurement.values.dpt, i); + console.log(this.charts); + }, spectrumCounter * 20); + spectrumCounter ++; + } + }); this.material = sData.material; this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ? Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']]; if ('condition_template' in this.sample.condition) { @@ -225,8 +274,8 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.setNewMaterial(); } - preventSubmit(event) { - if (event.key === 'Enter') { + preventDefault(event) { + if (event.key && event.key === 'Enter' || event.type === 'dragover') { event.preventDefault(); } } @@ -285,6 +334,7 @@ export class SampleComponent implements OnInit, AfterContentChecked { addMeasurement() { this.sample.measurements.push(new MeasurementModel(this.measurementTemplates[0]._id)); + this.charts.push(_.cloneDeep(this.chartInit)); } removeMeasurement(index) { @@ -292,14 +342,24 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.api.delete('/measurement/' + this.sample.measurements[index]._id); } this.sample.measurements.splice(index, 1); + this.charts.splice(index, 1); } - fileToArray(event, mIndex, parameter) { + clearChart(index) { + this.charts[index][0].data = []; + } + + fileToArray(files, mIndex, parameter) { const fileReader = new FileReader(); fileReader.onload = () => { this.sample.measurements[mIndex].values[parameter] = fileReader.result.toString().split('\r\n').map(e => e.split(',')); + this.generateChart(this.sample.measurements[mIndex].values[parameter], mIndex); }; - fileReader.readAsText(event.target.files[0]); + fileReader.readAsText(files[0]); + } + + generateChart(spectrum, index) { + this.charts[index][0].data = spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])})); } toggleCondition() { diff --git a/src/app/samples/samples.component.html b/src/app/samples/samples.component.html index ee76318..9aae131 100644 --- a/src/app/samples/samples.component.html +++ b/src/app/samples/samples.component.html @@ -45,7 +45,7 @@ -
+
@@ -54,11 +54,6 @@
- - - - -
@@ -91,8 +86,6 @@
{{key.label}} - -
diff --git a/src/app/samples/samples.component.scss b/src/app/samples/samples.component.scss index 0641a55..9b4a2f6 100644 --- a/src/app/samples/samples.component.scss +++ b/src/app/samples/samples.component.scss @@ -169,3 +169,8 @@ textarea.linkmodal { min-height: 200px; border: none; } + +.filter-inputs > * { + display: inline-block; + max-width: 250px; +} diff --git a/src/app/services/login.service.ts b/src/app/services/login.service.ts index 5cc336f..088792a 100644 --- a/src/app/services/login.service.ts +++ b/src/app/services/login.service.ts @@ -42,6 +42,11 @@ export class LoginService implements CanActivate { }); } + logout() { + this.storage.remove('basicAuth'); + this.loggedIn = false; + } + canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable { return new Observable(observer => { if (this.loggedIn === undefined) { @@ -60,4 +65,8 @@ export class LoginService implements CanActivate { get isLoggedIn() { return this.loggedIn; } + + get username() { + return atob(this.storage.get('basicAuth')).split(':')[0]; + } } From 18bbf145960215f866db4e7a4167705be0e1afc8 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Tue, 14 Jul 2020 15:58:33 +0200 Subject: [PATCH 6/7] added documentation and img-magnifier --- src/app/app-routing.module.ts | 2 + src/app/app.component.html | 1 + src/app/app.module.ts | 9 +++- .../documentation.component.html | 9 ++++ .../documentation.component.scss | 7 ++++ .../documentation.component.spec.ts | 25 +++++++++++ .../documentation/documentation.component.ts | 15 +++++++ .../img-magnifier.component.html | 17 ++++++++ .../img-magnifier.component.scss | 20 +++++++++ .../img-magnifier.component.spec.ts | 25 +++++++++++ .../img-magnifier/img-magnifier.component.ts | 41 +++++++++++++++++++ src/app/services/api.service.spec.ts | 4 +- src/assets/imgs/db_structure_latest.svg | 3 ++ 13 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 src/app/documentation/documentation.component.html create mode 100644 src/app/documentation/documentation.component.scss create mode 100644 src/app/documentation/documentation.component.spec.ts create mode 100644 src/app/documentation/documentation.component.ts create mode 100644 src/app/img-magnifier/img-magnifier.component.html create mode 100644 src/app/img-magnifier/img-magnifier.component.scss create mode 100644 src/app/img-magnifier/img-magnifier.component.spec.ts create mode 100644 src/app/img-magnifier/img-magnifier.component.ts create mode 100644 src/assets/imgs/db_structure_latest.svg diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e11eb05..d3071c5 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,6 +4,7 @@ import {HomeComponent} from './home/home.component'; import {LoginService} from './services/login.service'; import {SampleComponent} from './sample/sample.component'; import {SamplesComponent} from './samples/samples.component'; +import {DocumentationComponent} from './documentation/documentation.component'; const routes: Routes = [ @@ -12,6 +13,7 @@ const routes: Routes = [ {path: 'samples', component: SamplesComponent, canActivate: [LoginService]}, {path: 'samples/new', component: SampleComponent, canActivate: [LoginService]}, {path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]}, + {path: 'documentation', component: DocumentationComponent}, // if not authenticated { path: '**', redirectTo: '' } diff --git a/src/app/app.component.html b/src/app/app.component.html index 405c832..eadcb78 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,6 +2,7 @@ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 663e8e9..9e1268c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -18,6 +18,8 @@ import {CommonModule} from '@angular/common'; import { ErrorComponent } from './error/error.component'; import { ObjectPipe } from './object.pipe'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import { DocumentationComponent } from './documentation/documentation.component'; +import { ImgMagnifierComponent } from './img-magnifier/img-magnifier.component'; @NgModule({ declarations: [ @@ -28,7 +30,9 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; SampleComponent, ValidateDirective, ErrorComponent, - ObjectPipe + ObjectPipe, + DocumentationComponent, + ImgMagnifierComponent ], imports: [ LocalStorageModule.forRoot({ @@ -48,7 +52,8 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; ChartsModule ], providers: [ - ModalService + ModalService, + { provide: Window, useValue: window } ], bootstrap: [AppComponent] }) diff --git a/src/app/documentation/documentation.component.html b/src/app/documentation/documentation.component.html new file mode 100644 index 0000000..3ff12b0 --- /dev/null +++ b/src/app/documentation/documentation.component.html @@ -0,0 +1,9 @@ +

Samples

+ +

+ Find the API documentation here: https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/ +

+ +

Database model

+ + diff --git a/src/app/documentation/documentation.component.scss b/src/app/documentation/documentation.component.scss new file mode 100644 index 0000000..58a9ed8 --- /dev/null +++ b/src/app/documentation/documentation.component.scss @@ -0,0 +1,7 @@ +p { + margin-bottom: 20px; +} + +img#db-structure { + width: 100%; +} diff --git a/src/app/documentation/documentation.component.spec.ts b/src/app/documentation/documentation.component.spec.ts new file mode 100644 index 0000000..fec102e --- /dev/null +++ b/src/app/documentation/documentation.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DocumentationComponent } from './documentation.component'; + +describe('DocumentationComponent', () => { + let component: DocumentationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DocumentationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DocumentationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/documentation/documentation.component.ts b/src/app/documentation/documentation.component.ts new file mode 100644 index 0000000..c3f6071 --- /dev/null +++ b/src/app/documentation/documentation.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-documentation', + templateUrl: './documentation.component.html', + styleUrls: ['./documentation.component.scss'] +}) +export class DocumentationComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/app/img-magnifier/img-magnifier.component.html b/src/app/img-magnifier/img-magnifier.component.html new file mode 100644 index 0000000..a547720 --- /dev/null +++ b/src/app/img-magnifier/img-magnifier.component.html @@ -0,0 +1,17 @@ +
+
+
+ +
diff --git a/src/app/img-magnifier/img-magnifier.component.scss b/src/app/img-magnifier/img-magnifier.component.scss new file mode 100644 index 0000000..f72791e --- /dev/null +++ b/src/app/img-magnifier/img-magnifier.component.scss @@ -0,0 +1,20 @@ +@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; + +.img-container { + position:relative; + overflow: hidden; + + & > img { + width: 100%; + } +} + +.magnifier { + position: absolute; + background: #FFF; + background-repeat: no-repeat; + background-position: -500px -500px; + z-index: 99; + border: 1px solid #FFF; + box-shadow: 10px 10px 25px $color-bosch-light-gray-b25; +} diff --git a/src/app/img-magnifier/img-magnifier.component.spec.ts b/src/app/img-magnifier/img-magnifier.component.spec.ts new file mode 100644 index 0000000..753cad6 --- /dev/null +++ b/src/app/img-magnifier/img-magnifier.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ImgMagnifierComponent } from './img-magnifier.component'; + +describe('ImgMagnifierComponent', () => { + let component: ImgMagnifierComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ImgMagnifierComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ImgMagnifierComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/img-magnifier/img-magnifier.component.ts b/src/app/img-magnifier/img-magnifier.component.ts new file mode 100644 index 0000000..5a339db --- /dev/null +++ b/src/app/img-magnifier/img-magnifier.component.ts @@ -0,0 +1,41 @@ +import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; + +@Component({ + selector: 'app-img-magnifier', + templateUrl: './img-magnifier.component.html', + styleUrls: ['./img-magnifier.component.scss'] +}) +export class ImgMagnifierComponent implements OnInit, AfterViewInit { + + @Input('src') src: string; + @Input('zoom') zoom: number; + @Input('magnifierSize') magnifierSize: {width: number, height: number}; + @ViewChild('mainImg') mainImg: ElementRef; + + backgroundSize; + magnifierPos = {x: 0, y: 0}; + showMagnifier = false; + + constructor( + private window: Window + ) { } + + ngOnInit(): void { + } + + ngAfterViewInit() { + setTimeout(() => { + this.calcBackgroundSize(); + }, 1); + } + + calcPos(event) { + const img = this.mainImg.nativeElement.getBoundingClientRect(); + this.magnifierPos.x = Math.min(img.width - this.magnifierSize.width, Math.max(0, event.pageX - img.left - this.window.pageXOffset - this.magnifierSize.width / 2)); + this.magnifierPos.y = Math.min(img.height - this.magnifierSize.height + 7, Math.max(0, event.pageY - img.top - this.window.pageYOffset - this.magnifierSize.height / 2)); + } + + calcBackgroundSize() { + this.backgroundSize = this.mainImg ? (this.mainImg.nativeElement.width * this.zoom - this.magnifierSize.width) + 'px ' + (this.mainImg.nativeElement.height * this.zoom - this.magnifierSize.height) + 'px ' : '0 0'; + } +} diff --git a/src/app/services/api.service.spec.ts b/src/app/services/api.service.spec.ts index 58212b6..3782e9a 100644 --- a/src/app/services/api.service.spec.ts +++ b/src/app/services/api.service.spec.ts @@ -46,7 +46,7 @@ describe('ApiService', () => { modalServiceSpy.openComponent.and.returnValue({instance: {message: ''}} as any); apiService.get('/testurl'); - expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', {}); + expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object)); expect(modalServiceSpy.openComponent.calls.count()).toBe(1); }); @@ -133,4 +133,6 @@ describe('ApiService', () => { expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth'); }); })); + + // TODO: test return headers }); diff --git a/src/assets/imgs/db_structure_latest.svg b/src/assets/imgs/db_structure_latest.svg new file mode 100644 index 0000000..5dccaf4 --- /dev/null +++ b/src/assets/imgs/db_structure_latest.svg @@ -0,0 +1,3 @@ + + +
measurements
measurements
materials
materials
samples
samples
_id
_id
_id
_id
values
values
_id
_id
number
number
numbers
numbers
material_name
material_name
batch
batch
comment
comment
location
location
models
models
name
name
status
status
notes
notes
data
data
_id
_id
users
users
name
name
pass
pass
level
level
_id
_id
type
type
measurement_templates
measurement_templates
_id
_id
parameters
parameters
range
range
name
name
name
name
color
color
sample_references
sample_references
device_name
device_name
condition_templates
condition_templates
_id
_id
parameters
parameters
range
range
name
name
name
name
custom fields
custom fields
key
key
email
email
note_fields
note_fields
name
name
qty
qty
sample_id
sample_id
relation
relation
version
version
version
version
status
status
status
status
condition
condition
parameters
parameters
material_groups
material_groups
material_suppliers
material_suppliers
_id
_id
name
name
_id
_id
name
name
first_id
first_id
first_id
first_id
changelog
changelog
_id
_id
collection
collection
data
data
condition
condition
action
action
material_templates
material_templates
_id
_id
parameters
parameters
range
range
name
name
name
name
version
version
first_id
first_id
properties
properties
parameters
parameters
Viewer does not support full SVG 1.1
\ No newline at end of file From 93878e4e0a8f509277248ceba7c695c96c2b72b2 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 22 Jul 2020 10:45:34 +0200 Subject: [PATCH 7/7] implemented sample references --- manifest.yml | 3 +- src/Staticfile | 2 + src/app/app.module.ts | 4 +- src/app/exists.pipe.spec.ts | 8 + src/app/exists.pipe.ts | 14 ++ src/app/models/material.model.ts | 17 +- src/app/object.pipe.ts | 5 +- .../rb-table/rb-table/rb-table.component.html | 8 +- .../rb-table/rb-table/rb-table.component.scss | 15 ++ src/app/sample/sample.component.html | 46 ++--- src/app/sample/sample.component.ts | 185 +++++++++++++----- src/app/samples/samples.component.html | 80 ++++---- src/app/samples/samples.component.scss | 8 +- src/app/samples/samples.component.ts | 122 ++++++------ src/app/services/api.service.ts | 6 +- 15 files changed, 331 insertions(+), 192 deletions(-) create mode 100644 src/app/exists.pipe.spec.ts create mode 100644 src/app/exists.pipe.ts diff --git a/manifest.yml b/manifest.yml index 852c5cd..19a5de6 100644 --- a/manifest.yml +++ b/manifest.yml @@ -2,7 +2,8 @@ applications: - name: definma path: dist/UI - buildpack: staticfile_buildpack + buildpacks: + - staticfile_buildpack memory: 128M instances: 1 stack: cflinuxfs3 diff --git a/src/Staticfile b/src/Staticfile index 03b776c..1b9f882 100644 --- a/src/Staticfile +++ b/src/Staticfile @@ -1,2 +1,4 @@ pushstate: enabled force_https: true +root: UI +location_include: custom-header.conf diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9e1268c..4fd6a1c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -20,6 +20,7 @@ import { ObjectPipe } from './object.pipe'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import { DocumentationComponent } from './documentation/documentation.component'; import { ImgMagnifierComponent } from './img-magnifier/img-magnifier.component'; +import { ExistsPipe } from './exists.pipe'; @NgModule({ declarations: [ @@ -32,7 +33,8 @@ import { ImgMagnifierComponent } from './img-magnifier/img-magnifier.component'; ErrorComponent, ObjectPipe, DocumentationComponent, - ImgMagnifierComponent + ImgMagnifierComponent, + ExistsPipe ], imports: [ LocalStorageModule.forRoot({ diff --git a/src/app/exists.pipe.spec.ts b/src/app/exists.pipe.spec.ts new file mode 100644 index 0000000..110c53c --- /dev/null +++ b/src/app/exists.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ExistsPipe } from './exists.pipe'; + +describe('ExistsPipe', () => { + it('create an instance', () => { + const pipe = new ExistsPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/exists.pipe.ts b/src/app/exists.pipe.ts new file mode 100644 index 0000000..2fefbd7 --- /dev/null +++ b/src/app/exists.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'exists', + pure: true +}) +export class ExistsPipe implements PipeTransform { + + transform(value: unknown, key?): unknown { + // console.log(new Date().getTime()); + return value || value === 0 ? (key ? value[key] : value) : ''; + } + +} diff --git a/src/app/models/material.model.ts b/src/app/models/material.model.ts index e0e8821..30e37f6 100644 --- a/src/app/models/material.model.ts +++ b/src/app/models/material.model.ts @@ -7,21 +7,10 @@ export class MaterialModel extends BaseModel { name = ''; supplier = ''; group = ''; - mineral = 0; - glass_fiber = 0; - carbon_fiber = 0; - private numberTemplate = {color: '', number: ''}; - numbers: {color: string, number: string}[] = [_.cloneDeep(this.numberTemplate)]; + properties: {material_template: string, [prop: string]: string} = {material_template: null}; + numbers: string[] = ['']; sendFormat() { - return _.pick(this, ['name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers']); - } - - addNumber() { - this.numbers.push(_.cloneDeep(this.numberTemplate)); - } - - popNumber() { - this.numbers.pop(); + return _.pick(this, ['name', 'supplier', 'group', 'numbers', 'properties']); } } diff --git a/src/app/object.pipe.ts b/src/app/object.pipe.ts index cf7ced5..7dc3e50 100644 --- a/src/app/object.pipe.ts +++ b/src/app/object.pipe.ts @@ -1,12 +1,13 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ - name: 'object' + name: 'object', + pure: true }) export class ObjectPipe implements PipeTransform { transform(value: object): string { - return value ? Object.entries(value).map(e => e.join(': ')).join(', ') : ''; + return value ? JSON.stringify(value) : ''; } } diff --git a/src/app/rb-table/rb-table/rb-table.component.html b/src/app/rb-table/rb-table/rb-table.component.html index 49cef91..5e3c6a4 100644 --- a/src/app/rb-table/rb-table/rb-table.component.html +++ b/src/app/rb-table/rb-table/rb-table.component.html @@ -1,4 +1,6 @@ - - -
+
+ + +
+
diff --git a/src/app/rb-table/rb-table/rb-table.component.scss b/src/app/rb-table/rb-table/rb-table.component.scss index 80a2419..c1b1e0a 100644 --- a/src/app/rb-table/rb-table/rb-table.component.scss +++ b/src/app/rb-table/rb-table/rb-table.component.scss @@ -1,5 +1,16 @@ @import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors"; +.table-wrapper { + overflow-x: auto; + width: 100%; + + &, & > table { // scrollbar at the top + transform:rotateX(180deg); + -ms-transform:rotateX(180deg); /* IE 9 */ + -webkit-transform:rotateX(180deg); /* Safari and Chrome */ + } +} + table { width: 100%; border-collapse: collapse; @@ -9,6 +20,10 @@ table { ::ng-deep td, ::ng-deep th { padding: 8px 5px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + max-width: 200px; } ::ng-deep th { diff --git a/src/app/sample/sample.component.html b/src/app/sample/sample.component.html index 138e0e9..7401416 100644 --- a/src/app/sample/sample.component.html +++ b/src/app/sample/sample.component.html @@ -10,7 +10,7 @@ Cannot be empty Unknown material, add properties for new material - +
@@ -21,30 +21,16 @@ {{groupInput.errors.failure}} - - Invalid value - Minimum value is 0 - Maximum value is 100 - - - Invalid value - Minimum value is 0 - Maximum value is 100 - - - Invalid value - Minimum value is 0 - Maximum value is 100 - -
-
- - - - -
+
+ + + + + {{parameterInput.errors.failure}} + Cannot be empty +
  @@ -54,7 +40,7 @@ {{typeInput.errors.failure}} Cannot be empty - + {{colorInput.errors.failure}} Cannot be empty @@ -68,6 +54,17 @@ {{commentInput.errors.failure}} +
Sample references
+
+
+ + Unknown sample number + +
+ + Cannot be empty + +
Additional properties
@@ -79,7 +76,6 @@ Cannot be empty
-
  diff --git a/src/app/sample/sample.component.ts b/src/app/sample/sample.component.ts index ba13d26..5f68668 100644 --- a/src/app/sample/sample.component.ts +++ b/src/app/sample/sample.component.ts @@ -16,6 +16,7 @@ import {TemplateModel} from '../models/template.model'; import {MeasurementModel} from '../models/measurement.model'; import { ChartOptions } from 'chart.js'; import {animate, style, transition, trigger} from '@angular/animations'; +import {Observable} from 'rxjs'; // TODO: tests // TODO: confirmation for new group/supplier @@ -24,6 +25,9 @@ import {animate, style, transition, trigger} from '@angular/animations'; // TODO: multiple spectra // TODO: multiple samples for base data, extend multiple measurements, conditions +// TODO: material properties, color (in material and sample (not required)) + +// TODO: API $in Regex @Component({ selector: 'app-sample', @@ -55,11 +59,17 @@ export class SampleComponent implements OnInit, AfterContentChecked { groups: string[] = []; // all groups conditionTemplates: TemplateModel[]; // all conditions condition: TemplateModel | null = null; // selected condition + materialTemplates: TemplateModel[]; // all material templates + materialTemplate: TemplateModel | null = null; // selected material template materialNames = []; // names of all materials material = new MaterialModel(); // object of current selected material sample = new SampleModel(); customFields: [string, string][] = [['', '']]; + sampleReferences: [string, string, string][] = [['', '', '']]; + sampleReferenceFinds: {_id: string, number: string}[] = []; // raw sample reference data from db + currentSRIndex = 0; // index of last entered sample reference availableCustomFields: string[] = []; + sampleReferenceAutocomplete: string[][] = [[]]; responseData: SampleModel; // gets filled with response data after saving the sample measurementTemplates: TemplateModel[]; loading = 0; // number of currently loading instances @@ -96,7 +106,7 @@ export class SampleComponent implements OnInit, AfterContentChecked { ngOnInit(): void { this.new = this.router.url === '/samples/new'; - this.loading = 6; + this.loading = 7; this.api.get('/materials?status=all', (data: any) => { this.materials = data.map(e => new MaterialModel().deserialize(e)); this.materialNames = data.map(e => e.name); @@ -114,41 +124,63 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.conditionTemplates = data.map(e => new TemplateModel().deserialize(e)); this.loading--; }); + this.api.get('/template/materials', data => { + this.materialTemplates = data.map(e => new TemplateModel().deserialize(e)); + this.selectMaterialTemplate(this.materialTemplates[0]._id); + this.loading--; + }); this.api.get('/template/measurements', data => { this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e)); + if (!this.new) { + this.loading++; + this.api.get('/sample/' + this.route.snapshot.paramMap.get('id'), sData => { + this.sample.deserialize(sData); + this.charts = []; + const spectrumTemplate = this.measurementTemplates.find(e => e.name === 'spectrum')._id; + let spectrumCounter = 0; + this.sample.measurements.forEach((measurement, i) => { + this.charts.push(_.cloneDeep(this.chartInit)); + if (measurement.measurement_template === spectrumTemplate) { + setTimeout(() => { + this.generateChart(measurement.values.dpt, i); + console.log(this.charts); + }, spectrumCounter * 20); + spectrumCounter ++; + } + }); + this.material = sData.material; + this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ? Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']]; + if (this.sample.notes.sample_references.length) { + this.sampleReferences = []; + this.sampleReferenceAutocomplete = []; + let loadCounter = this.sample.notes.sample_references.length; + this.sample.notes.sample_references.forEach(reference => { + this.api.get('/sample/' + reference.sample_id, srData => { + this.sampleReferences.push([srData.number, reference.relation, reference.sample_id]); + this.sampleReferenceAutocomplete.push([srData.number]); + if (!--loadCounter) { + this.sampleReferences.push(['', '', '']); + this.sampleReferenceAutocomplete.push([]); + console.log(this.sampleReferences); + console.log(this.sampleReferenceAutocomplete); + } + }); + }); + } + if ('condition_template' in this.sample.condition) { + this.selectCondition(this.sample.condition.condition_template); + } + console.log('data loaded'); + this.loading--; + this.checkFormAfterInit = true; + }); + } this.loading--; }); this.api.get('/sample/notes/fields', data => { this.availableCustomFields = data.map(e => e.name); this.loading--; }); - if (!this.new) { - this.loading++; - this.api.get('/sample/' + this.route.snapshot.paramMap.get('id'), sData => { - this.sample.deserialize(sData); - this.charts = []; - const spectrumTemplate = this.measurementTemplates.find(e => e.name === 'spectrum')._id; - let spectrumCounter = 0; - this.sample.measurements.forEach((measurement, i) => { - this.charts.push(_.cloneDeep(this.chartInit)); - if (measurement.measurement_template === spectrumTemplate) { - setTimeout(() => { - this.generateChart(measurement.values.dpt, i); - console.log(this.charts); - }, spectrumCounter * 20); - spectrumCounter ++; - } - }); - this.material = sData.material; - this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ? Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']]; - if ('condition_template' in this.sample.condition) { - this.selectCondition(this.sample.condition.condition_template); - } - console.log('data loaded'); - this.loading--; - this.checkFormAfterInit = true; - }); - } } ngAfterContentChecked() { @@ -161,6 +193,15 @@ export class SampleComponent implements OnInit, AfterContentChecked { } } + // attach validators to dynamic material fields when all values are available and template was fully created + if (this.materialTemplate && this.materialTemplate.hasOwnProperty('parameters') && this.materialTemplate.parameters.length > 0 && this.materialTemplate.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('materialParameter0')) { + for (const i in this.materialTemplate.parameters) { + if (this.materialTemplate.parameters[i]) { + this.attachValidator('materialParameter' + i, this.materialTemplate.parameters[i].range, true); + } + } + } + if (this.sampleForm && this.sampleForm.form.get('measurementParameter0-0')) { this.sample.measurements.forEach((measurement, mIndex) => { const template = this.getMeasurementTemplate(measurement.measurement_template); @@ -209,11 +250,7 @@ export class SampleComponent implements OnInit, AfterContentChecked { saveSample() { new Promise(resolve => { if (this.newMaterial) { // save material first if new one exists - for (const i in this.material.numbers) { // remove empty numbers fields - if (this.material.numbers[i].color === '') { - this.material.numbers.splice(i as any as number, 1); - } - } + this.material.numbers = this.material.numbers.filter(e => e !== ''); this.api.post('/material/new', this.material.sendFormat(), data => { this.materials.push(data); // add material to data this.material = data; @@ -231,6 +268,7 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.sample.notes.custom_fields[element[0]] = element[1]; } }); + this.sample.notes.sample_references = this.sampleReferences.filter(e => e[0] && e[1] && e[2]).map(e => ({sample_id: e[2], relation: e[1]})); new Promise(resolve => { if (this.new) { this.api.post('/sample/new', this.sample.sendFormat(), resolve); @@ -269,6 +307,9 @@ export class SampleComponent implements OnInit, AfterContentChecked { this.sample.material_id = this.material._id; } else { + if (this.sample.material_id !== null) { // reset previous match + this.material = new MaterialModel(); + } this.sample.material_id = null; } this.setNewMaterial(); @@ -280,16 +321,12 @@ export class SampleComponent implements OnInit, AfterContentChecked { } } - getColors(material) { - return material ? material.numbers.map(e => e.color) : []; - } - // TODO: rework later setNewMaterial(value = null) { if (value === null) { this.newMaterial = !this.sample.material_id; } - else { + else if (value || (!value && this.sample.material_id !== null )) { // set to false only if material already exists this.newMaterial = value; } if (this.newMaterial) { @@ -303,19 +340,14 @@ export class SampleComponent implements OnInit, AfterContentChecked { handleMaterialNumbers() { const fieldNo = this.material.numbers.length; - let filledFields = 0; - this.material.numbers.forEach(mNumber => { - if (mNumber.color !== '') { - filledFields ++; - } - }); + const filledFields = this.material.numbers.filter(e => e !== '').length; // append new field if (filledFields === fieldNo) { - this.material.addNumber(); + this.material.numbers.push(''); } // remove if two end fields are empty - if (fieldNo > 1 && this.material.numbers[fieldNo - 1].color === '' && this.material.numbers[fieldNo - 2].color === '') { - this.material.popNumber(); + if (fieldNo > 1 && this.material.numbers[fieldNo - 1] === '' && this.material.numbers[fieldNo - 2] === '') { + this.material.numbers.pop(); } } @@ -328,6 +360,13 @@ export class SampleComponent implements OnInit, AfterContentChecked { } } + selectMaterialTemplate(id) { + this.materialTemplate = this.materialTemplates.find(e => e._id === id); + if ('material_template' in this.material.properties) { + this.material.properties.material_template = id; + } + } + getMeasurementTemplate(id): TemplateModel { return this.measurementTemplates && id ? this.measurementTemplates.find(e => e._id === id) : new TemplateModel(); } @@ -391,6 +430,58 @@ export class SampleComponent implements OnInit, AfterContentChecked { } } + checkSampleReference(value, index) { + if (value) { + this.sampleReferences[index][0] = value; + } + this.currentSRIndex = index; + const fieldNo = this.sampleReferences.length; + let filledFields = 0; + this.sampleReferences.forEach(field => { + if (field[0] !== '') { + filledFields ++; + } + }); + // append new field + if (filledFields === fieldNo) { + this.sampleReferences.push(['', '', '']); + this.sampleReferenceAutocomplete.push([]); + } + // remove if two end fields are empty + if (fieldNo > 1 && this.sampleReferences[fieldNo - 1][0] === '' && this.sampleReferences[fieldNo - 2][0] === '') { + this.sampleReferences.pop(); + this.sampleReferenceAutocomplete.pop(); + } + this.sampleReferenceIdFind(value); + } + + sampleReferenceList(value) { + return new Observable(observer => { + this.api.get<{_id: string, number: string}[]>('/samples?status=all&page-size=25&sort=number-asc&fields[]=number&fields[]=_id&filters[]=%7B%22mode%22%3A%22stringin%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22' + value + '%22%5D%7D', data => { + console.log(data); + this.sampleReferenceAutocomplete[this.currentSRIndex] = data.map(e => e.number); + this.sampleReferenceFinds = data; + observer.next(data.map(e => e.number)); + observer.complete(); + this.sampleReferenceIdFind(value); + }); + }); + } + + sampleReferenceIdFind(value) { + const idFind = this.sampleReferenceFinds.find(e => e.number === value); + if (idFind) { + this.sampleReferences[this.currentSRIndex][2] = idFind._id; + } + else { + this.sampleReferences[this.currentSRIndex][2] = ''; + } + } + + sampleReferenceListBind() { + return this.sampleReferenceList.bind(this); + } + uniqueCfValues(index) { // returns all names until index for unique check return this.customFields.slice(0, index).map(e => e[0]); } diff --git a/src/app/samples/samples.component.html b/src/app/samples/samples.component.html index 9aae131..1e453bb 100644 --- a/src/app/samples/samples.component.html +++ b/src/app/samples/samples.component.html @@ -6,7 +6,7 @@
-   Filter +   Filter
@@ -28,36 +28,37 @@ - + {{item.label}}
- - - - - - - - - - - -
- - - - - - - -
+ + + + + + + + + + + + + +
+ + + + + + + +
+
- - @@ -83,7 +84,7 @@ - +
{{key.label}} @@ -94,19 +95,18 @@ - {{sample.number}} - {{sample.material_number}} - {{materials[sample.material_id].name}} - {{materials[sample.material_id].supplier}} - {{materials[sample.material_id].group}} - {{materials[sample.material_id].glass_fiber}} - {{materials[sample.material_id].carbon_fiber}} - {{materials[sample.material_id].mineral}} - {{sample.type}} - {{sample.color}} - {{sample.batch}} - {{sample.added | date:'dd/MM/yy'}} - {{sample[key[1]] ? sample[key[1]][key[2]] : ''}} + {{sample.number}} + {{materials[sample.material_id].numbers}} + {{materials[sample.material_id].name}} + {{materials[sample.material_id].supplier}} + {{materials[sample.material_id].group}} + {{materials[sample.material_id].properties[key[2]] | exists}} + {{sample.type}} + {{sample.color}} + {{sample.batch}} + {{sample.notes | object}} + {{sample[key[1]] | exists: key[2]}} + {{sample.added | date:'dd/MM/yy'}} @@ -120,9 +120,9 @@ - of {{pages()}} ({{totalSamples}} samples) + of {{pages}} ({{totalSamples}} samples) -
diff --git a/src/app/samples/samples.component.scss b/src/app/samples/samples.component.scss index 9b4a2f6..55d78fd 100644 --- a/src/app/samples/samples.component.scss +++ b/src/app/samples/samples.component.scss @@ -13,6 +13,10 @@ } } +rb-table { + width: 100%; +} + .rb-ic.rb-ic-edit { font-size: 1.1rem; color: $color-gray-silver-sand; @@ -156,11 +160,13 @@ & > div { display: grid; grid-template-columns: auto auto 1fr; + float: left; + margin-right: 30px; } } .filtermode { - max-width: 100px; + max-width: 80px; } textarea.linkmodal { diff --git a/src/app/samples/samples.component.ts b/src/app/samples/samples.component.ts index 8e51b5a..0aa5c89 100644 --- a/src/app/samples/samples.component.ts +++ b/src/app/samples/samples.component.ts @@ -4,12 +4,16 @@ import {AutocompleteService} from '../services/autocomplete.service'; import _ from 'lodash'; - interface LoadSamplesOptions { toPage?: number; event?: Event; firstPage?: boolean; } +interface KeyInterface { + id: string; + label: string; + active: boolean; +} @Component({ selector: 'app-samples', @@ -17,6 +21,8 @@ interface LoadSamplesOptions { styleUrls: ['./samples.component.scss'] }) +// TODO: manage branches, introduce versioning, only upload ui from master +// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html export class SamplesComponent implements OnInit { @@ -24,7 +30,6 @@ export class SamplesComponent implements OnInit { @ViewChild('pageSizeSelection') pageSizeSelection: ElementRef; @ViewChild('linkarea') linkarea: ElementRef; - customFields = ['']; downloadCsv = false; materials = {}; samples = []; @@ -47,40 +52,29 @@ export class SamplesComponent implements OnInit { {field: 'type', label: 'Type', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'color', label: 'Color', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'batch', label: 'Batch', active: false, autocomplete: [], mode: 'eq', values: ['']}, + {field: 'notes', label: 'Notes', active: false, autocomplete: [], mode: 'eq', values: ['']}, {field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: [new Date()]} ] }; page = 1; + pages = 1; loadSamplesQueue = []; // arguments of queued up loadSamples() calls apiKey = ''; - activeKeys = { - number: true, - 'material.number': true, - 'material.name': true, - 'material.supplier': true, - 'material.group': false, - 'material.glass_fiber': false, - 'material.carbon_fiber': false, - 'material.mineral': false, - type: true, - color: true, - batch: true, - added: true, - }; - keys = [ - {id: 'number', label: 'Number'}, - {id: 'material.number', label: 'Material number'}, - {id: 'material.name', label: 'Material name'}, - {id: 'material.supplier', label: 'Supplier'}, - {id: 'material.group', label: 'Material'}, - {id: 'material.glass_fiber', label: 'GF'}, - {id: 'material.carbon_fiber', label: 'CF'}, - {id: 'material.mineral', label: 'M'}, - {id: 'type', label: 'Type'}, - {id: 'color', label: 'Color'}, - {id: 'batch', label: 'Batch'}, - {id: 'added', label: 'Added'} + keys: KeyInterface[] = [ + {id: 'number', label: 'Number', active: true}, + {id: 'material.numbers', label: 'Material numbers', active: true}, + {id: 'material.name', label: 'Material name', active: true}, + {id: 'material.supplier', label: 'Supplier', active: true}, + {id: 'material.group', label: 'Material', active: false}, + {id: 'type', label: 'Type', active: true}, + {id: 'color', label: 'Color', active: true}, + {id: 'batch', label: 'Batch', active: true}, + {id: 'notes', label: 'Notes', active: false}, + {id: 'added', label: 'Added', active: true} ]; + isActiveKey: {[key: string]: boolean} = {}; + activeKeys: KeyInterface[] = []; + activeTemplateKeys = {material: [], measurements: []}; constructor( @@ -90,6 +84,7 @@ export class SamplesComponent implements OnInit { } ngOnInit(): void { + this.calcFieldSelectKeys(); this.api.get('/materials?status=all', (mData: any) => { this.materials = {}; mData.forEach(material => { @@ -108,21 +103,35 @@ export class SamplesComponent implements OnInit { this.api.get('/material/groups', (data: any) => { this.filters.filters.find(e => e.field === 'material.group').autocomplete = data; }); - this.api.get('/template/measurements', (data: {name: string, parameters: {name: string, range: object}[]}[]) => { - const measurementKeys = []; + this.loadTemplateKeys('materials', 'type'); + this.loadTemplateKeys('measurements', 'added'); + } + + loadTemplateKeys(collection, insertBefore) { + this.api.get('/template/' + collection, (data: {name: string, parameters: {name: string, range: object}[]}[]) => { + const templateKeys = []; data.forEach(item => { item.parameters.forEach(parameter => { - this.activeKeys[`measurements.${item.name}.${encodeURIComponent(parameter.name)}`] = false; - measurementKeys.push({id: `measurements.${item.name}.${encodeURIComponent(parameter.name)}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`}); - this.filters.filters.push({field: `measurements.${item.name}.${parameter.name}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], mode: 'eq', values: ['']}); + templateKeys.push({id: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${encodeURIComponent(parameter.name)}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false}); + this.filters.filters.push({field: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${parameter.name}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], mode: 'eq', values: ['']}); }); }); - console.log(this.filters.filters); - this.keys = [...this.keys, ...measurementKeys]; + this.keys.splice(this.keys.findIndex(e => e.id === insertBefore), 0, ...templateKeys); + this.keys = [...this.keys]; // complete overwrite array to invoke update in rb-multiselect + this.updateActiveKeys(); + this.calcFieldSelectKeys(); }); } - loadSamples(options: LoadSamplesOptions = {}) { // set toPage to null to reload first page, queues calls + loadSamples(options: LoadSamplesOptions = {}, event = null) { // set toPage to null to reload first page, queues calls + if (event) { // adjust active keys + this.keys.forEach(key => { + if (event.hasOwnProperty(key.id)) { + key.active = event[key.id]; + } + }); + this.updateActiveKeys(); + } this.loadSamplesQueue.push(options); if (this.loadSamplesQueue.length <= 1) { // nothing queued up this.sampleLoader(this.loadSamplesQueue[0]); @@ -131,13 +140,11 @@ export class SamplesComponent implements OnInit { private sampleLoader(options: LoadSamplesOptions) { // actual loading of the sample, do not call directly this.api.get(this.sampleUrl({paging: true, pagingOptions: options}), (sData, ignore, headers) => { - if (!options.toPage) { + if (!options.toPage && headers['x-total-items']) { this.totalSamples = headers['x-total-items']; } + this.pages = Math.ceil(this.totalSamples / this.filters.pageSize); this.samples = sData as any; - this.samples.forEach(sample => { - sample.material_number = sample.color === '' ? '' : this.materials[sample.material_id].numbers.find(e => sample.color === e.color).number; - }); this.loadSamplesQueue.shift(); if (this.loadSamplesQueue.length > 0) { // execute next queue item this.sampleLoader(this.loadSamplesQueue[0]); @@ -146,7 +153,7 @@ export class SamplesComponent implements OnInit { } sampleUrl(options: {paging?: boolean, pagingOptions?: {firstPage?: boolean, toPage?: number, event?: Event}, csv?: boolean, export?: boolean, host?: boolean}) { // return url to fetch samples - const additionalTableKeys = ['material_id', '_id', 'color']; // keys which should always be added if export = false + const additionalTableKeys = ['material_id', '_id']; // keys which should always be added if export = false const query: string[] = []; query.push('status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated'))); if (options.paging) { @@ -170,23 +177,22 @@ export class SamplesComponent implements OnInit { if (options.export) { query.push('key=' + this.apiKey); } - Object.keys(this.activeKeys).forEach(key => { - if (this.activeKeys[key] && (options.export || (!options.export && key.indexOf('material') < 0))) { // do not load material properties for table - query.push('fields[]=' + key); + this.keys.forEach(key => { + if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) { // do not load material properties for table + query.push('fields[]=' + key.id); } }); console.log(this.filters.filters); query.push(..._.cloneDeep(this.filters.filters) .map(e => { - e.values = e.values.filter(el => el !== ''); - if (e.field === 'added') { - console.log(e.values); + e.values = e.values.filter(el => el !== ''); // do not include empty values + if (e.field === 'added') { // correct timezone e.values = e.values.map(el => new Date(new Date(el).getTime() - new Date(el).getTimezoneOffset() * 60000).toISOString()); } return e; }) - .filter(e => e.active && e.values[0] !== '') + .filter(e => e.active && e.values.length > 0) .map(e => 'filters[]=' + encodeURIComponent(JSON.stringify(_.pick(e, ['mode', 'field', 'values'])))) ); console.log(this.filters); @@ -230,21 +236,23 @@ export class SamplesComponent implements OnInit { } } - pages() { - return Math.ceil(this.totalSamples / this.filters.pageSize); - } - setSort(string) { this.filters.sort = string; this.loadSamples({firstPage: true}); } - activeKeysArray() { // array with all activeKeys names - return Object.keys(this.activeKeys).filter(e => this.activeKeys[e] === true).map(e => this.keys.find(el => el.id === e)); + updateActiveKeys() { // array with all activeKeys + this.activeKeys = this.keys.filter(e => e.active); + this.activeTemplateKeys.material = this.keys.filter(e => e.id.indexOf('material.properties.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el))); + this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el))); + console.log(this.activeTemplateKeys); + console.log(this.keys); // TODO: glass fiber filter not working } - activeMeasurementKeys() { - return Object.keys(this.activeKeys).filter(e => e.indexOf('measurements.') >= 0 && this.activeKeys[e]).map(e => e.split('.').map(el => decodeURIComponent(el))); + calcFieldSelectKeys() { + this.keys.forEach(key => { + this.isActiveKey[key.id] = key.active; + }); } preventDefault(event, key = 'all') { diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 0924640..60780d9 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -16,7 +16,8 @@ export class ApiService { constructor( private http: HttpClient, private storage: LocalStorageService, - private modalService: ModalService + private modalService: ModalService, + private window: Window ) { } get hostName() { @@ -49,6 +50,9 @@ export class ApiService { else { const modalRef = this.modalService.openComponent(ErrorComponent); modalRef.instance.message = 'Network request failed!'; + modalRef.result.then(() => { + this.window.location.reload(); + }); } }); }