Merge remote-tracking branch 'origin/master'
commit
bc1d1d4cae
@ -0,0 +1,171 @@
|
|||||||
|
<mat-spinner
|
||||||
|
*ngIf="isLoading"
|
||||||
|
[diameter]="32"
|
||||||
|
class="page-loading-spinner"
|
||||||
|
></mat-spinner>
|
||||||
|
<div class="page-wrapper" *ngIf="!isLoading && !data">
|
||||||
|
<h1>Seite konnte nicht gefunden werden :(</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-page-wrapper" *ngIf="data && !isLoading">
|
||||||
|
<h1 class="headline">
|
||||||
|
{{ getHeadline !== undefined ? getHeadline(data) : data[headlineDataPath] }}
|
||||||
|
<mat-icon>{{ headlineIconName }}</mat-icon>
|
||||||
|
</h1>
|
||||||
|
<ng-container *ngFor="let object of propertiesInfo">
|
||||||
|
<mat-card
|
||||||
|
class="inline-card"
|
||||||
|
*ngIf="
|
||||||
|
object.type === 'Group' &&
|
||||||
|
(!object.hideCondition || !object.hideCondition(data))
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<mat-card-title class="card-header">
|
||||||
|
<h2>{{ object.title }}</h2>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
*ngIf="data?.isLockedByMe && object.possibleObjects"
|
||||||
|
(click)="openSelectObjectDialog(object)"
|
||||||
|
>
|
||||||
|
<mat-icon>expand_more</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-card-title>
|
||||||
|
<ng-container *ngFor="let prop of object.properties">
|
||||||
|
<app-cell
|
||||||
|
*ngIf="
|
||||||
|
prop.type !== 'NumRange' &&
|
||||||
|
prop.type !== 'Link' &&
|
||||||
|
prop.type !== 'DateRange'
|
||||||
|
"
|
||||||
|
[isList]="prop.list"
|
||||||
|
[editable]="data?.isLockedByMe && prop.acceptedForUpdating"
|
||||||
|
[required]="prop.requiredForUpdating && data?.isLockedByMe"
|
||||||
|
(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>
|
||||||
|
<app-date-range-cell
|
||||||
|
*ngIf="prop.type === 'DateRange'"
|
||||||
|
[editable]="data?.isLockedByMe && prop.acceptedForUpdating"
|
||||||
|
[required]="prop.requiredForUpdating && data?.isLockedByMe"
|
||||||
|
(validityChange)="validityChange(prop.dataPath, $event)"
|
||||||
|
[(from)]="data[prop.dataPath + '.from']"
|
||||||
|
[(to)]="data[prop.dataPath + '.to']"
|
||||||
|
></app-date-range-cell>
|
||||||
|
<a
|
||||||
|
mat-button
|
||||||
|
class="link-button"
|
||||||
|
color="primary"
|
||||||
|
*ngIf="prop.type === 'Link'"
|
||||||
|
[routerLink]="prop.link(data)"
|
||||||
|
>{{ prop.linkText }}
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card
|
||||||
|
class="inline-card"
|
||||||
|
*ngIf="
|
||||||
|
object.type === 'ReferenceTable' &&
|
||||||
|
(!object.hideCondition || !object.hideCondition(data))
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<mat-card-title
|
||||||
|
><h2>{{ object.title }}</h2>
|
||||||
|
<a
|
||||||
|
mat-button
|
||||||
|
*ngIf="object.linkToTable"
|
||||||
|
color="primary"
|
||||||
|
[routerLink]="object.linkToTable(data)"
|
||||||
|
[queryParams]="
|
||||||
|
object.linkToTableParams ? object.linkToTableParams(data) : {}
|
||||||
|
"
|
||||||
|
matTooltip="Zur Tabelle"
|
||||||
|
>
|
||||||
|
<mat-icon>subdirectory_arrow_right</mat-icon>
|
||||||
|
<mat-icon>table_chart</mat-icon>
|
||||||
|
</a>
|
||||||
|
</mat-card-title>
|
||||||
|
<app-reference-table
|
||||||
|
[dataServiceThatProvidesThePossibleData]="object.dataService"
|
||||||
|
[nameToShowInSelection]="object.nameToShowInSelection"
|
||||||
|
[columnInfo]="object.columnInfo"
|
||||||
|
[data]="data[object.dataPath]"
|
||||||
|
[editable]="data?.isLockedByMe"
|
||||||
|
[tableDataGQLType]="object.tableDataGQLType"
|
||||||
|
(referenceIds)="addReferenceIdsToObject($event, object)"
|
||||||
|
[editableReferences]="object.editableReferences"
|
||||||
|
></app-reference-table>
|
||||||
|
</mat-card>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
(click)="reloadPageData()"
|
||||||
|
class="floating-fab-button-top"
|
||||||
|
[disabled]="isSavingOrLocking || isLoading"
|
||||||
|
color="primary"
|
||||||
|
matTooltip="Daten aktualisieren. Achtung! Alle ungespeicherten Änderungen gehen verloren."
|
||||||
|
>
|
||||||
|
<mat-icon>sync</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="floating-fab-button-box">
|
||||||
|
<button
|
||||||
|
mat-fab
|
||||||
|
(click)="lock()"
|
||||||
|
*ngIf="!data?.isLockedByMe && !data?.isLocked"
|
||||||
|
class="floating-fab-button"
|
||||||
|
color="primary"
|
||||||
|
[disabled]="isSavingOrLocking || isLoading"
|
||||||
|
>
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
(click)="cancel()"
|
||||||
|
*ngIf="data?.isLockedByMe"
|
||||||
|
class="floating-fab-button"
|
||||||
|
[disabled]="isSavingOrLocking || isLoading"
|
||||||
|
>
|
||||||
|
<mat-icon>cancel</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
*ngIf="data?.isLockedByMe"
|
||||||
|
[matTooltip]="
|
||||||
|
countUnvalidProperties() > 0
|
||||||
|
? 'Ungültige oder nicht ausgefüllte Pflichtfelder (rot): ' +
|
||||||
|
countUnvalidProperties()
|
||||||
|
: 'Abspeichern'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
matTooltip="Dieser Eintrag wird gerade von einem anderen Bearbeiter editiert. Aktualisieren Sie die Seite, um den neuen Status abzurufen."
|
||||||
|
class="floating-fab-button"
|
||||||
|
>
|
||||||
|
<mat-icon>locked</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,43 @@
|
|||||||
|
.page-loading-spinner {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.data-page-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 2em;
|
||||||
|
.inline-card {
|
||||||
|
display: inline-table;
|
||||||
|
min-width: 20em;
|
||||||
|
margin: 1em;
|
||||||
|
.link-button {
|
||||||
|
padding: 0;
|
||||||
|
margin-top: -16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#floating-fab-button-box {
|
||||||
|
z-index: 999;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2em;
|
||||||
|
right: 2em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
.floating-fab-button {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.floating-fab-button-top {
|
||||||
|
position: absolute;
|
||||||
|
top: 2em;
|
||||||
|
right: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.mat-icon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,246 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { deepen } from '../../helperFunctions/deepenObject';
|
||||||
|
import { flatten } from '../../helperFunctions/flattenObject';
|
||||||
|
import { SchemaService } from '../../services/schema.service';
|
||||||
|
import { SelectObjectDialogComponent } from '../select-object-dialog/select-object-dialog.component';
|
||||||
|
|
||||||
|
interface PropertyTypeInfo {
|
||||||
|
dataPath: string;
|
||||||
|
translation: string;
|
||||||
|
acceptedForUpdating?: boolean;
|
||||||
|
requiredForUpdating?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropertyGroupInfo {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
properties: PropertyTypeInfo[];
|
||||||
|
}
|
||||||
|
interface ReferenceTableInfo {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
dataPath: string;
|
||||||
|
dataService: any;
|
||||||
|
columnInfo: PropertyTypeInfo[];
|
||||||
|
nameToShowInSelection: any;
|
||||||
|
propertyNameOfUpdateInput: string;
|
||||||
|
referenceIds: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-admin-data-page',
|
||||||
|
templateUrl: './admin-data-page.component.html',
|
||||||
|
styleUrls: ['./admindata-page.component.scss'],
|
||||||
|
})
|
||||||
|
export class DataPageComponent implements OnInit, OnDestroy {
|
||||||
|
@Input()
|
||||||
|
propertiesInfo: Array<any> = [];
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
dataService: any;
|
||||||
|
|
||||||
|
/** specifies which property should be shown in the headline */
|
||||||
|
@Input()
|
||||||
|
headlineDataPath: string;
|
||||||
|
/** specifies which string should be shown in the headline. If this is provided headlineDataPath is ignored*/
|
||||||
|
@Input()
|
||||||
|
getHeadline: (any) => string;
|
||||||
|
@Input()
|
||||||
|
headlineIconName: string = 'help_outline';
|
||||||
|
@Input()
|
||||||
|
pageDataGQLType: string;
|
||||||
|
@Input()
|
||||||
|
pageDataGQLUpdateInputType: string;
|
||||||
|
@Input()
|
||||||
|
propertyNameOfUpdateInput: string;
|
||||||
|
|
||||||
|
relockingInterval = null;
|
||||||
|
@Input()
|
||||||
|
relockingIntervalDuration = 1000 * 60 * 1;
|
||||||
|
|
||||||
|
@Output() lockEvent = new EventEmitter();
|
||||||
|
@Output() saveEvent = new EventEmitter();
|
||||||
|
@Output() cancelEvent = new EventEmitter();
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
data: any = null;
|
||||||
|
isLoading: boolean = false;
|
||||||
|
isSavingOrLocking: boolean = false;
|
||||||
|
|
||||||
|
propertyValidity = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private schemaService: SchemaService,
|
||||||
|
public dialog: MatDialog
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.addPropertiesFromGQLSchemaToObject(this.propertiesInfo);
|
||||||
|
this.id = this.route.snapshot.paramMap.get('id');
|
||||||
|
this.reloadPageData();
|
||||||
|
this.dataService.pageData.subscribe((data) => {
|
||||||
|
if (data == null) {
|
||||||
|
this.data = null;
|
||||||
|
} else if (this.data?.isLockedByMe && data?.isLockedByMe) {
|
||||||
|
// dont overwrite data when in edit mode and relock is performed
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.data = flatten(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.dataService.isLoadingPageData.subscribe(
|
||||||
|
(isLoading) => (this.isLoading = isLoading)
|
||||||
|
);
|
||||||
|
this.dataService.loadingRowIds.subscribe((loadingRowIds) => {
|
||||||
|
this.isSavingOrLocking = loadingRowIds.includes(this.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.relockingInterval = setInterval(() => {
|
||||||
|
if (this.data?.isLockedByMe) {
|
||||||
|
this.lock();
|
||||||
|
}
|
||||||
|
}, this.relockingIntervalDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
clearInterval(this.relockingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
addPropertiesFromGQLSchemaToObject(infoObject: any) {
|
||||||
|
for (const prop of infoObject) {
|
||||||
|
if (prop.type === 'Link') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (prop.type === 'Group') {
|
||||||
|
this.addPropertiesFromGQLSchemaToObject(prop.properties);
|
||||||
|
} else if (prop.type === 'ReferenceTable') {
|
||||||
|
prop.tableDataGQLType =
|
||||||
|
prop.tableDataGQLType ||
|
||||||
|
this.schemaService.getTypeInformation(
|
||||||
|
this.pageDataGQLType,
|
||||||
|
prop.dataPath
|
||||||
|
).type;
|
||||||
|
if (!prop.type) {
|
||||||
|
console.error(
|
||||||
|
"Didn't found type for: " +
|
||||||
|
prop.dataPath +
|
||||||
|
' on ' +
|
||||||
|
this.pageDataGQLType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
prop.referenceIds = [];
|
||||||
|
} else {
|
||||||
|
const typeInformation = this.schemaService.getTypeInformation(
|
||||||
|
this.pageDataGQLType,
|
||||||
|
prop.dataPath
|
||||||
|
);
|
||||||
|
prop.type = prop.type || typeInformation.type;
|
||||||
|
prop.list = typeInformation.isList;
|
||||||
|
if (!prop.type) {
|
||||||
|
console.error(
|
||||||
|
"Didn't found type for: " +
|
||||||
|
prop.dataPath +
|
||||||
|
' on ' +
|
||||||
|
this.pageDataGQLType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
prop.required =
|
||||||
|
prop.required != null ? prop.required : typeInformation.isRequired;
|
||||||
|
|
||||||
|
const updateTypeInformation = this.schemaService.getTypeInformation(
|
||||||
|
this.pageDataGQLUpdateInputType,
|
||||||
|
prop.dataPath
|
||||||
|
);
|
||||||
|
prop.acceptedForUpdating =
|
||||||
|
prop.acceptedForUpdating != null
|
||||||
|
? prop.acceptedForUpdating
|
||||||
|
: updateTypeInformation.isPartOfType;
|
||||||
|
|
||||||
|
prop.requiredForUpdating =
|
||||||
|
prop.requiredForUpdating != null
|
||||||
|
? prop.requiredForUpdating
|
||||||
|
: prop.required || typeInformation.isRequired;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock() {
|
||||||
|
this.lockEvent.emit(deepen(this.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
validityChange(propName: string, isValid: Event) {
|
||||||
|
this.propertyValidity[propName] = 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(
|
||||||
|
this.pageDataGQLUpdateInputType,
|
||||||
|
deepen(this.data)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.cancelEvent.emit(deepen(this.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
openSelectObjectDialog(object: any) {
|
||||||
|
const dialogRef = this.dialog.open(SelectObjectDialogComponent, {
|
||||||
|
width: 'auto',
|
||||||
|
autoFocus: false,
|
||||||
|
data: {
|
||||||
|
nameToShowInSelection: object.nameToShowInSelection,
|
||||||
|
currentlySelectedObjectId: object.currentlySelectedObjectId(this.data),
|
||||||
|
possibleObjects: object.possibleObjects,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe((selectedObject) => {
|
||||||
|
if (selectedObject) {
|
||||||
|
this.data[object.propertyNameOfReferenceId] = selectedObject.id;
|
||||||
|
const newObjectFlattened = flatten(selectedObject);
|
||||||
|
for (const newProperty in newObjectFlattened) {
|
||||||
|
this.data[object.propertyPrefixToOverwrite + '.' + newProperty] =
|
||||||
|
newObjectFlattened[newProperty];
|
||||||
|
}
|
||||||
|
} else if (selectedObject === null) {
|
||||||
|
this.data[object.propertyNameOfReferenceId] = null;
|
||||||
|
for (const prop in this.data) {
|
||||||
|
if (prop.startsWith(object.propertyPrefixToOverwrite)) {
|
||||||
|
this.data[prop] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addReferenceIdsToObject(ids: string[], object) {
|
||||||
|
this.data[object.propertyNameOfUpdateInput] = ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadPageData() {
|
||||||
|
this.dataService.loadPageData({ id: this.id });
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue