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">
|
||||
<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>
|
||||
<app-table
|
||||
[columnInfo]="columnInfo"
|
||||
[dataService]="dataService"
|
||||
[tableDataGQLType]="tableDataGQLType"
|
||||
[tableDataGQLCreateInputType]="tableDataGQLCreateInputType"
|
||||
[tableDataGQLUpdateInputType]="tableDataGQLUpdateInputType"
|
||||
(createEvent)="create($event)"
|
||||
(editEvent)="edit($event)"
|
||||
(relockEvent)="relock($event)"
|
||||
(saveEvent)="save($event)"
|
||||
(cancelEvent)="cancel($event)"
|
||||
(deleteEvent)="delete($event)"
|
||||
></app-table>
|
||||
|
@ -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