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 { EquipmentComponent } from './pages/tables/equipment/equipment.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,
EquipmentComponent,
TimeFramesComponent,
NumberRangeCellComponent,
DateRangeCellComponent,
],
imports: [
BrowserModule,

@ -14,14 +14,25 @@
<ng-container *ngFor="let object of propertiesInfo">
<mat-card class="inline-card" *ngIf="object.type === 'Group'">
<mat-card-title>{{ object.title }}</mat-card-title>
<app-cell
*ngFor="let prop of object.properties"
[editable]="data.isLockedByMe && !prop.readonly"
[(value)]="data[prop.dataPath]"
[label]="prop.translation || prop.dataPath"
[inputType]="prop.type"
></app-cell>
<ng-container *ngFor="let prop of object.properties">
<app-cell
*ngIf="prop.type !== 'NumRange'"
[editable]="data.isLockedByMe && prop.acceptedForUpdating"
[required]="prop.requiredForUpdating"
(validityChange)="validityChange(prop.dataPath, $event)"
[(value)]="data[prop.dataPath]"
[label]="prop.translation || prop.dataPath"
[inputType]="prop.type"
></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 class="inline-card" *ngIf="object.type === 'ReferenceTable'">
@ -32,7 +43,9 @@
*ngIf="object.linkToTable"
color="primary"
[routerLink]="object.linkToTable(data)"
[queryParams]="object.linkToTableParams ? object.linkToTableParams(data) : {}"
[queryParams]="
object.linkToTableParams ? object.linkToTableParams(data) : {}
"
matTooltip="Zur Tabelle"
>
<mat-icon>subdirectory_arrow_right</mat-icon>
@ -83,15 +96,26 @@
>
<mat-icon>cancel</mat-icon>
</button>
<button
mat-fab
(click)="save()"
<div
*ngIf="data.isLockedByMe"
class="floating-fab-button"
[disabled]="isSavingOrLocking || isLoading"
[matTooltip]="
countUnvalidProperties() > 0
? 'Ungültige oder nicht ausgefüllte Pflichtfelder (rot): ' +
countUnvalidProperties()
: 'Abspeichern'
"
>
<mat-icon>save</mat-icon>
</button>
<button
mat-fab
(click)="save()"
class="floating-fab-button"
[disabled]="
isSavingOrLocking || isLoading || countUnvalidProperties() > 0
"
>
<mat-icon>save</mat-icon>
</button>
</div>
<button
mat-fab
*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 { deepen } from 'src/app/helperFunctions/deepenObject';
import { flatten } from 'src/app/helperFunctions/flattenObject';
@ -7,7 +14,8 @@ import { SchemaService } from 'src/app/services/schema.service';
interface PropertyTypeInfo {
dataPath: string;
translation: string;
readonly?: boolean;
acceptedForUpdating?: boolean;
requiredForUpdating?: boolean;
type?: string;
}
@ -61,6 +69,8 @@ export class DataPageComponent implements OnInit, OnDestroy {
isLoading: boolean = false;
isSavingOrLocking: boolean = false;
propertyValidity = {};
constructor(
private route: ActivatedRoute,
private schemaService: SchemaService
@ -115,7 +125,14 @@ export class DataPageComponent implements OnInit, OnDestroy {
this.pageDataGQLUpdateInputType,
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));
}
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() {
this.saveEvent.emit(
this.schemaService.filterObject(

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

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

@ -55,31 +55,6 @@
</mat-form-field>
</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
#otherInputType
*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,
AfterViewInit,
} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-cell',
@ -17,7 +18,18 @@ import {
})
export class CellComponent implements AfterViewInit {
@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;
maxValue: number;
@ -52,6 +64,8 @@ export class CellComponent implements AfterViewInit {
constructor(private cdr: ChangeDetectorRef, public datepipe: DatePipe) {}
ngOnInit() {}
ngAfterViewInit(): void {
if (this.required) {
this.input?.control?.markAsTouched();
@ -93,6 +107,12 @@ export class CellComponent implements AfterViewInit {
) {
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) {
this.value.min = this.toNumber(event.target.value);
this.value.min = Math.abs(this.toNumber(event.target.value));
this.valueChange.emit(this.value);
console.log(this.value);
this.checkIfRangeIsValid();
}
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);
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) {

@ -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',
title: 'Maße und Ladungen',
title: 'Maße und Ladung',
properties: [
{ dataPath: 'dimensionsAndLoad.bikeLength', translation: 'Länge' },
{ dataPath: 'dimensionsAndLoad.bikeWeight', translation: 'Gewicht' },

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

Loading…
Cancel
Save