Merge pull request #3 in ~VLE2FE/dfop-ui from development to master
* commit '15aeeb27ee2ed18aac9d6d944d3d08a6c37e5db2': implemented sample references added documentation and img-magnifier basic account management (Login/Logout) finished filters sorting implemented tests done except for sample and samples component first version of sample.component finished rb-table, samples table finished tests update to 8 before update init
This commit is contained in:
commit
23edb2d478
@ -22,11 +22,12 @@
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": false,
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{ "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" }
|
||||
{ "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" },
|
||||
"src/Staticfile"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
@ -46,7 +47,6 @@
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
@ -68,7 +68,8 @@
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "UI:build"
|
||||
"browserTarget": "UI:build",
|
||||
"proxyConfig": "src/proxy.conf.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
9
manifest.yml
Normal file
9
manifest.yml
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
applications:
|
||||
- name: definma
|
||||
path: dist/UI
|
||||
buildpacks:
|
||||
- staticfile_buildpack
|
||||
memory: 128M
|
||||
instances: 1
|
||||
stack: cflinuxfs3
|
7083
package-lock.json
generated
7083
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@ -4,36 +4,46 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build": "ng build --prod --aot",
|
||||
"build-push": "ng build --prod --aot && cf push",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"e2e": "ng e2e",
|
||||
"coverage": "ng test --no-watch --code-coverage",
|
||||
"api": "cd C:\\Users\\vle2fe\\Documents\\Code\\API && node dist\\index.js"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~8.2.14",
|
||||
"@angular/common": "~8.2.14",
|
||||
"@angular/compiler": "~8.2.14",
|
||||
"@angular/core": "~8.2.14",
|
||||
"@angular/forms": "~8.2.14",
|
||||
"@angular/platform-browser": "~8.2.14",
|
||||
"@angular/platform-browser-dynamic": "~8.2.14",
|
||||
"@angular/router": "~8.2.14",
|
||||
"@inst-iot/bosch-angular-ui-components": "^0.5.30",
|
||||
"@angular/animations": "~9.1.7",
|
||||
"@angular/common": "~9.1.7",
|
||||
"@angular/compiler": "~9.1.7",
|
||||
"@angular/core": "~9.1.7",
|
||||
"@angular/forms": "~9.1.7",
|
||||
"@angular/platform-browser": "~9.1.7",
|
||||
"@angular/platform-browser-dynamic": "~9.1.7",
|
||||
"@angular/router": "~9.1.7",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@inst-iot/bosch-angular-ui-components": "file:../Bosch-UI-Components/bosch-angular-ui-components/dist-lib/inst-iot-bosch-angular-ui-components-0.6.0.tgz",
|
||||
"angular-2-local-storage": "^3.0.2",
|
||||
"chart.js": "^2.9.3",
|
||||
"chartjs-plugin-datalabels": "^0.7.0",
|
||||
"flatpickr": "^4.6.3",
|
||||
"rxjs": "~6.4.0",
|
||||
"lodash": "^4.17.15",
|
||||
"ng2-charts": "^2.3.2",
|
||||
"quick-score": "0.0.8",
|
||||
"rxjs": "~6.5.5",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.9.1"
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.803.22",
|
||||
"@angular/cli": "~8.3.22",
|
||||
"@angular/compiler-cli": "~8.2.14",
|
||||
"@angular/language-service": "~8.2.14",
|
||||
"@types/node": "~8.9.4",
|
||||
"@angular-devkit/build-angular": "~0.901.6",
|
||||
"@angular/cli": "~9.1.6",
|
||||
"@angular/compiler-cli": "~9.1.7",
|
||||
"@angular/language-service": "~9.1.7",
|
||||
"@types/jasmine": "~3.3.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"codelyzer": "^5.0.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^5.1.2",
|
||||
"jasmine-core": "~3.4.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~4.1.0",
|
||||
@ -44,6 +54,6 @@
|
||||
"protractor": "~5.4.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.15.0",
|
||||
"typescript": "~3.5.3"
|
||||
"typescript": "~3.8.3"
|
||||
}
|
||||
}
|
||||
|
4
src/Staticfile
Normal file
4
src/Staticfile
Normal file
@ -0,0 +1,4 @@
|
||||
pushstate: enabled
|
||||
force_https: true
|
||||
root: UI
|
||||
location_include: custom-header.conf
|
@ -1,8 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import {HomeComponent} from './home/home.component';
|
||||
import {LoginService} from './services/login.service';
|
||||
import {SampleComponent} from './sample/sample.component';
|
||||
import {SamplesComponent} from './samples/samples.component';
|
||||
import {DocumentationComponent} from './documentation/documentation.component';
|
||||
|
||||
|
||||
const routes: Routes = [];
|
||||
const routes: Routes = [
|
||||
{path: '', component: HomeComponent},
|
||||
{path: 'home', component: HomeComponent},
|
||||
{path: 'samples', component: SamplesComponent, canActivate: [LoginService]},
|
||||
{path: 'samples/new', component: SampleComponent, canActivate: [LoginService]},
|
||||
{path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]},
|
||||
{path: 'documentation', component: DocumentationComponent},
|
||||
|
||||
// if not authenticated
|
||||
{ path: '**', redirectTo: '' }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
|
@ -1,6 +1,29 @@
|
||||
<rb-full-header>
|
||||
<nav *rbMainNavItems>
|
||||
<a routerLink="/" routerLinkActive="active" rbLoadingLink>Home</a>
|
||||
<a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a>
|
||||
<a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="loginService.isLoggedIn">Samples</a>
|
||||
<a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a>
|
||||
</nav>
|
||||
<div *rbSubBrandHeader>Digital Fingerprint of Plastics</div>
|
||||
|
||||
<ng-container *ngIf="loginService.isLoggedIn">
|
||||
<nav *rbActionNavItems>
|
||||
<a href="javascript:" [rbPopover]="userPopover" [anchor]="popoverAnchor">
|
||||
{{loginService.username}} <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>
|
||||
|
||||
<button type="button" class="rb-btn rb-primary" (click)="logout()">Logout</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<div *rbSubBrandHeader><span class="dev-label" *ngIf="devMode">DEVELOPMENT</span>Digital Fingerprint of Plastics</div>
|
||||
</rb-full-header>
|
||||
|
||||
<div class="container">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
@ -0,0 +1,5 @@
|
||||
.dev-label {
|
||||
color: #F00;
|
||||
font-size: 32px;
|
||||
margin-right: 40px;
|
||||
}
|
@ -1,30 +1,54 @@
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {LoginService} from './services/login.service';
|
||||
|
||||
let loginServiceSpy: jasmine.SpyObj<LoginService>;
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
let css; // get native element by css selector
|
||||
|
||||
beforeEach(async(() => {
|
||||
const loginSpy = jasmine.createSpyObj('LoginService', ['login', 'canActivate']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AppComponent ],
|
||||
imports: [
|
||||
RbUiComponentsModule,
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
providers: [
|
||||
{provide: LoginService, useValue: loginSpy}
|
||||
]
|
||||
}).compileComponents();
|
||||
loginServiceSpy = TestBed.inject(LoginService) as jasmine.SpyObj<LoginService>;
|
||||
}));
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
css = (selector) => fixture.debugElement.query(By.css(selector)).nativeElement;
|
||||
});
|
||||
|
||||
it(`should have the correct title'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('UI');
|
||||
it('should create the app', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have the header', () => {
|
||||
expect(css('rb-full-header')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have the correct app title', () => {
|
||||
expect(css('rb-full-header div.sub-brand-content > div').innerText).toBe('Digital Fingerprint of Plastics');
|
||||
});
|
||||
|
||||
it('should have the page container', () => {
|
||||
expect(css('.container')).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,4 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, isDevMode} from '@angular/core';
|
||||
import {LoginService} from './services/login.service';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
// TODO: add multiple samples at once
|
||||
// TODO: guess properties from material name
|
||||
// TODO: validation: VZ, Humidity: min/max value, DPT: filename
|
||||
// TODO: filter by not completely filled/no measurements
|
||||
// TODO: account
|
||||
// TODO: admin user handling, template pages, validation of samples
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -6,5 +15,19 @@ import { Component } from '@angular/core';
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'UI';
|
||||
|
||||
constructor(
|
||||
public loginService: LoginService,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
get devMode() {
|
||||
return isDevMode();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.loginService.logout();
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,62 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ChartsModule } from 'ng2-charts';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
|
||||
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, 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';
|
||||
import { ObjectPipe } from './object.pipe';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import { DocumentationComponent } from './documentation/documentation.component';
|
||||
import { ImgMagnifierComponent } from './img-magnifier/img-magnifier.component';
|
||||
import { ExistsPipe } from './exists.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
AppComponent,
|
||||
LoginComponent,
|
||||
HomeComponent,
|
||||
SamplesComponent,
|
||||
SampleComponent,
|
||||
ValidateDirective,
|
||||
ErrorComponent,
|
||||
ObjectPipe,
|
||||
DocumentationComponent,
|
||||
ImgMagnifierComponent,
|
||||
ExistsPipe
|
||||
],
|
||||
imports: [
|
||||
LocalStorageModule.forRoot({
|
||||
prefix: 'dfop',
|
||||
storageType: 'localStorage'
|
||||
}),
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
RbUiComponentsModule
|
||||
RbUiComponentsModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
RbTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldsModule,
|
||||
CommonModule,
|
||||
ChartsModule
|
||||
],
|
||||
providers: [
|
||||
ModalService,
|
||||
{ provide: Window, useValue: window }
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
9
src/app/documentation/documentation.component.html
Normal file
9
src/app/documentation/documentation.component.html
Normal file
@ -0,0 +1,9 @@
|
||||
<h2>Samples</h2>
|
||||
|
||||
<p>
|
||||
Find the API documentation here: <a href="https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/">https://definma-api.apps.de1.bosch-iot-cloud.com/api-doc/</a>
|
||||
</p>
|
||||
|
||||
<h4>Database model</h4>
|
||||
|
||||
<app-img-magnifier src="assets/imgs/db_structure_latest.svg" zoom="2" [magnifierSize]="{width: 400, height: 300}" id="db-structure"></app-img-magnifier>
|
7
src/app/documentation/documentation.component.scss
Normal file
7
src/app/documentation/documentation.component.scss
Normal file
@ -0,0 +1,7 @@
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
img#db-structure {
|
||||
width: 100%;
|
||||
}
|
25
src/app/documentation/documentation.component.spec.ts
Normal file
25
src/app/documentation/documentation.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DocumentationComponent } from './documentation.component';
|
||||
|
||||
describe('DocumentationComponent', () => {
|
||||
let component: DocumentationComponent;
|
||||
let fixture: ComponentFixture<DocumentationComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DocumentationComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DocumentationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
15
src/app/documentation/documentation.component.ts
Normal file
15
src/app/documentation/documentation.component.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-documentation',
|
||||
templateUrl: './documentation.component.html',
|
||||
styleUrls: ['./documentation.component.scss']
|
||||
})
|
||||
export class DocumentationComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
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 {
|
||||
}
|
||||
|
||||
}
|
8
src/app/exists.pipe.spec.ts
Normal file
8
src/app/exists.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ExistsPipe } from './exists.pipe';
|
||||
|
||||
describe('ExistsPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new ExistsPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
14
src/app/exists.pipe.ts
Normal file
14
src/app/exists.pipe.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'exists',
|
||||
pure: true
|
||||
})
|
||||
export class ExistsPipe implements PipeTransform {
|
||||
|
||||
transform(value: unknown, key?): unknown {
|
||||
// console.log(new Date().getTime());
|
||||
return value || value === 0 ? (key ? value[key] : value) : '';
|
||||
}
|
||||
|
||||
}
|
1
src/app/home/home.component.html
Normal file
1
src/app/home/home.component.html
Normal file
@ -0,0 +1 @@
|
||||
<app-login></app-login>
|
0
src/app/home/home.component.scss
Normal file
0
src/app/home/home.component.scss
Normal file
36
src/app/home/home.component.spec.ts
Normal file
36
src/app/home/home.component.spec.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HomeComponent } from './home.component';
|
||||
import {Component} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
|
||||
@Component({selector: 'app-login', template: ''})
|
||||
class LoginStubComponent {}
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
HomeComponent,
|
||||
LoginStubComponent
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should load the login component', () => {
|
||||
expect(fixture.debugElement.query(By.css('app-login'))).toBeTruthy();
|
||||
});
|
||||
});
|
15
src/app/home/home.component.ts
Normal file
15
src/app/home/home.component.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss']
|
||||
})
|
||||
export class HomeComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
17
src/app/img-magnifier/img-magnifier.component.html
Normal file
17
src/app/img-magnifier/img-magnifier.component.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="img-container">
|
||||
<div class="magnifier"
|
||||
[ngStyle]="{
|
||||
'background-size': backgroundSize,
|
||||
'background-image': 'url(\'' + src + '\')',
|
||||
'background-position': '-' + magnifierPos.x * zoom + 'px -' + magnifierPos.y * zoom + 'px',
|
||||
left: magnifierPos.x + 'px',
|
||||
top: magnifierPos.y + 'px',
|
||||
width: magnifierSize.width + 'px',
|
||||
height: magnifierSize.height + 'px'
|
||||
}"
|
||||
(mousemove)="calcPos($event)"
|
||||
(mouseleave)="showMagnifier = false"
|
||||
*ngIf="showMagnifier">
|
||||
</div>
|
||||
<img [src]="src" alt="" (mousemove)="calcPos($event)" (mouseenter)="showMagnifier = true" #mainImg>
|
||||
</div>
|
20
src/app/img-magnifier/img-magnifier.component.scss
Normal file
20
src/app/img-magnifier/img-magnifier.component.scss
Normal file
@ -0,0 +1,20 @@
|
||||
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
|
||||
|
||||
.img-container {
|
||||
position:relative;
|
||||
overflow: hidden;
|
||||
|
||||
& > img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.magnifier {
|
||||
position: absolute;
|
||||
background: #FFF;
|
||||
background-repeat: no-repeat;
|
||||
background-position: -500px -500px;
|
||||
z-index: 99;
|
||||
border: 1px solid #FFF;
|
||||
box-shadow: 10px 10px 25px $color-bosch-light-gray-b25;
|
||||
}
|
25
src/app/img-magnifier/img-magnifier.component.spec.ts
Normal file
25
src/app/img-magnifier/img-magnifier.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ImgMagnifierComponent } from './img-magnifier.component';
|
||||
|
||||
describe('ImgMagnifierComponent', () => {
|
||||
let component: ImgMagnifierComponent;
|
||||
let fixture: ComponentFixture<ImgMagnifierComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ImgMagnifierComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImgMagnifierComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
41
src/app/img-magnifier/img-magnifier.component.ts
Normal file
41
src/app/img-magnifier/img-magnifier.component.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-img-magnifier',
|
||||
templateUrl: './img-magnifier.component.html',
|
||||
styleUrls: ['./img-magnifier.component.scss']
|
||||
})
|
||||
export class ImgMagnifierComponent implements OnInit, AfterViewInit {
|
||||
|
||||
@Input('src') src: string;
|
||||
@Input('zoom') zoom: number;
|
||||
@Input('magnifierSize') magnifierSize: {width: number, height: number};
|
||||
@ViewChild('mainImg') mainImg: ElementRef;
|
||||
|
||||
backgroundSize;
|
||||
magnifierPos = {x: 0, y: 0};
|
||||
showMagnifier = false;
|
||||
|
||||
constructor(
|
||||
private window: Window
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.calcBackgroundSize();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
calcPos(event) {
|
||||
const img = this.mainImg.nativeElement.getBoundingClientRect();
|
||||
this.magnifierPos.x = Math.min(img.width - this.magnifierSize.width, Math.max(0, event.pageX - img.left - this.window.pageXOffset - this.magnifierSize.width / 2));
|
||||
this.magnifierPos.y = Math.min(img.height - this.magnifierSize.height + 7, Math.max(0, event.pageY - img.top - this.window.pageYOffset - this.magnifierSize.height / 2));
|
||||
}
|
||||
|
||||
calcBackgroundSize() {
|
||||
this.backgroundSize = this.mainImg ? (this.mainImg.nativeElement.width * this.zoom - this.magnifierSize.width) + 'px ' + (this.mainImg.nativeElement.height * this.zoom - this.magnifierSize.height) + 'px ' : '0 0';
|
||||
}
|
||||
}
|
15
src/app/login/login.component.html
Normal file
15
src/app/login/login.component.html
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="login-wrapper">
|
||||
<h2>Please log in</h2>
|
||||
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
12
src/app/login/login.component.scss
Normal file
12
src/app/login/login.component.scss
Normal file
@ -0,0 +1,12 @@
|
||||
.login-wrapper {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 13px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
margin-right: 10px;
|
||||
}
|
129
src/app/login/login.component.spec.ts
Normal file
129
src/app/login/login.component.spec.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import { LoginComponent } from './login.component';
|
||||
import {LoginService} from '../services/login.service';
|
||||
import {ValidationService} from '../services/validation.service';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {ValidateDirective} from '../validate.directive';
|
||||
import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
|
||||
|
||||
let validationServiceSpy: jasmine.SpyObj<ValidationService>;
|
||||
let loginServiceSpy: jasmine.SpyObj<LoginService>;
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
let css; // get native element by css selector
|
||||
let cssd; // get debug element by css selector
|
||||
|
||||
beforeEach(async(() => {
|
||||
const validationSpy = jasmine.createSpyObj('ValidationService', ['username', 'password']);
|
||||
const loginSpy = jasmine.createSpyObj('LoginService', ['login']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent, ValidateDirective ],
|
||||
imports: [
|
||||
RbUiComponentsModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [
|
||||
{provide: ValidationService, useValue: validationSpy},
|
||||
{provide: LoginService, useValue: loginSpy}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
validationServiceSpy = TestBed.inject(ValidationService) as jasmine.SpyObj<ValidationService>;
|
||||
loginServiceSpy = TestBed.inject(LoginService) as jasmine.SpyObj<LoginService>;
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
cssd = (selector) => fixture.debugElement.query(By.css(selector));
|
||||
css = (selector) => fixture.debugElement.query(By.css(selector)).nativeElement;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
validationServiceSpy.username.and.returnValue({ok: true, error: ''});
|
||||
validationServiceSpy.password.and.returnValue({ok: true, error: ''});
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have the `Please log in` heading', () => {
|
||||
expect(css('h2').innerText).toBe('Please log in');
|
||||
});
|
||||
|
||||
it('should have empty credential inputs', () => {
|
||||
expect(css('rb-form-input[label=username] input').value).toBe('');
|
||||
expect(css('rb-form-input[label=password] input').value).toBe('');
|
||||
});
|
||||
|
||||
it('should have an empty message at the beginning', () => {
|
||||
expect(css('.message').innerText).toBe('');
|
||||
});
|
||||
|
||||
it('should have a login button', async () => {
|
||||
validationServiceSpy.username.and.returnValue({ok: true, error: ''});
|
||||
validationServiceSpy.password.and.returnValue({ok: true, error: ''});
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
expect(css('.login-button')).toBeTruthy();
|
||||
expect(css('.login-button').disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should reject a wrong username', async () => {
|
||||
validationServiceSpy.username.and.returnValue({ok: false, error: 'username must only contain a-z0-9-_.'});
|
||||
component.username = 'ab#';
|
||||
fixture.detectChanges();
|
||||
await fixture.whenRenderingDone();
|
||||
expect(component.loginForm.controls.username.valid).toBeFalsy();
|
||||
expect(validationServiceSpy.username).toHaveBeenCalledWith('ab#');
|
||||
});
|
||||
|
||||
it('should reject a wrong password', async () => {
|
||||
validationServiceSpy.password.and.returnValue({ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'});
|
||||
component.password = 'abc';
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenRenderingDone();
|
||||
expect(component.loginForm.controls.password.valid).toBeFalsy();
|
||||
expect(validationServiceSpy.password).toHaveBeenCalledWith('abc');
|
||||
});
|
||||
|
||||
it('should enable the login button with valid credentials', async () => {
|
||||
validationServiceSpy.username.and.returnValue({ok: true, error: ''});
|
||||
validationServiceSpy.password.and.returnValue({ok: true, error: ''});
|
||||
loginServiceSpy.login.and.returnValue(new Promise(r => r(true)));
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenRenderingDone();
|
||||
|
||||
cssd('.login-button').triggerEventHandler('click', null);
|
||||
expect(css('.login-button').disabled).toBeFalsy();
|
||||
expect(loginServiceSpy.login.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('should call the LoginService with valid credentials', () => {
|
||||
validationServiceSpy.username.and.returnValue({ok: true, error: ''});
|
||||
validationServiceSpy.password.and.returnValue({ok: true, error: ''});
|
||||
loginServiceSpy.login.and.returnValue(new Promise(r => r(true)));
|
||||
|
||||
cssd('.login-button').triggerEventHandler('click', null);
|
||||
expect(loginServiceSpy.login.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('should display an error if the LoginService could not authenticate', fakeAsync(() => {
|
||||
validationServiceSpy.username.and.returnValue({ok: true, error: ''});
|
||||
validationServiceSpy.password.and.returnValue({ok: true, error: ''});
|
||||
loginServiceSpy.login.and.returnValue(new Promise(r => r(false)));
|
||||
|
||||
cssd('.login-button').triggerEventHandler('click', null);
|
||||
expect(loginServiceSpy.login.calls.count()).toBe(1);
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
expect(css('.message').innerText).toBe('Wrong credentials!');
|
||||
}));
|
||||
});
|
40
src/app/login/login.component.ts
Normal file
40
src/app/login/login.component.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {ValidationService} from '../services/validation.service';
|
||||
import {LoginService} from '../services/login.service';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
|
||||
username = ''; // credentials
|
||||
password = '';
|
||||
message = ''; // message below login fields
|
||||
@ViewChild('loginForm') loginForm;
|
||||
|
||||
|
||||
constructor(
|
||||
private validate: ValidationService,
|
||||
private loginService: LoginService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
login() {
|
||||
this.loginService.login(this.username, this.password).then(ok => {
|
||||
if (ok) {
|
||||
this.message = 'Login successful';
|
||||
this.router.navigate(['/samples']);
|
||||
}
|
||||
else {
|
||||
this.message = 'Wrong credentials!';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
7
src/app/models/base.model.spec.ts
Normal file
7
src/app/models/base.model.spec.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { BaseModel } from './base.model';
|
||||
|
||||
describe('BaseModel', () => {
|
||||
it('should create an instance', () => {
|
||||
expect(new BaseModel()).toBeTruthy();
|
||||
});
|
||||
});
|
10
src/app/models/base.model.ts
Normal file
10
src/app/models/base.model.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export class BaseModel {
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
|
||||
sendFormat(): this {
|
||||
return this;
|
||||
}
|
||||
}
|
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();
|
||||
});
|
||||
});
|
7
src/app/models/custom-fields.model.ts
Normal file
7
src/app/models/custom-fields.model.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {BaseModel} from './base.model';
|
||||
|
||||
|
||||
export class CustomFieldsModel extends BaseModel {
|
||||
name = '';
|
||||
qty = 0;
|
||||
}
|
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();
|
||||
});
|
||||
});
|
16
src/app/models/material.model.ts
Normal file
16
src/app/models/material.model.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import _ from 'lodash';
|
||||
import {IdModel} from './id.model';
|
||||
import {BaseModel} from './base.model';
|
||||
|
||||
export class MaterialModel extends BaseModel {
|
||||
_id: IdModel = null;
|
||||
name = '';
|
||||
supplier = '';
|
||||
group = '';
|
||||
properties: {material_template: string, [prop: string]: string} = {material_template: null};
|
||||
numbers: string[] = [''];
|
||||
|
||||
sendFormat() {
|
||||
return _.pick(this, ['name', 'supplier', 'group', 'numbers', 'properties']);
|
||||
}
|
||||
}
|
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 {BaseModel} from './base.model';
|
||||
|
||||
export class MeasurementModel extends BaseModel {
|
||||
_id: IdModel = null;
|
||||
sample_id: IdModel = null;
|
||||
measurement_template: IdModel;
|
||||
values: {[prop: string]: any} = {};
|
||||
|
||||
constructor(measurementTemplate: IdModel = null) {
|
||||
super();
|
||||
this.measurement_template = measurementTemplate;
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
36
src/app/models/sample.model.ts
Normal file
36
src/app/models/sample.model.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import _ from 'lodash';
|
||||
import {IdModel} from './id.model';
|
||||
import {MaterialModel} from './material.model';
|
||||
import {MeasurementModel} from './measurement.model';
|
||||
import {BaseModel} from './base.model';
|
||||
|
||||
export class SampleModel extends BaseModel {
|
||||
_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']);
|
||||
}
|
||||
}
|
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();
|
||||
});
|
||||
});
|
9
src/app/models/template.model.ts
Normal file
9
src/app/models/template.model.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {IdModel} from './id.model';
|
||||
import {BaseModel} from './base.model';
|
||||
|
||||
export class TemplateModel extends BaseModel {
|
||||
_id: IdModel = null;
|
||||
name = '';
|
||||
version = 1;
|
||||
parameters: {name: string, range: {[prop: string]: any}}[] = [];
|
||||
}
|
8
src/app/object.pipe.spec.ts
Normal file
8
src/app/object.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ObjectPipe } from './object.pipe';
|
||||
|
||||
describe('ObjectPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new ObjectPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
13
src/app/object.pipe.ts
Normal file
13
src/app/object.pipe.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'object',
|
||||
pure: true
|
||||
})
|
||||
export class ObjectPipe implements PipeTransform {
|
||||
|
||||
transform(value: object): string {
|
||||
return value ? JSON.stringify(value) : '';
|
||||
}
|
||||
|
||||
}
|
18
src/app/rb-table/rb-table.module.ts
Normal file
18
src/app/rb-table/rb-table.module.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RbTableComponent } from './rb-table/rb-table.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
RbTableComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule
|
||||
],
|
||||
exports: [
|
||||
RbTableComponent
|
||||
]
|
||||
})
|
||||
export class RbTableModule { }
|
6
src/app/rb-table/rb-table/rb-table.component.html
Normal file
6
src/app/rb-table/rb-table/rb-table.component.html
Normal file
@ -0,0 +1,6 @@
|
||||
<script src="rb-table.component.ts"></script>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<ng-content></ng-content>
|
||||
</table>
|
||||
</div>
|
35
src/app/rb-table/rb-table/rb-table.component.scss
Normal file
35
src/app/rb-table/rb-table/rb-table.component.scss
Normal file
@ -0,0 +1,35 @@
|
||||
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
|
||||
&, & > table { // scrollbar at the top
|
||||
transform:rotateX(180deg);
|
||||
-ms-transform:rotateX(180deg); /* IE 9 */
|
||||
-webkit-transform:rotateX(180deg); /* Safari and Chrome */
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
::ng-deep tr {
|
||||
border-bottom: 1px solid $color-gray-mercury;
|
||||
|
||||
::ng-deep td, ::ng-deep th {
|
||||
padding: 8px 5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
::ng-deep th {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
25
src/app/rb-table/rb-table/rb-table.component.spec.ts
Normal file
25
src/app/rb-table/rb-table/rb-table.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RbTableComponent } from './rb-table.component';
|
||||
|
||||
describe('RbTableComponent', () => {
|
||||
let component: RbTableComponent;
|
||||
let fixture: ComponentFixture<RbTableComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RbTableComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RbTableComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
15
src/app/rb-table/rb-table/rb-table.component.ts
Normal file
15
src/app/rb-table/rb-table/rb-table.component.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'rb-table',
|
||||
templateUrl: './rb-table.component.html',
|
||||
styleUrls: ['./rb-table.component.scss']
|
||||
})
|
||||
export class RbTableComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
159
src/app/sample/sample.component.html
Normal file
159
src/app/sample/sample.component.html
Normal file
@ -0,0 +1,159 @@
|
||||
<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)="preventDefault($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(!newMaterial)"><span class="rb-ic rb-ic-add"></span> New material</button>
|
||||
</div>
|
||||
|
||||
<div class="material shaded-container" *ngIf="newMaterial" [@inOut]>
|
||||
<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>
|
||||
<div class="material-numbers">
|
||||
<rb-form-input *ngFor="let ignore of [].constructor(material.numbers.length); index as i" label="material number" appValidate="string" [name]="'material.number-' + i" (keyup)="handleMaterialNumbers()" [(ngModel)]="material.numbers[i]" ngModel></rb-form-input>
|
||||
</div>
|
||||
<rb-form-select name="conditionSelect" label="Condition" (ngModelChange)="selectMaterialTemplate($event)" [ngModel]="material.properties.material_template">
|
||||
<option *ngFor="let m of materialTemplates" [value]="m._id">{{m.name}}</option>
|
||||
</rb-form-select>
|
||||
<rb-form-input *ngFor="let parameter of materialTemplate.parameters; index as i" [name]="'materialParameter' + i" [label]="parameter.name" appValidate="string" required [(ngModel)]="material.properties[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>
|
||||
<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" appValidate="string" 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>Sample references</h5>
|
||||
<div *ngFor="let reference of sampleReferences; index as i" class="two-col" [@inOut]>
|
||||
<div>
|
||||
<rb-form-input [name]="'sr-id' + i" label="sample number" [rbFormInputAutocomplete]="sampleReferenceListBind()" [rbDebounceTime]="300" appValidate="stringOf" [appValidateArgs]="[sampleReferenceAutocomplete[i]]" (ngModelChange)="checkSampleReference($event, i)" [ngModel]="reference[0]" #idInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">Unknown sample number</ng-template>
|
||||
</rb-form-input>
|
||||
</div>
|
||||
<rb-form-input [name]="'sr-relation' + i" label="relation" appValidate="string" [required]="reference[0] !== ''" [(ngModel)]="reference[1]">
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
</div>
|
||||
<h5>Additional properties</h5>
|
||||
<div *ngFor="let field of customFields; index as i" class="two-col" [@inOut]>
|
||||
<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>
|
||||
</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" [@inOut]>
|
||||
<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" [@inOut]>
|
||||
<rb-form-select name="measurementTemplateSelect" label="Template" [(ngModel)]="measurement.measurement_template" (ngModelChange)="clearChart(mIndex)">
|
||||
<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" (ngModelChange)="fileToArray($event, mIndex, parameter.name)" placeholder="Select file or drag and drop" dragDrop required ngModel>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-file>
|
||||
<canvas baseChart *ngIf="parameter.range.type && charts[mIndex][0].data.length > 0" class="dpt-chart" [@inOut]
|
||||
[datasets]="charts[mIndex]"
|
||||
[labels]="[]"
|
||||
[options]="chartOptions"
|
||||
[legend]="false"
|
||||
chartType="scatter">
|
||||
</canvas>
|
||||
</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>
|
21
src/app/sample/sample.component.scss
Normal file
21
src/app/sample/sample.component.scss
Normal file
@ -0,0 +1,21 @@
|
||||
::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;
|
||||
}
|
||||
|
||||
.dpt-chart {
|
||||
max-width: 400px;
|
||||
}
|
27
src/app/sample/sample.component.spec.ts
Normal file
27
src/app/sample/sample.component.spec.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
//
|
||||
// import { SampleComponent } from './sample.component';
|
||||
//
|
||||
// // TODO
|
||||
//
|
||||
// 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();
|
||||
// });
|
||||
// });
|
494
src/app/sample/sample.component.ts
Normal file
494
src/app/sample/sample.component.ts
Normal file
@ -0,0 +1,494 @@
|
||||
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';
|
||||
import { ChartOptions } from 'chart.js';
|
||||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
// TODO: tests
|
||||
// TODO: confirmation for new group/supplier
|
||||
// TODO: work on better recognition for file input
|
||||
// TODO: only show condition (if not set) and measurements in edit sample dialog at first
|
||||
// TODO: multiple spectra
|
||||
// TODO: multiple samples for base data, extend multiple measurements, conditions
|
||||
|
||||
// TODO: material properties, color (in material and sample (not required))
|
||||
|
||||
// TODO: API $in Regex
|
||||
|
||||
@Component({
|
||||
selector: 'app-sample',
|
||||
templateUrl: './sample.component.html',
|
||||
styleUrls: ['./sample.component.scss'],
|
||||
animations: [
|
||||
trigger(
|
||||
'inOut', [
|
||||
transition(':enter', [
|
||||
style({height: 0, opacity: 0}),
|
||||
animate('0.5s ease-out', style({height: '*', opacity: 1}))
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({height: '*', opacity: 1}),
|
||||
animate('0.5s ease-in', style({height: 0, opacity: 0}))
|
||||
])
|
||||
]
|
||||
)
|
||||
]
|
||||
})
|
||||
export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
@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
|
||||
materialTemplates: TemplateModel[]; // all material templates
|
||||
materialTemplate: TemplateModel | null = null; // selected material template
|
||||
materialNames = []; // names of all materials
|
||||
material = new MaterialModel(); // object of current selected material
|
||||
sample = new SampleModel();
|
||||
customFields: [string, string][] = [['', '']];
|
||||
sampleReferences: [string, string, string][] = [['', '', '']];
|
||||
sampleReferenceFinds: {_id: string, number: string}[] = []; // raw sample reference data from db
|
||||
currentSRIndex = 0; // index of last entered sample reference
|
||||
availableCustomFields: string[] = [];
|
||||
sampleReferenceAutocomplete: string[][] = [[]];
|
||||
responseData: SampleModel; // gets filled with response data after saving the sample
|
||||
measurementTemplates: TemplateModel[];
|
||||
loading = 0; // number of currently loading instances
|
||||
checkFormAfterInit = false;
|
||||
charts = []; // chart data for spectrums
|
||||
readonly chartInit = [{
|
||||
data: [],
|
||||
label: 'Spectrum',
|
||||
showLine: true,
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
borderColor: '#00a8b0',
|
||||
borderWidth: 2
|
||||
}];
|
||||
readonly chartOptions: ChartOptions = {
|
||||
scales: {
|
||||
xAxes: [{ticks: {min: 400, max: 4000, stepSize: 400, reverse: true}}],
|
||||
yAxes: [{ticks: {min: 0, max: 1}}]
|
||||
},
|
||||
responsive: true,
|
||||
tooltips: {enabled: false},
|
||||
hover: {mode: null},
|
||||
maintainAspectRatio: true,
|
||||
plugins: {datalabels: {display: false}}
|
||||
};
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private api: ApiService,
|
||||
private validation: ValidationService,
|
||||
public autocomplete: AutocompleteService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.new = this.router.url === '/samples/new';
|
||||
this.loading = 7;
|
||||
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/materials', data => {
|
||||
this.materialTemplates = data.map(e => new TemplateModel().deserialize(e));
|
||||
this.selectMaterialTemplate(this.materialTemplates[0]._id);
|
||||
this.loading--;
|
||||
});
|
||||
this.api.get<TemplateModel[]>('/template/measurements', data => {
|
||||
this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e));
|
||||
if (!this.new) {
|
||||
this.loading++;
|
||||
this.api.get<SampleModel>('/sample/' + this.route.snapshot.paramMap.get('id'), sData => {
|
||||
this.sample.deserialize(sData);
|
||||
this.charts = [];
|
||||
const spectrumTemplate = this.measurementTemplates.find(e => e.name === 'spectrum')._id;
|
||||
let spectrumCounter = 0;
|
||||
this.sample.measurements.forEach((measurement, i) => {
|
||||
this.charts.push(_.cloneDeep(this.chartInit));
|
||||
if (measurement.measurement_template === spectrumTemplate) {
|
||||
setTimeout(() => {
|
||||
this.generateChart(measurement.values.dpt, i);
|
||||
console.log(this.charts);
|
||||
}, spectrumCounter * 20);
|
||||
spectrumCounter ++;
|
||||
}
|
||||
});
|
||||
this.material = sData.material;
|
||||
this.customFields = this.sample.notes.custom_fields && this.sample.notes.custom_fields !== {} ? Object.keys(this.sample.notes.custom_fields).map(e => [e, this.sample.notes.custom_fields[e]]) : [['', '']];
|
||||
if (this.sample.notes.sample_references.length) {
|
||||
this.sampleReferences = [];
|
||||
this.sampleReferenceAutocomplete = [];
|
||||
let loadCounter = this.sample.notes.sample_references.length;
|
||||
this.sample.notes.sample_references.forEach(reference => {
|
||||
this.api.get<SampleModel>('/sample/' + reference.sample_id, srData => {
|
||||
this.sampleReferences.push([srData.number, reference.relation, reference.sample_id]);
|
||||
this.sampleReferenceAutocomplete.push([srData.number]);
|
||||
if (!--loadCounter) {
|
||||
this.sampleReferences.push(['', '', '']);
|
||||
this.sampleReferenceAutocomplete.push([]);
|
||||
console.log(this.sampleReferences);
|
||||
console.log(this.sampleReferenceAutocomplete);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if ('condition_template' in this.sample.condition) {
|
||||
this.selectCondition(this.sample.condition.condition_template);
|
||||
}
|
||||
console.log('data loaded');
|
||||
this.loading--;
|
||||
this.checkFormAfterInit = true;
|
||||
});
|
||||
}
|
||||
this.loading--;
|
||||
});
|
||||
this.api.get<TemplateModel[]>('/sample/notes/fields', data => {
|
||||
this.availableCustomFields = data.map(e => e.name);
|
||||
this.loading--;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attach validators to dynamic material fields when all values are available and template was fully created
|
||||
if (this.materialTemplate && this.materialTemplate.hasOwnProperty('parameters') && this.materialTemplate.parameters.length > 0 && this.materialTemplate.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('materialParameter0')) {
|
||||
for (const i in this.materialTemplate.parameters) {
|
||||
if (this.materialTemplate.parameters[i]) {
|
||||
this.attachValidator('materialParameter' + i, this.materialTemplate.parameters[i].range, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sampleForm && this.sampleForm.form.get('measurementParameter0-0')) {
|
||||
this.sample.measurements.forEach((measurement, mIndex) => {
|
||||
const template = this.getMeasurementTemplate(measurement.measurement_template);
|
||||
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
|
||||
this.material.numbers = this.material.numbers.filter(e => e !== '');
|
||||
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];
|
||||
}
|
||||
});
|
||||
this.sample.notes.sample_references = this.sampleReferences.filter(e => e[0] && e[1] && e[2]).map(e => ({sample_id: e[2], relation: e[1]}));
|
||||
new Promise<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 {
|
||||
if (this.sample.material_id !== null) { // reset previous match
|
||||
this.material = new MaterialModel();
|
||||
}
|
||||
this.sample.material_id = null;
|
||||
}
|
||||
this.setNewMaterial();
|
||||
}
|
||||
|
||||
preventDefault(event) {
|
||||
if (event.key && event.key === 'Enter' || event.type === 'dragover') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: rework later
|
||||
setNewMaterial(value = null) {
|
||||
if (value === null) {
|
||||
this.newMaterial = !this.sample.material_id;
|
||||
}
|
||||
else if (value || (!value && this.sample.material_id !== null )) { // set to false only if material already exists
|
||||
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;
|
||||
const filledFields = this.material.numbers.filter(e => e !== '').length;
|
||||
// append new field
|
||||
if (filledFields === fieldNo) {
|
||||
this.material.numbers.push('');
|
||||
}
|
||||
// remove if two end fields are empty
|
||||
if (fieldNo > 1 && this.material.numbers[fieldNo - 1] === '' && this.material.numbers[fieldNo - 2] === '') {
|
||||
this.material.numbers.pop();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
selectMaterialTemplate(id) {
|
||||
this.materialTemplate = this.materialTemplates.find(e => e._id === id);
|
||||
if ('material_template' in this.material.properties) {
|
||||
this.material.properties.material_template = id;
|
||||
}
|
||||
}
|
||||
|
||||
getMeasurementTemplate(id): TemplateModel {
|
||||
return this.measurementTemplates && id ? this.measurementTemplates.find(e => e._id === id) : new TemplateModel();
|
||||
}
|
||||
|
||||
addMeasurement() {
|
||||
this.sample.measurements.push(new MeasurementModel(this.measurementTemplates[0]._id));
|
||||
this.charts.push(_.cloneDeep(this.chartInit));
|
||||
}
|
||||
|
||||
removeMeasurement(index) {
|
||||
if (this.sample.measurements[index]._id !== null) {
|
||||
this.api.delete('/measurement/' + this.sample.measurements[index]._id);
|
||||
}
|
||||
this.sample.measurements.splice(index, 1);
|
||||
this.charts.splice(index, 1);
|
||||
}
|
||||
|
||||
clearChart(index) {
|
||||
this.charts[index][0].data = [];
|
||||
}
|
||||
|
||||
fileToArray(files, mIndex, parameter) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
this.sample.measurements[mIndex].values[parameter] = fileReader.result.toString().split('\r\n').map(e => e.split(','));
|
||||
this.generateChart(this.sample.measurements[mIndex].values[parameter], mIndex);
|
||||
};
|
||||
fileReader.readAsText(files[0]);
|
||||
}
|
||||
|
||||
generateChart(spectrum, index) {
|
||||
this.charts[index][0].data = spectrum.map(e => ({x: parseFloat(e[0]), y: parseFloat(e[1])}));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
checkSampleReference(value, index) {
|
||||
if (value) {
|
||||
this.sampleReferences[index][0] = value;
|
||||
}
|
||||
this.currentSRIndex = index;
|
||||
const fieldNo = this.sampleReferences.length;
|
||||
let filledFields = 0;
|
||||
this.sampleReferences.forEach(field => {
|
||||
if (field[0] !== '') {
|
||||
filledFields ++;
|
||||
}
|
||||
});
|
||||
// append new field
|
||||
if (filledFields === fieldNo) {
|
||||
this.sampleReferences.push(['', '', '']);
|
||||
this.sampleReferenceAutocomplete.push([]);
|
||||
}
|
||||
// remove if two end fields are empty
|
||||
if (fieldNo > 1 && this.sampleReferences[fieldNo - 1][0] === '' && this.sampleReferences[fieldNo - 2][0] === '') {
|
||||
this.sampleReferences.pop();
|
||||
this.sampleReferenceAutocomplete.pop();
|
||||
}
|
||||
this.sampleReferenceIdFind(value);
|
||||
}
|
||||
|
||||
sampleReferenceList(value) {
|
||||
return new Observable(observer => {
|
||||
this.api.get<{_id: string, number: string}[]>('/samples?status=all&page-size=25&sort=number-asc&fields[]=number&fields[]=_id&filters[]=%7B%22mode%22%3A%22stringin%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22' + value + '%22%5D%7D', data => {
|
||||
console.log(data);
|
||||
this.sampleReferenceAutocomplete[this.currentSRIndex] = data.map(e => e.number);
|
||||
this.sampleReferenceFinds = data;
|
||||
observer.next(data.map(e => e.number));
|
||||
observer.complete();
|
||||
this.sampleReferenceIdFind(value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sampleReferenceIdFind(value) {
|
||||
const idFind = this.sampleReferenceFinds.find(e => e.number === value);
|
||||
if (idFind) {
|
||||
this.sampleReferences[this.currentSRIndex][2] = idFind._id;
|
||||
}
|
||||
else {
|
||||
this.sampleReferences[this.currentSRIndex][2] = '';
|
||||
}
|
||||
}
|
||||
|
||||
sampleReferenceListBind() {
|
||||
return this.sampleReferenceList.bind(this);
|
||||
}
|
||||
|
||||
uniqueCfValues(index) { // returns all names until index for unique check
|
||||
return this.customFields.slice(0, index).map(e => e[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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^^
|
130
src/app/samples/samples.component.html
Normal file
130
src/app/samples/samples.component.html
Normal file
@ -0,0 +1,130 @@
|
||||
<div class="header-addnew">
|
||||
<h2>Samples</h2>
|
||||
<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 [open]="false"><span class="rb-ic rb-ic-filter"></span> Filter</rb-accordion-title>
|
||||
<rb-accordion-body>
|
||||
<form class="filters">
|
||||
<div class="status-selection">
|
||||
<label class="label">Status</label>
|
||||
<rb-form-checkbox name="status-validated" [(ngModel)]="filters.status.validated" [disabled]="!filters.status.new" (ngModelChange)="loadSamples({firstPage: true})">
|
||||
validated
|
||||
</rb-form-checkbox>
|
||||
<rb-form-checkbox name="status-new" [(ngModel)]="filters.status.new" [disabled]="!filters.status.validated" (ngModelChange)="loadSamples({firstPage: true})">
|
||||
new
|
||||
</rb-form-checkbox>
|
||||
</div>
|
||||
<rb-form-select name="pageSizeSelection" label="page size" [(ngModel)]="filters.pageSize" class="selection" (ngModelChange)="loadSamples({firstPage: true})" #pageSizeSelection>
|
||||
<option value="3">3</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="250">250</option>
|
||||
<option value="500">500</option>
|
||||
</rb-form-select>
|
||||
|
||||
<rb-form-multi-select name="fieldSelect" idField="id" [items]="keys" [(ngModel)]="isActiveKey" label="Fields" class="selection" (ngModelChange)="loadSamples({}, $event)">
|
||||
<span *rbFormMultiSelectOption="let item" class="load-first-page">{{item.label}}</span>
|
||||
</rb-form-multi-select>
|
||||
|
||||
<div class="fieldfilters">
|
||||
<div *ngFor="let filter of filters.filters">
|
||||
<ng-container *ngIf="isActiveKey[filter.field]">
|
||||
<rb-form-checkbox [name]="'filteractive-' + filter.field" [(ngModel)]="filter.active" (ngModelChange)="loadSamples({firstPage: true})"></rb-form-checkbox>
|
||||
<rb-form-select [name]="'filtermode-' + filter.field" class="filtermode" [(ngModel)]="filter.mode" (ngModelChange)="updateFilterFields(filter.field)">
|
||||
<option value="eq" title="field is equal to value">=</option>
|
||||
<option value="ne" title="field is not equal to value">≠</option>
|
||||
<option value="lt" title="field is lower than value"><</option>
|
||||
<option value="lte" title="field is lower than or equal to value">≤</option>
|
||||
<option value="gt" title="field is greater than value">></option>
|
||||
<option value="gte" title="field is greater than or equal to value">≥</option>
|
||||
<option value="stringin" title="field contains value">⊇</option>
|
||||
<option value="in" title="field is one of the values">∈</option>
|
||||
<option value="nin" title="field is not one of the values">∉</option>
|
||||
</rb-form-select>
|
||||
<div class="filter-inputs">
|
||||
<ng-container *ngFor="let ignore of [].constructor(filter.values.length); index as i">
|
||||
<rb-form-date-input *ngIf="filter.field === 'added'; else noDate" [name]="'filter-' + filter.field + i" [label]="filter.label" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)"></rb-form-date-input>
|
||||
<ng-template #noDate>
|
||||
<rb-form-input *ngIf="!filter.autocomplete.length" [name]="'filter-' + filter.field + i" [label]="filter.label" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)"></rb-form-input>
|
||||
<rb-form-input *ngIf="filter.autocomplete.length" [name]="'filter-' + filter.field + i" [label]="filter.label" [rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)" [rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')" [(ngModel)]="filter.values[i]" (ngModelChange)="updateFilterFields(filter.field)" ngModel></rb-form-input>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rb-accordion-body>
|
||||
</rb-accordion>
|
||||
|
||||
|
||||
<ng-container *ngTemplateOutlet="paging"></ng-container>
|
||||
|
||||
<div class="download">
|
||||
<button class="rb-btn rb-secondary" type="button" [rbModal]="linkModal" ><span class="rb-ic rb-ic-download"></span> JSON download link
|
||||
</button>
|
||||
<ng-template #linkModal let-close="close">
|
||||
URL for JSON download:
|
||||
<textarea class="linkmodal" #linkarea [value]="sampleUrl({export: true, host: true})" (keydown)="preventDefault($event)"></textarea>
|
||||
<rb-form-checkbox name="download-csv" [(ngModel)]="downloadCsv">
|
||||
add spectra
|
||||
</rb-form-checkbox>
|
||||
<button class="rb-btn rb-secondary" type="button" (click)="clipboard()"><span class="rb-ic rb-ic-clipboard"></span> Copy to clipboard</button>
|
||||
</ng-template>
|
||||
<a [href]="csvUrl" download="samples.csv">
|
||||
<button class="rb-btn rb-secondary" type="button" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})"><span class="rb-ic rb-ic-download"></span> Download result as CSV</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<rb-table>
|
||||
<tr>
|
||||
<th *ngFor="let key of activeKeys">
|
||||
<div class="sort-header">
|
||||
<span>{{key.label}}</span>
|
||||
<span class="rb-ic rb-ic-up sort-arr-up" (click)="setSort(key.id + '-' + 'desc')"><span *ngIf="filters.sort === key.id + '-' + 'desc'"></span></span>
|
||||
<span class="rb-ic rb-ic-down sort-arr-down" (click)="setSort(key.id + '-' + 'asc')"><span *ngIf="filters.sort === key.id + '-' + 'asc'"></span></span>
|
||||
</div>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr *ngFor="let sample of samples">
|
||||
<td *ngIf="isActiveKey['number']">{{sample.number}}</td>
|
||||
<td *ngIf="isActiveKey['material.numbers']">{{materials[sample.material_id].numbers}}</td>
|
||||
<td *ngIf="isActiveKey['material.name']">{{materials[sample.material_id].name}}</td>
|
||||
<td *ngIf="isActiveKey['material.supplier']">{{materials[sample.material_id].supplier}}</td>
|
||||
<td *ngIf="isActiveKey['material.group']">{{materials[sample.material_id].group}}</td>
|
||||
<td *ngFor="let key of activeTemplateKeys.material">{{materials[sample.material_id].properties[key[2]] | exists}}</td>
|
||||
<td *ngIf="isActiveKey['type']">{{sample.type}}</td>
|
||||
<td *ngIf="isActiveKey['color']">{{sample.color}}</td>
|
||||
<td *ngIf="isActiveKey['batch']">{{sample.batch}}</td>
|
||||
<td *ngIf="isActiveKey['notes']">{{sample.notes | object}}</td>
|
||||
<td *ngFor="let key of activeTemplateKeys.measurements">{{sample[key[1]] | exists: key[2]}}</td>
|
||||
<td *ngIf="isActiveKey['added']">{{sample.added | date:'dd/MM/yy'}}</td>
|
||||
<td><a [routerLink]="'/samples/edit/' + sample._id"><span class="rb-ic rb-ic-edit"></span></a></td>
|
||||
</tr>
|
||||
</rb-table>
|
||||
|
||||
<ng-container *ngTemplateOutlet="paging"></ng-container>
|
||||
|
||||
<ng-template #paging>
|
||||
<div class="paging">
|
||||
<button class="rb-btn rb-link" type="button" (click)="loadPage(-1)" [disabled]="page === 1">
|
||||
<span class="rb-ic rb-ic-back-left"></span>
|
||||
</button>
|
||||
<rb-form-input label="page" (change)="loadPage({toPage: $event.target.value - page})" [ngModel]="page"></rb-form-input>
|
||||
<span>
|
||||
of {{pages}} ({{totalSamples}} samples)
|
||||
</span>
|
||||
<button class="rb-btn rb-link" type="button" (click)="loadPage(1)" [disabled]="page >= pages">
|
||||
<span class="rb-ic rb-ic-forward-right"></span>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
182
src/app/samples/samples.component.scss
Normal file
182
src/app/samples/samples.component.scss
Normal file
@ -0,0 +1,182 @@
|
||||
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
|
||||
|
||||
.header-addnew {
|
||||
margin-bottom: 40px;
|
||||
|
||||
& > * {
|
||||
display: inline;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
rb-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rb-ic.rb-ic-edit {
|
||||
font-size: 1.1rem;
|
||||
color: $color-gray-silver-sand;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.status-selection {
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
float: left;
|
||||
margin-right: 15px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
rb-form-checkbox {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.selection {
|
||||
max-width: 230px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.paging {
|
||||
rb-form-input {
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
> * {
|
||||
float: left;
|
||||
}
|
||||
|
||||
> button {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-top: 20px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-header {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-column-gap: 5px;
|
||||
width: 100%;
|
||||
|
||||
:first-child {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
margin-bottom: -3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:nth-child(3) {
|
||||
margin-top: -3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-active-asc {
|
||||
color: $color-bosch-dark-blue;
|
||||
background: linear-gradient(to bottom, #FFF 17%, $color-bosch-light-blue-w50 17%);;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.sort-active-desc {
|
||||
color: $color-bosch-dark-blue;
|
||||
background: linear-gradient(to top, #FFF 17%, $color-bosch-light-blue-w50 17%);;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.filters:after {
|
||||
content:"";
|
||||
clear:both;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.download {
|
||||
margin-top: 5px;
|
||||
float: right;
|
||||
|
||||
& > rb-form-checkbox {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-arr-up {
|
||||
position: relative;
|
||||
|
||||
& > span {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6.3px solid transparent;
|
||||
border-right: 6.3px solid transparent;
|
||||
border-bottom: 6.3px solid #000;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
display: block;
|
||||
left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-arr-down {
|
||||
position: relative;
|
||||
|
||||
& > span {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6.3px solid transparent;
|
||||
border-right: 6.3px solid transparent;
|
||||
border-top: 6.3px solid #000;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
display: block;
|
||||
left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.fieldfilters {
|
||||
clear: both;
|
||||
|
||||
& > div {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
float: left;
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.filtermode {
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
textarea.linkmodal {
|
||||
display: block;
|
||||
min-width: 600px;
|
||||
min-height: 200px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.filter-inputs > * {
|
||||
display: inline-block;
|
||||
max-width: 250px;
|
||||
}
|
27
src/app/samples/samples.component.spec.ts
Normal file
27
src/app/samples/samples.component.spec.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
//
|
||||
// import { SamplesComponent } from './samples.component';
|
||||
//
|
||||
// // TODO
|
||||
//
|
||||
// describe('SamplesComponent', () => {
|
||||
// let component: SamplesComponent;
|
||||
// let fixture: ComponentFixture<SamplesComponent>;
|
||||
//
|
||||
// beforeEach(async(() => {
|
||||
// TestBed.configureTestingModule({
|
||||
// declarations: [ SamplesComponent ]
|
||||
// })
|
||||
// .compileComponents();
|
||||
// }));
|
||||
//
|
||||
// beforeEach(() => {
|
||||
// fixture = TestBed.createComponent(SamplesComponent);
|
||||
// component = fixture.componentInstance;
|
||||
// fixture.detectChanges();
|
||||
// });
|
||||
//
|
||||
// it('should create', () => {
|
||||
// expect(component).toBeTruthy();
|
||||
// });
|
||||
// });
|
273
src/app/samples/samples.component.ts
Normal file
273
src/app/samples/samples.component.ts
Normal file
@ -0,0 +1,273 @@
|
||||
import {Component, ElementRef, isDevMode, OnInit, ViewChild} from '@angular/core';
|
||||
import {ApiService} from '../services/api.service';
|
||||
import {AutocompleteService} from '../services/autocomplete.service';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
interface LoadSamplesOptions {
|
||||
toPage?: number;
|
||||
event?: Event;
|
||||
firstPage?: boolean;
|
||||
}
|
||||
interface KeyInterface {
|
||||
id: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-samples',
|
||||
templateUrl: './samples.component.html',
|
||||
styleUrls: ['./samples.component.scss']
|
||||
})
|
||||
|
||||
// TODO: manage branches, introduce versioning, only upload ui from master
|
||||
// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html
|
||||
|
||||
|
||||
export class SamplesComponent implements OnInit {
|
||||
|
||||
@ViewChild('pageSizeSelection') pageSizeSelection: ElementRef<HTMLElement>;
|
||||
@ViewChild('linkarea') linkarea: ElementRef<HTMLTextAreaElement>;
|
||||
|
||||
downloadCsv = false;
|
||||
materials = {};
|
||||
samples = [];
|
||||
totalSamples = 0; // total number of samples
|
||||
csvUrl = ''; // store url separate so it only has to be generated when clicking the download button
|
||||
filters = {
|
||||
status: {new: true, validated: true},
|
||||
pageSize: 25,
|
||||
toPage: 0,
|
||||
sort: 'added-asc',
|
||||
filters: [
|
||||
{field: 'number', label: 'Number', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'material.number', label: 'Material number', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'material.name', label: 'Material name', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'material.supplier', label: 'Supplier', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'material.group', label: 'Material', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'material.glass_fiber', label: 'GF', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'material.carbon_fiber', label: 'CF', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'material.mineral', label: 'M', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'type', label: 'Type', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'color', label: 'Color', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'batch', label: 'Batch', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'notes', label: 'Notes', active: false, autocomplete: [], mode: 'eq', values: ['']},
|
||||
{field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: [new Date()]}
|
||||
]
|
||||
};
|
||||
page = 1;
|
||||
pages = 1;
|
||||
loadSamplesQueue = []; // arguments of queued up loadSamples() calls
|
||||
apiKey = '';
|
||||
keys: KeyInterface[] = [
|
||||
{id: 'number', label: 'Number', active: true},
|
||||
{id: 'material.numbers', label: 'Material numbers', active: true},
|
||||
{id: 'material.name', label: 'Material name', active: true},
|
||||
{id: 'material.supplier', label: 'Supplier', active: true},
|
||||
{id: 'material.group', label: 'Material', active: false},
|
||||
{id: 'type', label: 'Type', active: true},
|
||||
{id: 'color', label: 'Color', active: true},
|
||||
{id: 'batch', label: 'Batch', active: true},
|
||||
{id: 'notes', label: 'Notes', active: false},
|
||||
{id: 'added', label: 'Added', active: true}
|
||||
];
|
||||
isActiveKey: {[key: string]: boolean} = {};
|
||||
activeKeys: KeyInterface[] = [];
|
||||
activeTemplateKeys = {material: [], measurements: []};
|
||||
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
public autocomplete: AutocompleteService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.calcFieldSelectKeys();
|
||||
this.api.get('/materials?status=all', (mData: any) => {
|
||||
this.materials = {};
|
||||
mData.forEach(material => {
|
||||
this.materials[material._id] = material;
|
||||
});
|
||||
this.filters.filters.find(e => e.field === 'material.name').autocomplete = mData.map(e => e.name);
|
||||
this.filters.filters.find(e => e.field === 'color').autocomplete = [...new Set(mData.reduce((s, e) => {s.push(...e.numbers.map(el => el.color)); return s; }, []))];
|
||||
this.loadSamples();
|
||||
});
|
||||
this.api.get('/user/key', (data: {key: string}) => {
|
||||
this.apiKey = data.key;
|
||||
});
|
||||
this.api.get<string[]>('/material/suppliers', (data: any) => {
|
||||
this.filters.filters.find(e => e.field === 'material.supplier').autocomplete = data;
|
||||
});
|
||||
this.api.get<string[]>('/material/groups', (data: any) => {
|
||||
this.filters.filters.find(e => e.field === 'material.group').autocomplete = data;
|
||||
});
|
||||
this.loadTemplateKeys('materials', 'type');
|
||||
this.loadTemplateKeys('measurements', 'added');
|
||||
}
|
||||
|
||||
loadTemplateKeys(collection, insertBefore) {
|
||||
this.api.get('/template/' + collection, (data: {name: string, parameters: {name: string, range: object}[]}[]) => {
|
||||
const templateKeys = [];
|
||||
data.forEach(item => {
|
||||
item.parameters.forEach(parameter => {
|
||||
templateKeys.push({id: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${encodeURIComponent(parameter.name)}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false});
|
||||
this.filters.filters.push({field: `${collection === 'materials' ? 'material' : collection}.${collection === 'materials' ? 'properties' : item.name}.${parameter.name}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, autocomplete: [], mode: 'eq', values: ['']});
|
||||
});
|
||||
});
|
||||
this.keys.splice(this.keys.findIndex(e => e.id === insertBefore), 0, ...templateKeys);
|
||||
this.keys = [...this.keys]; // complete overwrite array to invoke update in rb-multiselect
|
||||
this.updateActiveKeys();
|
||||
this.calcFieldSelectKeys();
|
||||
});
|
||||
}
|
||||
|
||||
loadSamples(options: LoadSamplesOptions = {}, event = null) { // set toPage to null to reload first page, queues calls
|
||||
if (event) { // adjust active keys
|
||||
this.keys.forEach(key => {
|
||||
if (event.hasOwnProperty(key.id)) {
|
||||
key.active = event[key.id];
|
||||
}
|
||||
});
|
||||
this.updateActiveKeys();
|
||||
}
|
||||
this.loadSamplesQueue.push(options);
|
||||
if (this.loadSamplesQueue.length <= 1) { // nothing queued up
|
||||
this.sampleLoader(this.loadSamplesQueue[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private sampleLoader(options: LoadSamplesOptions) { // actual loading of the sample, do not call directly
|
||||
this.api.get(this.sampleUrl({paging: true, pagingOptions: options}), (sData, ignore, headers) => {
|
||||
if (!options.toPage && headers['x-total-items']) {
|
||||
this.totalSamples = headers['x-total-items'];
|
||||
}
|
||||
this.pages = Math.ceil(this.totalSamples / this.filters.pageSize);
|
||||
this.samples = sData as any;
|
||||
this.loadSamplesQueue.shift();
|
||||
if (this.loadSamplesQueue.length > 0) { // execute next queue item
|
||||
this.sampleLoader(this.loadSamplesQueue[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sampleUrl(options: {paging?: boolean, pagingOptions?: {firstPage?: boolean, toPage?: number, event?: Event}, csv?: boolean, export?: boolean, host?: boolean}) { // return url to fetch samples
|
||||
const additionalTableKeys = ['material_id', '_id']; // keys which should always be added if export = false
|
||||
const query: string[] = [];
|
||||
query.push('status=' + (this.filters.status.new && this.filters.status.validated ? 'all' : (this.filters.status.new ? 'new' : 'validated')));
|
||||
if (options.paging) {
|
||||
if (this.samples[0]) { // do not include from-id when page size was changed
|
||||
if (!options.pagingOptions.firstPage) {
|
||||
query.push('from-id=' + this.samples[0]._id);
|
||||
}
|
||||
else {
|
||||
this.page = 1;
|
||||
}
|
||||
}
|
||||
if (options.pagingOptions.toPage) {
|
||||
query.push('to-page=' + options.pagingOptions.toPage);
|
||||
}
|
||||
query.push('page-size=' + this.filters.pageSize);
|
||||
}
|
||||
query.push('sort=' + this.filters.sort);
|
||||
if (options.csv) {
|
||||
query.push('csv=true');
|
||||
}
|
||||
if (options.export) {
|
||||
query.push('key=' + this.apiKey);
|
||||
}
|
||||
this.keys.forEach(key => {
|
||||
if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) { // do not load material properties for table
|
||||
query.push('fields[]=' + key.id);
|
||||
}
|
||||
});
|
||||
console.log(this.filters.filters);
|
||||
|
||||
query.push(..._.cloneDeep(this.filters.filters)
|
||||
.map(e => {
|
||||
e.values = e.values.filter(el => el !== ''); // do not include empty values
|
||||
if (e.field === 'added') { // correct timezone
|
||||
e.values = e.values.map(el => new Date(new Date(el).getTime() - new Date(el).getTimezoneOffset() * 60000).toISOString());
|
||||
}
|
||||
return e;
|
||||
})
|
||||
.filter(e => e.active && e.values.length > 0)
|
||||
.map(e => 'filters[]=' + encodeURIComponent(JSON.stringify(_.pick(e, ['mode', 'field', 'values']))))
|
||||
);
|
||||
console.log(this.filters);
|
||||
if (!options.export) {
|
||||
additionalTableKeys.forEach(key => {
|
||||
if (query.indexOf('fields[]=' + key) < 0) { // add key if not already added
|
||||
query.push('fields[]=' + key);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (this.downloadCsv) {
|
||||
query.push('fields[]=measurements.spectrum.dpt');
|
||||
}
|
||||
console.log('/samples?' + query.join('&'));
|
||||
return (options.host && isDevMode() ? window.location.host : '') + (options.export ? this.api.hostName : '') + '/samples?' + query.join('&');
|
||||
}
|
||||
|
||||
loadPage(delta) {
|
||||
if (!/[0-9]+/.test(delta) || (this.page <= 1 && delta < 0)) { // invalid delta
|
||||
return;
|
||||
}
|
||||
this.page += delta;
|
||||
this.loadSamples({toPage: delta});
|
||||
}
|
||||
|
||||
updateFilterFields(field) {
|
||||
const filter = this.filters.filters.find(e => e.field === field);
|
||||
if (filter.mode === 'in' || filter.mode === 'nin') {
|
||||
if (filter.values[filter.values.length - 1] === '' && filter.values[filter.values.length - 2] === '') {
|
||||
filter.values.pop();
|
||||
}
|
||||
else if (filter.values[filter.values.length - 1] !== '') {
|
||||
filter.values.push((filter.field === 'added' ? new Date() : '') as string & Date);
|
||||
}
|
||||
}
|
||||
else {
|
||||
filter.values = [filter.values[0] as string & Date];
|
||||
}
|
||||
if (filter.active) {
|
||||
this.loadSamples({firstPage: true});
|
||||
}
|
||||
}
|
||||
|
||||
setSort(string) {
|
||||
this.filters.sort = string;
|
||||
this.loadSamples({firstPage: true});
|
||||
}
|
||||
|
||||
updateActiveKeys() { // array with all activeKeys
|
||||
this.activeKeys = this.keys.filter(e => e.active);
|
||||
this.activeTemplateKeys.material = this.keys.filter(e => e.id.indexOf('material.properties.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el)));
|
||||
this.activeTemplateKeys.measurements = this.keys.filter(e => e.id.indexOf('measurements.') >= 0 && e.active).map(e => e.id.split('.').map(el => decodeURIComponent(el)));
|
||||
console.log(this.activeTemplateKeys);
|
||||
console.log(this.keys); // TODO: glass fiber filter not working
|
||||
}
|
||||
|
||||
calcFieldSelectKeys() {
|
||||
this.keys.forEach(key => {
|
||||
this.isActiveKey[key.id] = key.active;
|
||||
});
|
||||
}
|
||||
|
||||
preventDefault(event, key = 'all') {
|
||||
if (key === 'all' || event.key === key) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
clipboard() {
|
||||
this.linkarea.nativeElement.select();
|
||||
this.linkarea.nativeElement.setSelectionRange(0, 99999);
|
||||
document.execCommand('copy');
|
||||
}
|
||||
|
||||
ucFirst(string) {
|
||||
return string[0].toUpperCase() + string.slice(1);
|
||||
}
|
||||
}
|
138
src/app/services/api.service.spec.ts
Normal file
138
src/app/services/api.service.spec.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import {async, TestBed} from '@angular/core/testing';
|
||||
import { ApiService } from './api.service';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {LocalStorageService} from 'angular-2-local-storage';
|
||||
import {Observable} from 'rxjs';
|
||||
import {ModalService} from '@inst-iot/bosch-angular-ui-components';
|
||||
|
||||
let apiService: ApiService;
|
||||
let httpClientSpy: jasmine.SpyObj<HttpClient>;
|
||||
let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
|
||||
let modalServiceSpy: jasmine.SpyObj<ModalService>;
|
||||
|
||||
// TODO: test options
|
||||
|
||||
describe('ApiService', () => {
|
||||
beforeEach(() => {
|
||||
const httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);
|
||||
const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['get']);
|
||||
const modalSpy = jasmine.createSpyObj('ModalService', ['openComponent']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ApiService,
|
||||
{provide: HttpClient, useValue: httpSpy},
|
||||
{provide: LocalStorageService, useValue: localStorageSpy},
|
||||
{provide: ModalService, useValue: modalSpy}
|
||||
]
|
||||
});
|
||||
|
||||
apiService = TestBed.inject(ApiService);
|
||||
httpClientSpy = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
|
||||
localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
|
||||
modalServiceSpy = TestBed.inject(ModalService) as jasmine.SpyObj<ModalService>;
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(apiService).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows an error message when the request fails', () => {
|
||||
const getReturn = new Observable(observer => {
|
||||
observer.error('error');
|
||||
});
|
||||
httpClientSpy.get.and.returnValue(getReturn);
|
||||
localStorageServiceSpy.get.and.returnValue(undefined);
|
||||
modalServiceSpy.openComponent.and.returnValue({instance: {message: ''}} as any);
|
||||
|
||||
apiService.get('/testurl');
|
||||
expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object));
|
||||
expect(modalServiceSpy.openComponent.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('returns the error message if the callback function had an error parameter', () => {
|
||||
const getReturn = new Observable(observer => {
|
||||
observer.error('error');
|
||||
});
|
||||
httpClientSpy.get.and.returnValue(getReturn);
|
||||
localStorageServiceSpy.get.and.returnValue(undefined);
|
||||
modalServiceSpy.openComponent.and.returnValue({instance: {message: ''}} as any);
|
||||
|
||||
apiService.get('/testurl', (data, error) => {
|
||||
expect(modalServiceSpy.openComponent.calls.count()).toBe(0);
|
||||
expect(error).toBe('error');
|
||||
});
|
||||
});
|
||||
|
||||
it('should do get requests without auth if not available', async(() => {
|
||||
const getReturn = new Observable(observer => {
|
||||
observer.next('data');
|
||||
});
|
||||
httpClientSpy.get.and.returnValue(getReturn);
|
||||
localStorageServiceSpy.get.and.returnValue(undefined);
|
||||
|
||||
apiService.get('/testurl', res => {
|
||||
expect(res).toBe('data');
|
||||
expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', {});
|
||||
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should do get requests with basic auth if available', async(() => {
|
||||
const getReturn = new Observable(observer => {
|
||||
observer.next('data');
|
||||
});
|
||||
httpClientSpy.get.and.returnValue(getReturn);
|
||||
localStorageServiceSpy.get.and.returnValue('basicAuth');
|
||||
|
||||
apiService.get('/testurl', res => {
|
||||
expect(res).toBe('data');
|
||||
expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object)); // could not test http headers better
|
||||
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should do post requests', async(() => {
|
||||
const resReturn = new Observable(observer => {
|
||||
observer.next('data');
|
||||
});
|
||||
httpClientSpy.post.and.returnValue(resReturn);
|
||||
localStorageServiceSpy.get.and.returnValue('basicAuth');
|
||||
|
||||
apiService.post('/testurl', 'reqData', res => {
|
||||
expect(res).toBe('data');
|
||||
expect(httpClientSpy.post).toHaveBeenCalledWith('/api/testurl', 'reqData', jasmine.any(Object));
|
||||
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should do put requests', async(() => {
|
||||
const resReturn = new Observable(observer => {
|
||||
observer.next('data');
|
||||
});
|
||||
httpClientSpy.put.and.returnValue(resReturn);
|
||||
localStorageServiceSpy.get.and.returnValue('basicAuth');
|
||||
|
||||
apiService.put('/testurl', 'reqData', res => {
|
||||
expect(res).toBe('data');
|
||||
expect(httpClientSpy.put).toHaveBeenCalledWith('/api/testurl', 'reqData', jasmine.any(Object));
|
||||
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should do delete requests', async(() => {
|
||||
const resReturn = new Observable(observer => {
|
||||
observer.next('data');
|
||||
});
|
||||
httpClientSpy.delete.and.returnValue(resReturn);
|
||||
localStorageServiceSpy.get.and.returnValue('basicAuth');
|
||||
|
||||
apiService.delete('/testurl', res => {
|
||||
expect(res).toBe('data');
|
||||
expect(httpClientSpy.delete).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object));
|
||||
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
|
||||
});
|
||||
}));
|
||||
|
||||
// TODO: test return headers
|
||||
});
|
73
src/app/services/api.service.ts
Normal file
73
src/app/services/api.service.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Injectable, isDevMode } 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 = isDevMode() ? '/api' : 'https://definma-api.apps.de1.bosch-iot-cloud.com';
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private storage: LocalStorageService,
|
||||
private modalService: ModalService,
|
||||
private window: Window
|
||||
) { }
|
||||
|
||||
get hostName() {
|
||||
return this.host;
|
||||
}
|
||||
|
||||
get<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
|
||||
this.requestErrorHandler<T>(this.http.get(this.host + url, this.options()), f);
|
||||
}
|
||||
|
||||
post<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
|
||||
this.requestErrorHandler<T>(this.http.post(this.host + url, data, this.options()), f);
|
||||
}
|
||||
|
||||
put<T>(url, data = null, f: (data?: T, err?, headers?) => void = () => {}) {
|
||||
this.requestErrorHandler<T>(this.http.put(this.host + url, data, this.options()), f);
|
||||
}
|
||||
|
||||
delete<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
|
||||
this.requestErrorHandler<T>(this.http.delete(this.host + url, this.options()), f);
|
||||
}
|
||||
|
||||
private requestErrorHandler<T>(observable: Observable<any>, f: (data?: T, err?, headers?) => void) {
|
||||
observable.subscribe(data => {
|
||||
f(data.body, undefined, data.headers.keys().reduce((s, e) => {s[e.toLowerCase()] = data.headers.get(e); return s; }, {}));
|
||||
}, err => {
|
||||
if (f.length === 2) {
|
||||
f(undefined, err);
|
||||
}
|
||||
else {
|
||||
const modalRef = this.modalService.openComponent(ErrorComponent);
|
||||
modalRef.instance.message = 'Network request failed!';
|
||||
modalRef.result.then(() => {
|
||||
this.window.location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private options(): {headers: HttpHeaders, observe: 'body'} {
|
||||
return {headers: this.authOptions(), observe: 'response' as 'body'};
|
||||
}
|
||||
|
||||
private authOptions(): HttpHeaders {
|
||||
const auth = this.storage.get('basicAuth');
|
||||
if (auth) {
|
||||
return new HttpHeaders({Authorization: 'Basic ' + auth});
|
||||
}
|
||||
else {
|
||||
return new HttpHeaders();
|
||||
}
|
||||
}
|
||||
}
|
35
src/app/services/autocomplete.service.spec.ts
Normal file
35
src/app/services/autocomplete.service.spec.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AutocompleteService } from './autocomplete.service';
|
||||
|
||||
let autocompleteService: AutocompleteService;
|
||||
|
||||
describe('AutocompleteService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [AutocompleteService]
|
||||
});
|
||||
autocompleteService = TestBed.inject(AutocompleteService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(autocompleteService).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should should return a bind function', () => {
|
||||
expect(autocompleteService.bind('a', ['b'])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return search results', () => {
|
||||
autocompleteService.search(['aa', 'ab', 'bb'], 'a').subscribe(res => {
|
||||
expect(res).toEqual(['aa', 'ab']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an empty array if no result was found', () => {
|
||||
autocompleteService.search(['aa', 'ab', 'bb'], 'c').subscribe(res => {
|
||||
expect(res).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
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: string[]) {
|
||||
return this.search.bind(ref, list);
|
||||
}
|
||||
|
||||
search(arr: string[], str: string) {
|
||||
const qs = new QuickScore(arr);
|
||||
return of(str === '' ? [] : qs.search(str).map(e => e.item));
|
||||
}
|
||||
}
|
87
src/app/services/login.service.spec.ts
Normal file
87
src/app/services/login.service.spec.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginService } from './login.service';
|
||||
import {LocalStorageService} from 'angular-2-local-storage';
|
||||
import {ApiService} from './api.service';
|
||||
|
||||
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.callFake(() => {});
|
||||
loginService.login('username', 'password');
|
||||
expect(localStorageServiceSpy.set).toHaveBeenCalledWith('basicAuth', 'dXNlcm5hbWU6cGFzc3dvcmQ=');
|
||||
});
|
||||
|
||||
it('should remove the basic auth if login fails', () => {
|
||||
localStorageServiceSpy.set.and.returnValue(true);
|
||||
localStorageServiceSpy.remove.and.returnValue(true);
|
||||
apiServiceSpy.get.and.callFake((a, b) => {b(undefined, 'error'); });
|
||||
loginService.login('username', 'password');
|
||||
expect(localStorageServiceSpy.remove.calls.count()).toBe(1);
|
||||
expect(localStorageServiceSpy.remove).toHaveBeenCalledWith('basicAuth');
|
||||
});
|
||||
|
||||
it('should resolve true when login succeeds', async () => {
|
||||
localStorageServiceSpy.set.and.returnValue(true);
|
||||
apiServiceSpy.get.and.callFake((a, b) => {b({status: 'Authorization successful', method: 'basic'} as any, undefined); });
|
||||
expect(await loginService.login('username', 'password')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should resolve false when a wrong result comes in', async () => {
|
||||
localStorageServiceSpy.set.and.returnValue(true);
|
||||
apiServiceSpy.get.and.callFake((a, b) => {b({status: 'xxx', method: 'basic'} as any, undefined); });
|
||||
expect(await loginService.login('username', 'password')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should resolve false on an error', async () => {
|
||||
localStorageServiceSpy.set.and.returnValue(true);
|
||||
apiServiceSpy.get.and.callFake((a, b) => {b(undefined, 'error'); });
|
||||
expect(await loginService.login('username', 'password')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('canActivate', () => {
|
||||
it('should return false at first', done => {
|
||||
apiServiceSpy.get.and.callFake((a, b) => {b(undefined, 'error'); });
|
||||
loginService.canActivate(null, null).subscribe(res => {
|
||||
expect(res).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true if login was successful', async done => {
|
||||
localStorageServiceSpy.set.and.returnValue(true);
|
||||
apiServiceSpy.get.and.callFake((a, b) => {b({status: 'Authorization successful', method: 'basic'} as any, undefined); });
|
||||
await loginService.login('username', 'password');
|
||||
loginService.canActivate(null, null).subscribe(res => {
|
||||
expect(res).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
72
src/app/services/login.service.ts
Normal file
72
src/app/services/login.service.ts
Normal file
@ -0,0 +1,72 @@
|
||||
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;
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
private storage: LocalStorageService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
login(username = '', password = '') {
|
||||
return new Promise(resolve => {
|
||||
if (username !== '') {
|
||||
this.storage.set('basicAuth', btoa(username + ':' + password));
|
||||
}
|
||||
this.api.get('/authorized', (data: any, error) => {
|
||||
if (!error) {
|
||||
if (data.status === 'Authorization successful') {
|
||||
this.loggedIn = true;
|
||||
resolve(true);
|
||||
} else {
|
||||
this.loggedIn = false;
|
||||
this.storage.remove('basicAuth');
|
||||
resolve(false);
|
||||
}
|
||||
} else {
|
||||
this.loggedIn = false;
|
||||
this.storage.remove('basicAuth');
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.storage.remove('basicAuth');
|
||||
this.loggedIn = false;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isLoggedIn() {
|
||||
return this.loggedIn;
|
||||
}
|
||||
|
||||
get username() {
|
||||
return atob(this.storage.get('basicAuth')).split(':')[0];
|
||||
}
|
||||
}
|
114
src/app/services/validation.service.spec.ts
Normal file
114
src/app/services/validation.service.spec.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ValidationService } from './validation.service';
|
||||
|
||||
let validationService: ValidationService;
|
||||
|
||||
describe('ValidationService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ValidationService]
|
||||
});
|
||||
|
||||
validationService = TestBed.inject(ValidationService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(validationService).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true on a correct username', () => {
|
||||
expect(validationService.username('abc')).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on an incorrect username', () => {
|
||||
expect(validationService.username('abc#')).toEqual({ok: false, error: 'username must only contain a-z0-9-_.'});
|
||||
});
|
||||
|
||||
it('should return true on a correct password', () => {
|
||||
expect(validationService.password('Abc123!#')).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on a password too short', () => {
|
||||
expect(validationService.password('Abc123')).toEqual({ok: false, error: 'password must have at least 8 characters'});
|
||||
});
|
||||
|
||||
it('should return an error on a password without a lowercase letter', () => {
|
||||
expect(validationService.password('ABC123!#')).toEqual({ok: false, error: 'password must have at least one lowercase character'});
|
||||
});
|
||||
|
||||
it('should return an error on a password without an uppercase letter', () => {
|
||||
expect(validationService.password('abc123!#')).toEqual({ok: false, error: 'password must have at least one uppercase character'});
|
||||
});
|
||||
|
||||
it('should return an error on a password without a number', () => {
|
||||
expect(validationService.password('Abcabc!#')).toEqual({ok: false, error: 'password must have at least one number'});
|
||||
});
|
||||
|
||||
it('should return an error on a password without a special character', () => {
|
||||
expect(validationService.password('Abc12345')).toEqual({ok: false, error: 'password must have at least one of the following characters !"#%&\'()*+,-.\\/:;<=>?@[]^_`{|}~'});
|
||||
});
|
||||
|
||||
it('should return an error on a password with a character not allowed', () => {
|
||||
expect(validationService.password('Abc123!€')).toEqual({ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'});
|
||||
});
|
||||
|
||||
it('should return true on a correct string', () => {
|
||||
expect(validationService.string('Abc')).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on a string too long', () => {
|
||||
expect(validationService.string('abcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbacab')).toEqual({ok: false, error: 'must contain max 128 characters'});
|
||||
});
|
||||
|
||||
it('should return true on a string in the list', () => {
|
||||
expect(validationService.stringOf('Abc', ['Abc', 'Def'])).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on a string not in the list', () => {
|
||||
expect(validationService.stringOf('abc', ['Abc', 'Def'])).toEqual({ok: false, error: 'must be one of Abc, Def'});
|
||||
});
|
||||
|
||||
it('should return true on a string of correct length', () => {
|
||||
expect(validationService.stringLength('Abc', 5)).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on a string longer than specified', () => {
|
||||
expect(validationService.stringLength('Abc', 2)).toEqual({ok: false, error: 'must contain max 2 characters'});
|
||||
});
|
||||
|
||||
it('should return true on a number in the range', () => {
|
||||
expect(validationService.minMax(2, -2, 2)).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on a number below the range', () => {
|
||||
expect(validationService.minMax(0, 1, 3)).toEqual({ok: false, error: 'must be between 1 and 3'});
|
||||
});
|
||||
|
||||
it('should return an error on a number above the range', () => {
|
||||
expect(validationService.minMax(3.1, 1, 3)).toEqual({ok: false, error: 'must be between 1 and 3'});
|
||||
});
|
||||
|
||||
it('should return true on a number above min', () => {
|
||||
expect(validationService.min(2, -2)).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on a number below min', () => {
|
||||
expect(validationService.min(0, 1)).toEqual({ok: false, error: 'must not be below 1'});
|
||||
});
|
||||
|
||||
it('should return true on a number below max', () => {
|
||||
expect(validationService.max(2, 2)).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on a number above max', () => {
|
||||
expect(validationService.max(2, 1)).toEqual({ok: false, error: 'must not be above 1'});
|
||||
});
|
||||
|
||||
it('should return true on a string not in the list', () => {
|
||||
expect(validationService.unique('Abc', ['Def', 'Ghi'])).toEqual({ok: true, error: ''});
|
||||
});
|
||||
|
||||
it('should return an error on a string from the list', () => {
|
||||
expect(validationService.unique('Abc', ['Abc', 'Def'])).toEqual({ok: false, error: 'values must be unique'});
|
||||
});
|
||||
});
|
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('').max(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 };
|
||||
}
|
||||
}
|
3
src/assets/imgs/db_structure_latest.svg
Normal file
3
src/assets/imgs/db_structure_latest.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 90 KiB |
6
src/proxy.conf.json
Normal file
6
src/proxy.conf.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:3000",
|
||||
"secure": false
|
||||
}
|
||||
}
|
@ -1,3 +1,17 @@
|
||||
$font-path: "../node_modules/@inst-iot/bosch-angular-ui-components/assets/";
|
||||
$rb-extended-breakpoints: false; // whether to use extended breakpoints xxl and xxxl
|
||||
@import "~@inst-iot/bosch-angular-ui-components/styles/global.scss";
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a, a:active, a:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/test.ts",
|
||||
|
@ -21,6 +21,7 @@
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true
|
||||
"strictInjectionParameters": true,
|
||||
"debug": true
|
||||
}
|
||||
}
|
||||
|
25
tslint.json
25
tslint.json
@ -3,6 +3,13 @@
|
||||
"rules": {
|
||||
"array-type": false,
|
||||
"arrow-parens": false,
|
||||
"brace-style": [
|
||||
true,
|
||||
"stroustrup",
|
||||
{
|
||||
"allowSingleLine": true
|
||||
}
|
||||
],
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
@ -27,10 +34,7 @@
|
||||
],
|
||||
"interface-name": false,
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"max-line-length": false,
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
@ -66,6 +70,7 @@
|
||||
"as-needed"
|
||||
],
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": false,
|
||||
"ordered-imports": false,
|
||||
"quotemark": [
|
||||
true,
|
||||
@ -83,9 +88,17 @@
|
||||
"template-banana-in-box": true,
|
||||
"template-no-negated-async": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true
|
||||
"use-pipe-transform-interface": true,
|
||||
"variable-name": {
|
||||
"options": [
|
||||
"allow-leading-underscore",
|
||||
"allow-pascal-case",
|
||||
"allow-snake-case",
|
||||
"check-format"
|
||||
]
|
||||
}
|
||||
},
|
||||
"rulesDirectory": [
|
||||
"codelyzer"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user