Create table template component
parent
0efe4a1050
commit
314fb3c33d
@ -0,0 +1,237 @@
|
|||||||
|
<div class="table-page-wrapper">
|
||||||
|
<div class="table-control">
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
class="table-control-button"
|
||||||
|
disabled
|
||||||
|
i18n
|
||||||
|
>
|
||||||
|
Alle ausgewählten Fahrräder bearbeiten
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
class="table-control-button"
|
||||||
|
matTooltip="Tabllendaten aktualisieren. Achtung! Alle ungespeicherten Änderungen gehen verloren."
|
||||||
|
(click)="reloadTable()"
|
||||||
|
[disabled]="reloadingTable"
|
||||||
|
>
|
||||||
|
<mat-icon class="spin">sync</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
class="table-control-button"
|
||||||
|
(click)="addNewObject()"
|
||||||
|
[disabled]="reloadingTable"
|
||||||
|
>
|
||||||
|
<mat-icon class="spin">add</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-form-field class="filter">
|
||||||
|
<mat-label>Filter</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="filter.includesString"
|
||||||
|
(input)="applyFilter()"
|
||||||
|
placeholder="Suchbegriff eingeben..."
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
*ngIf="!filter.onlyUnsaved && countUnsavedRows() > 0"
|
||||||
|
mat-raised-button
|
||||||
|
color="accent"
|
||||||
|
class="table-control-button"
|
||||||
|
(click)="showOnlyUnsavedElements(true)"
|
||||||
|
[disabled]="reloadingTable"
|
||||||
|
i18n
|
||||||
|
>
|
||||||
|
{{ countUnsavedRows() }} ungespeicherte(s) Element(e) anzeigen
|
||||||
|
</button>
|
||||||
|
<mat-checkbox
|
||||||
|
*ngIf="filter.onlyUnsaved"
|
||||||
|
(change)="showOnlyUnsavedElements(false)"
|
||||||
|
[(ngModel)]="filter.onlyUnsaved"
|
||||||
|
>
|
||||||
|
nur ungespeicherte Elemente anzeigen
|
||||||
|
</mat-checkbox>
|
||||||
|
<mat-paginator
|
||||||
|
[pageSizeOptions]="[15, 25, 30, 50, 100]"
|
||||||
|
showFirstLastButtons
|
||||||
|
></mat-paginator>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table
|
||||||
|
mat-table
|
||||||
|
class="mat-elevation-z8"
|
||||||
|
matSort
|
||||||
|
cdkDropList
|
||||||
|
cdkDropListOrientation="horizontal"
|
||||||
|
(cdkDropListDropped)="drop($event)"
|
||||||
|
[dataSource]="data"
|
||||||
|
>
|
||||||
|
<!--- Note that these columns can be defined in any order.
|
||||||
|
The actual rendered columns are set as a property on the row definition" -->
|
||||||
|
|
||||||
|
<!-- Checkbox Column -->
|
||||||
|
<ng-container matColumnDef="select" sticky>
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<mat-checkbox
|
||||||
|
(change)="$event ? masterToggle() : null"
|
||||||
|
[checked]="selection.hasValue() && isAllSelected()"
|
||||||
|
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||||
|
>
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let row">
|
||||||
|
<mat-checkbox
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
(change)="$event ? selection.toggle(row) : null"
|
||||||
|
[checked]="selection.isSelected(row)"
|
||||||
|
>
|
||||||
|
</mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Other Columns -->
|
||||||
|
<ng-container
|
||||||
|
*ngFor="let column of columnInfo"
|
||||||
|
[matColumnDef]="column.name"
|
||||||
|
[sticky]="isStickyColumn(column.name)"
|
||||||
|
>
|
||||||
|
<!-- add cdkDrag to make columns draggable-->
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
|
{{ getTranslation(column.name) }}
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let element">
|
||||||
|
<app-cell
|
||||||
|
*ngIf="
|
||||||
|
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.name, $event)"
|
||||||
|
[(value)]="element[column.name]"
|
||||||
|
[inputType]="column.type"
|
||||||
|
[link]="column.link ? column.link(element) : null"
|
||||||
|
></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" *ngIf="!element.newObject">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="edit(element)"
|
||||||
|
*ngIf="
|
||||||
|
!element.isLockedByMe &&
|
||||||
|
!element.isLocked &&
|
||||||
|
!isLoading(element.id)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
[matMenuTriggerFor]="menu"
|
||||||
|
*ngIf="
|
||||||
|
!element.isLockedByMe &&
|
||||||
|
!isLoading(element.id) &&
|
||||||
|
!element.isLocked
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #menu="matMenu">
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="openDeleteConfirmationDialog(element)"
|
||||||
|
>
|
||||||
|
<mat-icon>delete</mat-icon>Löschen
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item>
|
||||||
|
<mat-icon>content_copy</mat-icon>Duplizieren
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
|
||||||
|
<mat-spinner
|
||||||
|
[diameter]="32"
|
||||||
|
*ngIf="isLoading(element.id)"
|
||||||
|
></mat-spinner>
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
*ngIf="element.isLockedByMe && !isLoading(element.id)"
|
||||||
|
(click)="save(element)"
|
||||||
|
>
|
||||||
|
<mat-icon>save</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="Alle ungespeicherten Änderungen verwerfen."
|
||||||
|
*ngIf="element.isLockedByMe && !isLoading(element.id)"
|
||||||
|
(click)="cancel(element)"
|
||||||
|
>
|
||||||
|
<mat-icon>cancel</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-icon
|
||||||
|
*ngIf="element.isLocked"
|
||||||
|
matTooltip="Dieser Eintrag wird gerade von einem anderen Bearbeiter editiert. Aktualisieren Sie die Tabelle, um den neuen Status abzurufen."
|
||||||
|
>locked</mat-icon
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="button-wrapper"
|
||||||
|
*ngIf="element.newObject"
|
||||||
|
[matTooltip]="
|
||||||
|
'Nicht ausgefüllte Pflichtfelder (rot): ' +
|
||||||
|
countUnvalidFields(element)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
[disabled]="countUnvalidFields(element) > 0"
|
||||||
|
(click)="create(element)"
|
||||||
|
>
|
||||||
|
<mat-icon>save</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="Verwerfen"
|
||||||
|
(click)="deleteNewObject(element)"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<mat-card
|
||||||
|
*ngIf="!isLoaded"
|
||||||
|
style="display: flex; justify-content: center; align-items: center"
|
||||||
|
>
|
||||||
|
<mat-spinner [diameter]="32"></mat-spinner>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,53 @@
|
|||||||
|
.table-page-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
.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 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-table-sticky {
|
||||||
|
filter: brightness(90%);
|
||||||
|
//opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,364 @@
|
|||||||
|
import { SelectionModel } from '@angular/cdk/collections';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
|
import { BikesService, CargoBikeResult } from 'src/app/services/bikes.service';
|
||||||
|
import { flatten } from 'src/app/helperFunctions/flattenObject';
|
||||||
|
import { deepen } from 'src/app/helperFunctions/deepenObject';
|
||||||
|
import { SchemaService } from 'src/app/services/schema.service';
|
||||||
|
|
||||||
|
import { logArrayInColumnInfoForm } from 'src/app/helperFunctions/logArrayInColumnInfoForm';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { MatSort } from '@angular/material/sort';
|
||||||
|
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-table',
|
||||||
|
templateUrl: './table.component.html',
|
||||||
|
styleUrls: ['./table.component.scss'],
|
||||||
|
})
|
||||||
|
export class TableComponent {
|
||||||
|
/** this array defines the columns and translations of the table and the order they are displayed */
|
||||||
|
@Input()
|
||||||
|
columnInfo: {
|
||||||
|
name: string;
|
||||||
|
translation: string;
|
||||||
|
acceptedForCreation?: boolean;
|
||||||
|
requiredForCreation?: boolean;
|
||||||
|
sticky?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
type?: string;
|
||||||
|
link?: (row: any) => string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
dataService: any;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
tableDataGQLType: string;
|
||||||
|
@Input()
|
||||||
|
tableDataGQLCreateInputType: string;
|
||||||
|
@Input()
|
||||||
|
tableDataGQLUpdateInputType: string;
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
additionalColumnsFront: string[] = ['select'];
|
||||||
|
additionalColumnsBack: string[] = ['buttons'];
|
||||||
|
displayedColumns: string[] = [];
|
||||||
|
|
||||||
|
loadingRowIds: string[] = [];
|
||||||
|
|
||||||
|
/** data source of the table */
|
||||||
|
data: MatTableDataSource<any> = new MatTableDataSource();
|
||||||
|
selection = new SelectionModel<CargoBikeResult>(true, []);
|
||||||
|
|
||||||
|
reloadingTable = false;
|
||||||
|
|
||||||
|
relockingInterval = null;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
relockingIntervalDuration = 1000 * 60 * 1;
|
||||||
|
filter = { includesString: '', onlyUnsaved: false };
|
||||||
|
initialFilter = this.filter;
|
||||||
|
isLoaded = false;
|
||||||
|
|
||||||
|
@Output() createEvent = new EventEmitter();
|
||||||
|
@Output() editEvent = new EventEmitter();
|
||||||
|
@Output() saveEvent = new EventEmitter();
|
||||||
|
@Output() cancelEvent = new EventEmitter();
|
||||||
|
@Output() deleteEvent = new EventEmitter();
|
||||||
|
@Output() relockEvent = new EventEmitter();
|
||||||
|
|
||||||
|
constructor(private schemaService: SchemaService, public dialog: MatDialog) {}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.addColumnPropertiesFromGQLSchemaToColumnInfo();
|
||||||
|
this.data.paginator = this.paginator;
|
||||||
|
this.data.sortingDataAccessor = (item, columnName) => {
|
||||||
|
if (typeof item[columnName] === 'string') {
|
||||||
|
return item[columnName].toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
return item[columnName];
|
||||||
|
};
|
||||||
|
this.data.sort = this.sort;
|
||||||
|
|
||||||
|
this.data.filter = (this.filter as unknown) as string;
|
||||||
|
this.data.filterPredicate = (data, filter: any) => {
|
||||||
|
const a = !filter.onlyUnsaved || data.newObject || data.isLockedByMe;
|
||||||
|
const b =
|
||||||
|
!filter.includesString ||
|
||||||
|
Object.keys(data).some(
|
||||||
|
(k) =>
|
||||||
|
data[k] != null &&
|
||||||
|
data[k]
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(filter.includesString.toLowerCase())
|
||||||
|
);
|
||||||
|
return a && b;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.columnInfo.forEach((column) =>
|
||||||
|
this.displayedColumns.push(column.name)
|
||||||
|
);
|
||||||
|
this.displayedColumns.unshift(this.additionalColumnsFront[0]);
|
||||||
|
this.displayedColumns.push(this.additionalColumnsBack[0]);
|
||||||
|
|
||||||
|
this.dataService.loadingRowIds.subscribe((rowIds) => {
|
||||||
|
this.loadingRowIds = rowIds;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataService.tableData.subscribe((newTableDataSource) => {
|
||||||
|
this.reloadingTable = false;
|
||||||
|
const tempDataSource = [];
|
||||||
|
for (const row of newTableDataSource) {
|
||||||
|
this.isLoaded = true;
|
||||||
|
const oldRow = this.getRowById(row.id);
|
||||||
|
/** make sure to not overwrite a row that is being edited */
|
||||||
|
if (!oldRow) {
|
||||||
|
tempDataSource.push(flatten(row));
|
||||||
|
} else if (!(oldRow.isLockedByMe && row.isLockedByMe)) {
|
||||||
|
tempDataSource.push(flatten(row));
|
||||||
|
} else if (!!oldRow) {
|
||||||
|
tempDataSource.push(oldRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const oldRow of this.data.data) {
|
||||||
|
if (oldRow.newObject) {
|
||||||
|
tempDataSource.unshift(oldRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.data.data = tempDataSource;
|
||||||
|
});
|
||||||
|
this.dataService.loadTableData();
|
||||||
|
|
||||||
|
this.relockingInterval = setInterval(() => {
|
||||||
|
for (const row of this.data.data) {
|
||||||
|
if (row.isLockedByMe) {
|
||||||
|
this.relock(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.relockingIntervalDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
clearInterval(this.relockingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
addColumnPropertiesFromGQLSchemaToColumnInfo() {
|
||||||
|
for (const column of this.columnInfo) {
|
||||||
|
const typeInformation = this.schemaService.getTypeInformation(
|
||||||
|
this.tableDataGQLType,
|
||||||
|
column.name
|
||||||
|
);
|
||||||
|
column.type = column.type || typeInformation.type;
|
||||||
|
}
|
||||||
|
for (const column of this.columnInfo) {
|
||||||
|
const typeInformation = this.schemaService.getTypeInformation(
|
||||||
|
this.tableDataGQLUpdateInputType,
|
||||||
|
column.name
|
||||||
|
);
|
||||||
|
column.readonly = column.readonly || !typeInformation.isPartOfType;
|
||||||
|
}
|
||||||
|
for (const column of this.columnInfo) {
|
||||||
|
const typeInformation = this.schemaService.getTypeInformation(
|
||||||
|
this.tableDataGQLCreateInputType,
|
||||||
|
column.name
|
||||||
|
);
|
||||||
|
column.requiredForCreation = typeInformation.isRequired;
|
||||||
|
column.acceptedForCreation = typeInformation.isPartOfType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTranslation(propertyName: string) {
|
||||||
|
return (
|
||||||
|
this.columnInfo.find((column) => column.name === propertyName)
|
||||||
|
?.translation || propertyName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isStickyColumn(propertyName: string) {
|
||||||
|
return (
|
||||||
|
this.columnInfo.find((column) => column.name === propertyName)?.sticky ||
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading(id: string) {
|
||||||
|
return this.loadingRowIds.includes(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
validityChange(row: any, columnName: string, isValid: Event) {
|
||||||
|
if (!row.FieldsValidity) {
|
||||||
|
row['FieldsValidity'] = {};
|
||||||
|
}
|
||||||
|
row['FieldsValidity'][columnName] = isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
countUnvalidFields(row: any) {
|
||||||
|
let unvalidFieldsCount = 0;
|
||||||
|
if (!row.FieldsValidity) {
|
||||||
|
return 99;
|
||||||
|
}
|
||||||
|
for (const prop in row.FieldsValidity) {
|
||||||
|
if (!row.FieldsValidity[prop]) {
|
||||||
|
unvalidFieldsCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unvalidFieldsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadTable() {
|
||||||
|
this.reloadingTable = true;
|
||||||
|
this.isLoaded = false;
|
||||||
|
this.data.data = [];
|
||||||
|
this.dataService.loadTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewObject() {
|
||||||
|
this.paginator.firstPage();
|
||||||
|
this.setFilter({ ...this.filter, includesString: '' });
|
||||||
|
this.resetSorting();
|
||||||
|
this.data.data = [
|
||||||
|
{ newObject: true, id: this.getNewId() },
|
||||||
|
...this.data.data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewId(): string {
|
||||||
|
let id = -1;
|
||||||
|
while (this.getRowById(id.toString())) {
|
||||||
|
id--;
|
||||||
|
}
|
||||||
|
return id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNewObject(row: any) {
|
||||||
|
this.data.data = this.data.data.filter((element) => row.id !== element.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(row: any) {
|
||||||
|
const newRow = this.schemaService.filterObject(
|
||||||
|
this.tableDataGQLCreateInputType,
|
||||||
|
deepen(row)
|
||||||
|
);
|
||||||
|
this.createEvent.emit(newRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
edit(row: any) {
|
||||||
|
this.editEvent.emit(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
relock(row: any) {
|
||||||
|
this.relockEvent.emit(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
countUnsavedRows(): number {
|
||||||
|
let unsavedCount = 0;
|
||||||
|
for (const row of this.data.data) {
|
||||||
|
if (row.isLockedByMe || row.newObject) {
|
||||||
|
unsavedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unsavedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(row: any) {
|
||||||
|
const deepenRow = this.schemaService.filterObject(
|
||||||
|
this.tableDataGQLUpdateInputType,
|
||||||
|
deepen(row)
|
||||||
|
);
|
||||||
|
this.saveEvent.emit(deepenRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(row: any) {
|
||||||
|
this.cancelEvent.emit(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(row: any) {
|
||||||
|
this.deleteEvent.emit(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeleteConfirmationDialog(row: any) {
|
||||||
|
const dialogRef = this.dialog.open(DeleteConfirmationDialog, {
|
||||||
|
width: '250px',
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((result) => {
|
||||||
|
if (result === true) {
|
||||||
|
this.delete(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowById(id: string) {
|
||||||
|
return this.data.data.find((row) => row.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(event: CdkDragDrop<string[]>) {
|
||||||
|
moveItemInArray(
|
||||||
|
this.displayedColumns,
|
||||||
|
event.previousIndex + 2,
|
||||||
|
event.currentIndex + 2
|
||||||
|
); // +2 because the first 2 (selection + name) columns are not dragable
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the number of selected elements matches the total number of rows. */
|
||||||
|
isAllSelected() {
|
||||||
|
const numSelected = this.selection.selected.length;
|
||||||
|
const numRows = this.data.data.length;
|
||||||
|
return numSelected === numRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Selects all rows if they are not all selected; otherwise clear selection. */
|
||||||
|
masterToggle() {
|
||||||
|
this.isAllSelected()
|
||||||
|
? this.selection.clear()
|
||||||
|
: this.data.data.forEach((row) => this.selection.select(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
showOnlyUnsavedElements(value: boolean) {
|
||||||
|
this.filter.onlyUnsaved = value;
|
||||||
|
this.filter.includesString = '';
|
||||||
|
this.applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter() {
|
||||||
|
this.data.filter = ({
|
||||||
|
...this.filter,
|
||||||
|
includesString: this.filter.includesString.trim().toLowerCase(),
|
||||||
|
} as unknown) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilter(filterObject) {
|
||||||
|
this.filter = filterObject;
|
||||||
|
this.applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSorting() {
|
||||||
|
this.sort.sort({ id: null, start: 'asc', disableClear: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'delete-confirmation-dialog',
|
||||||
|
templateUrl: 'delete-confirmation-dialog.html',
|
||||||
|
})
|
||||||
|
export class DeleteConfirmationDialog {
|
||||||
|
constructor(public dialogRef: MatDialogRef<DeleteConfirmationDialog>) {}
|
||||||
|
|
||||||
|
onConfirmClick(): void {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
onNoClick(): void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
}
|
@ -1,236 +1,13 @@
|
|||||||
<div class="table-page-wrapper">
|
<app-table
|
||||||
<div class="table-control">
|
[columnInfo]="columnInfo"
|
||||||
<button
|
[dataService]="dataService"
|
||||||
mat-raised-button
|
[tableDataGQLType]="tableDataGQLType"
|
||||||
color="primary"
|
[tableDataGQLCreateInputType]="tableDataGQLCreateInputType"
|
||||||
class="table-control-button"
|
[tableDataGQLUpdateInputType]="tableDataGQLUpdateInputType"
|
||||||
disabled
|
(createEvent)="create($event)"
|
||||||
i18n
|
(editEvent)="edit($event)"
|
||||||
>
|
(relockEvent)="relock($event)"
|
||||||
Alle ausgewählten Fahrräder bearbeiten
|
(saveEvent)="save($event)"
|
||||||
</button>
|
(cancelEvent)="cancel($event)"
|
||||||
<button
|
(deleteEvent)="delete($event)"
|
||||||
mat-raised-button
|
></app-table>
|
||||||
class="table-control-button"
|
|
||||||
matTooltip="Tabllendaten aktualisieren. Achtung! Alle ungespeicherten Änderungen gehen verloren."
|
|
||||||
(click)="reloadTable()"
|
|
||||||
[disabled]="reloadingTable"
|
|
||||||
>
|
|
||||||
<mat-icon class="spin">sync</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
mat-raised-button
|
|
||||||
class="table-control-button"
|
|
||||||
(click)="addNewObject()"
|
|
||||||
[disabled]="reloadingTable"
|
|
||||||
>
|
|
||||||
<mat-icon class="spin">add</mat-icon>
|
|
||||||
</button>
|
|
||||||
<mat-form-field class="filter">
|
|
||||||
<mat-label>Filter</mat-label>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
[(ngModel)]="filter.includesString"
|
|
||||||
(input)="applyFilter()"
|
|
||||||
placeholder="Suchbegriff eingeben..."
|
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
<button
|
|
||||||
*ngIf="!filter.onlyUnsaved && countUnsavedRows() > 0"
|
|
||||||
mat-raised-button
|
|
||||||
color="accent"
|
|
||||||
class="table-control-button"
|
|
||||||
(click)="showOnlyUnsavedElements(true)"
|
|
||||||
[disabled]="reloadingTable"
|
|
||||||
i18n
|
|
||||||
>
|
|
||||||
{{ countUnsavedRows() }} ungespeicherte(s) Element(e) anzeigen
|
|
||||||
</button>
|
|
||||||
<mat-checkbox
|
|
||||||
*ngIf="filter.onlyUnsaved"
|
|
||||||
(change)="showOnlyUnsavedElements(false)"
|
|
||||||
[(ngModel)]="filter.onlyUnsaved"
|
|
||||||
>
|
|
||||||
nur ungespeicherte Elemente anzeigen
|
|
||||||
</mat-checkbox>
|
|
||||||
<mat-paginator
|
|
||||||
[pageSizeOptions]="[15, 25, 30, 50, 100]"
|
|
||||||
showFirstLastButtons
|
|
||||||
></mat-paginator>
|
|
||||||
</div>
|
|
||||||
<div class="table-container">
|
|
||||||
<table
|
|
||||||
mat-table
|
|
||||||
class="mat-elevation-z8"
|
|
||||||
matSort
|
|
||||||
cdkDropList
|
|
||||||
cdkDropListOrientation="horizontal"
|
|
||||||
(cdkDropListDropped)="drop($event)"
|
|
||||||
[dataSource]="data"
|
|
||||||
>
|
|
||||||
<!--- Note that these columns can be defined in any order.
|
|
||||||
The actual rendered columns are set as a property on the row definition" -->
|
|
||||||
|
|
||||||
<!-- Checkbox Column -->
|
|
||||||
<ng-container matColumnDef="select" sticky>
|
|
||||||
<th mat-header-cell *matHeaderCellDef>
|
|
||||||
<mat-checkbox
|
|
||||||
(change)="$event ? masterToggle() : null"
|
|
||||||
[checked]="selection.hasValue() && isAllSelected()"
|
|
||||||
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
|
||||||
>
|
|
||||||
</mat-checkbox>
|
|
||||||
</th>
|
|
||||||
<td mat-cell *matCellDef="let row">
|
|
||||||
<mat-checkbox
|
|
||||||
(click)="$event.stopPropagation()"
|
|
||||||
(change)="$event ? selection.toggle(row) : null"
|
|
||||||
[checked]="selection.isSelected(row)"
|
|
||||||
>
|
|
||||||
</mat-checkbox>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Other Columns -->
|
|
||||||
<ng-container
|
|
||||||
*ngFor="let column of columnInfo"
|
|
||||||
[matColumnDef]="column.name"
|
|
||||||
[sticky]="isStickyColumn(column.name)"
|
|
||||||
>
|
|
||||||
<!-- add cdkDrag to make columns draggable-->
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
|
||||||
{{ getTranslation(column.name) }}
|
|
||||||
</th>
|
|
||||||
<td mat-cell *matCellDef="let element">
|
|
||||||
<app-cell
|
|
||||||
*ngIf="
|
|
||||||
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.name, $event)"
|
|
||||||
[(value)]="element[column.name]"
|
|
||||||
[inputType]="column.type"
|
|
||||||
[link]="column.link ? column.link(element) : null"
|
|
||||||
></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" *ngIf="!element.newObject">
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
(click)="edit(element)"
|
|
||||||
*ngIf="
|
|
||||||
!element.isLockedByMe &&
|
|
||||||
!element.isLocked &&
|
|
||||||
!isLoading(element.id)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
[matMenuTriggerFor]="menu"
|
|
||||||
*ngIf="
|
|
||||||
!element.isLockedByMe &&
|
|
||||||
!isLoading(element.id) &&
|
|
||||||
!element.isLocked
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<mat-icon>more_vert</mat-icon>
|
|
||||||
</button>
|
|
||||||
<mat-menu #menu="matMenu">
|
|
||||||
<button
|
|
||||||
mat-menu-item
|
|
||||||
(click)="openDeleteConfirmationDialog(element)"
|
|
||||||
>
|
|
||||||
<mat-icon>delete</mat-icon>Löschen
|
|
||||||
</button>
|
|
||||||
<button mat-menu-item>
|
|
||||||
<mat-icon>content_copy</mat-icon>Duplizieren
|
|
||||||
</button>
|
|
||||||
</mat-menu>
|
|
||||||
|
|
||||||
<mat-spinner
|
|
||||||
[diameter]="32"
|
|
||||||
*ngIf="isLoading(element.id)"
|
|
||||||
></mat-spinner>
|
|
||||||
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
*ngIf="element.isLockedByMe && !isLoading(element.id)"
|
|
||||||
(click)="save(element)"
|
|
||||||
>
|
|
||||||
<mat-icon>save</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
matTooltip="Alle ungespeicherten Änderungen verwerfen."
|
|
||||||
*ngIf="element.isLockedByMe && !isLoading(element.id)"
|
|
||||||
(click)="cancel(element)"
|
|
||||||
>
|
|
||||||
<mat-icon>cancel</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<mat-icon
|
|
||||||
*ngIf="element.isLocked"
|
|
||||||
matTooltip="Dieser Eintrag wird gerade von einem anderen Bearbeiter editiert. Aktualisieren Sie die Tabelle, um den neuen Status abzurufen."
|
|
||||||
>locked</mat-icon
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="button-wrapper"
|
|
||||||
*ngIf="element.newObject"
|
|
||||||
[matTooltip]="
|
|
||||||
'Nicht ausgefüllte Pflichtfelder (rot): ' +
|
|
||||||
countUnvalidFields(element)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
[disabled]="countUnvalidFields(element) > 0"
|
|
||||||
(click)="create(element)"
|
|
||||||
>
|
|
||||||
<mat-icon>save</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
matTooltip="Verwerfen"
|
|
||||||
(click)="deleteNewObject(element)"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<mat-card
|
|
||||||
*ngIf="!isLoaded"
|
|
||||||
style="display: flex; justify-content: center; align-items: center"
|
|
||||||
>
|
|
||||||
<mat-spinner [diameter]="32"></mat-spinner>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
.table-page-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
.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 {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-table-sticky {
|
|
||||||
filter: brightness(90%);
|
|
||||||
//opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue