first version of sample.component finished
This commit is contained in:
		@@ -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<HttpClient>;
 | 
			
		||||
let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
 | 
			
		||||
 | 
			
		||||
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<HttpClient>;
 | 
			
		||||
    localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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: '' }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,21 @@
 | 
			
		||||
    <a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a>
 | 
			
		||||
    <a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="loginService.canActivate()">Samples</a>
 | 
			
		||||
  </nav>
 | 
			
		||||
 | 
			
		||||
  <nav *rbActionNavItems>
 | 
			
		||||
    <a href="javascript:"  [rbPopover]="userPopover" [anchor]="popoverAnchor">
 | 
			
		||||
      Account <span class="rb-ic rb-ic-my-brand-frame" #popoverAnchor></span></a>
 | 
			
		||||
  </nav>
 | 
			
		||||
  <ng-template #userPopover>
 | 
			
		||||
    <div class="spacing">
 | 
			
		||||
      <p>
 | 
			
		||||
        Some user specific information
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      <a href="javascript:" class="rb-btn rb-primary">Logout</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <div *rbSubBrandHeader>Digital Fingerprint of Plastics</div>
 | 
			
		||||
</rb-full-header>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								src/app/error/error.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/app/error/error.component.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
<rb-alert alertTitle="Error" type="error" okBtnLabel="Discard">
 | 
			
		||||
  {{message}}
 | 
			
		||||
</rb-alert>
 | 
			
		||||
							
								
								
									
										0
									
								
								src/app/error/error.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/app/error/error.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										45
									
								
								src/app/error/error.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/app/error/error.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<ErrorComponent>;
 | 
			
		||||
  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');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										17
									
								
								src/app/error/error.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/app/error/error.component.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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 {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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<ApiService>;
 | 
			
		||||
let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
 | 
			
		||||
 | 
			
		||||
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<ApiService>;
 | 
			
		||||
    localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,10 +1,15 @@
 | 
			
		||||
<div class="login-wrapper">
 | 
			
		||||
  <h2>Please log in</h2>
 | 
			
		||||
 | 
			
		||||
  <form>
 | 
			
		||||
    <rb-form-input name="username" label="username" [(ngModel)]="username"></rb-form-input>
 | 
			
		||||
    <rb-form-input type="password" name="password" label="password" [(ngModel)]="password"></rb-form-input>
 | 
			
		||||
 | 
			
		||||
  <form #loginForm="ngForm">
 | 
			
		||||
    <rb-form-input name="username" label="username" appValidate="username" required [(ngModel)]="username" #usernameInput="ngModel">
 | 
			
		||||
      <ng-template rbFormValidationMessage="failure">{{usernameInput.errors.failure}}</ng-template>
 | 
			
		||||
    </rb-form-input>
 | 
			
		||||
    <rb-form-input type="password" name="password" label="password" appValidate="password" required [(ngModel)]="password" #passwordInput="ngModel">
 | 
			
		||||
      <ng-template rbFormValidationMessage="failure">{{passwordInput.errors.failure}}</ng-template>
 | 
			
		||||
    </rb-form-input>
 | 
			
		||||
    <button class="rb-btn rb-primary login-button" (click)="login()" type="submit" [disabled]="!loginForm.form.valid">Login</button>
 | 
			
		||||
    <span class="message">{{message}}</span>
 | 
			
		||||
    <button class="rb-btn rb-primary login-button" (click)="login()" type="submit">Login</button>
 | 
			
		||||
  </form>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,5 +8,5 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login-button {
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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!');
 | 
			
		||||
  }));
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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!';
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								src/app/models/custom-fields.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/app/models/custom-fields.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { CustomFieldsModel } from './custom-fields.model';
 | 
			
		||||
 | 
			
		||||
