added templates component
This commit is contained in:
@ -5,6 +5,7 @@ import {LoginService} from './services/login.service';
|
||||
import {SampleComponent} from './sample/sample.component';
|
||||
import {SamplesComponent} from './samples/samples.component';
|
||||
import {DocumentationComponent} from './documentation/documentation.component';
|
||||
import {TemplatesComponent} from './templates/templates.component';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
@ -13,6 +14,8 @@ const routes: Routes = [
|
||||
{path: 'samples', component: SamplesComponent, canActivate: [LoginService]},
|
||||
{path: 'samples/new', component: SampleComponent, canActivate: [LoginService]},
|
||||
{path: 'samples/edit/:id', component: SampleComponent, canActivate: [LoginService]},
|
||||
{path: 'templates', component: TemplatesComponent}, // TODO: change after development
|
||||
// {path: 'templates', component: TemplatesComponent, canActivate: [LoginService]},
|
||||
{path: 'documentation', component: DocumentationComponent},
|
||||
|
||||
// if not authenticated
|
||||
|
@ -2,6 +2,7 @@
|
||||
<nav *rbMainNavItems>
|
||||
<a routerLink="/home" routerLinkActive="active" rbLoadingLink>Home</a>
|
||||
<a routerLink="/samples" routerLinkActive="active" rbLoadingLink *ngIf="loginService.isLoggedIn">Samples</a>
|
||||
<a routerLink="/templates" routerLinkActive="active" rbLoadingLink *ngIf="loginService.isMaintain">Templates</a>
|
||||
<a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a>
|
||||
</nav>
|
||||
|
||||
|
@ -8,6 +8,13 @@ import {Router} from '@angular/router';
|
||||
// TODO: filter by not completely filled/no measurements
|
||||
// TODO: account
|
||||
// TODO: admin user handling, template pages, validation of samples
|
||||
// TODO: activate filter on start typing
|
||||
|
||||
// TODO: Build IconComponent free lib version because of CSP
|
||||
// TODO: more helmet headers, UI presentatin plan
|
||||
// TODO: sort material numbers, filter field measurements
|
||||
// TODO: get rid of chart.js (+moment.js) and lodash
|
||||
// TODO: look into CSS/XHR/Anfragen tab of console
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -11,7 +11,7 @@ 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 {RbCustomInputsModule} from './rb-custom-inputs/rb-custom-inputs.module';
|
||||
import { SampleComponent } from './sample/sample.component';
|
||||
import { ValidateDirective } from './validate.directive';
|
||||
import {CommonModule} from '@angular/common';
|
||||
@ -21,6 +21,8 @@ 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';
|
||||
import { TemplatesComponent } from './templates/templates.component';
|
||||
import { ParametersPipe } from './parameters.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -34,7 +36,9 @@ import { ExistsPipe } from './exists.pipe';
|
||||
ObjectPipe,
|
||||
DocumentationComponent,
|
||||
ImgMagnifierComponent,
|
||||
ExistsPipe
|
||||
ExistsPipe,
|
||||
TemplatesComponent,
|
||||
ParametersPipe
|
||||
],
|
||||
imports: [
|
||||
LocalStorageModule.forRoot({
|
||||
@ -47,7 +51,7 @@ import { ExistsPipe } from './exists.pipe';
|
||||
RbUiComponentsModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
RbTableModule,
|
||||
RbCustomInputsModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldsModule,
|
||||
CommonModule,
|
||||
|
@ -17,6 +17,7 @@ export class SampleModel extends BaseModel {
|
||||
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: {}};
|
||||
added: Date = null;
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
@ -27,6 +28,9 @@ export class SampleModel extends BaseModel {
|
||||
if (input.hasOwnProperty('measurements')) {
|
||||
this.measurements = input.measurements.map(e => new MeasurementModel().deserialize(e));
|
||||
}
|
||||
if (input.hasOwnProperty('added')) {
|
||||
this.added = new Date(input.added);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import {BaseModel} from './base.model';
|
||||
export class TemplateModel extends BaseModel {
|
||||
_id: IdModel = null;
|
||||
name = '';
|
||||
version = 1;
|
||||
parameters: {name: string, range: {[prop: string]: any}}[] = [];
|
||||
version = 0;
|
||||
first_id: IdModel = null;
|
||||
parameters: {name: string, range: {[prop: string]: any}, rangeString?: string}[] = [];
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import _ from 'lodash';
|
||||
|
||||
@Pipe({
|
||||
name: 'object',
|
||||
@ -6,8 +7,9 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
})
|
||||
export class ObjectPipe implements PipeTransform {
|
||||
|
||||
transform(value: object): string {
|
||||
return value ? JSON.stringify(value) : '';
|
||||
transform(value: object, omit: string[] = []): string {
|
||||
const res = _.omit(value, omit);
|
||||
return res && Object.keys(res).length ? JSON.stringify(res) : '';
|
||||
}
|
||||
|
||||
}
|
||||
|
8
src/app/parameters.pipe.spec.ts
Normal file
8
src/app/parameters.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ParametersPipe } from './parameters.pipe';
|
||||
|
||||
describe('ParametersPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new ParametersPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
12
src/app/parameters.pipe.ts
Normal file
12
src/app/parameters.pipe.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'parameters'
|
||||
})
|
||||
export class ParametersPipe implements PipeTransform {
|
||||
|
||||
transform(value: {name: string, range: object}[]): string {
|
||||
return `{${value.map(e => `${e.name}: <${JSON.stringify(e.range).replace('{}', 'any').replace(/["{}]/g, '')}>`).join(', ')}}`;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ArrayInputHelperService } from './array-input-helper.service';
|
||||
|
||||
describe('ArrayInputHelperService', () => {
|
||||
let service: ArrayInputHelperService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ArrayInputHelperService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {Observable, Subject} from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ArrayInputHelperService {
|
||||
|
||||
com: Subject<{ id: string, index: number, value: any }> = new Subject();
|
||||
|
||||
constructor() { }
|
||||
|
||||
values(id: string) {
|
||||
return new Observable<{index: number, value: any}>(observer => {
|
||||
this.com.subscribe(data => {
|
||||
if (data.id === id) {
|
||||
observer.next({index: data.index, value: data.value});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
newValue(id: string, index: number, value: any) {
|
||||
this.com.next({id, index, value});
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<ng-container *ngFor="let ignore of [].constructor(values.length); index as i">
|
||||
<ng-container *ngTemplateOutlet="item.templateRef; context: {$implicit: {i: i, value: values[i]}}"></ng-container>
|
||||
</ng-container>
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RbArrayInputComponent } from './rb-array-input.component';
|
||||
|
||||
describe('RbArrayInputComponent', () => {
|
||||
let component: RbArrayInputComponent;
|
||||
let fixture: ComponentFixture<RbArrayInputComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RbArrayInputComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RbArrayInputComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,107 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ContentChild,
|
||||
Directive,
|
||||
forwardRef,
|
||||
HostListener,
|
||||
Input,
|
||||
OnInit,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||||
import _ from 'lodash';
|
||||
import {ArrayInputHelperService} from './array-input-helper.service';
|
||||
|
||||
// TODO: implement everywhere
|
||||
|
||||
@Directive({ // directive for template and input values
|
||||
// tslint:disable-next-line:directive-selector
|
||||
selector: '[rbArrayInputItem]'
|
||||
})
|
||||
export class RbArrayInputItemDirective {
|
||||
constructor(public templateRef: TemplateRef<any>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({ // directive for change detection
|
||||
// tslint:disable-next-line:directive-selector
|
||||
selector: '[rbArrayInputListener]'
|
||||
})
|
||||
export class RbArrayInputListenerDirective {
|
||||
|
||||
@Input() rbArrayInputListener: string;
|
||||
@Input() index: number;
|
||||
|
||||
constructor(
|
||||
private helperService: ArrayInputHelperService
|
||||
) { }
|
||||
|
||||
@HostListener('ngModelChange', ['$event'])
|
||||
onChange(event) {
|
||||
console.log(event);
|
||||
this.helperService.newValue(this.rbArrayInputListener, this.index, event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'rb-array-input',
|
||||
templateUrl: './rb-array-input.component.html',
|
||||
styleUrls: ['./rb-array-input.component.scss'],
|
||||
providers: [{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RbArrayInputComponent), multi: true}]
|
||||
})
|
||||
export class RbArrayInputComponent implements ControlValueAccessor, OnInit, AfterViewInit {
|
||||
|
||||
@Input() pushTemplate: any;
|
||||
@Input() pushPath: string;
|
||||
|
||||
@ContentChild(RbArrayInputItemDirective) item: RbArrayInputItemDirective;
|
||||
@ContentChild(RbArrayInputListenerDirective) item2: RbArrayInputListenerDirective;
|
||||
|
||||
values = []; // main array to display
|
||||
|
||||
onChange = (ignore?: any): void => {};
|
||||
onTouched = (ignore?: any): void => {};
|
||||
|
||||
|
||||
constructor(
|
||||
private helperService: ArrayInputHelperService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => { // needed to find reference
|
||||
this.helperService.values(this.item2.rbArrayInputListener).subscribe(data => { // action on value change
|
||||
this.values[data.index][this.pushPath] = data.value;
|
||||
console.log(this.values);
|
||||
if (this.values[this.values.length - 1][this.pushPath] === '' && this.values[this.values.length - 2][this.pushPath] === '') { // remove last element if last two are empty
|
||||
this.values.pop();
|
||||
}
|
||||
else if (this.values[this.values.length - 1][this.pushPath] !== '') { // add element if last one is filled
|
||||
this.values.push(_.cloneDeep(this.pushTemplate));
|
||||
}
|
||||
this.onChange(this.values.filter(e => e !== '')); // trigger ngModel with filled elements
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
writeValue(obj: any) { // add empty value on init
|
||||
this.values = obj ? obj : [];
|
||||
if (this.values.length === 0 || this.values[0] !== '') {
|
||||
console.log(this.values);
|
||||
this.values.push(_.cloneDeep(this.pushTemplate));
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: any) {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any) {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
}
|
32
src/app/rb-custom-inputs/rb-custom-inputs.module.ts
Normal file
32
src/app/rb-custom-inputs/rb-custom-inputs.module.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RbTableComponent } from './rb-table/rb-table.component';
|
||||
import {RbArrayInputComponent, RbArrayInputListenerDirective, RbArrayInputItemDirective} from './rb-array-input/rb-array-input.component';
|
||||
import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import { RbIconButtonComponent } from './rb-icon-button/rb-icon-button.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
RbTableComponent,
|
||||
RbArrayInputComponent,
|
||||
RbArrayInputListenerDirective,
|
||||
RbArrayInputItemDirective,
|
||||
RbIconButtonComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RbUiComponentsModule
|
||||
],
|
||||
exports: [
|
||||
RbTableComponent,
|
||||
RbArrayInputComponent,
|
||||
RbArrayInputListenerDirective,
|
||||
RbArrayInputItemDirective,
|
||||
RbIconButtonComponent
|
||||
]
|
||||
})
|
||||
export class RbCustomInputsModule { }
|
@ -0,0 +1,4 @@
|
||||
<button class="rb-btn rb" [ngClass]="'rb-' + mode" type="button" [disabled]="disabled">
|
||||
<span class="rb-ic" [ngClass]="'rb-ic-' + icon"></span>
|
||||
<ng-content></ng-content>
|
||||
</button>
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RbIconButtonComponent } from './rb-icon-button.component';
|
||||
|
||||
describe('RbIconButtonComponent', () => {
|
||||
let component: RbIconButtonComponent;
|
||||
let fixture: ComponentFixture<RbIconButtonComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RbIconButtonComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RbIconButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
|
||||
// TODO: apply everywhere
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'rb-icon-button',
|
||||
templateUrl: './rb-icon-button.component.html',
|
||||
styleUrls: ['./rb-icon-button.component.scss']
|
||||
})
|
||||
export class RbIconButtonComponent implements OnInit {
|
||||
|
||||
@Input() icon: string;
|
||||
@Input() mode: string;
|
||||
@Input() disabled;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'rb-table',
|
||||
templateUrl: './rb-table.component.html',
|
||||
styleUrls: ['./rb-table.component.scss']
|
@ -1,18 +0,0 @@
|
||||
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 { }
|
@ -1,3 +1,4 @@
|
||||
<script src="samples.component.ts"></script>
|
||||
<div class="header-addnew">
|
||||
<h2>Samples</h2>
|
||||
<a routerLink="/samples/new">
|
||||
@ -87,8 +88,10 @@
|
||||
<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>
|
||||
<ng-container *ngIf="key.sortable">
|
||||
<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>
|
||||
</ng-container>
|
||||
</div>
|
||||
</th>
|
||||
<th></th>
|
||||
@ -104,7 +107,7 @@
|
||||
<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 *ngIf="isActiveKey['notes']">{{sample.notes | object: ['_id', 'sample_references']}}</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>
|
||||
|
@ -178,5 +178,5 @@ textarea.linkmodal {
|
||||
|
||||
.filter-inputs > * {
|
||||
display: inline-block;
|
||||
max-width: 250px;
|
||||
width: 220px;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {Component, ElementRef, isDevMode, OnInit, ViewChild} from '@angular/core
|
||||
import {ApiService} from '../services/api.service';
|
||||
import {AutocompleteService} from '../services/autocomplete.service';
|
||||
import _ from 'lodash';
|
||||
import {SampleModel} from '../models/sample.model';
|
||||
|
||||
|
||||
interface LoadSamplesOptions {
|
||||
@ -13,6 +14,7 @@ interface KeyInterface {
|
||||
id: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
sortable: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -21,9 +23,8 @@ interface KeyInterface {
|
||||
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
|
||||
|
||||
// TODO: check if custom-header.conf works, add headers from helmet https://docs.cloudfoundry.org/buildpacks/staticfile/index.html
|
||||
|
||||
export class SamplesComponent implements OnInit {
|
||||
|
||||
@ -32,7 +33,7 @@ export class SamplesComponent implements OnInit {
|
||||
|
||||
downloadCsv = false;
|
||||
materials = {};
|
||||
samples = [];
|
||||
samples: SampleModel[] = [];
|
||||
totalSamples = 0; // total number of samples
|
||||
csvUrl = ''; // store url separate so it only has to be generated when clicking the download button
|
||||
filters = {
|
||||
@ -61,16 +62,16 @@ export class SamplesComponent implements OnInit {
|
||||
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}
|
||||
{id: 'number', label: 'Number', active: true, sortable: true},
|
||||
{id: 'material.numbers', label: 'Material numbers', active: true, sortable: false},
|
||||
{id: 'material.name', label: 'Material name', active: true, sortable: true},
|
||||
{id: 'material.supplier', label: 'Supplier', active: true, sortable: true},
|
||||
{id: 'material.group', label: 'Material', active: false, sortable: true},
|
||||
{id: 'type', label: 'Type', active: true, sortable: true},
|
||||
{id: 'color', label: 'Color', active: true, sortable: true},
|
||||
{id: 'batch', label: 'Batch', active: true, sortable: true},
|
||||
{id: 'notes', label: 'Notes', active: false, sortable: false},
|
||||
{id: 'added', label: 'Added', active: true, sortable: true},
|
||||
];
|
||||
isActiveKey: {[key: string]: boolean} = {};
|
||||
activeKeys: KeyInterface[] = [];
|
||||
@ -112,8 +113,11 @@ export class SamplesComponent implements OnInit {
|
||||
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: ['']});
|
||||
const parameterName = encodeURIComponent(parameter.name);
|
||||
if (parameter.name !== 'dpt' && !templateKeys.find(e => new RegExp('.' + parameterName + '$').test(e.id))) { // exclude spectrum
|
||||
templateKeys.push({id: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`, label: `${this.ucFirst(item.name)} ${this.ucFirst(parameter.name)}`, active: false, sortable: true});
|
||||
this.filters.filters.push({field: `${collection === 'materials' ? 'material.properties' : collection + '.' + item.name}.${parameterName}`, 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);
|
||||
@ -220,6 +224,7 @@ export class SamplesComponent implements OnInit {
|
||||
|
||||
updateFilterFields(field) {
|
||||
const filter = this.filters.filters.find(e => e.field === field);
|
||||
filter.active = true;
|
||||
if (filter.mode === 'in' || filter.mode === 'nin') {
|
||||
if (filter.values[filter.values.length - 1] === '' && filter.values[filter.values.length - 2] === '') {
|
||||
filter.values.pop();
|
||||
@ -250,6 +255,8 @@ export class SamplesComponent implements OnInit {
|
||||
}
|
||||
|
||||
calcFieldSelectKeys() {
|
||||
console.log('CALC');
|
||||
console.log(this.keys);
|
||||
this.keys.forEach(key => {
|
||||
this.isActiveKey[key.id] = key.active;
|
||||
});
|
||||
|
@ -9,7 +9,10 @@ import {Observable} from 'rxjs';
|
||||
})
|
||||
export class LoginService implements CanActivate {
|
||||
|
||||
private maintainPaths = ['templates'];
|
||||
|
||||
private loggedIn;
|
||||
private level;
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
@ -27,6 +30,7 @@ export class LoginService implements CanActivate {
|
||||
if (!error) {
|
||||
if (data.status === 'Authorization successful') {
|
||||
this.loggedIn = true;
|
||||
this.level = data.level;
|
||||
resolve(true);
|
||||
} else {
|
||||
this.loggedIn = false;
|
||||
@ -49,14 +53,21 @@ export class LoginService implements CanActivate {
|
||||
|
||||
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);
|
||||
const isMaintainPath = this.maintainPaths.indexOf(route.url[0].path) >= 0;
|
||||
if (!isMaintainPath || (isMaintainPath && this.isMaintain)) {
|
||||
if (this.loggedIn === undefined) {
|
||||
this.login().then(res => {
|
||||
observer.next(res as any);
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
else {
|
||||
observer.next(this.loggedIn);
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
observer.next(this.loggedIn);
|
||||
observer.next(false);
|
||||
observer.complete();
|
||||
}
|
||||
});
|
||||
@ -66,6 +77,10 @@ export class LoginService implements CanActivate {
|
||||
return this.loggedIn;
|
||||
}
|
||||
|
||||
get isMaintain() {
|
||||
return this.level === 'maintain' || this.level === 'admin';
|
||||
}
|
||||
|
||||
get username() {
|
||||
return atob(this.storage.get('basicAuth')).split(':')[0];
|
||||
}
|
||||
|
@ -121,4 +121,53 @@ export class ValidationService {
|
||||
}
|
||||
return {ok: true, error: ''};
|
||||
}
|
||||
|
||||
parameterName(data) {
|
||||
const {ignore, error} = Joi.string()
|
||||
.max(128)
|
||||
.invalid('condition_template', 'material_template')
|
||||
.pattern(/^[^.]+$/)
|
||||
.required()
|
||||
.messages({'string.pattern.base': 'name must not contain a dot'})
|
||||
.validate(data);
|
||||
if (error) {
|
||||
return {ok: false, error: error.details[0].message};
|
||||
}
|
||||
return {ok: true, error: ''};
|
||||
}
|
||||
|
||||
parameterRange(data) {
|
||||
if (data) {
|
||||
try {
|
||||
const {ignore, error} = Joi.object({
|
||||
values: Joi.array()
|
||||
.min(1),
|
||||
|
||||
min: Joi.number(),
|
||||
|
||||
max: Joi.number(),
|
||||
|
||||
type: Joi.string()
|
||||
.valid('array')
|
||||
})
|
||||
.oxor('values', 'min')
|
||||
.oxor('values', 'max')
|
||||
.oxor('type', 'values')
|
||||
.oxor('type', 'min')
|
||||
.oxor('type', 'max')
|
||||
.required()
|
||||
.validate(JSON.parse(data));
|
||||
if (error) {
|
||||
return {ok: false, error: error.details[0].message};
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
return {ok: false, error: `no valid JSON`};
|
||||
}
|
||||
return {ok: true, error: ''};
|
||||
}
|
||||
else {
|
||||
return {ok: false, error: `no valid value`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
src/app/templates/templates.component.html
Normal file
66
src/app/templates/templates.component.html
Normal file
@ -0,0 +1,66 @@
|
||||
<h2>Templates</h2>
|
||||
|
||||
<rb-form-select name="collectionSelection" label="collection"
|
||||
[(ngModel)]="collection" (ngModelChange)="loadTemplates()">
|
||||
<option value="material">Materials</option>
|
||||
<option value="measurement">Measurements</option>
|
||||
<option value="condition">Conditions</option>
|
||||
</rb-form-select>
|
||||
|
||||
|
||||
<rb-icon-button icon="add" mode="primary" (click)="newTemplate()">New template</rb-icon-button>
|
||||
|
||||
<div class="list">
|
||||
<div class="row">
|
||||
<div class="header">Name</div>
|
||||
<div class="header">Version</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngFor="let group of groupsView">
|
||||
<div class="row clickable">
|
||||
<div (click)="group.expanded = !group.expanded">{{group.name}}</div>
|
||||
<div (click)="group.expanded = !group.expanded">{{group.version}}</div>
|
||||
</div>
|
||||
<div class="row" *ngIf="group.expanded" [@inOut]>
|
||||
<div class="details">
|
||||
<ng-container *ngFor="let template of group.entries">
|
||||
<div>{{template.name}}</div>
|
||||
<div>{{template.version}}</div>
|
||||
<div>{{template.parameters | parameters}}</div>
|
||||
</ng-container>
|
||||
<div class="template-actions">
|
||||
<form #templateForm="ngForm">
|
||||
<div *ngIf="group.edit">
|
||||
<rb-form-input [name]="'name-' + group.name" label="name" appValidate="string" required
|
||||
[(ngModel)]="templateEdit[group.first_id].name" #supplierInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{supplierInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-array-input [(ngModel)]="templateEdit[group.first_id].parameters" [name]="'parameters-' + group.name"
|
||||
[pushTemplate]="{name: '', range: {}, rangeString: '{}'}" pushPath="name"
|
||||
class="parameters">
|
||||
<ng-container *rbArrayInputItem="let item">
|
||||
<rb-form-input [rbArrayInputListener]="'parameter-name-' + group.name" appValidate="parameterName"
|
||||
[index]="item.i" [name]="'parameter-name-' + group.name + item.i" label="parameter name"
|
||||
[ngModel]="item.value.name" #parameterName="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{parameterName.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-textarea [name]="'parameter-range-' + group.name + item.i" label="range" appValidate="parameterRange"
|
||||
[(ngModel)]="item.value.rangeString" #parameterRange="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{parameterRange.errors.failure}}</ng-template>
|
||||
</rb-form-textarea>
|
||||
</ng-container>
|
||||
</rb-array-input>
|
||||
</div>
|
||||
<rb-icon-button icon="edit" mode="secondary" (click)="group.edit = !group.edit">
|
||||
Edit template
|
||||
</rb-icon-button>
|
||||
<rb-icon-button icon="save" mode="primary" (click)="saveTemplate(group.first_id)" *ngIf="group.edit">
|
||||
Save template
|
||||
</rb-icon-button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
44
src/app/templates/templates.component.scss
Normal file
44
src/app/templates/templates.component.scss
Normal file
@ -0,0 +1,44 @@
|
||||
@import "~@inst-iot/bosch-angular-ui-components/styles/variables/colors";
|
||||
|
||||
.list {
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 4fr;
|
||||
border-bottom: 1px solid $color-gray-mercury;
|
||||
overflow: hidden;
|
||||
|
||||
& > div {
|
||||
padding: 8px 5px;
|
||||
|
||||
&.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.details {
|
||||
grid-column: span 2;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 3fr;
|
||||
background: $color-gray-alabaster;
|
||||
|
||||
.template-actions {
|
||||
grid-column: span 3;
|
||||
margin-top: 10px;
|
||||
|
||||
.parameters {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
|
||||
rb-icon-button[icon="save"] {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
25
src/app/templates/templates.component.spec.ts
Normal file
25
src/app/templates/templates.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TemplatesComponent } from './templates.component';
|
||||
|
||||
describe('TemplatesComponent', () => {
|
||||
let component: TemplatesComponent;
|
||||
let fixture: ComponentFixture<TemplatesComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ TemplatesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TemplatesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
124
src/app/templates/templates.component.ts
Normal file
124
src/app/templates/templates.component.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {ApiService} from '../services/api.service';
|
||||
import {TemplateModel} from '../models/template.model';
|
||||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
import {ValidationService} from '../services/validation.service';
|
||||
import _ from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'app-templates',
|
||||
templateUrl: './templates.component.html',
|
||||
styleUrls: ['./templates.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 TemplatesComponent implements OnInit {
|
||||
|
||||
collection = 'measurement';
|
||||
templates: TemplateModel[] = [];
|
||||
templateGroups: {[first_id: string]: TemplateModel[]} = {}; // templates grouped by first_id
|
||||
templateEdit: {[first_id: string]: TemplateModel} = {}; // latest template of each first_id for editing
|
||||
groupsView: {first_id: string, name: string, version: number, expanded: boolean, edit: boolean, entries: TemplateModel[]}[] = [];
|
||||
arr = ['testA', 'testB', 'testC'];
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
private validate: ValidationService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTemplates();
|
||||
}
|
||||
|
||||
loadTemplates() {
|
||||
this.api.get<TemplateModel[]>(`/template/${this.collection}s`, data => {
|
||||
this.templates = data;
|
||||
this.templateFormat();
|
||||
});
|
||||
}
|
||||
|
||||
templateFormat() {
|
||||
this.templateGroups = {};
|
||||
this.templateEdit = {};
|
||||
this.templates.forEach(template => {
|
||||
if (this.templateGroups[template.first_id]) {
|
||||
this.templateGroups[template.first_id].push(template);
|
||||
}
|
||||
else {
|
||||
this.templateGroups[template.first_id] = [template];
|
||||
}
|
||||
});
|
||||
Object.keys(this.templateGroups).forEach(id => {
|
||||
this.templateGroups[id] = this.templateGroups[id].sort((a, b) => a.version - b.version);
|
||||
this.templateEdit[id] = _.cloneDeep(this.templateGroups[id][this.templateGroups[id].length - 1]);
|
||||
this.templateEdit[id].parameters = this.templateEdit[id].parameters.map(e => {e.rangeString = JSON.stringify(e.range, null, 2); return e; });
|
||||
});
|
||||
this.groupsView = Object.values(this.templateGroups)
|
||||
.map(e => ({
|
||||
first_id: e[e.length - 1].first_id,
|
||||
name: e[e.length - 1].name,
|
||||
version: e[e.length - 1].version,
|
||||
expanded: false,
|
||||
edit: false,
|
||||
entries: e
|
||||
}));
|
||||
}
|
||||
|
||||
saveTemplate(first_id) {
|
||||
const template = _.cloneDeep(this.templateEdit[first_id]);
|
||||
template.parameters = template.parameters.filter(e => e.name !== '');
|
||||
let valid = true;
|
||||
valid = valid && this.validate.string(template.name).ok;
|
||||
template.parameters.forEach(parameter => {
|
||||
valid = valid && this.validate.parameterName(parameter.name).ok;
|
||||
valid = valid && this.validate.parameterRange(parameter.rangeString).ok;
|
||||
if (valid) {
|
||||
parameter.range = JSON.parse(parameter.rangeString);
|
||||
}
|
||||
});
|
||||
if (valid) {
|
||||
console.log('valid', template);
|
||||
const sendData = {name: template.name, parameters: template.parameters.map(e => _.omit(e, ['rangeString']))};
|
||||
if (first_id === 'null') {
|
||||
this.api.post<TemplateModel>(`/template/${this.collection}/new`, sendData, data => {
|
||||
if (data.version > template.version) { // there were actual changes and a new version was created
|
||||
this.templates.push(data);
|
||||
}
|
||||
this.templateFormat();
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.api.put<TemplateModel>(`/template/${this.collection}/${template.first_id}`, sendData, data => {
|
||||
if (data.version > template.version) { // there were actual changes and a new version was created
|
||||
this.templates.push(data);
|
||||
}
|
||||
this.templateFormat();
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('not valid');
|
||||
}
|
||||
}
|
||||
|
||||
newTemplate() {
|
||||
if (!this.templateEdit.null) {
|
||||
const template = new TemplateModel();
|
||||
template.name = 'new template';
|
||||
this.groupsView.push({first_id: 'null', name: 'new template', version: 0, expanded: true, edit: true, entries: [template]});
|
||||
this.templateEdit.null = new TemplateModel();
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user