diff --git a/src/app/workspace-admin/backend.service.ts b/src/app/workspace-admin/backend.service.ts index 60362daae9368f880e5f29145921b7c746ceac91..30f8d14a75895a943c1c769abc613b4655623244 100644 --- a/src/app/workspace-admin/backend.service.ts +++ b/src/app/workspace-admin/backend.service.ts @@ -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} `); diff --git a/src/app/workspace-admin/files/files.component.css b/src/app/workspace-admin/files/files.component.css index 096db424bf60c672dd265c032000a8a5c79a92b6..5f743c14703005a953316a5e6f976c3a528c383a 100644 --- a/src/app/workspace-admin/files/files.component.css +++ b/src/app/workspace-admin/files/files.component.css @@ -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; } diff --git a/src/app/workspace-admin/files/files.component.html b/src/app/workspace-admin/files/files.component.html index 48c8c6c04e568c02e85797c20ac268ef307bd000..72bfa7f9d4cdd7e5a7f2ab32ec8ae8c99ffede7b 100644 --- a/src/app/workspace-admin/files/files.component.html +++ b/src/app/workspace-admin/files/files.component.html @@ -1,75 +1,75 @@ <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)"> - ({{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> diff --git a/src/app/workspace-admin/files/files.component.ts b/src/app/workspace-admin/files/files.component.ts index 848caef0bdc4f895d1184f5ea67433a463b17e77..8e338ac0a0b6cdbb0ddc0b14a2983cac0ba156fe 100644 --- a/src/app/workspace-admin/files/files.component.ts +++ b/src/app/workspace-admin/files/files.component.ts @@ -1,10 +1,8 @@ -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'))); + }); + } } diff --git a/src/app/workspace-admin/workspace.interfaces.ts b/src/app/workspace-admin/workspace.interfaces.ts index a0061258a6f3323c2379490da8f7c7887f66d9fa..c87ddc5cdbf044209207b09c02694ede2714285d 100644 --- a/src/app/workspace-admin/workspace.interfaces.ts +++ b/src/app/workspace-admin/workspace.interfaces.ts @@ -1,8 +1,11 @@ -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;