Merge pull request #5 in ~VLE2FE/dfop-ui from development to master
* commit 'f799ecc4a6cf550136f335d17115eb51c6b59c01': implemented icon buttons, array input, reformatting, minor sample component improvements settings and users dialog added templates component
This commit is contained in:
commit
ff733b0d05
@ -26,8 +26,7 @@
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{ "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" },
|
||||
"src/Staticfile"
|
||||
{ "glob": "**/*", "input": "./node_modules/@inst-iot/bosch-angular-ui-components/assets", "output": "./assets" }
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
|
@ -1,4 +1,4 @@
|
||||
pushstate: enabled
|
||||
force_https: true
|
||||
root: UI
|
||||
location_include: custom-header.conf
|
||||
location_include: ../../headers.conf
|
9
cf_config/headers.conf
Normal file
9
cf_config/headers.conf
Normal file
@ -0,0 +1,9 @@
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src https://definma-api.apps.de1.bosch-iot-cloud.com; form-action 'none'; frame-ancestors 'none'; base-uri 'self'";
|
||||
add_header X-Frame-Options DENY
|
||||
add_header X-DNS-Prefetch-Control off
|
||||
add_header Strict-Transport-Security max-age=15552000
|
||||
add_header X-Download-Options noopen
|
||||
add_header X-Content-Type-Options nosniff
|
||||
add_header X-Permitted-Cross-Domain-Policies none
|
||||
add_header Referrer-Policy no-referrer
|
||||
add_header X-XSS-Protection "1; mode=block"
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
applications:
|
||||
- name: definma
|
||||
path: dist/UI
|
||||
path: dist
|
||||
buildpacks:
|
||||
- staticfile_buildpack
|
||||
memory: 128M
|
||||
memory: 64M
|
||||
instances: 1
|
||||
stack: cflinuxfs3
|
||||
|
104
package-lock.json
generated
104
package-lock.json
generated
@ -2316,6 +2316,12 @@
|
||||
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
|
||||
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
|
||||
"dev": true
|
||||
},
|
||||
"adm-zip": {
|
||||
"version": "0.4.13",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz",
|
||||
@ -2892,6 +2898,18 @@
|
||||
"callsite": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bfj": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz",
|
||||
"integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bluebird": "^3.5.5",
|
||||
"check-types": "^8.0.3",
|
||||
"hoopy": "^0.1.4",
|
||||
"tryer": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
@ -3393,6 +3411,12 @@
|
||||
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-0.7.0.tgz",
|
||||
"integrity": "sha512-PKVUX14nYhH0wcdCpgOoC39Gbzvn6cZ7O9n+bwc02yKD9FTnJ7/TSrBcfebmolFZp1Rcicr9xbT0a5HUbigS7g=="
|
||||
},
|
||||
"check-types": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
|
||||
"integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
|
||||
@ -4782,6 +4806,12 @@
|
||||
"is-obj": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"duplexer": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
|
||||
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
|
||||
"dev": true
|
||||
},
|
||||
"duplexify": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
||||
@ -4810,6 +4840,12 @@
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
|
||||
"dev": true
|
||||
},
|
||||
"ejs": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz",
|
||||
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==",
|
||||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.446",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
||||
@ -5483,6 +5519,12 @@
|
||||
"minimatch": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"filesize": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
|
||||
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
|
||||
"dev": true
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@ -5879,6 +5921,16 @@
|
||||
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"gzip-size": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
|
||||
"integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"duplexer": "^0.1.1",
|
||||
"pify": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"handle-thing": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
|
||||
@ -6063,6 +6115,12 @@
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"hoopy": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
|
||||
"integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz",
|
||||
@ -9242,6 +9300,12 @@
|
||||
"is-wsl": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"opener": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
|
||||
"integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
|
||||
"dev": true
|
||||
},
|
||||
"opn": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
|
||||
@ -12324,6 +12388,11 @@
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
|
||||
"dev": true
|
||||
},
|
||||
"str-compare": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/str-compare/-/str-compare-0.1.2.tgz",
|
||||
"integrity": "sha1-eOaGGlccGiKhnq4Q5wmWt9z9r0Y="
|
||||
},
|
||||
"stream-browserify": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
|
||||
@ -12914,6 +12983,12 @@
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true
|
||||
},
|
||||
"tryer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
|
||||
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz",
|
||||
@ -13726,6 +13801,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"webpack-bundle-analyzer": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.8.0.tgz",
|
||||
"integrity": "sha512-PODQhAYVEourCcOuU+NiYI7WdR8QyELZGgPvB1y2tjbUpbmcQOt5Q7jEK+ttd5se0KSBKD9SXHCEozS++Wllmw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"acorn": "^7.1.1",
|
||||
"acorn-walk": "^7.1.1",
|
||||
"bfj": "^6.1.1",
|
||||
"chalk": "^2.4.1",
|
||||
"commander": "^2.18.0",
|
||||
"ejs": "^2.6.1",
|
||||
"express": "^4.16.3",
|
||||
"filesize": "^3.6.1",
|
||||
"gzip-size": "^5.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"mkdirp": "^0.5.1",
|
||||
"opener": "^1.5.1",
|
||||
"ws": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz",
|
||||
"integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"webpack-dev-middleware": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz",
|
||||
|
12
package.json
12
package.json
@ -5,12 +5,13 @@
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build --prod --aot",
|
||||
"build-push": "ng build --prod --aot && cf push",
|
||||
"build-push": "ng build --prod --aot && copy /Y cf_config\\ dist && cf push",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"coverage": "ng test --no-watch --code-coverage",
|
||||
"api": "cd C:\\Users\\vle2fe\\Documents\\Code\\API && node dist\\index.js"
|
||||
"api": "cd C:\\Users\\vle2fe\\Documents\\Code\\API && node dist\\index.js",
|
||||
"bundle-report": "ng build --prod --aot --stats-json && webpack-bundle-analyzer dist/UI/stats-es2015.json"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@ -23,7 +24,8 @@
|
||||
"@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",
|
||||
"@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",
|
||||
@ -32,6 +34,7 @@
|
||||
"ng2-charts": "^2.3.2",
|
||||
"quick-score": "0.0.8",
|
||||
"rxjs": "~6.5.5",
|
||||
"str-compare": "^0.1.2",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
@ -54,6 +57,7 @@
|
||||
"protractor": "~5.4.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.15.0",
|
||||
"typescript": "~3.8.3"
|
||||
"typescript": "~3.8.3",
|
||||
"webpack-bundle-analyzer": "^3.8.0"
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ 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';
|
||||
import {SettingsComponent} from './settings/settings.component';
|
||||
import {UsersComponent} from './users/users.component';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
@ -13,6 +16,10 @@ 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, canActivate: [LoginService]},
|
||||
// {path: 'users', component: UsersComponent, canActivate: [LoginService]},
|
||||
{path: 'users', component: UsersComponent}, // TODO: change
|
||||
{path: 'settings', component: SettingsComponent, canActivate: [LoginService]},
|
||||
{path: 'documentation', component: DocumentationComponent},
|
||||
|
||||
// if not authenticated
|
||||
|
@ -2,6 +2,8 @@
|
||||
<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.is('maintain')">Templates</a>
|
||||
<a routerLink="/users" routerLinkActive="active" rbLoadingLink *ngIf="loginService.is('admin')">Users</a>
|
||||
<a routerLink="/documentation" routerLinkActive="active" rbLoadingLink>Documentation</a>
|
||||
</nav>
|
||||
|
||||
@ -10,12 +12,9 @@
|
||||
<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>
|
||||
<ng-template #userPopover let-close="close">
|
||||
<div class="spacing">
|
||||
<p>
|
||||
<!-- Some user specific information-->
|
||||
</p>
|
||||
|
||||
<a routerLink="/settings" (click)="close()"><span class="rb-ic rb-ic-settings"></span> Settings</a>
|
||||
<button type="button" class="rb-btn rb-primary" (click)="logout()">Logout</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -3,3 +3,9 @@
|
||||
font-size: 32px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
|
@ -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,10 @@ 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';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { UsersComponent } from './users/users.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -34,7 +38,11 @@ import { ExistsPipe } from './exists.pipe';
|
||||
ObjectPipe,
|
||||
DocumentationComponent,
|
||||
ImgMagnifierComponent,
|
||||
ExistsPipe
|
||||
ExistsPipe,
|
||||
TemplatesComponent,
|
||||
ParametersPipe,
|
||||
SettingsComponent,
|
||||
UsersComponent
|
||||
],
|
||||
imports: [
|
||||
LocalStorageModule.forRoot({
|
||||
@ -47,7 +55,7 @@ import { ExistsPipe } from './exists.pipe';
|
||||
RbUiComponentsModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
RbTableModule,
|
||||
RbCustomInputsModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldsModule,
|
||||
CommonModule,
|
||||
|
@ -6,10 +6,14 @@
|
||||
<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">
|
||||
<rb-form-input *ngIf="!passreset" 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>
|
||||
<rb-form-input *ngIf="passreset" type="email" name="email" label="email" email required [(ngModel)]="email" #emailInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{emailInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<a href="#" class="forgot-pass" (click)="passreset = !passreset">Forgot password</a>
|
||||
<button class="rb-btn rb-primary login-button" (click)="login()" type="submit" [disabled]="!loginForm.form.valid">{{passreset ? 'Send' : 'Login'}}</button>
|
||||
<div class="message">{{message}}</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -4,9 +4,14 @@
|
||||
|
||||
.message {
|
||||
font-size: 13px;
|
||||
white-space: pre-line;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.forgot-pass {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {ValidationService} from '../services/validation.service';
|
||||
import {LoginService} from '../services/login.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {ApiService} from '../services/api.service';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -13,13 +14,17 @@ export class LoginComponent implements OnInit {
|
||||
|
||||
username = ''; // credentials
|
||||
password = '';
|
||||
email = '';
|
||||
message = ''; // message below login fields
|
||||
passreset = false;
|
||||
|
||||
@ViewChild('loginForm') loginForm;
|
||||
|
||||
|
||||
constructor(
|
||||
private validate: ValidationService,
|
||||
private loginService: LoginService,
|
||||
private api: ApiService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
@ -27,14 +32,26 @@ export class LoginComponent implements OnInit {
|
||||
}
|
||||
|
||||
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!';
|
||||
}
|
||||
});
|
||||
if (this.passreset) {
|
||||
this.api.post('/user/passreset', {name: this.username, email: this.email}, (data, err) => {
|
||||
if (err) {
|
||||
this.message = 'Could not find a valid user';
|
||||
}
|
||||
else {
|
||||
this.message = 'Password reset, check your inbox';
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.loginService.login(this.username, this.password).then(ok => {
|
||||
if (ok) {
|
||||
this.message = 'Login successful';
|
||||
this.router.navigate(['/samples']);
|
||||
}
|
||||
else {
|
||||
this.message = 'Wrong credentials!';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}[] = [];
|
||||
}
|
||||
|
7
src/app/models/user.model.spec.ts
Normal file
7
src/app/models/user.model.spec.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { UserModel } from './user.model';
|
||||
|
||||
describe('User.Model', () => {
|
||||
it('should create an instance', () => {
|
||||
expect(new UserModel()).toBeTruthy();
|
||||
});
|
||||
});
|
28
src/app/models/user.model.ts
Normal file
28
src/app/models/user.model.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import _ from 'lodash';
|
||||
import {BaseModel} from './base.model';
|
||||
import {IdModel} from './id.model';
|
||||
|
||||
export class UserModel extends BaseModel{
|
||||
_id: IdModel = null;
|
||||
name = '';
|
||||
origName = '';
|
||||
email = '';
|
||||
level = '';
|
||||
location = '';
|
||||
device_name = '';
|
||||
edit = false;
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
this.origName = this.name;
|
||||
return this;
|
||||
}
|
||||
|
||||
sendFormat(mode = 'user') {
|
||||
const keys = ['name', 'email', 'location', 'device_name'];
|
||||
if (mode === 'admin') {
|
||||
keys.push('level');
|
||||
}
|
||||
return _.pick(this, keys);
|
||||
}
|
||||
}
|
@ -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,149 @@
|
||||
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';
|
||||
|
||||
|
||||
@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;
|
||||
|
||||
constructor(
|
||||
private helperService: ArrayInputHelperService
|
||||
) { }
|
||||
|
||||
@HostListener('ngModelChange', ['$event'])
|
||||
onChange(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 {
|
||||
|
||||
pushTemplate: any = '';
|
||||
@Input('pushTemplate') set _pushTemplate(value) {
|
||||
this.pushTemplate = value;
|
||||
if (this.values.length) {
|
||||
this.updateArray();
|
||||
}
|
||||
}
|
||||
@Input() pushPath: string = null;
|
||||
|
||||
@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
|
||||
// assign value
|
||||
if (this.pushPath) {
|
||||
this.values[data.index][this.pushPath] = data.value;
|
||||
}
|
||||
else {
|
||||
this.values[data.index] = data.value;
|
||||
}
|
||||
console.log(111, this.values);
|
||||
this.updateArray();
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
updateArray() {
|
||||
let res;
|
||||
// adjust fields if pushTemplate is specified
|
||||
if (this.pushTemplate !== null) {
|
||||
if (this.pushPath) {
|
||||
// remove last element if last two are empty
|
||||
if (this.values[this.values.length - 1][this.pushPath] === '' && this.values[this.values.length - 2][this.pushPath] === '') {
|
||||
this.values.pop();
|
||||
}
|
||||
// add element if last all are filled
|
||||
else if (this.values.filter(e => e[this.pushPath] !== '').length === this.values.length) {
|
||||
this.values.push(_.cloneDeep(this.pushTemplate));
|
||||
}
|
||||
res = this.values.filter(e => e[this.pushPath] !== '');
|
||||
}
|
||||
else {
|
||||
// remove last element if last two are empty
|
||||
if (this.values[this.values.length - 1] === '' && this.values[this.values.length - 2] === '') {
|
||||
this.values.pop();
|
||||
}
|
||||
else if (this.values.filter(e => e !== '').length === this.values.length) { // add element if all are is filled
|
||||
this.values.push(_.cloneDeep(this.pushTemplate));
|
||||
}
|
||||
res = this.values.filter(e => e !== '');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.values = [this.values[0]];
|
||||
res = this.values;
|
||||
}
|
||||
if (!res.length) {
|
||||
res = [''];
|
||||
}
|
||||
this.onChange(res); // trigger ngModel with filled elements
|
||||
}
|
||||
|
||||
writeValue(obj: any) { // add empty value on init
|
||||
this.values = obj ? obj : [];
|
||||
if (this.values.length === 0 || this.values[0] !== '') {
|
||||
// add empty last field if pushTemplate is specified
|
||||
if (this.pushTemplate !== null) {
|
||||
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]="type" [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';
|
||||
|
||||
|
||||
@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;
|
||||
@Input() type = 'button';
|
||||
|
||||
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,4 +1,4 @@
|
||||
<h2>{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}</h2>
|
||||
<script src="../models/template.model.ts"></script><h2>{{new ? 'Add new sample' : 'Edit sample ' + sample.number}}</h2>
|
||||
|
||||
<rb-loading-spinner *ngIf="loading"></rb-loading-spinner>
|
||||
|
||||
@ -6,28 +6,48 @@
|
||||
<!--<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">
|
||||
<rb-form-input name="materialname" label="material name" [rbDebounceTime]="0" [rbInitialOpen]="true"
|
||||
[rbFormInputAutocomplete]="autocomplete.bind(this, ac.materialNames)" appValidate="stringOf"
|
||||
(keydown)="preventDefault($event)" (ngModelChange)="findMaterial($event)" ngModel
|
||||
[appValidateArgs]="[ac.materialName]" required [(ngModel)]="material.name" [autofocus]="true">
|
||||
<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>
|
||||
<rb-icon-button icon="add" mode="secondary" (click)="setNewMaterial(!newMaterial)">New material</rb-icon-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">
|
||||
<rb-form-input name="supplier" label="supplier" [rbFormInputAutocomplete]="autocomplete.bind(this, ac.supplier)"
|
||||
[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
|
||||
[(ngModel)]="material.supplier" #supplierInput="ngModel"
|
||||
(focusout)="checkTypo('supplier', modalWarning)">
|
||||
<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">
|
||||
<rb-form-input name="group" label="group" [rbFormInputAutocomplete]="autocomplete.bind(this, ac.mgroup)"
|
||||
[rbDebounceTime]="0" [rbInitialOpen]="true" appValidate="string" required
|
||||
[(ngModel)]="material.group" #groupInput="ngModel"
|
||||
(focusout)="checkTypo('mgroup', modalWarning)">
|
||||
<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">
|
||||
<ng-template #modalWarning>
|
||||
<rb-alert alertTitle="Warning" type="warning" okBtnLabel="Use suggestion" cancelBtnLabel="Keep value">
|
||||
The specified {{modalText.list}} could not be found in the list. <br>
|
||||
Did you mean {{modalText.suggestion}}?
|
||||
</rb-alert>
|
||||
</ng-template>
|
||||
<rb-array-input [(ngModel)]="material.numbers" name="materialNumbers" [pushTemplate]="''">
|
||||
<rb-form-input *rbArrayInputItem="let item" [rbArrayInputListener]="'materialNumber'" [index]="item.i"
|
||||
label="material number" appValidate="string" [name]="'materialNumber-' + item.i"
|
||||
[ngModel]="item.value"></rb-form-input>
|
||||
</rb-array-input>
|
||||
<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">
|
||||
<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>
|
||||
@ -36,11 +56,13 @@
|
||||
|
||||
|
||||
<div>
|
||||
<rb-form-input name="type" label="type" appValidate="string" required [(ngModel)]="sample.type" #typeInput="ngModel">
|
||||
<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">
|
||||
<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>
|
||||
@ -51,31 +73,43 @@
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<rb-form-input name="comment" label="comment" appValidate="stringLength" [appValidateArgs]="[512]" [(ngModel)]="sample.notes.comment" #commentInput="ngModel">
|
||||
<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">
|
||||
<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]" 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]">
|
||||
<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-array-input [(ngModel)]="customFields" name="customFields" [pushTemplate]="['', '']" pushPath="0"
|
||||
class="two-col" [@inOut]>
|
||||
<ng-container *rbArrayInputItem="let item">
|
||||
<div>
|
||||
<rb-form-input [name]="'cf-key' + item.i" label="key" [rbArrayInputListener]="'cf-key'" [index]="item.i"
|
||||
[rbFormInputAutocomplete]="autocomplete.bind(this, availableCustomFields)" [rbDebounceTime]="0"
|
||||
[rbInitialOpen]="true" appValidate="unique" [appValidateArgs]="[uniqueCfValues(item.i)]"
|
||||
[ngModel]="item.value[0]" #keyInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{keyInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
</div>
|
||||
<rb-form-input [name]="'cf-value' + item.i" label="value" appValidate="string" [required]="item.value[0] !== ''"
|
||||
[ngModel]="item.value[1]">
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</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>
|
||||
</ng-container>
|
||||
</rb-array-input>
|
||||
</div>
|
||||
|
||||
|
||||
@ -83,14 +117,18 @@
|
||||
<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>
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
@ -102,16 +140,23 @@
|
||||
<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)">
|
||||
<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">
|
||||
<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>
|
||||
<rb-form-file *ngIf="parameter.range.type" [name]="'measurementParameter' + mIndex + '-' + pIndex"
|
||||
[label]="parameter.name" maxSize="10000000" multiple
|
||||
(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]
|
||||
@ -122,20 +167,26 @@
|
||||
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>
|
||||
<rb-icon-button icon="delete" mode="danger" (click)="removeMeasurement(mIndex)"
|
||||
[disabled]="!measurementTemplates">
|
||||
Delete measurement
|
||||
</rb-icon-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>
|
||||
<rb-icon-button icon="add" mode="secondary" (click)="addMeasurement()" [disabled]="!measurementTemplates">
|
||||
New measurement
|
||||
</rb-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<button class="rb-btn rb-primary" type="submit" (click)="saveSample()" [disabled]="!sampleForm.form.valid">Save sample</button>
|
||||
<button class="rb-btn rb-primary" type="submit" (click)="saveSample()" [disabled]="!sampleForm.form.valid">
|
||||
Save sample
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
//
|
||||
// import { SampleComponent } from './sample.component';
|
||||
//
|
||||
// // TODO
|
||||
// // TODO: tests
|
||||
//
|
||||
// describe('SampleComponent', () => {
|
||||
// let component: SampleComponent;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import strCompare from 'str-compare';
|
||||
import {
|
||||
AfterContentChecked,
|
||||
Component,
|
||||
OnInit,
|
||||
OnInit, TemplateRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
@ -17,18 +18,14 @@ import {MeasurementModel} from '../models/measurement.model';
|
||||
import { ChartOptions } from 'chart.js';
|
||||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
import {Observable} from 'rxjs';
|
||||
import {ModalService} from '@inst-iot/bosch-angular-ui-components';
|
||||
import {UserModel} from '../models/user.model';
|
||||
|
||||
|
||||
// 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',
|
||||
@ -55,16 +52,18 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
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
|
||||
ac: {[group: string]: string[]} = { // autocomplete data
|
||||
supplier: [],
|
||||
group: [],
|
||||
materialName: []
|
||||
};
|
||||
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][] = [['', '']];
|
||||
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
|
||||
@ -74,7 +73,9 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
measurementTemplates: TemplateModel[];
|
||||
loading = 0; // number of currently loading instances
|
||||
checkFormAfterInit = false;
|
||||
charts = []; // chart data for spectrums
|
||||
modalText = {list: '', suggestion: ''};
|
||||
charts = []; // chart data for spectra
|
||||
defaultDevice = '';
|
||||
readonly chartInit = [{
|
||||
data: [],
|
||||
label: 'Spectrum',
|
||||
@ -101,7 +102,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
private route: ActivatedRoute,
|
||||
private api: ApiService,
|
||||
private validation: ValidationService,
|
||||
public autocomplete: AutocompleteService
|
||||
public autocomplete: AutocompleteService,
|
||||
private modal: ModalService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -109,15 +111,15 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
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.ac.materialName = data.map(e => e.name);
|
||||
this.loading--;
|
||||
});
|
||||
this.api.get<string[]>('/material/suppliers', (data: any) => {
|
||||
this.suppliers = data;
|
||||
this.ac.supplier = data;
|
||||
this.loading--;
|
||||
});
|
||||
this.api.get<string[]>('/material/groups', (data: any) => {
|
||||
this.groups = data;
|
||||
this.ac.mgroup = data;
|
||||
this.loading--;
|
||||
});
|
||||
this.api.get<TemplateModel[]>('/template/conditions', data => {
|
||||
@ -129,6 +131,9 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
this.selectMaterialTemplate(this.materialTemplates[0]._id);
|
||||
this.loading--;
|
||||
});
|
||||
this.api.get<UserModel>('/user', data => {
|
||||
this.defaultDevice = data.device_name;
|
||||
});
|
||||
this.api.get<TemplateModel[]>('/template/measurements', data => {
|
||||
this.measurementTemplates = data.map(e => new TemplateModel().deserialize(e));
|
||||
if (!this.new) {
|
||||
@ -149,7 +154,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
});
|
||||
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]]) : [['', '']];
|
||||
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 = [];
|
||||
@ -185,7 +191,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
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')) {
|
||||
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);
|
||||
@ -194,7 +201,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
|
||||
// attach validators to dynamic material fields when all values are available and template was fully created
|
||||
if (this.materialTemplate && this.materialTemplate.hasOwnProperty('parameters') && this.materialTemplate.parameters.length > 0 && this.materialTemplate.parameters[0].hasOwnProperty('range') && this.sampleForm && this.sampleForm.form.get('materialParameter0')) {
|
||||
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);
|
||||
@ -250,7 +258,6 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
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;
|
||||
@ -268,7 +275,9 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
this.sample.notes.custom_fields[element[0]] = element[1];
|
||||
}
|
||||
});
|
||||
this.sample.notes.sample_references = this.sampleReferences.filter(e => e[0] && e[1] && e[2]).map(e => ({sample_id: e[2], relation: e[1]}));
|
||||
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);
|
||||
@ -289,7 +298,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
this.api.post<MeasurementModel>('/measurement/new', measurement.sendFormat());
|
||||
}
|
||||
else { // update measurement
|
||||
this.api.put<MeasurementModel>('/measurement/' + measurement._id, measurement.sendFormat(['sample_id', 'measurement_template']));
|
||||
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
|
||||
@ -321,7 +331,6 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: rework later
|
||||
setNewMaterial(value = null) {
|
||||
if (value === null) {
|
||||
this.newMaterial = !this.sample.material_id;
|
||||
@ -333,24 +342,12 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
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')
|
||||
.setValidators([Validators.required, this.validation.generate('stringOf', [this.ac.materialName])]);
|
||||
}
|
||||
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);
|
||||
@ -372,7 +369,8 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
|
||||
addMeasurement() {
|
||||
this.sample.measurements.push(new MeasurementModel(this.measurementTemplates[0]._id));
|
||||
this.sample.measurements.push(new MeasurementModel(this.measurementTemplates.filter(e => e.name === 'spectrum').reverse()[0]._id));
|
||||
this.sample.measurements[this.sample.measurements.length - 1].values.device = this.defaultDevice;
|
||||
this.charts.push(_.cloneDeep(this.chartInit));
|
||||
}
|
||||
|
||||
@ -389,12 +387,22 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
|
||||
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]);
|
||||
for (const i in files) {
|
||||
if (files.hasOwnProperty(i)) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
let index: number = mIndex;
|
||||
if (Number(i) > 0) { // append further spectra
|
||||
this.addMeasurement();
|
||||
index = this.sample.measurements.length - 1;
|
||||
}
|
||||
this.sample.measurements[index].values[parameter] =
|
||||
fileReader.result.toString().split('\r\n').map(e => e.split(','));
|
||||
this.generateChart(this.sample.measurements[index].values[parameter], index);
|
||||
};
|
||||
fileReader.readAsText(files[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generateChart(spectrum, index) {
|
||||
@ -411,22 +419,17 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
checkTypo(list, modal: TemplateRef<any>) {
|
||||
if (this.ac[list].indexOf(this.material[list]) < 0) { // entry is not in lise
|
||||
this.modalText.list = list;
|
||||
this.modalText.suggestion = this.ac[list] // find possible entry from list
|
||||
.map(e => ({v: e, s: strCompare.sorensenDice(e, this.material[list])}))
|
||||
.sort((a, b) => b.s - a.s)[0].v;
|
||||
this.modal.open(modal).then(result => {
|
||||
if (result) { // use suggestion
|
||||
this.material[list] = this.modalText.suggestion;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -457,7 +460,9 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
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 => {
|
||||
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;
|
||||
@ -483,12 +488,14 @@ export class SampleComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
|
||||
uniqueCfValues(index) { // returns all names until index for unique check
|
||||
return this.customFields.slice(0, index).map(e => e[0]);
|
||||
return this.customFields ? 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
|
||||
// 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^^
|
||||
// 3. Die Dirty-Variante: Pack in deine ngFor ein {{ onFirstLoad(data) }} rein, das einfach ausgeführt wird. müsstest dann natürlich
|
||||
// abfangen, dass das nicht nach jedem view-cycle neu getriggert wird. Schön ist das nicht, aber besser als mit Timeouts^^
|
||||
|
@ -1,7 +1,8 @@
|
||||
<script src="samples.component.ts"></script>
|
||||
<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>
|
||||
<rb-icon-button icon="add" mode="primary">New sample</rb-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -11,14 +12,17 @@
|
||||
<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})">
|
||||
<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})">
|
||||
<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>
|
||||
<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>
|
||||
@ -28,15 +32,18 @@
|
||||
<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)">
|
||||
<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)">
|
||||
<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>
|
||||
@ -48,13 +55,26 @@
|
||||
<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>
|
||||
<rb-array-input [(ngModel)]="filter.values" [name]="'filter-' + filter.field"
|
||||
[pushTemplate]="!(filter.mode === 'in' || filter.mode === 'nin') ? null :''"
|
||||
(ngModelChange)="updateFilterFields(filter.field)">
|
||||
<ng-container *rbArrayInputItem="let item"
|
||||
[ngSwitch]="(filter.autocomplete.length ? 'autocomplete' : '') +
|
||||
(filter.field == 'added' ? 'date' : '')">
|
||||
<rb-form-date-input *ngSwitchCase="'date'" [rbArrayInputListener]="'filter-' + filter.field"
|
||||
[name]="'filter-' + filter.field + item.i" [index]="item.i"
|
||||
[label]="filter.label" [(ngModel)]="item.value"></rb-form-date-input>
|
||||
<rb-form-input *ngSwitchCase="''" [rbArrayInputListener]="'filter-' + filter.field"
|
||||
[name]="'filter-' + filter.field + item.i" [index]="item.i"
|
||||
[label]="filter.label" [(ngModel)]="item.value"></rb-form-input>
|
||||
<rb-form-input *ngSwitchCase="'autocomplete'" [rbArrayInputListener]="'filter-' + filter.field"
|
||||
[name]="'filter-' + filter.field + item.i" [index]="item.i"
|
||||
[label]="filter.label" [(ngModel)]="item.value"
|
||||
[rbDebounceTime]="0" (keydown)="preventDefault($event, 'Enter')"
|
||||
[rbFormInputAutocomplete]="autocomplete.bind(this, filter.autocomplete)"
|
||||
ngModel></rb-form-input>
|
||||
</ng-container>
|
||||
</rb-array-input>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@ -67,18 +87,20 @@
|
||||
<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-icon-button icon="download" mode="secondary" [rbModal]="linkModal">JSON download link</rb-icon-button>
|
||||
<ng-template #linkModal>
|
||||
<label for="jsonUrl">URL for JSON download</label>
|
||||
<textarea class="linkmodal" id="jsonUrl" #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>
|
||||
<rb-icon-button icon="clipboard" mode="secondary" (click)="clipboard()">Copy to clipboard</rb-icon-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>
|
||||
<rb-icon-button icon="download" mode="secondary" (mousedown)="csvUrl = sampleUrl({csv: true, export: true})">
|
||||
Download result as CSV
|
||||
</rb-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -87,8 +109,14 @@
|
||||
<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>
|
||||
@ -100,11 +128,13 @@
|
||||
<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 *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 *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>
|
||||
@ -118,7 +148,8 @@
|
||||
<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>
|
||||
<rb-form-input label="page" (change)="loadPage({toPage: $event.target.value - page})" [ngModel]="page">
|
||||
</rb-form-input>
|
||||
<span>
|
||||
of {{pages}} ({{totalSamples}} samples)
|
||||
</span>
|
||||
|
@ -178,5 +178,5 @@ textarea.linkmodal {
|
||||
|
||||
.filter-inputs > * {
|
||||
display: inline-block;
|
||||
max-width: 250px;
|
||||
width: 220px;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
//
|
||||
// import { SamplesComponent } from './samples.component';
|
||||
//
|
||||
// // TODO
|
||||
// // TODO: tests
|
||||
//
|
||||
// describe('SamplesComponent', () => {
|
||||
// let component: SamplesComponent;
|
||||
|
@ -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 = {
|
||||
@ -53,7 +54,7 @@ export class SamplesComponent implements OnInit {
|
||||
{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()]}
|
||||
{field: 'added', label: 'Added', active: false, autocomplete: [], mode: 'eq', values: ['']}
|
||||
]
|
||||
};
|
||||
page = 1;
|
||||
@ -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[] = [];
|
||||
@ -91,7 +92,8 @@ export class SamplesComponent implements OnInit {
|
||||
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.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}) => {
|
||||
@ -112,8 +114,24 @@ 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);
|
||||
// exclude spectrum
|
||||
if (parameter.name !== 'dpt' && !templateKeys.find(e => new RegExp('.' + parameterName + '$').test(e.id))) {
|
||||
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);
|
||||
@ -152,10 +170,22 @@ export class SamplesComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
sampleUrl(options: {paging?: boolean, pagingOptions?: {firstPage?: boolean, toPage?: number, event?: Event}, csv?: boolean, export?: boolean, host?: boolean}) { // return url to fetch samples
|
||||
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')));
|
||||
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) {
|
||||
@ -178,11 +208,11 @@ export class SamplesComponent implements OnInit {
|
||||
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
|
||||
// do not load material properties for table
|
||||
if (key.active && (options.export || (!options.export && key.id.indexOf('material') < 0))) {
|
||||
query.push('fields[]=' + key.id);
|
||||
}
|
||||
});
|
||||
console.log(this.filters.filters);
|
||||
|
||||
query.push(..._.cloneDeep(this.filters.filters)
|
||||
.map(e => {
|
||||
@ -195,7 +225,6 @@ export class SamplesComponent implements OnInit {
|
||||
.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
|
||||
@ -206,8 +235,9 @@ export class SamplesComponent implements OnInit {
|
||||
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('&');
|
||||
return (options.host && isDevMode() ? window.location.host : '') +
|
||||
(options.export ? this.api.hostName : '') +
|
||||
'/samples?' + query.join('&');
|
||||
}
|
||||
|
||||
loadPage(delta) {
|
||||
@ -220,17 +250,7 @@ export class SamplesComponent implements OnInit {
|
||||
|
||||
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];
|
||||
}
|
||||
filter.active = true;
|
||||
if (filter.active) {
|
||||
this.loadSamples({firstPage: true});
|
||||
}
|
||||
@ -243,10 +263,13 @@ export class SamplesComponent implements OnInit {
|
||||
|
||||
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
|
||||
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))); // TODO: glass fiber filter not working
|
||||
}
|
||||
|
||||
calcFieldSelectKeys() {
|
||||
|
@ -9,7 +9,20 @@ import {Observable} from 'rxjs';
|
||||
})
|
||||
export class LoginService implements CanActivate {
|
||||
|
||||
private pathPermissions = [
|
||||
{path: 'templates', permission: 'maintain'},
|
||||
{path: 'users', permission: 'admin'}
|
||||
];
|
||||
readonly levels = [
|
||||
'read',
|
||||
'write',
|
||||
'maintain',
|
||||
'dev',
|
||||
'admin'
|
||||
];
|
||||
|
||||
private loggedIn;
|
||||
private level;
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
@ -20,13 +33,30 @@ export class LoginService implements CanActivate {
|
||||
|
||||
login(username = '', password = '') {
|
||||
return new Promise(resolve => {
|
||||
if (username !== '') {
|
||||
this.storage.set('basicAuth', btoa(username + ':' + password));
|
||||
if (username !== '' || password !== '') { // some credentials given
|
||||
let credentials: string[];
|
||||
const credentialString: string = this.storage.get('basicAuth');
|
||||
if (credentialString) { // found stored credentials
|
||||
credentials = atob(credentialString).split(':');
|
||||
}
|
||||
else {
|
||||
credentials = ['', ''];
|
||||
}
|
||||
if (username !== '' && password !== '') { // all credentials given
|
||||
this.storage.set('basicAuth', btoa(username + ':' + password));
|
||||
}
|
||||
else if (username !== '') { // username given
|
||||
this.storage.set('basicAuth', btoa(username + ':' + credentials[1]));
|
||||
}
|
||||
else if (password !== '') { // password given
|
||||
this.storage.set('basicAuth', btoa(credentials[0] + ':' + password));
|
||||
}
|
||||
}
|
||||
this.api.get('/authorized', (data: any, error) => {
|
||||
if (!error) {
|
||||
if (data.status === 'Authorization successful') {
|
||||
this.loggedIn = true;
|
||||
this.level = data.level;
|
||||
resolve(true);
|
||||
} else {
|
||||
this.loggedIn = false;
|
||||
@ -49,14 +79,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 pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0);
|
||||
if (!pathPermission || this.is(pathPermission.permission)) { // check if level is permitted for path
|
||||
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 +103,10 @@ export class LoginService implements CanActivate {
|
||||
return this.loggedIn;
|
||||
}
|
||||
|
||||
is(level) {
|
||||
return this.levels.indexOf(this.level) >= this.levels.indexOf(level);
|
||||
}
|
||||
|
||||
get username() {
|
||||
return atob(this.storage.get('basicAuth')).split(':')[0];
|
||||
}
|
||||
|
@ -66,10 +66,16 @@ export class ValidationService {
|
||||
return {ok: true, error: ''};
|
||||
}
|
||||
|
||||
string(data) {
|
||||
const {ignore, error} = Joi.string().max(128).allow('').validate(data);
|
||||
string(data, option = null) {
|
||||
let validator = Joi.string().max(128).allow('');
|
||||
let errorMsg = 'must contain max 128 characters';
|
||||
if (option === 'alphanum') {
|
||||
validator = validator.alphanum();
|
||||
errorMsg = 'must contain max 128 alphanumerical characters';
|
||||
}
|
||||
const {ignore, error} = validator.validate(data);
|
||||
if (error) {
|
||||
return {ok: false, error: 'must contain max 128 characters'};
|
||||
return {ok: false, error: errorMsg};
|
||||
}
|
||||
return {ok: true, error: ''};
|
||||
}
|
||||
@ -121,4 +127,60 @@ export class ValidationService {
|
||||
}
|
||||
return {ok: true, error: ''};
|
||||
}
|
||||
|
||||
equal(data, compare) {
|
||||
if (data !== compare) {
|
||||
return {ok: false, error: `must be equal`};
|
||||
}
|
||||
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`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
45
src/app/settings/settings.component.html
Normal file
45
src/app/settings/settings.component.html
Normal file
@ -0,0 +1,45 @@
|
||||
<h2>Settings</h2>
|
||||
|
||||
<form #userForm="ngForm">
|
||||
<rb-form-input name="name" label="user name" appValidate="username" required [(ngModel)]="user.name"
|
||||
#nameInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-input name="email" label="email" email required [(ngModel)]="user.email" ngModel>
|
||||
<ng-template rbFormValidationMessage="email">Invalid email</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-input name="location" label="location" appValidate="string" required [appValidateArgs]="['alphanum']"
|
||||
[(ngModel)]="user.location" #locationInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-input name="device" label="device" appValidate="string" [(ngModel)]="user.device_name"
|
||||
#deviceInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!userForm.form.valid" (click)="saveUser()">
|
||||
Save change
|
||||
</rb-icon-button>
|
||||
<span class="message">{{messageUser}}</span>
|
||||
</form>
|
||||
|
||||
|
||||
<h4 class="pass-heading">Change password</h4>
|
||||
|
||||
<form #passForm="ngForm">
|
||||
<rb-form-input name="passA" type="password" label="new password" appValidate="password" required
|
||||
[(ngModel)]="password" #passAInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{passAInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-input name="passB" type="password" label="confirm password" appValidate="equal"
|
||||
[appValidateArgs]="[password]" required #passBInput="ngModel" ngModel>
|
||||
<ng-template rbFormValidationMessage="failure">{{passBInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<button class="rb-btn rb-primary" type="submit" [disabled]="!passForm.form.valid" (click)="savePass()">
|
||||
Change password
|
||||
</button>
|
||||
<span class="message">{{messagePass}}</span>
|
||||
</form>
|
||||
|
7
src/app/settings/settings.component.scss
Normal file
7
src/app/settings/settings.component.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.pass-heading {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-left: 20px;
|
||||
}
|
25
src/app/settings/settings.component.spec.ts
Normal file
25
src/app/settings/settings.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SettingsComponent } from './settings.component';
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SettingsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
68
src/app/settings/settings.component.ts
Normal file
68
src/app/settings/settings.component.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {ApiService} from '../services/api.service';
|
||||
import {UserModel} from '../models/user.model';
|
||||
import {Router} from '@angular/router';
|
||||
import {LoginService} from '../services/login.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
|
||||
user: UserModel = new UserModel();
|
||||
password = '';
|
||||
messageUser = '';
|
||||
messagePass = '';
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
private login: LoginService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.api.get<UserModel>('/user', data => {
|
||||
this.user.deserialize(data);
|
||||
});
|
||||
}
|
||||
|
||||
saveUser() {
|
||||
this.api.put<UserModel>('/user', this.user.sendFormat(), (data, err) => {
|
||||
if (err) {
|
||||
this.messageUser = err.error.status;
|
||||
}
|
||||
else {
|
||||
this.login.login(data.name).then(res => {
|
||||
if (res) {
|
||||
this.router.navigate(['/samples']);
|
||||
}
|
||||
else {
|
||||
this.messageUser = 'request not successful, try again';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
savePass() {
|
||||
this.api.put<UserModel>('/user', {pass: this.password}, (ignore, err) => {
|
||||
if (err) {
|
||||
this.messagePass = err.error.status;
|
||||
}
|
||||
else {
|
||||
this.login.login('', this.password).then(res => {
|
||||
if (res) {
|
||||
this.router.navigate(['/samples']);
|
||||
}
|
||||
else {
|
||||
this.messagePass = 'request not successful, try again';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
67
src/app/templates/templates.component.html
Normal file
67
src/app/templates/templates.component.html
Normal file
@ -0,0 +1,67 @@
|
||||
<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>
|
||||
|
40
src/app/templates/templates.component.scss
Normal file
40
src/app/templates/templates.component.scss
Normal file
@ -0,0 +1,40 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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();
|
||||
}
|
||||
}
|
||||
}
|
94
src/app/users/users.component.html
Normal file
94
src/app/users/users.component.html
Normal file
@ -0,0 +1,94 @@
|
||||
<h2>Users</h2>
|
||||
|
||||
<rb-icon-button icon="add" mode="primary" (click)="addNewUser()">New user</rb-icon-button>
|
||||
|
||||
<form *ngIf="newUser" #userForm="ngForm">
|
||||
<rb-form-input name="name" label="user name" appValidate="username" required [(ngModel)]="newUser.name"
|
||||
#nameInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-input name="email" label="email" email required [(ngModel)]="newUser.email" ngModel>
|
||||
<ng-template rbFormValidationMessage="email">Invalid email</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-select name="level" label="level" required [(ngModel)]="newUser.level">
|
||||
<option *ngFor="let level of login.levels" [value]="level">{{level}}</option>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-select>
|
||||
<rb-form-input name="location" label="location" appValidate="string" required [appValidateArgs]="['alphanum']"
|
||||
[(ngModel)]="newUser.location" #locationInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-input name="device" label="device" appValidate="string" [(ngModel)]="newUser.device_name"
|
||||
#deviceInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-input name="passA" type="password" label="new password" appValidate="password" required
|
||||
[(ngModel)]="newUserPass" #passAInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{passAInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-form-input name="passB" type="password" label="confirm password" appValidate="equal"
|
||||
[appValidateArgs]="[newUserPass]" required #passBInput="ngModel" ngModel>
|
||||
<ng-template rbFormValidationMessage="failure">{{passBInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
<rb-icon-button icon="save" mode="primary" type="submit" [disabled]="!userForm.form.valid" (click)="saveNewUser()">
|
||||
Save user
|
||||
</rb-icon-button>
|
||||
</form>
|
||||
|
||||
<rb-table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Level</th>
|
||||
<th>Location</th>
|
||||
<th>Device</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr *ngFor="let user of users">
|
||||
<ng-container *ngIf="!user.edit; else editUser">
|
||||
<td>{{user.name}}</td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>{{user.level}}</td>
|
||||
<td>{{user.location}}</td>
|
||||
<td>{{user.device_name}}</td>
|
||||
<td><span class="rb-ic rb-ic-edit clickable" (click)="user.edit = true"></span></td>
|
||||
</ng-container>
|
||||
<ng-template #editUser>
|
||||
<td>
|
||||
<rb-form-input [name]="'name-' + user.name" appValidate="username" required [(ngModel)]="user.name" #nameInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{nameInput.errors.failure}}</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
</td>
|
||||
<td>
|
||||
<rb-form-input [name]="'email-' + user.name" email required [(ngModel)]="user.email" #emailInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="email">Invalid email</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
</td>
|
||||
<td>
|
||||
<rb-form-select [name]="'level-' + user.name" [(ngModel)]="user.level">
|
||||
<option *ngFor="let level of login.levels" [value]="level">{{level}}</option>
|
||||
</rb-form-select>
|
||||
</td>
|
||||
<td>
|
||||
<rb-form-input [name]="'location-' + user.name" appValidate="string" required [appValidateArgs]="['alphanum']"
|
||||
[(ngModel)]="user.location" #locationInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{locationInput.errors.failure}}</ng-template>
|
||||
<ng-template rbFormValidationMessage="required">Cannot be empty</ng-template>
|
||||
</rb-form-input>
|
||||
</td>
|
||||
<td>
|
||||
<rb-form-input [name]="'device-' + user.name" appValidate="string" [(ngModel)]="user.device_name"
|
||||
#deviceInput="ngModel">
|
||||
<ng-template rbFormValidationMessage="failure">{{deviceInput.errors.failure}}</ng-template>
|
||||
</rb-form-input>
|
||||
</td>
|
||||
<td><rb-icon-button icon="save" mode="primary" (click)="saveUser(user)" [disabled]="nameInput.invalid || emailInput.invalid || locationInput.invalid || deviceInput.invalid">Save</rb-icon-button></td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</rb-table>
|
3
src/app/users/users.component.scss
Normal file
3
src/app/users/users.component.scss
Normal file
@ -0,0 +1,3 @@
|
||||
::ng-deep td .error-messages {
|
||||
position: absolute;
|
||||
}
|
25
src/app/users/users.component.spec.ts
Normal file
25
src/app/users/users.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UsersComponent } from './users.component';
|
||||
|
||||
describe('UsersComponent', () => {
|
||||
let component: UsersComponent;
|
||||
let fixture: ComponentFixture<UsersComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ UsersComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UsersComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
47
src/app/users/users.component.ts
Normal file
47
src/app/users/users.component.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {ApiService} from '../services/api.service';
|
||||
import {UserModel} from '../models/user.model';
|
||||
import {LoginService} from '../services/login.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-users',
|
||||
templateUrl: './users.component.html',
|
||||
styleUrls: ['./users.component.scss']
|
||||
})
|
||||
export class UsersComponent implements OnInit {
|
||||
|
||||
users: UserModel[] = [];
|
||||
newUser: UserModel | null = null;
|
||||
newUserPass = '';
|
||||
|
||||
constructor(
|
||||
private api: ApiService,
|
||||
public login: LoginService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.api.get<UserModel[]>('/users', data => {
|
||||
this.users = data.map(e => new UserModel().deserialize(e));
|
||||
});
|
||||
}
|
||||
|
||||
saveUser(user: UserModel) {
|
||||
this.api.put<UserModel>('/user/' + user.origName, user.sendFormat('admin'), data => {
|
||||
user.deserialize(data);
|
||||
user.edit = false;
|
||||
});
|
||||
}
|
||||
|
||||
saveNewUser() {
|
||||
this.api.post('/user/new', {...this.newUser.sendFormat('admin'), pass: this.newUserPass}, data => {
|
||||
this.newUser = null;
|
||||
this.users.push(new UserModel().deserialize(data));
|
||||
});
|
||||
}
|
||||
|
||||
addNewUser() {
|
||||
this.newUser = this.newUser ? null : new UserModel();
|
||||
}
|
||||
|
||||
}
|
82
src/assets/imgs/supergraphic.svg
Normal file
82
src/assets/imgs/supergraphic.svg
Normal file
@ -0,0 +1,82 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="300" width="720" version="1.1" y="0" x="0" viewBox="0 0 720 300">
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: url("#SVGID_1_");
|
||||
}
|
||||
.st1 {
|
||||
fill: url("#SVGID_2_");
|
||||
}
|
||||
.st2 {
|
||||
fill: url("#SVGID_3_");
|
||||
}
|
||||
.st3 {
|
||||
fill: url("#SVGID_4_");
|
||||
}
|
||||
.st4 {
|
||||
fill: url("#SVGID_5_");
|
||||
}
|
||||
.st5 {
|
||||
fill: #AF2024;
|
||||
}
|
||||
.st6 {
|
||||
fill: url("#SVGID_6_");
|
||||
}
|
||||
.st7 {
|
||||
fill: #941B1E;
|
||||
}
|
||||
.st8 {
|
||||
fill: #B12739;
|
||||
}
|
||||
.st9 {
|
||||
fill: #952432;
|
||||
}
|
||||
.st10 {
|
||||
fill: #D42027;
|
||||
}
|
||||
.st11 {
|
||||
fill: url("#SVGID_7_");
|
||||
}
|
||||
.st12 {
|
||||
fill: url("#SVGID_8_");
|
||||
}
|
||||
.st13 {
|
||||
fill: #1C9A48;
|
||||
}
|
||||
.st14 {
|
||||
fill: url("#SVGID_9_");
|
||||
}
|
||||
.st15 {
|
||||
fill: url("#SVGID_10_");
|
||||
}
|
||||
.st16 {
|
||||
fill: #2A3886;
|
||||
}
|
||||
.st17 {
|
||||
fill: url("#SVGID_11_");
|
||||
}
|
||||
.st18 {
|
||||
fill: url("#SVGID_12_");
|
||||
}
|
||||
.st19 {
|
||||
fill: url("#SVGID_13_");
|
||||
}
|
||||
.st20 {
|
||||
fill: url("#SVGID_14_");
|
||||
}
|
||||
</style>
|
||||
<g transform="translate(-1.55,-3.3)">
|
||||
<linearGradient id="SVGID_1_" y2="-32.663" gradientUnits="userSpaceOnUse" y1="-32.663" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="842.08" x1="118.98"><stop stop-color="#952331" offset="0"/><stop stop-color="#921C1D" offset=".036094"/><stop stop-color="#B02739" offset=".084649"/><stop stop-color="#AD1F24" offset=".1237"/><stop stop-color="#C72026" offset=".1509"/><stop stop-color="#D42027" offset=".1697"/><stop stop-color="#CC2431" offset=".1758"/><stop stop-color="#B72B4C" offset=".1888"/><stop stop-color="#953371" offset=".2074"/><stop stop-color="#88357F" offset=".2142"/><stop stop-color="#853681" offset=".2436"/><stop stop-color="#6F368B" offset=".2638"/><stop stop-color="#39428F" offset=".2911"/><stop stop-color="#233D7D" offset=".3242"/><stop stop-color="#322C6F" offset=".4181"/><stop stop-color="#2A3885" offset=".494"/><stop stop-color="#1D62A1" offset=".5581"/><stop stop-color="#276CA5" offset=".5702"/><stop stop-color="#438EB3" offset=".6103"/><stop stop-color="#55A5BC" offset=".6399"/><stop stop-color="#5CAFBF" offset=".6556"/><stop stop-color="#56ABBD" offset=".6777"/><stop stop-color="#439FB8" offset=".7058"/><stop stop-color="#188EAF" offset=".7372"/><stop stop-color="#038BAE" offset=".7426"/><stop stop-color="#069292" offset=".7898"/><stop stop-color="#05A14B" offset=".8875"/><stop stop-color="#03927E" offset="1"/></linearGradient><rect width="723.1" y="0" x="0" height="306.4" class="st0" fill="url(#SVGID_1_)"/>
|
||||
<linearGradient id="SVGID_2_" y2="-109.26" gradientUnits="userSpaceOnUse" y1="-109.26" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="235.98" x1="325.08"><stop stop-color="#893680" offset="0"/><stop stop-color="#893680" offset=".3354"/><stop stop-color="#8D316D" offset=".5025"/><stop stop-color="#90294D" offset=".8398"/><stop stop-color="#902541" offset="1"/></linearGradient><polygon points="175.1 153.2 117 306.4 206.1 306.4" fill="url(#SVGID_2_)" class="st1"/>
|
||||
<linearGradient id="SVGID_3_" y2="-82.284" gradientUnits="userSpaceOnUse" y1="120.24" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="446.55" x1="478.93"><stop stop-color="#322C6F" offset="0"/><stop stop-color="#322C6F" offset=".2427"/><stop stop-color="#302F72" offset=".4599"/><stop stop-color="#2A3A7E" offset=".7155"/><stop stop-color="#154A93" offset=".9896"/><stop stop-color="#134B94" offset="1"/></linearGradient><polygon points="288.4 153.2 310.7 306.4 358.1 306.4 358.1 0 312.9 0" fill="url(#SVGID_3_)" class="st2"/>
|
||||
<linearGradient id="SVGID_4_" y2="-32.663" gradientUnits="userSpaceOnUse" y1="-32.663" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="372.88" x1="294.08"><stop stop-color="#6F378D" offset="0"/><stop stop-color="#3A4291" offset="1"/></linearGradient><polygon points="175.1 153.2 206.1 306.4 253.9 153.2 209.4 0 209.4 0" fill="url(#SVGID_4_)" class="st3"/>
|
||||
<linearGradient id="SVGID_5_" y2="-32.663" gradientUnits="userSpaceOnUse" y1="-32.663" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="325.08" x1="431.88"><stop stop-color="#233D7D" offset="0"/><stop stop-color="#293D7D" offset=".2495"/><stop stop-color="#3A3C80" offset=".5446"/><stop stop-color="#513B84" offset=".8616"/><stop stop-color="#5D3A86" offset="1"/></linearGradient><polygon points="253.9 153.2 206.1 306.4 310.7 306.4 288.4 153.2 312.9 0 209.4 0" fill="url(#SVGID_5_)" class="st4"/><polygon points="116.1 0 55.7 0 55.7 94.8 89.9 153.2 55.7 211.6 55.7 306.4 117 306.4 95.2 153.2" fill="#af2024" class="st5"/>
|
||||
<linearGradient id="SVGID_6_" y2="43.937" gradientUnits="userSpaceOnUse" y1="43.937" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="232.67" x1="329.11"><stop stop-color="#893680" offset="0"/><stop stop-color="#893680" offset=".3354"/><stop stop-color="#8D316D" offset=".5025"/><stop stop-color="#90294D" offset=".8398"/><stop stop-color="#902541" offset="1"/></linearGradient><polygon points="175.1 153.2 209.4 0 116.1 0" fill="url(#SVGID_6_)" class="st6"/><polygon points="55.7 94.8 55.7 0 0 0" fill="#941b1e" class="st7"/><polygon points="55.7 211.6 89.9 153.2 55.7 94.8" fill="#b12739" class="st8"/><polygon points="55.7 211.6 0 306.4 55.7 306.4" fill="#941b1e" class="st7"/><polygon points="55.7 94.8 0 0 0 306.4 55.7 211.6" fill="#952432" class="st9"/><polygon points="116.1 0 95.2 153.2 117 306.4 175.1 153.2" fill="#d42027" class="st10"/>
|
||||
<linearGradient id="SVGID_7_" y2="-186.06" gradientUnits="userSpaceOnUse" y1="120.44" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="748.96" x1="748.96"><stop stop-color="#94BE55" offset="0"/><stop stop-color="#93BD58" offset=".044340"/><stop stop-color="#8BBC6A" offset=".3891"/><stop stop-color="#86BC75" offset=".7149"/><stop stop-color="#84BC79" offset="1"/></linearGradient><path d="m641.6 259.6c1.7-25.4 10-54.6 18.8-85.6 1.4-5 2.8-10 4.2-15.1-1.4-5.5-2.8-10.9-4.2-16.2-8.8-33.3-17-64.7-18.8-92-1.4-21.2 1.4-37 8.9-50.6h-45.9c-7.5 18.3-10.3 29.1-8.9 50.3 1.7 27.3 10 58.7 18.8 92 13 49.3 28 106.2 23.2 164.2h12.9c-7.6-12.8-10.4-27.3-9-47z" class="st11" fill="url(#SVGID_7_)"/>
|
||||
<linearGradient id="SVGID_8_" y2="-184.45" gradientUnits="userSpaceOnUse" y1="117.29" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="733.49" x1="653.76"><stop stop-color="#08A24B" offset="0"/><stop stop-color="#0AA14E" offset=".1678"/><stop stop-color="#0B9E57" offset=".4047"/><stop stop-color="#099A67" offset=".6827"/><stop stop-color="#04947D" offset=".9898"/><stop stop-color="#04937E" offset="1"/></linearGradient><path d="m614.5 142.3c-8.8-33.3-17-64.7-18.8-92-1.4-21.2 1.4-32 8.9-50.3h-35.4c5.7 53.9-3.8 106.7-13.6 166.8-5.7 35-11.7 71.3-13.2 100.6-1.1 21.1 0.4 32.8 1.8 39h93.5c4.8-57.9-10.3-114.8-23.2-164.1z" class="st12" fill="url(#SVGID_8_)"/><path class="st13" fill="#1c9a48" d="m664.6 158.9c-1.4 5.1-2.8 10.1-4.2 15.1-8.8 31-17 60.2-18.8 85.6-1.4 19.7 1.4 34.2 9 46.9h33c4.2-51.8-7.2-102.3-19-147.6z"/>
|
||||
<linearGradient id="SVGID_9_" y2="-185.96" gradientUnits="userSpaceOnUse" y1="120.54" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="812.83" x1="812.83"><stop stop-color="#69A060" offset="0"/><stop stop-color="#639D5C" offset=".039895"/><stop stop-color="#4C944F" offset=".2192"/><stop stop-color="#378E47" offset=".4184"/><stop stop-color="#298B44" offset=".6515"/><stop stop-color="#238A43" offset="1"/></linearGradient><path d="m680.5 0c10.7 55.3-2.5 110.4-15.9 158.9 11.7 45.3 23.2 95.8 18.9 147.6h39.6v-306.5h-42.6z" class="st14" fill="url(#SVGID_9_)"/>
|
||||
<linearGradient id="SVGID_10_" y2="-185.86" gradientUnits="userSpaceOnUse" y1="120.54" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="652.45" x1="652.45"><stop stop-color="#05B5DC" offset="0"/><stop stop-color="#04B0D7" offset=".2197"/><stop stop-color="#05A4C9" offset=".5371"/><stop stop-color="#0591B4" offset=".9122"/><stop stop-color="#058CAE" offset="1"/></linearGradient><path d="m542.3 267.4c1.5-29.4 7.5-65.6 13.2-100.6 9.8-60.1 19.3-112.8 13.6-166.8h-70.8c-1.4 11.4-2.9 19.2-1.8 41.8 1.5 31.6 7.5 70.5 13.2 108.2 8.4 55.4 16.6 108.8 15.1 156.4h19.2c-1.3-6.2-2.8-17.9-1.7-39z" class="st15" fill="url(#SVGID_10_)"/><polygon points="375.7 153.2 358.1 0 358.1 306.4" fill="#2a3886" class="st16"/>
|
||||
<linearGradient id="SVGID_11_" y2="77.136" gradientUnits="userSpaceOnUse" y1="-4.3281" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="796.71" x1="751.05"><stop stop-color="#62B16E" offset="0"/><stop stop-color="#87B957" offset="1"/></linearGradient><path d="m641.6 50.6c1.7 27.3 10 58.7 18.8 92 1.4 5.3 2.8 10.7 4.2 16.2 13.5-48.4 26.6-103.5 15.9-158.8h-30c-7.5 13.6-10.3 29.4-8.9 50.6z" class="st17" fill="url(#SVGID_11_)"/>
|
||||
<linearGradient id="SVGID_12_" y2="-189.28" gradientUnits="userSpaceOnUse" y1="113.71" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="631.59" x1="550.4"><stop stop-color="#069AD4" offset="0"/><stop stop-color="#30A0CE" offset=".3525"/><stop stop-color="#5BB0C0" offset="1"/></linearGradient><path d="m509.8 150c-5.7-37.7-11.7-76.6-13.2-108.2-1.1-22.7 0.4-30.4 1.8-41.8h-41.5c1.5 40.1-1.5 85.3-7 160.8-3.1 43.5-8 110.5-7 145.7h82.1c1.4-47.7-6.8-101.1-15.2-156.5z" class="st18" fill="url(#SVGID_12_)"/>
|
||||
<linearGradient id="SVGID_13_" y2="-185.86" gradientUnits="userSpaceOnUse" y1="120.54" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="505.33" x1="505.33"><stop stop-color="#1E458E" offset="0"/><stop stop-color="#1F4F96" offset=".2411"/><stop stop-color="#2B6AAB" offset=".7292"/><stop stop-color="#337BB9" offset="1"/></linearGradient><polygon points="358.1 306.4 414.6 306.4 414.6 0 358.1 0 375.7 153.2" fill="url(#SVGID_13_)" class="st19"/>
|
||||
<linearGradient id="SVGID_14_" y2="120.54" gradientUnits="userSpaceOnUse" y1="-185.86" gradientTransform="matrix(1 0 0 -1 -118.98 120.54)" x2="554.92" x1="554.92"><stop stop-color="#3F9AC9" offset="0"/><stop stop-color="#2062A2" offset="1"/></linearGradient><path d="m449.9 160.8c5.5-75.5 8.5-120.6 7-160.8h-42.2l-0.1 306.4h28.3c-1-35.1 3.8-102.1 7-145.6z" class="st20" fill="url(#SVGID_14_)"/></g>
|
||||
</svg>
|
After Width: | Height: | Size: 9.9 KiB |
@ -15,3 +15,11 @@ a, a:active, a:focus {
|
||||
button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.supergraphic {
|
||||
background-image: url("assets/imgs/supergraphic.svg");
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user