describe('CustomFieldsModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new CustomFieldsModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										13
									
								
								src/app/models/custom-fields.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/app/models/custom-fields.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								src/app/models/deserializable.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/app/models/deserializable.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
// import { DeserializableModel } from './deserializable.model';
 | 
			
		||||
//
 | 
			
		||||
// describe('DeserializableModel', () => {
 | 
			
		||||
//
 | 
			
		||||
// });
 | 
			
		||||
							
								
								
									
										3
									
								
								src/app/models/deserializable.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/app/models/deserializable.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export interface Deserializable {
 | 
			
		||||
  deserialize(input: any): this;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								src/app/models/id.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/app/models/id.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import { IdModel } from './id.model';
 | 
			
		||||
 | 
			
		||||
describe('IdModel', () => {
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										1
									
								
								src/app/models/id.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/models/id.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export type IdModel = string | null;
 | 
			
		||||
							
								
								
									
										7
									
								
								src/app/models/material.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/app/models/material.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { MaterialModel } from './material.model';
 | 
			
		||||
 | 
			
		||||
describe('MaterialModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new MaterialModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										33
									
								
								src/app/models/material.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/app/models/material.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/app/models/measurement.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/app/models/measurement.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { MeasurementModel } from './measurement.model';
 | 
			
		||||
 | 
			
		||||
describe('MeasurementModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new MeasurementModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										29
									
								
								src/app/models/measurement.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/app/models/measurement.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/app/models/sample.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/app/models/sample.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { SampleModel } from './sample.model';
 | 
			
		||||
 | 
			
		||||
describe('SampleModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new SampleModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										37
									
								
								src/app/models/sample.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/app/models/sample.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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']);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								src/app/models/sendformat.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/app/models/sendformat.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
// import { SendformatModel } from './sendformat.model';
 | 
			
		||||
//
 | 
			
		||||
// describe('SendformatModel', () => {
 | 
			
		||||
//
 | 
			
		||||
// });
 | 
			
		||||
							
								
								
									
										3
									
								
								src/app/models/sendformat.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/app/models/sendformat.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export interface SendFormat {
 | 
			
		||||
  sendFormat(omit?: string[]): {[prop: string]: any};
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/app/models/template.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/app/models/template.model.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { TemplateModel } from './template.model';
 | 
			
		||||
 | 
			
		||||
describe('TemplateModel', () => {
 | 
			
		||||
  it('should create an instance', () => {
 | 
			
		||||
    expect(new TemplateModel()).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										14
									
								
								src/app/models/template.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/app/models/template.model.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										157
									
								
								src/app/sample/sample.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/app/sample/sample.component.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
			
		||||
<h2>{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}</h2>
 | 
			
		||||
 | 
			
		||||
<rb-loading-spinner *ngIf="loading"></rb-loading-spinner>
 | 
			
		||||
 | 
			
		||||
<form #sampleForm="ngForm" *ngIf="!responseData && !loading">
 | 
			
		||||
<!--<form #sampleForm="ngForm">-->
 | 
			
		||||
  <div class="sample">
 | 
			
		||||
    <div>
 | 
			
		||||
      <rb-form-input name="materialname" label="material name" [rbFormInputAutocomplete]="autocomplete.bind(this, materialNames)" [rbDebounceTime]="0" [rbInitialOpen]="true" (keydown)="preventSubmit($event)" (ngModelChange)="findMaterial($event)" appValidate="stringOf" [appValidateArgs]="[materialNames]" required [(ngModel)]="material.name" [autofocus]="true" #materialNameInput="ngModel">
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="failure">Unknown material, add properties for new material</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <button class="rb-btn rb-secondary" type="button" (click)="setNewMaterial(true)"><span class="rb-ic rb-ic-add"></span> New material</button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="material shaded-container" *ngIf="newMaterial">
 | 
			
		||||
      <h4>Material properties</h4>
 | 
			
		||||
      <rb-form-input name="supplier" label="supplier" [rbFormInputAutocomplete]="autocomplete.bind(this, suppliers)" [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required [(ngModel)]="material.supplier" #supplierInput="ngModel">
 | 
			
		||||
        <ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <rb-form-input name="group" label="group" [rbFormInputAutocomplete]="autocomplete.bind(this, groups)" [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required [(ngModel)]="material.group" #groupInput="ngModel">
 | 
			
		||||
        <ng-template rbFormValidationMessage="failure">{{groupInput.errors.failure}}</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <rb-form-input name="mineral" label="mineral" type="number" required rbNumberConverter rbMin="0" rbMax="100" [(ngModel)]="material.mineral" ngModel>
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Invalid value</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="rbMin">Minimum value is 0</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="rbMax">Maximum value is 100</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <rb-form-input name="glass_fiber" label="glass_fiber" type="number" required rbNumberConverter rbMin="0" rbMax="100" [(ngModel)]="material.glass_fiber" ngModel>
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Invalid value</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="rbMin">Minimum value is 0</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="rbMax">Maximum value is 100</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <rb-form-input name="carbon_fiber" label="carbon_fiber" type="number" required rbNumberConverter rbMin="0" rbMax="100" [(ngModel)]="material.carbon_fiber" ngModel>
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Invalid value</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="rbMin">Minimum value is 0</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="rbMax">Maximum value is 100</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
 | 
			
		||||
      <div class="material-numbers">
 | 
			
		||||
        <div *ngFor="let number of material.numbers; index as i" class="two-col">
 | 
			
		||||
          <rb-form-input label="color" appValidate="string" [required]="i < material.numbers.length - 1" (keyup)="handleMaterialNumbers()" [(ngModel)]="number.color" ngModel [ngModelOptions]="{standalone: true}">
 | 
			
		||||
          </rb-form-input>
 | 
			
		||||
          <rb-form-input label="material number" appValidate="string" [(ngModel)]="number.number" ngModel [ngModelOptions]="{standalone: true}">
 | 
			
		||||
          </rb-form-input>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
     
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
      <rb-form-input name="type" label="type" appValidate="string" required [(ngModel)]="sample.type" #typeInput="ngModel">
 | 
			
		||||
        <ng-template rbFormValidationMessage="failure">{{typeInput.errors.failure}}</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <rb-form-input name="color" label="color" [rbFormInputAutocomplete]="autocomplete.bind(this, getColors(material))" [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="stringOf" [appValidateArgs]="[getColors(material)]" required [(ngModel)]="sample.color" #colorInput="ngModel">
 | 
			
		||||
        <ng-template rbFormValidationMessage="failure">{{colorInput.errors.failure}}</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
      <rb-form-input name="batch" label="batch" appValidate="string" [(ngModel)]="sample.batch" #batchInput="ngModel">
 | 
			
		||||
        <ng-template rbFormValidationMessage="failure">{{batchInput.errors.failure}}</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="notes">
 | 
			
		||||
    <rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]" [(ngModel)]="sample.notes.comment" #commentInput="ngModel">
 | 
			
		||||
      <ng-template rbFormValidationMessage="failure">{{commentInput.errors.failure}}</ng-template>
 | 
			
		||||
    </rb-form-input>
 | 
			
		||||
    <h5>Additional properties</h5>
 | 
			
		||||
    <div *ngFor="let field of customFields; index as i" class="two-col">
 | 
			
		||||
      <div>
 | 
			
		||||
        <rb-form-input [name]="'cf-key' + i" label="key" [rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)" [rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(i)]" (ngModelChange)="adjustCustomFields($event, i)" [ngModel]="field[0]" #keyInput="ngModel">
 | 
			
		||||
          <ng-template rbFormValidationMessage="failure">{{keyInput.errors.failure}}</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
      </div>
 | 
			
		||||
      <rb-form-input [name]="'cf-value' + i" label="value" appValidate="string" [required]="field[0] !== ''" [(ngModel)]="field[1]">
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
    </div>
 | 
			
		||||
<!--    TODO: Sample reference-->
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
 | 
			
		||||
  <div class="conditions shaded-container">
 | 
			
		||||
    <h4>
 | 
			
		||||
      Condition
 | 
			
		||||
      <button class="rb-btn rb-secondary condition-set" type="button" (click)="toggleCondition()" [disabled]="!conditionTemplates">{{condition ? 'Do not set condition' : 'Set condition'}}</button>
 | 
			
		||||
    </h4>
 | 
			
		||||
    <div *ngIf="condition">
 | 
			
		||||
      <rb-form-select name="conditionSelect" label="Condition" (ngModelChange)="selectCondition($event)" [ngModel]="condition._id">
 | 
			
		||||
        <option *ngFor="let c of conditionTemplates" [value]="c._id">{{c.name}}</option>
 | 
			
		||||
      </rb-form-select>
 | 
			
		||||
 | 
			
		||||
      <rb-form-input *ngFor="let parameter of condition.parameters; index as i" [name]="'conditionParameter' + i" [label]="parameter.name" appValidate="string" required [(ngModel)]="sample.condition[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
        <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
        <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
      </rb-form-input>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
 | 
			
		||||
  <div class="measurements shaded-container">
 | 
			
		||||
    <h4>Measurements</h4>
 | 
			
		||||
    <div *ngFor="let measurement of sample.measurements; index as mIndex">
 | 
			
		||||
      <rb-form-select name="measurementTemplateSelect" label="Template" [(ngModel)]="measurement.measurement_template">
 | 
			
		||||
        <option *ngFor="let m of measurementTemplates" [value]="m._id">{{m.name}}</option>
 | 
			
		||||
      </rb-form-select>
 | 
			
		||||
 | 
			
		||||
      <div *ngFor="let parameter of getMeasurementTemplate(measurement.measurement_template).parameters; index as pIndex">
 | 
			
		||||
        <rb-form-input *ngIf="!parameter.range.type" [name]="'measurementParameter' + mIndex + '-' + pIndex" [label]="parameter.name" appValidate="string" [(ngModel)]="measurement.values[parameter.name]" #parameterInput="ngModel">
 | 
			
		||||
          <ng-template rbFormValidationMessage="failure">{{parameterInput.errors.failure}}</ng-template>
 | 
			
		||||
          <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
        </rb-form-input>
 | 
			
		||||
        <rb-form-file *ngIf="parameter.range.type" [name]="'measurementParameter' + mIndex + '-' + pIndex" [label]="parameter.name" maxSize="10000000" (change)="fileToArray($event, mIndex, parameter.name)" required ngModel>
 | 
			
		||||
          <ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
 | 
			
		||||
        </rb-form-file>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <button class="rb-btn rb-danger" type="button" (click)="removeMeasurement(mIndex)"><span class="rb-ic rb-ic-delete"></span> Delete measurement</button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
     
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
      <button class="rb-btn rb-secondary" type="button" (click)="addMeasurement()" [disabled]="!measurementTemplates"><span class="rb-ic rb-ic-add"></span> New measurement</button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
 | 
			
		||||
  <div>
 | 
			
		||||
    <button class="rb-btn rb-primary" type="submit" (click)="saveSample()" [disabled]="!sampleForm.form.valid">Save sample</button>
 | 
			
		||||
  </div>
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
<div *ngIf="responseData">
 | 
			
		||||
  <h3>Successfully added sample:</h3>
 | 
			
		||||
  <rb-table id="response-data">
 | 
			
		||||
    <tr><td>Sample number</td><td>{{responseData.number}}</td></tr>
 | 
			
		||||
    <tr><td>Type</td><td>{{responseData.type}}</td></tr>
 | 
			
		||||
    <tr><td>color</td><td>{{responseData.color}}</td></tr>
 | 
			
		||||
    <tr><td>Batch</td><td>{{responseData.batch}}</td></tr>
 | 
			
		||||
    <tr><td>Material</td><td>{{material.name}}</td></tr>
 | 
			
		||||
  </rb-table>
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
 | 
			
		||||
  <div>
 | 
			
		||||
    <a routerLink="/samples">
 | 
			
		||||
      <button class="rb-btn rb-primary" type="button">Return to samples</button>
 | 
			
		||||
    </a>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										17
									
								
								src/app/sample/sample.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/app/sample/sample.component.scss
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/app/sample/sample.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/app/sample/sample.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<SampleComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ SampleComponent ]
 | 
			
		||||
    })
 | 
			
		||||
    .compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    fixture = TestBed.createComponent(SampleComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										341
									
								
								src/app/sample/sample.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								src/app/sample/sample.component.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<MaterialModel[]>('/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<string[]>('/material/suppliers', (data: any) => {
 | 
			
		||||
      this.suppliers = data;
 | 
			
		||||
      this.loading--;
 | 
			
		||||
    });
 | 
			
		||||
    this.api.get<string[]>('/material/groups', (data: any) => {
 | 
			
		||||
      this.groups = data;
 | 
			
		||||
      this.loading--;
 | 
			
		||||
    });
 | 
			
		||||
    this.api.get<TemplateModel[]>('/template/conditions', data => {
 | 
			
		||||
      this.conditionTemplates = data.map(e => new TemplateModel().deserialize(e));
 | 
			
		||||
      this.loading--;
 | 
			
		||||
    });
 | 
			
		||||
    this.api.get<TemplateModel[]>('/template/measurements', data => {
 | 
			
		||||
      this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e));
 | 
			
		||||
      this.loading--;
 | 
			
		||||
    });
 | 
			
		||||
    this.api.get<TemplateModel[]>('/sample/notes/fields', data => {
 | 
			
		||||
      this.availableCustomFields = data.map(e => e.name);
 | 
			
		||||
      this.loading--;
 | 
			
		||||
    });
 | 
			
		||||
    if (!this.new) {
 | 
			
		||||
      this.loading++;
 | 
			
		||||
      this.api.get<SampleModel>('/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<void>(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<MaterialModel>('/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<SampleModel>(resolve => {
 | 
			
		||||
        if (this.new) {
 | 
			
		||||
          this.api.post<SampleModel>('/sample/new', this.sample.sendFormat(), resolve);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          this.api.put<SampleModel>('/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<MeasurementModel>('/measurement/new', measurement.sendFormat());
 | 
			
		||||
            }
 | 
			
		||||
            else {  // update measurement
 | 
			
		||||
              this.api.put<MeasurementModel>('/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^^
 | 
			
		||||
@@ -1,12 +1,22 @@
 | 
			
		||||
<div class="header-addnew">
 | 
			
		||||
  <h2>Samples</h2>
 | 
			
		||||
  <button class="rb-btn rb-primary"><span class="rb-ic rb-ic-add"></span>  New sample</button>
 | 
			
		||||
  <a routerLink="/samples/new">
 | 
			
		||||
    <button class="rb-btn rb-primary"><span class="rb-ic rb-ic-add"></span>  New sample</button>
 | 
			
		||||
  </a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<rb-accordion>
 | 
			
		||||
  <rb-accordion-title><span class="rb-ic rb-ic-filter"></span>  Filter</rb-accordion-title>
 | 
			
		||||
  <rb-accordion-body>
 | 
			
		||||
    Not implemented (yet)
 | 
			
		||||
    <form>
 | 
			
		||||
      <rb-form-select name="statusSelect" label="Status" [(ngModel)]="filters.status">
 | 
			
		||||
        <option value="validated">validated</option>
 | 
			
		||||
        <option value="new">new</option>
 | 
			
		||||
        <option value="all">all</option>
 | 
			
		||||
      </rb-form-select>
 | 
			
		||||
 | 
			
		||||
      <button class="rb-btn rb-secondary" (click)="loadSamples()">Apply filters</button>
 | 
			
		||||
    </form>
 | 
			
		||||
  </rb-accordion-body>
 | 
			
		||||
</rb-accordion>
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +33,7 @@
 | 
			
		||||
    <th>type</th>
 | 
			
		||||
    <th>Color</th>
 | 
			
		||||
    <th>Batch</th>
 | 
			
		||||
    <th></th>
 | 
			
		||||
  </tr>
 | 
			
		||||
 | 
			
		||||
  <tr *ngFor="let sample of samples">
 | 
			
		||||
@@ -37,5 +48,6 @@
 | 
			
		||||
    <td>{{sample.type}}</td>
 | 
			
		||||
    <td>{{sample.color}}</td>
 | 
			
		||||
    <td>{{sample.batch}}</td>
 | 
			
		||||
    <td><a [routerLink]="'/samples/edit/' + sample._id"><span class="rb-ic rb-ic-edit"></span></a></td>
 | 
			
		||||
  </tr>
 | 
			
		||||
</rb-table>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								src/app/services/api.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/app/services/api.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<HttpClient>;
 | 
			
		||||
// let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
 | 
			
		||||
//
 | 
			
		||||
// 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<HttpClient>;
 | 
			
		||||
//     localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
 | 
			
		||||
//   });
 | 
			
		||||
//
 | 
			
		||||
//   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');
 | 
			
		||||
//   });
 | 
			
		||||
// });
 | 
			
		||||
							
								
								
									
										55
									
								
								src/app/services/api.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/app/services/api.service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<T>(url, f: (data?: T, err?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.get(this.host + url, this.authOptions()), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  post<T>(url, data = null, f: (data?: T, err?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.post(this.host + url, data, this.authOptions()), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  put<T>(url, data = null, f: (data?: T, err?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.put(this.host + url, data, this.authOptions()), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  delete<T>(url, f: (data?: T, err?) => void = () => {}) {
 | 
			
		||||
    this.requestErrorHandler<T>(this.http.delete(this.host + url, this.authOptions()), f);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private requestErrorHandler<T>(observable: Observable<any>, 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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								src/app/services/autocomplete.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/app/services/autocomplete.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										20
									
								
								src/app/services/autocomplete.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/app/services/autocomplete.service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								src/app/services/login.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/app/services/login.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<ApiService>;
 | 
			
		||||
// let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
 | 
			
		||||
//
 | 
			
		||||
// 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<ApiService>;
 | 
			
		||||
//     localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
 | 
			
		||||
//   });
 | 
			
		||||
//
 | 
			
		||||
//   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();
 | 
			
		||||
//     });
 | 
			
		||||
//   });
 | 
			
		||||
// });
 | 
			
		||||
@@ -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<boolean> {
 | 
			
		||||
    return new Observable<boolean>(observer => {
 | 
			
		||||
      if (this.loggedIn === undefined) {
 | 
			
		||||
        this.login().then(res => {
 | 
			
		||||
          observer.next(res as any);
 | 
			
		||||
          observer.complete();
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        observer.next(this.loggedIn);
 | 
			
		||||
        observer.complete();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								src/app/services/validation.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/app/services/validation.service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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: ''};
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/app/validate.directive.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/app/validate.directive.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
// import { ValidateDirective } from './validate.directive';
 | 
			
		||||
//
 | 
			
		||||
// describe('ValidateDirective', () => {
 | 
			
		||||
//   it('should create an instance', () => {
 | 
			
		||||
//     const directive = new ValidateDirective();
 | 
			
		||||
//     expect(directive).toBeTruthy();
 | 
			
		||||
//   });
 | 
			
		||||
// });
 | 
			
		||||
							
								
								
									
										28
									
								
								src/app/validate.directive.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/app/validate.directive.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<any>;
 | 
			
		||||
 | 
			
		||||
  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 };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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: ''};
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user