Adds settings screen and refactors get/set app defaults

This commit is contained in:
Aaron Louie 2020-09-23 13:09:00 -04:00
parent 0d4ed8f6d9
commit 69d490d76f
13 changed files with 277 additions and 104 deletions

View File

@ -34,7 +34,6 @@ env:
- BASE_HREF=/
- DEPLOY_URL=/
- HOME_ROUTE=home
- IRB_URL=http://localhost:5001/
- PORT0=4200
- PRODUCTION=false
script:
@ -42,7 +41,7 @@ script:
deploy:
provider: script
script: bash ./deploy.sh sartography/cr-connect-frontend
script: bash ./deploy.sh sartography/uva-covid19-testing-kiosk
on:
all_branches: true
condition: $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|master|rrt\/.*)$

40
package-lock.json generated
View File

@ -3790,6 +3790,15 @@
"requires": {
"p-try": "^2.0.0"
}
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
}
}
},
@ -10404,7 +10413,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"requires": {
"safe-buffer": "^5.1.0"
}
@ -10951,8 +10959,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-regex": {
"version": "1.1.0",
@ -11165,10 +11172,9 @@
}
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true,
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
"requires": {
"randombytes": "^2.1.0"
}
@ -12351,6 +12357,15 @@
"p-try": "^2.0.0"
}
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -13499,6 +13514,17 @@
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
"worker-farm": "^1.7.0"
},
"dependencies": {
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
}
}
},
"to-regex-range": {

View File

@ -39,6 +39,7 @@
"ngx-qrcode-svg": "^2.0.0",
"rfdc": "^1.1.4",
"rxjs": "~6.6.3",
"serialize-javascript": "^5.0.1",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},

View File

@ -5,10 +5,12 @@ import {FlexLayoutModule} from '@angular/flex-layout';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatOptionModule} from '@angular/material/core';
import {MAT_FORM_FIELD_DEFAULT_OPTIONS, MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatSelectModule} from '@angular/material/select';
import {MatToolbarModule} from '@angular/material/toolbar';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {FormlyModule} from '@ngx-formly/core';
@ -70,7 +72,10 @@ export function getBaseHref(platformLocation: PlatformLocation): string {
MatIconModule,
MatFormFieldModule,
QRCodeSVGModule,
AppRoutingModule, // <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
AppRoutingModule,
MatOptionModule,
MatSelectModule,
// <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
],
providers: [
{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}},

View File

@ -1,31 +1,60 @@
import {AppDefaults} from '../interfaces/appDefaults.interface';
import {LabelLayout, LabelLayoutType} from '../interfaces/labelLayout';
// Default form field and data values
export const defaults: AppDefaults = {
countsCollection: 'counts', // Name of collection for Line Counts in Firebase.
samplesCollection: 'samples', // Name of collection for Line Counts in Firebase.
dateEncodedFormat: 'yyyyMMddHHmm', // Format for dates when encoded in IDs for database records.
dateDisplayFormat: 'MM/dd/yyyy, hh:mm aa', // Format for dates when displayed to user.
numCopies: 3, // Default number of copies of labels to print. Can be overridden by user setting.
labelLayout: 'round_32mm_1up' as LabelLayoutType, // Which label layout to use for printing. Can be overridden by user setting.
locationId: '0000', // Default location ID. Can be overridden by user setting.
lineCountRegex: /^[\d]{4}-[\d]{12}$/, // ID format for Line Count records.
qrCodeRegex: /^[\d]{9}-[a-zA-Z]+-[\d]{12}-[\d]{4}$/, // ID format for QR Code records.
barCodeNumLength: 9, // Number of digits in Bar Code.
barCodeRegex: /^[\d]{14}$|^[\d]{9}$/, // Pattern for Bar Code data. Scanned barcodes will be either 9 or 14 digits long.
// Manually-entered ID numbers will be exactly 9 digits long.
initialsLength: 5,
initialsRegex: /^[a-zA-Z]{2,5}$/,
};
import createClone from 'rfdc';
import serializeJs from 'serialize-javascript';
import {AppDefaults, AppDefaultsOptions} from '../interfaces/appDefaults.interface';
import {LabelLayout} from '../interfaces/labelLayout.interface';
export const labelLayouts = {
round_32mm_1up: new LabelLayout({
name: '32mm Round Label - 1up',
type: 'round_32mm_1up',
numCols: 1,
columnGap: 0,
}),
round_32mm_2up: new LabelLayout({
name: '32mm Round Label - 2up',
type: 'round_32mm_2up',
numCols: 2,
columnGap: 1.3,
}),
};
// Default form field and data values
export const defaults: AppDefaults = new AppDefaults({
barCodeNumLength: 9, // Number of digits in Bar Code.
barCodeRegExp: /^[\d]{14}$|^[\d]{9}$/, // Pattern for Bar Code data. Scanned barcodes will be either 9 or 14 digits long. Manually-entered ID numbers will be exactly 9 digits long.
countsCollection: 'counts', // Name of collection for Line Counts in Firebase.
dateDisplayFormat: 'MM/dd/yyyy, hh:mm aa', // Format for dates when displayed to user.
dateEncodedFormat: 'yyyyMMddHHmm', // Format for dates when encoded in IDs for database records.
initialsLength: 5,
initialsRegExp: /^[a-zA-Z]{2,5}$/,
labelLayout: labelLayouts.round_32mm_1up, // Which label layout to use for printing. Can be overridden by user setting.
lineCountRegExp: /^[\d]{4}-[\d]{12}$/, // ID format for Line Count records.
locationId: '0000', // Default location ID. Can be overridden by user setting.
locationIdRegExp: /^[\d]{4}$/, // ID format for Line Count records.
numCopies: 1, // Default number of copies of labels to print. Can be overridden by user setting.
qrCodeRegExp: /^[\d]{9}-[a-zA-Z]+-[\d]{12}-[\d]{4}$/, // ID format for QR Code records.
samplesCollection: 'samples', // Name of collection for Line Counts in Firebase.
});
export const getSettings = (): AppDefaults => {
const storedSettings = localStorage.getItem('settings');
if (storedSettings) {
// tslint:disable-next-line:no-eval
return new AppDefaults(eval(`(${storedSettings})`));
} else {
localStorage.setItem('settings', serializeJs(defaults));
return defaults;
}
};
export const saveSettings = (newSettings: AppDefaultsOptions): AppDefaults => {
const settings: AppDefaults = createClone()(getSettings());
Object.keys(newSettings).forEach(k => {
settings[k] = newSettings[k];
});
localStorage.setItem('settings', serializeJs(settings));
return getSettings();
};

View File

@ -1,17 +1,51 @@
import {LabelLayoutType} from './labelLayout';
import {LabelLayout} from './labelLayout.interface';
export interface AppDefaults {
countsCollection: string;
samplesCollection: string;
dateEncodedFormat: string;
dateDisplayFormat: string;
numCopies: number;
labelLayout: LabelLayoutType;
locationId: string;
lineCountRegex: RegExp;
qrCodeRegex: RegExp;
barCodeRegex: RegExp;
barCodeNumLength: number;
initialsRegex: RegExp;
initialsLength: number;
export interface AppDefaultsOptions {
barCodeNumLength?: number;
barCodeRegExp?: RegExp | string;
countsCollection?: string;
dateDisplayFormat?: string;
dateEncodedFormat?: string;
initialsLength?: number;
initialsRegExp?: RegExp | string;
labelLayout?: LabelLayout;
lineCountRegExp?: RegExp | string;
locationId?: string;
locationIdRegExp?: RegExp | string;
numCopies?: number;
qrCodeRegExp?: RegExp | string;
samplesCollection?: string;
}
export class AppDefaults {
barCodeNumLength: number;
barCodeRegExp: RegExp;
countsCollection: string;
dateDisplayFormat: string;
dateEncodedFormat: string;
initialsLength: number;
initialsRegExp: RegExp;
labelLayout: LabelLayout;
lineCountRegExp: RegExp;
locationId: string;
locationIdRegExp: RegExp;
numCopies: number;
qrCodeRegExp: RegExp;
samplesCollection: string;
constructor(options: AppDefaultsOptions) {
console.log('options', options);
const keys = Object.keys(options);
keys.forEach(k => {
if (k.includes('RegExp')) {
if (typeof options[k] === 'string') {
this[k] = new RegExp(options[k]);
} else {
this[k] = options[k];
}
} else {
this[k] = options[k];
}
});
}
}

View File

@ -1,6 +1,6 @@
export declare type LabelLayoutType = 'round_32mm_1up' | 'round_32mm_2up';
export interface LayoutOptions {
type?: string;
name?: string;
units?: string;
pointsPerUnit?: number;
labelSize?: number;
@ -17,6 +17,8 @@ export interface LayoutOptions {
}
export class LabelLayout {
type = 'round_32mm_1up';
name = '32mm Round Label - 1up';
units = 'mm';
pointsPerUnit = 0.3528;
labelSize = 28.6;
@ -32,18 +34,10 @@ export class LabelLayout {
numCopies = 1;
constructor(private options: LayoutOptions) {
this.units = options.units || this.units;
this.pointsPerUnit = options.pointsPerUnit || this.pointsPerUnit;
this.marginSize = options.marginSize || this.marginSize;
this.labelSize = options.labelSize || this.labelSize;
this.numCols = options.numCols || this.numCols;
this.columnGap = options.columnGap || this.columnGap;
this.sideTextWidth = options.sideTextWidth || this.sideTextWidth;
this.sideTextTop = options.sideTextTop || this.sideTextTop;
this.sideTextMargin = options.sideTextMargin || this.sideTextMargin;
this.topTextMargin = options.topTextMargin || this.topTextMargin;
this.bottomTextMargin = options.bottomTextMargin || this.bottomTextMargin;
this.fontSizePt = options.fontSizePt || this.fontSizePt;
const keys = Object.keys(options);
keys.forEach(k => {
this[k] = options[k];
});
}
get dimensions() {

View File

@ -43,7 +43,7 @@ export class PrintComponent implements OnInit {
}
get columns() {
return Array(defaults.labelLayout === 'round_32mm_2up' ? 2 : 1).fill('');
return Array(defaults.labelLayout.type === 'round_32mm_2up' ? 2 : 1).fill('');
}
ngOnInit(): void {

View File

@ -18,16 +18,6 @@
type="text"
[formControl]="barCodeFormControl"
>
<button
mat-button
*ngIf="barCodeValue"
matSuffix
mat-icon-button
aria-label="Clear"
(click)="barCodeFormControl.patchValue('')"
>
<mat-icon>close</mat-icon>
</button>
<mat-error *ngIf="barCodeFormControl.hasError('required')">This field is required.</mat-error>
<mat-error *ngIf="barCodeFormControl.hasError('pattern')">Please enter exactly 9 or 14 digits.</mat-error>
</mat-form-field>
@ -39,31 +29,30 @@
type="text"
[formControl]="initialsFormControl"
>
<button
mat-button
*ngIf="initialsValue"
matSuffix
mat-icon-button
aria-label="Clear"
(click)="initialsFormControl.patchValue('')"
>
<mat-icon>close</mat-icon>
</button>
<mat-error *ngIf="initialsFormControl.hasError('required')">This field is required.</mat-error>
<mat-error *ngIf="initialsFormControl.hasError('pattern')">Please enter only letters.</mat-error>
<mat-error *ngIf="initialsFormControl.hasError('pattern')">Please enter only 2-5 letters.</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button
#nextButton="matButton"
mat-flat-button
class="btn-xl"
color="accent"
fxFlex="50%"
[disabled]="!hasInfo"
routerLink="/print"
[queryParams]="queryParams"
>Next</button>
<button mat-flat-button class="btn-xl" fxFlex="50%" routerLink="/">Cancel</button>
>Next <mat-icon>navigate_next</mat-icon></button>
<button
mat-flat-button
class="btn-xl"
(click)="resetForm()"
><mat-icon>restore_page</mat-icon> Reset</button>
<button
mat-flat-button
class="btn-xl"
routerLink="/"
><mat-icon>cancel</mat-icon> Cancel</button>
</mat-card-actions>
</mat-card>

View File

@ -1,8 +1,10 @@
import {AfterViewInit, ChangeDetectorRef, Component, OnInit, ViewChild} from '@angular/core';
import {FormControl, Validators} from '@angular/forms';
import {MatButton} from '@angular/material/button';
import {MatInput} from '@angular/material/input';
import {Params} from '@angular/router';
import {defaults} from '../config/defaults';
import {defaults, getSettings} from '../config/defaults';
import {AppDefaults} from '../interfaces/appDefaults.interface';
@Component({
@ -11,18 +13,18 @@ import {defaults} from '../config/defaults';
styleUrls: ['./sample.component.scss']
})
export class SampleComponent implements AfterViewInit {
barCodeErrorMessage = '';
initialsErrorMessage = '';
settings: AppDefaults = getSettings();
barCodeFormControl = new FormControl('', [
Validators.required,
Validators.pattern(defaults.barCodeRegex),
Validators.pattern(this.settings.barCodeRegExp),
]);
initialsFormControl = new FormControl('', [
Validators.required,
Validators.pattern(defaults.initialsRegex),
Validators.pattern(this.settings.initialsRegExp),
]);
@ViewChild('barCodeInput') barCodeInput: MatInput;
@ViewChild('initialsInput') initialsInput: MatInput;
@ViewChild('nextButton') nextButton: MatButton;
get queryParams(): Params {
return {
@ -32,9 +34,7 @@ export class SampleComponent implements AfterViewInit {
}
constructor(private changeDetector: ChangeDetectorRef) {
this.barCodeFormControl.registerOnChange(() => {
this.checkBarCodeValue();
});
this.barCodeFormControl.registerOnChange(() => this.checkBarCodeValue());
this.initialsFormControl.registerOnChange(() => this.checkInitialsValue());
}
@ -47,11 +47,11 @@ export class SampleComponent implements AfterViewInit {
}
get hasBarCode(): boolean {
return defaults.barCodeRegex.test(this.barCodeValue);
return this.settings.barCodeRegExp.test(this.barCodeValue);
}
get hasInitials(): boolean {
return defaults.initialsRegex.test(this.initialsValue);
return this.settings.initialsRegExp.test(this.initialsValue);
}
get hasInfo(): boolean {
@ -66,19 +66,21 @@ export class SampleComponent implements AfterViewInit {
checkBarCodeValue() {
console.log('--- checkBarCodeValue ---');
if (this.hasBarCode) {
this.barCodeErrorMessage = '';
this.initialsInput.focus();
this.changeDetector.detectChanges();
} else {
this.barCodeErrorMessage = 'Wrong barcode.';
}
}
checkInitialsValue() {
if (this.hasInitials) {
this.initialsErrorMessage = '';
} else {
this.initialsErrorMessage = 'Wrong barcode.';
console.log('--- checkInitialsValue ---');
if (this.hasInitials && this.hasBarCode) {
this.nextButton.focus();
this.changeDetector.detectChanges();
}
}
resetForm() {
this.barCodeFormControl.patchValue('');
this.initialsFormControl.patchValue('');
}
}

View File

@ -1 +1,64 @@
<p>settings works!</p>
<div
class="full-height bg-primary"
fxLayout="row"
fxLayoutAlign="center center"
fxLayoutGap="40px"
>
<mat-card fxFlex="50%">
<mat-card-header>
<h1>Scan Barcode</h1>
</mat-card-header>
<mat-card-content fxLayout="row wrap" fxLayoutAlign="center center">
<mat-form-field class="barcode-input" fxFlex="95%">
<mat-label>Location ID #</mat-label>
<input
#locationIdInput="matInput"
matInput
type="text"
[formControl]="locationIdFormControl"
>
<mat-error *ngIf="locationIdFormControl.hasError('required')">This field is required.</mat-error>
<mat-error *ngIf="locationIdFormControl.hasError('pattern')">Please enter exactly 4 digits.</mat-error>
</mat-form-field>
<mat-form-field class="initials-input" fxFlex="95%">
<mat-label>Label layout</mat-label>
<mat-select
#labelLayoutSelect="matSelect"
[formControl]="labelLayoutFormControl"
[value]="settings.labelLayout.type"
>
<mat-option *ngFor="let layout of labelLayouts" [value]="layout.type">{{layout.name}}</mat-option>
</mat-select>
<mat-error *ngIf="labelLayoutFormControl.hasError('required')">This field is required.</mat-error>
</mat-form-field>
<mat-form-field class="initials-input" fxFlex="95%">
<mat-label>Number of copies of label</mat-label>
<input
#numCopiesInput="matInput"
matInput
type="number"
[formControl]="numCopiesFormControl"
[value]="settings.numCopies.toString()"
>
<mat-error *ngIf="numCopiesFormControl.hasError('required')">This field is required.</mat-error>
<mat-error *ngIf="numCopiesFormControl.hasError('pattern')">Please enter only 2-5 letters.</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button
mat-flat-button
class="btn-xl"
color="accent"
[disabled]="!hasInfo"
(click)="save()"
>Save <mat-icon>save</mat-icon></button>
<button
mat-flat-button
class="btn-xl"
routerLink="/"
><mat-icon>cancel</mat-icon> Cancel</button>
</mat-card-actions>
</mat-card>
</div>

View File

@ -1,4 +1,11 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit, ViewChild} from '@angular/core';
import {FormControl, Validators} from '@angular/forms';
import {MatInput} from '@angular/material/input';
import {MatSelect} from '@angular/material/select';
import {getSettings, labelLayouts, saveSettings} from '../config/defaults';
import {AppDefaults} from '../interfaces/appDefaults.interface';
import {LabelLayout} from '../interfaces/labelLayout.interface';
@Component({
selector: 'app-settings',
@ -6,10 +13,33 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
settings: AppDefaults = getSettings();
numCopiesFormControl = new FormControl(this.settings.numCopies, [
Validators.required,
]);
labelLayoutFormControl = new FormControl(this.settings.labelLayout.type, [
Validators.required,
]);
locationIdFormControl = new FormControl(this.settings.locationId, [
Validators.required,
Validators.pattern(this.settings.locationIdRegExp),
]);
constructor() { }
labelLayouts: LabelLayout[] = Object.values(labelLayouts);
constructor() {
}
get hasInfo(): boolean {
return this.numCopiesFormControl.valid && this.locationIdFormControl.valid;
}
ngOnInit(): void {
}
save() {
saveSettings({
});
}
}

View File

@ -61,3 +61,4 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
(window as any).global = window;