Skip to content
Snippets Groups Projects
Commit 85b41fce authored by paf's avatar paf
Browse files

group files by type in 5 different tables

parent f8ab53e1
No related branches found
No related tags found
No related merge requests found
......@@ -31,9 +31,9 @@ export class BackendService {
);
}
getFiles(): Observable<GetFileResponseData[]> {
getFiles(): Observable<GetFileResponseData> {
return this.http
.get<GetFileResponseData[]>(`${this.serverUrl}workspace/${this.wds.wsId}/files`)
.get<GetFileResponseData>(`${this.serverUrl}workspace/${this.wds.wsId}/files`)
.pipe(
catchError((err: ApiError) => {
console.warn(`getFiles Api-Error: ${err.code} ${err.info} `);
......
......@@ -11,6 +11,11 @@
flex: 10 0 400px;
}
mat-table {
margin-top: 1em;
margin-bottom: 2em;
}
.checkboxcell {
overflow: visible;
flex: 0 0 30px;
......@@ -20,6 +25,11 @@
flex: 3 3 60px;
}
.namecell .mat-subheading-1 {
padding: 0 16px;
margin-bottom: 0;
}
.datecell {
flex: 1 1 5px;
}
......
<div class="columnhost">
<div class="filelist">
<mat-table #table [dataSource]="serverfiles" matSort>
<ng-container matColumnDef="checked">
<ng-container *ngFor="let type of fileTypes">
<mat-table *ngIf="files && files[type]" [dataSource]="files[type]" matSort (matSortChange)="setTableSorting($event)">
<ng-container matColumnDef="checked">
<mat-header-cell *matHeaderCellDef class="checkboxcell">
<mat-checkbox (change)="checkAll($event.checked)"></mat-checkbox>
<mat-checkbox (change)="checkAll($event.checked, type)"></mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let element" class="checkboxcell">
<mat-checkbox [checked]="element.isChecked" (change)="element.isChecked=$event.checked"></mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header> Typ </mat-header-cell>
<mat-cell *matCellDef="let element">{{typeLabels[element.type]}}</mat-cell>
</ng-container>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header class="namecell"> Name </mat-header-cell>
<mat-cell *matCellDef="let element" class="namecell">
<div class="file-report">
<button mat-button (click)="download(element)">{{element.name}}</button>
<div *ngFor="let level of ['error', 'warning', 'info']" class="vertical-align-middle">
<ng-container *ngIf="element.report[level] && element.report[level].length">
<div>{{element.report[level].length}}</div>
<mat-icon class="report-{{level}}">{{level}}</mat-icon>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header class="namecell">
<div class="mat-subheading-1">{{files[type].data.length}} {{typeLabels[type]}}
<span *ngIf="type=='Testtakers'">({{fileStats.testtakers}} Teilnehmer)</span>
</div>
<mat-card class="full-file-report">
<mat-card-header *ngIf="element.info.label || element.id">
<mat-card-title>
{{element.info.label}}
<span
*ngIf="element.id !== element.name.toUpperCase()"
style="{{element.info.label ? 'color:silver' : ''}}">
<alert level="error" *ngIf="fileStats.invalid[type]" text="`{{fileStats.invalid[type]}}` Fehlerhaft"></alert>
</mat-header-cell>
<mat-cell *matCellDef="let element" class="namecell">
<div class="file-report">
<button mat-button (click)="download(element)">{{element.name}}</button>
<div *ngFor="let level of ['error', 'warning', 'info']" class="vertical-align-middle">
<ng-container *ngIf="element.report[level] && element.report[level].length">
<div>{{element.report[level].length}}</div>
<mat-icon class="report-{{level}}">{{level}}</mat-icon>
</ng-container>
</div>
<mat-card class="full-file-report">
<mat-card-header *ngIf="element.info.label || element.id">
<mat-card-title>
{{element.info.label}}
<span
*ngIf="element.id !== element.name.toUpperCase()"
style="{{element.info.label ? 'color:silver' : ''}}">
#{{element.id}}
</span>
</mat-card-title>
<mat-card-subtitle>{{element.info.description}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<ng-container *ngFor="let level of ['error', 'warning', 'info']">
<div *ngFor="let message of element.report[level]">
<alert [level]="level" [text]="message"></alert>
</div>
</ng-container>
</mat-card-content>
</mat-card>
</div>
</mat-cell>
</ng-container>
</mat-card-title>
<mat-card-subtitle>{{element.info.description}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<ng-container *ngFor="let level of ['error', 'warning', 'info']">
<div *ngFor="let message of element.report[level]">
<alert [level]="level" [text]="message"></alert>
</div>
</ng-container>
</mat-card-content>
</mat-card>
</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="modificationTime">
<mat-header-cell *matHeaderCellDef mat-sort-header class="datecell"> Datum </mat-header-cell>
<mat-cell *matCellDef="let element" class="datecell">
{{(element.modificationTime * 1000) | date: 'd.M.yy hh:mm'}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="modificationTime">
<mat-header-cell *matHeaderCellDef mat-sort-header class="datecell"> Letzte Änderung </mat-header-cell>
<mat-cell *matCellDef="let element" class="datecell">
{{(element.modificationTime * 1000) | date: 'dd.MM.yy hh:mm'}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="size">
<mat-header-cell *matHeaderCellDef mat-sort-header> Größe </mat-header-cell>
<mat-cell *matCellDef="let element" style="white-space: nowrap;">
{{element.size | bytes}}
<span *ngIf="element.info.totalSize && (element.info.totalSize != element.size)">
&nbsp;({{element.info.totalSize | bytes }})
</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="size">
<mat-header-cell *matHeaderCellDef mat-sort-header> Volle Größe </mat-header-cell>
<mat-cell *matCellDef="let element" style="white-space: nowrap;">
{{(element.info.totalSize || element.size) | bytes}}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
</ng-container>
</div>
<div class="sidebar">
......@@ -93,17 +93,11 @@
(uploadCompleteEvent)="updateFileList()">
</iqb-files-upload-queue>
<ng-container *ngFor="let stat of fileStats.types | keyvalue">
<alert level="info" text="{{stat.value.total - stat.value.invalid}} valide
`{{stat.key}}`-Datei{{stat.value.invalid == 1 ? '' : 'en'}}
{{(stat.value.total > stat.value.invalid) ? '(von ' + stat.value.total + ')' : ''}}">
</alert>
</ng-container>
<alert level="info" text="{{fileStats.testtakers}} Testteilnehmer definiert."></alert>
<alert *ngIf="fileStats.invalid" level="error" text="`{{fileStats.invalid}}` Datei{{fileStats.invalid == 1 ? '' : 'en'}}
von {{fileStats.total}} sind nicht valide oder haben fehlende Abhängigkeiten und werden ignoriert!">
<alert *ngIf="fileStats.invalid" level="error" text="{{fileStats.total.invalid}}
Datei{{fileStats.total.invalid == 1 ? '' : 'en'}} von {{fileStats.total.count}}
{{fileStats.total.invalid == 1 ? 'ist' : 'sind'}} nicht valide oder
{{fileStats.total.invalid == 1 ? 'hat' : 'haben'}} fehlende Abhängigkeiten
und {{fileStats.total.invalid == 1 ? 'wird' : 'werden'}} ignoriert!">
</alert>
</div>
</div>
import {
Component, OnInit, Inject, ViewChild
} from '@angular/core';
import { Component, OnInit, Inject } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort';
import { Sort } from '@angular/material/sort';
import { saveAs } from 'file-saver';
import {
......@@ -13,19 +11,20 @@ import {
} from 'iqb-components';
import { map } from 'rxjs/operators';
import { WorkspaceDataService } from '../workspacedata.service';
import { GetFileResponseData } from '../workspace.interfaces';
import {
IQBFileType, GetFileResponseData, IQBFile, IQBFileTypes
} from '../workspace.interfaces';
import { BackendService, FileDeletionReport } from '../backend.service';
import { MainDataService } from '../../maindata.service';
interface FileStats {
types: {
[type: string]: {
total: number;
invalid: number;
}
invalid: {
[type in IQBFileType]?: number;
}
total: number;
invalid: number;
total: {
count: number;
invalid: number;
};
testtakers: number;
}
......@@ -34,28 +33,32 @@ interface FileStats {
styleUrls: ['./files.component.css']
})
export class FilesComponent implements OnInit {
public serverfiles: MatTableDataSource<GetFileResponseData>;
public displayedColumns = ['checked', 'name', 'type', 'size', 'modificationTime'];
public files: {[type in IQBFileType]?: MatTableDataSource<IQBFile>} = {};
public fileTypes = IQBFileTypes;
public displayedColumns = ['checked', 'name', 'size', 'modificationTime'];
// for fileupload
public uploadUrl = '';
public fileNameAlias = 'fileforvo';
public typeLabels = {
Testtakers: 'Teilnehmerliste',
Booklet: 'Testheft',
SysCheck: 'Systemcheck',
Resource: 'Ressource',
Unit: 'Unit',
Player: 'Player'
public lastSort:Sort = {
active: 'name',
direction: 'asc'
};
@ViewChild(MatSort, { static: true }) sort: MatSort;
public typeLabels = {
Testtakers: 'Teilnehmerlisten',
Booklet: 'Testhefte',
SysCheck: 'System-Check-Definitionen',
Resource: 'Ressourcen',
Unit: 'Units'
};
public fileStats: FileStats = {
types: {},
total: 0,
invalid: 0,
total: {
count: 0,
invalid: 0
},
invalid: {},
testtakers: 0
};
......@@ -78,110 +81,124 @@ export class FilesComponent implements OnInit {
});
}
public checkAll(isChecked: boolean): void {
this.serverfiles.data.forEach(element => {
public checkAll(isChecked: boolean, type: IQBFileType): void {
this.files[type].data = this.files[type].data.map(file => {
// eslint-disable-next-line no-param-reassign
element.isChecked = isChecked;
file.isChecked = isChecked;
return file;
});
}
public deleteFiles(): void {
if (this.wds.wsRole === 'RW') {
const filesToDelete = [];
this.serverfiles.data.forEach(element => {
if (element.isChecked) {
filesToDelete.push(`${element.type}/${element.name}`);
if (this.wds.wsRole !== 'RW') {
return;
}
const filesToDelete = [];
Object(this.files).keys.forEach(type => {
this.files[type].forEach(file => {
if (file.isChecked) {
filesToDelete.push(`${file.type}/${file.name}`);
}
});
});
if (filesToDelete.length > 0) {
const p = filesToDelete.length > 1;
const dialogRef = this.confirmDialog.open(ConfirmDialogComponent, {
width: '400px',
data: <ConfirmDialogData>{
title: 'Löschen von Dateien',
content: `Sie haben ${p ? filesToDelete.length : 'eine'} Datei${p ? 'en' : ''}\`
ausgewählt. Soll${p ? 'en' : ''} diese gelöscht werden?`,
confirmbuttonlabel: 'Löschen',
showcancel: true
}
});
if (filesToDelete.length > 0) {
const p = filesToDelete.length > 1;
const dialogRef = this.confirmDialog.open(ConfirmDialogComponent, {
width: '400px',
data: <ConfirmDialogData>{
title: 'Löschen von Dateien',
content: `Sie haben ${p ? filesToDelete.length : 'eine'} Datei${p ? 'en' : ''}\`
ausgewählt. Soll${p ? 'en' : ''} diese gelöscht werden?`,
confirmbuttonlabel: 'Löschen',
showcancel: true
}
});
dialogRef.afterClosed().subscribe(result => {
if (result !== false) {
this.mds.setSpinnerOn();
this.bs.deleteFiles(filesToDelete).subscribe((fileDeletionReport: FileDeletionReport) => {
const message = [];
if (fileDeletionReport.deleted.length > 0) {
message.push(`${fileDeletionReport.deleted.length} Dateien erfolgreich gelöscht.`);
}
if (fileDeletionReport.not_allowed.length > 0) {
message.push(`${fileDeletionReport.not_allowed.length} Dateien konnten nicht gelöscht werden.`);
}
this.snackBar.open(message.join('<br>'), message.length > 1 ? 'Achtung' : '', { duration: 1000 });
this.updateFileList();
});
}
});
} else {
this.messageDialog.open(MessageDialogComponent, {
width: '400px',
data: <MessageDialogData>{
title: 'Löschen von Dateien',
content: 'Bitte markieren Sie erst Dateien!',
type: MessageType.error
}
});
}
dialogRef.afterClosed().subscribe(result => {
if (result !== false) {
this.mds.setSpinnerOn();
this.bs.deleteFiles(filesToDelete).subscribe((fileDeletionReport: FileDeletionReport) => {
const message = [];
if (fileDeletionReport.deleted.length > 0) {
message.push(`${fileDeletionReport.deleted.length} Dateien erfolgreich gelöscht.`);
}
if (fileDeletionReport.not_allowed.length > 0) {
message.push(`${fileDeletionReport.not_allowed.length} Dateien konnten nicht gelöscht werden.`);
}
this.snackBar.open(message.join('<br>'), message.length > 1 ? 'Achtung' : '', { duration: 1000 });
this.updateFileList();
});
}
});
} else {
this.messageDialog.open(MessageDialogComponent, {
width: '400px',
data: <MessageDialogData>{
title: 'Löschen von Dateien',
content: 'Bitte markieren Sie erst Dateien!',
type: MessageType.error
}
});
}
}
public updateFileList(empty = false): void {
if (empty) {
this.serverfiles = new MatTableDataSource([]);
this.files = {};
this.mds.setSpinnerOff();
} else {
this.bs.getFiles()
.pipe(map(fileList => this.addFrontendChecksToFiles(fileList)))
.subscribe((fileList: GetFileResponseData[]) => {
this.serverfiles = new MatTableDataSource(fileList);
this.serverfiles.sort = this.sort;
.subscribe(fileList => {
this.files = {};
Object.keys(fileList)
.forEach(type => {
this.files[type] = new MatTableDataSource(fileList[type]);
});
this.fileStats = FilesComponent.getStats(fileList);
this.setTableSorting(this.lastSort);
this.mds.setSpinnerOff();
});
}
}
private static getStats(fileList: GetFileResponseData[]): FileStats {
private static getStats(fileList: GetFileResponseData): FileStats {
const stats: FileStats = {
types: {},
total: 0,
invalid: 0,
total: {
count: 0,
invalid: 0
},
invalid: {},
testtakers: 0
};
fileList.forEach(file => {
if (typeof stats.types[file.type] === 'undefined') {
stats.types[file.type] = {
total: 0,
invalid: 0
};
}
stats.types[file.type].total += 1;
stats.total += 1;
if (file.report.error && file.report.error.length) {
stats.invalid += 1;
stats.types[file.type].invalid += 1;
stats.testtakers += (typeof file.info.testtakers === 'number') ? file.info.testtakers : 0;
}
});
Object.keys(fileList)
.forEach(type => {
fileList[type].forEach(file => {
if (typeof stats.invalid[type] === 'undefined') {
stats.invalid[type] = 0;
}
stats.total.count += 1;
if (file.report.error && file.report.error.length) {
stats.invalid[type] += 1;
stats.total.invalid += 1;
stats.testtakers += (typeof file.info.testtakers === 'number') ? file.info.testtakers : 0;
}
});
});
return stats;
}
private addFrontendChecksToFiles(fileList: GetFileResponseData[]): GetFileResponseData[] {
return fileList.map(files => this.addFrontendChecksToFile(files));
private addFrontendChecksToFiles(fileList: GetFileResponseData): GetFileResponseData {
Object.keys(fileList).forEach(type => {
// eslint-disable-next-line no-param-reassign
fileList[type] = fileList[type].map(files => this.addFrontendChecksToFile(files));
});
return fileList;
}
private addFrontendChecksToFile(file: GetFileResponseData): GetFileResponseData {
private addFrontendChecksToFile(file: IQBFile): IQBFile {
if (typeof file.info['verona-version'] !== 'undefined') {
const fileMayor = file.info['verona-version'].toString().split('.').shift();
const systemMayor = this.veronaApiVersionSupported.split('.').shift();
......@@ -197,16 +214,30 @@ export class FilesComponent implements OnInit {
return file;
}
public download(element: GetFileResponseData): void {
public download(file: IQBFile): void {
this.mds.setSpinnerOn();
this.bs.downloadFile(element.type, element.name)
this.bs.downloadFile(file.type, file.name)
.subscribe(
(fileData: Blob|boolean) => {
this.mds.setSpinnerOff();
if (fileData !== false) {
saveAs(fileData as Blob, element.name);
saveAs(fileData as Blob, file.name);
}
}
);
}
setTableSorting(sort: Sort): void {
this.lastSort = sort;
function compare(a: number | string, b: number | string, isAsc: boolean) {
if ((typeof a === 'string') && (typeof b === 'string')) {
return a.localeCompare(b) * (isAsc ? 1 : -1);
}
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
Object.keys(this.files).forEach(type => {
this.files[type].data = this.files[type].data
.sort((a, b) => compare(a[sort.active], b[sort.active], (sort.direction === 'asc')));
});
}
}
export interface GetFileResponseData {
export const IQBFileTypes = ['Testtakers', 'Booklet', 'SysCheck', 'Resource', 'Unit'] as const;
export type IQBFileType = (typeof IQBFileTypes)[number];
export interface IQBFile {
name: string;
size: number;
modificationTime: string;
type: string;
type: IQBFileType;
isChecked: boolean;
report: {
error: string[];
......@@ -14,6 +17,10 @@ export interface GetFileResponseData {
}
}
export type GetFileResponseData = {
[type in IQBFileType]: IQBFile[]
};
export interface UnitResponse {
groupname: string;
loginname: string;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment