Add reference table

urls
Max Ehrlicher-Schmidt 4 years ago
parent 80771eded3
commit 260e105504

@ -27,7 +27,7 @@ import {MatSelectModule} from '@angular/material/select';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import {MatDialogModule} from '@angular/material/dialog';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import { AppRoutingModule } from './app-routing.module';
@ -49,7 +49,8 @@ import { TableComponent, DeleteConfirmationDialog } from './components/table/tab
import { DataPageComponent } from './components/data-page/data-page.component';
import { EquipmentTypesComponent } from './pages/tables/equipment-types/equipment-types.component';
import { EngagementTypesComponent } from './pages/tables/engagement-types/engagement-types.component';
import { WorkshopsComponent } from './pages/tables/workshops/workshops.component'
import { WorkshopsComponent } from './pages/tables/workshops/workshops.component';
import { ReferenceTableComponent } from './components/reference-table/reference-table.component'
@NgModule({
@ -69,7 +70,8 @@ import { WorkshopsComponent } from './pages/tables/workshops/workshops.component
DataPageComponent,
EquipmentTypesComponent,
EngagementTypesComponent,
WorkshopsComponent
WorkshopsComponent,
ReferenceTableComponent
],
imports: [
BrowserModule,
@ -100,7 +102,8 @@ import { WorkshopsComponent } from './pages/tables/workshops/workshops.component
MatSelectModule,
MatPaginatorModule,
MatSortModule,
MatDialogModule
MatDialogModule,
MatAutocompleteModule
],
providers: [NavService,
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }],

@ -12,16 +12,30 @@
{{ data[headlineDataPath] }}
</h1>
<ng-container *ngFor="let object of propertiesInfo">
<mat-card class="card" *ngIf="object.isGroup">
<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"
<app-cell
*ngFor="let prop of object.properties"
[editable]="data.isLockedByMe && !prop.readonly"
[(value)]="data[prop.name]"
[label]="prop.translation || prop.name"
[inputType]="prop.type"
></app-cell>
</mat-card>
<mat-card class="inline-card" *ngIf="object.type === 'ReferenceTable'">
<mat-card-title>{{ object.title }}</mat-card-title>
<app-reference-table
[dataServiceThatProvidesThePossibleData]="object.dataService"
[nameToShowInSelection]="object.nameToShowInSelection"
[columnInfo]="object.columnInfo"
[data]="data[object.name]"
[editable]="data.isLockedByMe"
[tableDataGQLType]="object.tableDataGQLType"
(referenceIds)="addReferenceIdsToObject($event, object)"
></app-reference-table>
</mat-card>
</ng-container>
</div>

@ -6,9 +6,9 @@
box-sizing: border-box;
overflow: auto;
padding: 2em;
.card {
.inline-card {
display: inline-table;
width: 20em;
min-width: 20em;
margin: 1em;
}
}

@ -4,17 +4,27 @@ import { deepen } from 'src/app/helperFunctions/deepenObject';
import { flatten } from 'src/app/helperFunctions/flattenObject';
import { SchemaService } from 'src/app/services/schema.service';
interface PropertyInfoType {
interface PropertyTypeInfo {
name: string;
translation: string;
readonly?: boolean;
type?: string;
}
interface PropertyGroup {
isGroup: boolean;
interface PropertyGroupInfo {
type: string;
title: string;
properties: PropertyInfoType[];
properties: PropertyTypeInfo[];
}
interface ReferenceTableInfo {
type: string;
title: string;
name: string;
dataService: any;
columnInfo: PropertyTypeInfo[];
nameToShowInSelection: any;
propertyNameOfUpdateInput: string;
referenceIds: Array<string>;
}
@Component({
@ -24,7 +34,7 @@ interface PropertyGroup {
})
export class DataPageComponent implements OnInit {
@Input()
propertiesInfo: Array<PropertyInfoType | PropertyGroup> = [];
propertiesInfo: Array<any> = [];
@Input()
dataService: any;
@ -32,9 +42,11 @@ export class DataPageComponent implements OnInit {
@Input()
headlineDataPath: string;
@Input()
pageDataGQLType: string = 'CargoBike';
pageDataGQLType: string;
@Input()
pageDataGQLUpdateInputType: string = 'CargoBikeUpdateInput';
pageDataGQLUpdateInputType: string;
@Input()
propertyNameOfUpdateInput: string;
@Output() lockEvent = new EventEmitter();
@Output() saveEvent = new EventEmitter();
@ -67,8 +79,14 @@ export class DataPageComponent implements OnInit {
addPropertiesFromGQLSchemaToObject(infoObject: any) {
for (const prop of infoObject) {
if (prop.isGroup) {
if (prop.type === 'Group') {
this.addPropertiesFromGQLSchemaToObject(prop.properties);
} else if (prop.type === 'ReferenceTable') {
prop.tableDataGQLType =
prop.tableDataGQLType ||
this.schemaService.getTypeInformation(this.pageDataGQLType, prop.name)
.type;
prop.referenceIds = [];
} else {
const typeInformation = this.schemaService.getTypeInformation(
this.pageDataGQLType,
@ -98,7 +116,11 @@ export class DataPageComponent implements OnInit {
}
cancel() {
this.cancelEvent.emit(deepen(this.data))
this.cancelEvent.emit(deepen(this.data));
}
addReferenceIdsToObject(ids: string[], object) {
this.data[object.propertyNameOfUpdateInput] = ids;
}
reloadPageData() {

@ -0,0 +1,93 @@
<div class="table-page-wrapper">
<div class="table-control">
<form [formGroup]="addForm">
<mat-form-field>
<input
type="text"
matInput
placeholder="Element hinzufügen"
formControlName="addGroup"
[matAutocomplete]="autoGroup"
/>
<mat-autocomplete #autoGroup="matAutocomplete">
<mat-option
*ngFor="let element of possibleValueOptions"
[value]="nameToShowInSelection(element)"
(click)="addReference(element)"
>
{{ nameToShowInSelection(element) }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>
<mat-form-field class="filter">
<mat-label>Tabelle filtern</mat-label>
<input
matInput
(input)="applyTableFilter()"
[(ngModel)]="tableFilterString"
placeholder="Suchbegriff eingeben..."
/>
</mat-form-field>
<mat-paginator
[pageSizeOptions]="[15, 25]"
showFirstLastButtons
></mat-paginator>
</div>
<div class="table-container">
<table mat-table class="mat-elevation-z8" matSort [dataSource]="dataSource">
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" -->
<!-- Other Columns -->
<ng-container
*ngFor="let column of columnInfo"
[matColumnDef]="column.name"
[sticky]="isStickyColumn(column.name)"
>
<th mat-header-cell *matHeaderCellDef mat-sort-header>
{{ column.translation || column.name }}
</th>
<td mat-cell *matCellDef="let element">
<app-cell
*ngIf="column.type === 'Boolean'; else stringValue"
[editable]="false"
[(value)]="element[column.name]"
[inputType]="column.type"
></app-cell>
<ng-template #stringValue>
<span *ngIf="!column.link">{{ element[column.name] }}</span>
<a
mat-button
color="primary"
*ngIf="column.link"
[routerLink]="column.link(element)"
>{{ element[column.name] }}</a
>
</ng-template>
</td>
</ng-container>
<!-- Buttons Column -->
<ng-container matColumnDef="buttons" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">
<div class="button-wrapper">
<button
mat-icon-button
matTooltip="Entfernen"
(click)="delete(element)"
[disabled]="!editable"
>
<mat-icon>delete</mat-icon>
</button>
</div>
</td>
</ng-container>
<!-- Table Definition -->
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
</div>

@ -0,0 +1,60 @@
.table-page-wrapper {
display: flex;
flex-direction: column;
height: 100%;
.headline {
margin: 0 0.5em;
}
.table-control {
margin: 0.5em;
flex: none;
.table-control-button {
margin: 0.25em;
}
.filter {
margin-right: 0.5em;
}
.mat-paginator {
display: inline-block;
width: 50em;
margin-right: 0.5em;
}
}
.table-container {
flex: 1;
width: auto;
margin-left: 0.5em;
margin-right: 0.5em;
max-width: 100%;
overflow: auto;
table {
max-width: 100%;
margin: 0 auto;
.mat-header-cell,
.mat-footer-cell,
.mat-cell {
min-width: 3em;
box-sizing: border-box;
padding: 0 0.25em;
::ng-deep.mat-form-field-infix {
width: auto !important;
min-width: 50px !important;
}
}
::ng-deep.mat-form-field {
width: 100%;
}
.mat-table-sticky {
filter: brightness(90%);
//opacity: 1;
}
.button-wrapper {
display: flex;
flex-direction: row;
}
}
}
}

@ -0,0 +1,189 @@
import {
Component,
EventEmitter,
Input,
Output,
ViewChild,
} from '@angular/core';
import { flatten } from 'src/app/helperFunctions/flattenObject';
import { SchemaService } from 'src/app/services/schema.service';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { FormControl, FormGroup } from '@angular/forms';
@Component({
selector: 'app-reference-table',
templateUrl: './reference-table.component.html',
styleUrls: ['./reference-table.component.scss'],
})
export class ReferenceTableComponent {
/** this array defines the columns and translations of the table and the order they are displayed */
@Input()
columnInfo: {
name: string;
translation: string;
sticky?: boolean;
type?: string;
link?: (row: any) => string;
}[] = [];
@Input()
dataServiceThatProvidesThePossibleData: any;
@Input()
nameToShowInSelection: any;
@Input()
set editable(value: boolean) {
this._editable = value;
value
? this.addForm.get('addGroup').enable()
: this.addForm.get('addGroup').disable();
}
get editable() {
return this._editable;
}
_editable: boolean = false;
@Input()
set data(newdata) {
if (!newdata) return;
this.dataSource.data = [];
for (const element of newdata) {
this.dataSource.data.push(flatten(element));
}
this.dataSource.data = this.dataSource.data;
this.onReferenceChange();
}
get data(): any {
return this.dataSource.data;
}
@Input()
tableDataGQLType: string;
@Output() referenceIds = new EventEmitter();
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
displayedColumns: string[] = [];
/** data source of the table */
dataSource: MatTableDataSource<any> = new MatTableDataSource([]);
possibleValues: Array<any> = [];
possibleValueOptions: Array<any> = [];
reloadingTable = false;
addForm: FormGroup = new FormGroup({ addGroup: new FormControl() });
tableFilterString: string = '';
constructor(private schemaService: SchemaService) {}
ngOnInit() {
this.addColumnPropertiesFromGQLSchemaToColumnInfo();
this.columnInfo.forEach((column) => {
this.displayedColumns.push(column.name);
});
this.displayedColumns.push('buttons');
this.addForm
.get('addGroup')
.valueChanges.subscribe(() => this.filterPossibleValueOptions());
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (item, columnName) => {
if (typeof item[columnName] === 'string') {
return item[columnName].toLocaleLowerCase();
}
return item[columnName];
};
this.dataServiceThatProvidesThePossibleData.tableData.subscribe((data) => {
this.possibleValues = [];
if (data) {
for (const row of data) {
this.possibleValues.push(flatten(row));
}
}
this.filterPossibleValueOptions();
});
this.dataServiceThatProvidesThePossibleData.loadTableData();
}
addColumnPropertiesFromGQLSchemaToColumnInfo() {
for (const column of this.columnInfo) {
const typeInformation = this.schemaService.getTypeInformation(
this.tableDataGQLType,
column.name
);
column.type = column.type || typeInformation.type;
}
}
isStickyColumn(propertyName: string) {
return (
this.columnInfo.find((column) => column.name === propertyName)?.sticky ||
false
);
}
delete(row: any) {
const index = this.dataSource.data.findIndex(
(element) => element.id === row.id
);
if (index === -1) return;
this.dataSource.data.splice(index, 1);
this.dataSource.data = this.dataSource.data; //needed to trigger update lol
this.filterPossibleValueOptions();
this.onReferenceChange();
}
addReference(row: any) {
this.addForm.get('addGroup').reset();
this.dataSource.data = [row, ...this.dataSource.data];
this.tableFilterString = '';
this.applyTableFilter();
this.filterPossibleValueOptions();
this.onReferenceChange();
}
getRowById(id: string) {
return this.dataSource.data.find((row) => row.id === id);
}
filterPossibleValueOptions() {
this.possibleValueOptions = this.possibleValues.filter(
(element) => !this.dataSource.data.find((row) => row.id === element.id)
);
let searchString = this.addForm.get('addGroup').value;
if (!searchString) {
return;
}
searchString = searchString.toLocaleLowerCase();
this.possibleValueOptions = this.possibleValueOptions.filter((element) =>
this.nameToShowInSelection(element)
.toLocaleLowerCase()
.includes(searchString)
);
}
applyTableFilter() {
this.dataSource.filter = this.tableFilterString;
}
onReferenceChange() {
const ids = [];
for (const element of this.data) {
ids.push(element.id);
}
this.referenceIds.emit(ids);
}
}

@ -1,5 +1,5 @@
<div class="table-page-wrapper">
<h1 class="headline">
<h1 class="headline" *ngIf="headline">
{{ headline }}
</h1>
<div class="table-control">

@ -24,7 +24,7 @@ import { MatDialog, MatDialogRef } from '@angular/material/dialog';
})
export class TableComponent {
@Input()
headline: string = "Keine Überschrift";
headline: string = null;
/** this array defines the columns and translations of the table and the order they are displayed */
@Input()
columnInfo: {

@ -1,5 +1,8 @@
export function deepen(object: Object): any {
let newObject = {};
let newObject;
if (Array.isArray(object)) {
newObject = new Array();
} else newObject = new Object();
for (const prop in object) {
if (prop.includes('.')) {
const splittedProp = prop.split('.');

@ -2,11 +2,15 @@ export function flatten(object: Object, prefix: string = ''): any {
let newObject = {};
for (const prop in object) {
let propName = prefix + prop;
if (typeof object[prop] === 'object' && object[prop] !== null) {
if (Array.isArray(object[prop])) {
newObject[propName] = [];
for (const arrayElement of object[prop]) {
newObject[propName].push(flatten(arrayElement));
}
} else if (typeof object[prop] === 'object' && object[prop] !== null) {
const flattenedObject = flatten(object[prop], propName + '.');
for (const flattenedProperty in flattenedObject) {
newObject[flattenedProperty] =
flattenedObject[flattenedProperty];
newObject[flattenedProperty] = flattenedObject[flattenedProperty];
}
} else {
newObject[propName] = object[prop];

@ -4,6 +4,7 @@
[headlineDataPath]="headlineDataPath"
[pageDataGQLType]="pageDataGQLType"
[pageDataGQLUpdateInputType]="pageDataGQLUpdateInputType"
[propertyNameOfUpdateInput]="propertyNameOfUpdateInput"
(lockEvent)="lock($event)"
(saveEvent)="save($event)"
(cancelEvent)="cancel($event)"

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { BikesService } from 'src/app/services/bikes.service';
import { EquipmentTypeService } from 'src/app/services/equipmentType.service';
@Component({
selector: 'app-bike',
@ -9,17 +10,17 @@ import { BikesService } from 'src/app/services/bikes.service';
export class BikeComponent implements OnInit {
propertiesInfo = [
{
isGroup: true,
type: 'Group',
title: 'Allgemein',
properties: [
{ name: 'name', translation: 'Name' },
{ name: 'id', translation: 'ID', readonly: true },
{ name: 'group', translation: 'Gruppe' },
{ name: 'Group', translation: 'Gruppe' },
{ name: 'modelName', translation: 'Modell' },
],
},
{
isGroup: true,
type: 'Group',
title: 'Versicherungsdaten',
properties: [
{
@ -51,7 +52,7 @@ export class BikeComponent implements OnInit {
],
},
{
isGroup: true,
type: 'Group',
title: 'Maße und Ladungen',
properties: [
{ name: 'dimensionsAndLoad.bikeLength', translation: 'Länge' },
@ -85,7 +86,7 @@ export class BikeComponent implements OnInit {
],
},
{
isGroup: true,
type: 'Group',
title: 'Sicherheitsinformationen',
properties: [
{ name: 'security.frameNumber', translation: 'Rahmennummer' },
@ -102,7 +103,7 @@ export class BikeComponent implements OnInit {
],
},
{
isGroup: true,
type: 'Group',
title: 'Ausstattung',
properties: [
{ name: 'technicalEquipment.bicycleShift', translation: 'Schaltung' },
@ -118,7 +119,7 @@ export class BikeComponent implements OnInit {
],
},
{
isGroup: true,
type: 'Group',
title: 'Sonstiges',
properties: [
{ name: 'stickerBikeNameState', translation: 'Aufkleber Status' },
@ -131,7 +132,7 @@ export class BikeComponent implements OnInit {
],
},
{
isGroup: true,
type: 'Group',
title: 'provider',
properties: [
{ name: 'provider.id', translation: '' },
@ -147,7 +148,7 @@ export class BikeComponent implements OnInit {
],
},
{
isGroup: true,
type: 'Group',
title: 'lendingstation',
properties: [
{ name: 'lendingStation.id', translation: '' },
@ -157,6 +158,15 @@ export class BikeComponent implements OnInit {
{ name: 'lendingStation.address.zip', translation: '' },
],
},
{
type: 'ReferenceTable',
title: 'Equipmenttypen',
name: 'equipmentType',
dataService: null,
columnInfo: [{name: 'name', translation: "Name"}, {name: 'description', translation: "Beschreibung"}],
nameToShowInSelection: (element) => {return element.name},
propertyNameOfUpdateInput: "equipmentTypeIds"
},
];
headlineDataPath = 'name';
@ -165,7 +175,14 @@ export class BikeComponent implements OnInit {
dataService: any;
constructor(private bikesService: BikesService) {}
constructor(
private bikesService: BikesService,
private equipmentTypeService: EquipmentTypeService
) {
this.propertiesInfo.find(
(prop) => prop.name === 'equipmentType'
).dataService = this.equipmentTypeService;
}
ngOnInit(): void {
this.dataService = this.bikesService;

@ -9,7 +9,7 @@
</mat-grid-list>
<div>
<mat-grid-list cols="4" rowHeight="30px">
<mat-grid-tile routerLink = "/table/bikes">Tabelle 5</mat-grid-tile>
<mat-grid-tile routerLink = "/table/equipmentTypes">Equipmenttypen</mat-grid-tile>
<mat-grid-tile routerLink = "/table/bikes">Tabelle 7</mat-grid-tile>
<mat-grid-tile routerLink = "/table/bikes">Tabelle 8</mat-grid-tile>
<mat-grid-tile routerLink = "/table/bikes">Tabelle 9</mat-grid-tile>

@ -1,5 +1,6 @@
mat-grid-tile {
background: gray;
cursor: pointer;
}
.grid-list-spacer{

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TableOverviewComponent } from './table-overview.component';
describe('TableOverviewComponent', () => {
let component: TableOverviewComponent;
let fixture: ComponentFixture<TableOverviewComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TableOverviewComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TableOverviewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -63,7 +63,7 @@ export class BikesService {
loadTableData() {
this.tableData.next(null);
this.getCargoBikesGQL.fetch().subscribe((result) => {
this.tableData.next(result.data.cargoBikes);
this.tableData.next(result.data?.cargoBikes);
});
}
@ -85,7 +85,7 @@ export class BikesService {
this.reloadCargoBikeByIdGQL
.fetch(variables)
.subscribe((result) => {
this.updateDataRowFromResponse(result.data.cargoBikeById)
this.updateDataRowFromResponse(result.data.cargoBikeById);
})
.add(() => {
this.removeLoadingRowId(variables.id);
@ -153,11 +153,13 @@ export class BikesService {
}
private updateDataRowFromResponse(rowFromResponse: any) {
if (this.tableData.value) {
const newTableData = this.tableData.value.map((row) =>
rowFromResponse.id === row.id ? rowFromResponse : row
);
this.tableData.next(newTableData);
if ((rowFromResponse.id === this.pageData?.value?.id)) {
}
if (rowFromResponse.id === this.pageData?.value?.id) {
this.pageData.next(rowFromResponse);
}
}

@ -32,7 +32,7 @@ export class EquipmentTypeService {
private updateEquipmentTypeGQL: UpdateEquipmentTypeGQL,
private lockEquipmentTypeGQL: LockEquipmentTypeGQL,
private unlockEquipmentTypeGQL: UnlockEquipmentTypeGQL,
private deleteEquipmentTypeGQL: DeleteEquipmentTypeGQL,
private deleteEquipmentTypeGQL: DeleteEquipmentTypeGQL
) {}
addLoadingRowId(id: string) {
@ -116,9 +116,11 @@ export class EquipmentTypeService {
}
private updateDataRowFromResponse(rowFromResponse: any) {
if (this.tableData.value) {
const newTableData = this.tableData.value.map((row) =>
rowFromResponse.id === row.id ? rowFromResponse : row
);
this.tableData.next(newTableData);
}
}
}

@ -95,7 +95,12 @@ export class SchemaService {
}
filterObject(graphQLTypeName: string, object: object): any {
const filteredObject = {};
let filteredObject;
if (Array.isArray(object)) {
return object;
//TODO: check if array consists of objects?
} else filteredObject = new Object();
for (const prop in object) {
if (typeof object[prop] === 'object' && object[prop] !== null) {
const info = this.getTypeInformation(graphQLTypeName, prop);

Loading…
Cancel
Save