Add NumRanges

urls
Max Ehrlicher-Schmidt 4 years ago
parent 2fc902403e
commit 65761b500f

@ -60,6 +60,8 @@ import { ReferenceTableComponent } from './components/reference-table/reference-
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { EquipmentComponent } from './pages/tables/equipment/equipment.component'; import { EquipmentComponent } from './pages/tables/equipment/equipment.component';
import { TimeFramesComponent } from './pages/tables/time-frames/time-frames.component'; import { TimeFramesComponent } from './pages/tables/time-frames/time-frames.component';
import { NumberRangeCellComponent } from './components/tableComponents/number-range-cell/number-range-cell.component';
import { DateRangeCellComponent } from './components/tableComponents/date-range-cell/date-range-cell.component';
@ -84,6 +86,8 @@ import { TimeFramesComponent } from './pages/tables/time-frames/time-frames.comp
ReferenceTableComponent, ReferenceTableComponent,
EquipmentComponent, EquipmentComponent,
TimeFramesComponent, TimeFramesComponent,
NumberRangeCellComponent,
DateRangeCellComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

@ -14,14 +14,25 @@
<ng-container *ngFor="let object of propertiesInfo"> <ng-container *ngFor="let object of propertiesInfo">
<mat-card class="inline-card" *ngIf="object.type === 'Group'"> <mat-card class="inline-card" *ngIf="object.type === 'Group'">
<mat-card-title>{{ object.title }}</mat-card-title> <mat-card-title>{{ object.title }}</mat-card-title>
<ng-container *ngFor="let prop of object.properties">
<app-cell <app-cell
*ngFor="let prop of object.properties" *ngIf="prop.type !== 'NumRange'"
[editable]="data.isLockedByMe && !prop.readonly" [editable]="data.isLockedByMe && prop.acceptedForUpdating"
[required]="prop.requiredForUpdating"
(validityChange)="validityChange(prop.dataPath, $event)"
[(value)]="data[prop.dataPath]" [(value)]="data[prop.dataPath]"
[label]="prop.translation || prop.dataPath" [label]="prop.translation || prop.dataPath"
[inputType]="prop.type" [inputType]="prop.type"
></app-cell> ></app-cell>
<app-number-range-cell
*ngIf="prop.type === 'NumRange'"
[editable]="data.isLockedByMe && prop.acceptedForUpdating"
(validityChange)="validityChange(prop.dataPath, $event)"
[(min)]="data[prop.dataPath + '.min']"
[(max)]="data[prop.dataPath + '.max']"
[label]="prop.translation || prop.dataPath"
></app-number-range-cell
></ng-container>
</mat-card> </mat-card>
<mat-card class="inline-card" *ngIf="object.type === 'ReferenceTable'"> <mat-card class="inline-card" *ngIf="object.type === 'ReferenceTable'">
@ -32,7 +43,9 @@
*ngIf="object.linkToTable" *ngIf="object.linkToTable"
color="primary" color="primary"
[routerLink]="object.linkToTable(data)" [routerLink]="object.linkToTable(data)"
[queryParams]="object.linkToTableParams ? object.linkToTableParams(data) : {}" [queryParams]="
object.linkToTableParams ? object.linkToTableParams(data) : {}
"
matTooltip="Zur Tabelle" matTooltip="Zur Tabelle"
> >
<mat-icon>subdirectory_arrow_right</mat-icon> <mat-icon>subdirectory_arrow_right</mat-icon>
@ -83,15 +96,26 @@
> >
<mat-icon>cancel</mat-icon> <mat-icon>cancel</mat-icon>
</button> </button>
<div
*ngIf="data.isLockedByMe"
[matTooltip]="
countUnvalidProperties() > 0
? 'Ungültige oder nicht ausgefüllte Pflichtfelder (rot): ' +
countUnvalidProperties()
: 'Abspeichern'
"
>
<button <button
mat-fab mat-fab
(click)="save()" (click)="save()"
*ngIf="data.isLockedByMe"
class="floating-fab-button" class="floating-fab-button"
[disabled]="isSavingOrLocking || isLoading" [disabled]="
isSavingOrLocking || isLoading || countUnvalidProperties() > 0
"
> >
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
</button> </button>
</div>
<button <button
mat-fab mat-fab
*ngIf="data.isLocked" *ngIf="data.isLocked"

@ -1,4 +1,11 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { deepen } from 'src/app/helperFunctions/deepenObject'; import { deepen } from 'src/app/helperFunctions/deepenObject';
import { flatten } from 'src/app/helperFunctions/flattenObject'; import { flatten } from 'src/app/helperFunctions/flattenObject';
@ -7,7 +14,8 @@ import { SchemaService } from 'src/app/services/schema.service';
interface PropertyTypeInfo { interface PropertyTypeInfo {
dataPath: string; dataPath: string;
translation: string; translation: string;
readonly?: boolean; acceptedForUpdating?: boolean;
requiredForUpdating?: boolean;
type?: string; type?: string;
} }
@ -61,6 +69,8 @@ export class DataPageComponent implements OnInit, OnDestroy {
isLoading: boolean = false; isLoading: boolean = false;
isSavingOrLocking: boolean = false; isSavingOrLocking: boolean = false;
propertyValidity = {};
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private schemaService: SchemaService private schemaService: SchemaService
@ -115,7 +125,14 @@ export class DataPageComponent implements OnInit, OnDestroy {
this.pageDataGQLUpdateInputType, this.pageDataGQLUpdateInputType,
prop.dataPath prop.dataPath
); );
prop.readonly = prop.readonly || !updateTypeInformation.isPartOfType; prop.acceptedForUpdating =
prop.acceptedForUpdating != null
? prop.acceptedForUpdating
: updateTypeInformation.isPartOfType;
prop.requiredForUpdating =
prop.requiredForUpdating != null
? prop.requiredForUpdating
: updateTypeInformation.isRequired;
} }
} }
} }
@ -124,6 +141,20 @@ export class DataPageComponent implements OnInit, OnDestroy {
this.lockEvent.emit(deepen(this.data)); this.lockEvent.emit(deepen(this.data));
} }
validityChange(columnName: string, isValid: Event) {
this.propertyValidity[columnName] = isValid;
}
countUnvalidProperties() {
let unvalidFieldsCount = 0;
for (const prop in this.propertyValidity) {
if (!this.propertyValidity[prop]) {
unvalidFieldsCount++;
}
}
return unvalidFieldsCount;
}
save() { save() {
this.saveEvent.emit( this.saveEvent.emit(
this.schemaService.filterObject( this.schemaService.filterObject(

@ -116,20 +116,27 @@
{{ getTranslation(column.dataPath) }} {{ getTranslation(column.dataPath) }}
</th> </th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<ng-container
*ngIf="column.type !== 'DateRange' && column.type !== 'NumRange'"
>
<app-cell <app-cell
*ngIf=" *ngIf="
column.type === 'DateRange' ||
column.type === 'Boolean' || column.type === 'Boolean' ||
element.newObject || (element.newObject && column.acceptedForCreation) ||
(!column.readonly && element.isLockedByMe); (column.acceptedForUpdating && element.isLockedByMe);
else stringValue else stringValue
" "
[editable]=" [editable]="
(element.newObject && column.acceptedForCreation) || (element.newObject && column.acceptedForCreation) ||
(!column.readonly && element.isLockedByMe) (column.acceptedForUpdating && element.isLockedByMe)
"
[required]="
(element.newObject && column.requiredForCreation) ||
(element.isLockedByMe && column.requiredForCreation)
"
(validityChange)="
validityChange(element, column.dataPath, $event)
" "
[required]="element.newObject && column.requiredForCreation"
(validityChange)="validityChange(element, column.dataPath, $event)"
[(value)]="element[column.dataPath]" [(value)]="element[column.dataPath]"
[inputType]="column.type" [inputType]="column.type"
[link]="column.link ? column.link(element) : null" [link]="column.link ? column.link(element) : null"
@ -144,6 +151,24 @@
>{{ element[column.dataPath] }}</a >{{ element[column.dataPath] }}</a
> >
</ng-template> </ng-template>
</ng-container>
<ng-container *ngIf="column.type === 'NumRange'">
<app-number-range-cell
[editable]="
(element.newObject && column.acceptedForCreation) ||
(column.acceptedForUpdating && element.isLockedByMe)
"
[required]="
(element.newObject && column.requiredForCreation) ||
(element.isLockedByMe && column.requiredForCreation)
"
(validityChange)="
validityChange(element, column.dataPath, $event)
"
[(min)]="element[column.dataPath + '.min']"
[(max)]="element[column.dataPath + '.max']"
></app-number-range-cell>
</ng-container>
</td> </td>
</ng-container> </ng-container>
@ -190,14 +215,23 @@
[diameter]="32" [diameter]="32"
*ngIf="isLoading(element.id)" *ngIf="isLoading(element.id)"
></mat-spinner> ></mat-spinner>
<div
*ngIf="element.isLockedByMe && !isLoading(element.id)"
[matTooltip]="
countUnvalidFields(element) > 0
? 'Ungültige oder nicht ausgefüllte Pflichtfelder (rot): ' +
countUnvalidFields(element)
: 'Abspeichern'
"
>
<button <button
mat-icon-button mat-icon-button
*ngIf="element.isLockedByMe && !isLoading(element.id)" [disabled]="countUnvalidFields(element) > 0"
(click)="save(element)" (click)="save(element)"
> >
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
</button> </button>
</div>
<button <button
mat-icon-button mat-icon-button
matTooltip="Alle ungespeicherten Änderungen verwerfen." matTooltip="Alle ungespeicherten Änderungen verwerfen."
@ -218,7 +252,7 @@
*ngIf="element.newObject" *ngIf="element.newObject"
[matTooltip]=" [matTooltip]="
countUnvalidFields(element) > 0 countUnvalidFields(element) > 0
? 'Nicht ausgefüllte Pflichtfelder (rot): ' + ? 'Ungültige oder nicht ausgefüllte Pflichtfelder (rot): ' +
countUnvalidFields(element) countUnvalidFields(element)
: 'Erstellen' : 'Erstellen'
" "

@ -20,6 +20,7 @@ import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/internal/operators/debounceTime'; import { debounceTime } from 'rxjs/internal/operators/debounceTime';
import { type } from 'os';
@Component({ @Component({
selector: 'app-table', selector: 'app-table',
@ -37,7 +38,8 @@ export class TableComponent implements AfterViewInit {
acceptedForCreation?: boolean; acceptedForCreation?: boolean;
requiredForCreation?: boolean; requiredForCreation?: boolean;
sticky?: boolean; sticky?: boolean;
readonly?: boolean; acceptedForUpdating?: boolean;
requiredForUpdating?: boolean;
type?: string; type?: string;
link?: (row: any) => string; link?: (row: any) => string;
}[] = []; }[] = [];
@ -193,7 +195,14 @@ export class TableComponent implements AfterViewInit {
this.tableDataGQLUpdateInputType, this.tableDataGQLUpdateInputType,
column.dataPath column.dataPath
); );
column.readonly = column.readonly !== null ? column.readonly : !typeInformation.isPartOfType; column.acceptedForUpdating =
column.acceptedForUpdating != null
? column.acceptedForUpdating
: typeInformation.isPartOfType;
column.requiredForUpdating =
column.requiredForUpdating != null
? column.requiredForUpdating
: typeInformation.isRequired;
} }
for (const column of this.columnInfo) { for (const column of this.columnInfo) {
const typeInformation = this.schemaService.getTypeInformation( const typeInformation = this.schemaService.getTypeInformation(
@ -201,9 +210,13 @@ export class TableComponent implements AfterViewInit {
column.dataPath column.dataPath
); );
column.requiredForCreation = column.requiredForCreation =
column.requiredForCreation || typeInformation.isRequired; column.requiredForCreation != null
? column.requiredForCreation
: typeInformation.isRequired;
column.acceptedForCreation = column.acceptedForCreation =
column.acceptedForCreation || typeInformation.isPartOfType; column.acceptedForCreation != null
? column.acceptedForCreation
: typeInformation.isPartOfType;
} }
} }
@ -235,7 +248,7 @@ export class TableComponent implements AfterViewInit {
countUnvalidFields(row: any) { countUnvalidFields(row: any) {
let unvalidFieldsCount = 0; let unvalidFieldsCount = 0;
if (!row.FieldsValidity) { if (!row.FieldsValidity) {
return 99; return 0;
} }
for (const prop in row.FieldsValidity) { for (const prop in row.FieldsValidity) {
if (!row.FieldsValidity[prop]) { if (!row.FieldsValidity[prop]) {

@ -55,31 +55,6 @@
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>
<div *ngIf="htmlInputType === 'numberRange'" class="number-range-wrapper">
<mat-form-field class="number-range-form-field">
<mat-label *ngIf="label">{{ label }}</mat-label>
<input
matInput
type="number"
[disabled]="!editable"
[value]="value.min"
(input)="minValueChange($event)"
[required]="required"
/>
</mat-form-field>
<span class="range-spacer">-</span>
<mat-form-field class="number-range-form-field">
<input
matInput
type="number"
[disabled]="!editable"
[value]="value.max"
(input)="maxValueChange($event)"
[required]="required"
/>
</mat-form-field>
</div>
<div <div
#otherInputType #otherInputType
*ngIf="htmlInputType === 'number' || htmlInputType === 'text'" *ngIf="htmlInputType === 'number' || htmlInputType === 'text'"

@ -1,8 +0,0 @@
.number-range-wrapper {
display: flex;
flex-direction: row;
align-items: center;
.range-spacer {
margin: 0 0.5em;
}
}

@ -9,6 +9,7 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
AfterViewInit, AfterViewInit,
} from '@angular/core'; } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({ @Component({
selector: 'app-cell', selector: 'app-cell',
@ -17,7 +18,18 @@ import {
}) })
export class CellComponent implements AfterViewInit { export class CellComponent implements AfterViewInit {
@Input() @Input()
value: any; // number | string | boolean | { start: string; end: string; }; set value(value: any) {
this._value = value;
setTimeout(() => {
this.checkIfValid();
});
} // number | string | boolean | { start: string; end: string; };
get value(): any {
return this._value;
}
_value: any;
rangeForm: FormGroup;
minValue: number; minValue: number;
maxValue: number; maxValue: number;
@ -52,6 +64,8 @@ export class CellComponent implements AfterViewInit {
constructor(private cdr: ChangeDetectorRef, public datepipe: DatePipe) {} constructor(private cdr: ChangeDetectorRef, public datepipe: DatePipe) {}
ngOnInit() {}
ngAfterViewInit(): void { ngAfterViewInit(): void {
if (this.required) { if (this.required) {
this.input?.control?.markAsTouched(); this.input?.control?.markAsTouched();
@ -93,6 +107,12 @@ export class CellComponent implements AfterViewInit {
) { ) {
this.value = { min: null, max: null }; this.value = { min: null, max: null };
} }
this.rangeForm = new FormGroup({
minValue: new FormControl(),
maxValue: new FormControl(),
});
this.rangeForm.controls['minValue'].markAsTouched();
this.rangeForm.controls['maxValue'].markAsTouched();
} }
} }
@ -125,16 +145,38 @@ export class CellComponent implements AfterViewInit {
} }
minValueChange(event) { minValueChange(event) {
this.value.min = this.toNumber(event.target.value); this.value.min = Math.abs(this.toNumber(event.target.value));
this.valueChange.emit(this.value); this.valueChange.emit(this.value);
console.log(this.value); this.checkIfRangeIsValid();
} }
maxValueChange(event) { maxValueChange(event) {
this.value.max = this.toNumber(event.target.value); this.value.max = Math.abs(this.toNumber(event.target.value));
this.valueChange.emit(this.value); this.valueChange.emit(this.value);
console.log(this.value); this.checkIfRangeIsValid();
}
checkIfRangeIsValid() {
if (this.value.min === null || this.value.max === null) {
this.setRangeError(false);
return;
}
if (this.value.min <= this.value.max) {
this.setRangeError(false);
return;
}
this.setRangeError(true);
}
setRangeError(error: boolean): void {
this.validityChange.emit(!error);
if (error) {
this.rangeForm.controls['minValue'].setErrors({ rangeError: true });
this.rangeForm.controls['maxValue'].setErrors({ rangeError: true });
} else {
this.rangeForm.controls['minValue'].setErrors(null);
this.rangeForm.controls['maxValue'].setErrors(null);
}
} }
transformDate(date) { transformDate(date) {

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-date-range-cell',
templateUrl: './date-range-cell.component.html',
styleUrls: ['./date-range-cell.component.scss']
})
export class DateRangeCellComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

@ -0,0 +1,28 @@
<div
*ngIf="editable || label"
class="number-range-wrapper"
[formGroup]="rangeForm"
>
<mat-form-field class="number-range-form-field">
<mat-label *ngIf="label">{{ label }} min</mat-label>
<input
matInput
type="number"
formControlName="minValue"
(input)="minValueChange($event)"
/>
</mat-form-field>
<span class="range-spacer">-</span>
<mat-form-field class="number-range-form-field">
<mat-label *ngIf="label">{{ label }} max</mat-label>
<input
matInput
formControlName="maxValue"
type="number"
(input)="maxValueChange($event)"
/>
</mat-form-field>
</div>
<div *ngIf="!editable && !label">
{{ min }} - {{ max }}
</div>

@ -0,0 +1,8 @@
.number-range-wrapper {
display: flex;
flex-direction: row;
align-items: center;
.range-spacer {
margin: 0 0.5em;
}
}

@ -0,0 +1,123 @@
import { DatePipe } from '@angular/common';
import {
Component,
Input,
Output,
EventEmitter,
ChangeDetectorRef,
OnInit,
} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-number-range-cell',
templateUrl: './number-range-cell.component.html',
styleUrls: ['./number-range-cell.component.scss'],
})
export class NumberRangeCellComponent implements OnInit {
@Input()
set min(value: number) {
this._min = value;
this.rangeForm?.controls['minValue'].setValue(value);
}
get min() {
return this._min;
}
_min: number;
@Input()
set max(value: number) {
this._max = value;
this.rangeForm?.controls['maxValue'].setValue(value);
}
get max() {
return this._max;
}
_max: number;
rangeForm: FormGroup;
@Output() minChange = new EventEmitter<number>();
@Output() maxChange = new EventEmitter<number>();
@Input()
set editable(value) {
this._editable = value;
if (value) {
this.rangeForm?.controls['minValue'].enable();
this.rangeForm?.controls['maxValue'].enable();
} else {
this.rangeForm?.controls['minValue'].disable();
this.rangeForm?.controls['maxValue'].disable();
}
}
_editable = false;
get editable() {
return this._editable;
}
@Input()
required = false;
@Input()
label: string = null;
@Output() validityChange = new EventEmitter<boolean>();
isValid = true;
constructor(private cdr: ChangeDetectorRef, public datepipe: DatePipe) {}
ngOnInit() {
this.rangeForm = new FormGroup({
minValue: new FormControl(),
maxValue: new FormControl(),
});
this.rangeForm.controls['minValue'].markAsTouched();
this.rangeForm.controls['maxValue'].markAsTouched();
this.rangeForm?.controls['minValue'].setValue(this.min);
this.rangeForm?.controls['maxValue'].setValue(this.max);
}
minValueChange(event) {
this.min = this.toPositiveNumber(event.target.value);
this.minChange.emit(this.min);
this.checkIfRangeIsValid();
}
maxValueChange(event) {
this.max = this.toPositiveNumber(event.target.value);
this.maxChange.emit(this.max);
this.checkIfRangeIsValid();
}
checkIfRangeIsValid() {
if (this.min == null || this.max == null) {
this.setRangeError(false);
return;
}
if (this.min <= this.max) {
this.setRangeError(false);
return;
}
this.setRangeError(true);
}
setRangeError(error: boolean): void {
this.validityChange.emit(!error);
if (error) {
this.rangeForm.controls['minValue'].setErrors({ rangeError: true });
this.rangeForm.controls['maxValue'].setErrors({ rangeError: true });
} else {
this.rangeForm.controls['minValue'].setErrors(null);
this.rangeForm.controls['maxValue'].setErrors(null);
}
}
toPositiveNumber(str: string): number {
if (str === '') {
return null;
}
const number = +str;
if (number < 0) {
return Math.abs(number);
}
return number;
}
}

@ -60,7 +60,7 @@ export class BikeComponent implements OnInit {
}, },
{ {
type: 'Group', type: 'Group',
title: 'Maße und Ladungen', title: 'Maße und Ladung',
properties: [ properties: [
{ dataPath: 'dimensionsAndLoad.bikeLength', translation: 'Länge' }, { dataPath: 'dimensionsAndLoad.bikeLength', translation: 'Länge' },
{ dataPath: 'dimensionsAndLoad.bikeWeight', translation: 'Gewicht' }, { dataPath: 'dimensionsAndLoad.bikeWeight', translation: 'Gewicht' },

@ -4,8 +4,8 @@
export const environment = { export const environment = {
production: false, production: false,
apiUrl: "https://flotte.duckdns.org", // without /graphql !!! apiUrl: "http://173.212.197.169:4000", // without /graphql !!!
authUrl: "https://flotte.duckdns.org/user-management" authUrl: "http://173.212.197.169:8080"
}; };
/* /*

Loading…
Cancel
Save