File
Implements
Metadata
styleUrls |
./files.component.css |
templateUrl |
./files.component.html |
Methods
Private
addFrontendChecksToFile
|
addFrontendChecksToFile(file: IQBFile)
|
|
|
deleteFiles
|
deleteFiles()
|
|
|
setTableSorting
|
setTableSorting(sort: Sort)
|
|
Parameters :
Name |
Type |
Optional |
sort |
Sort
|
No
|
|
updateFileList
|
updateFileList(empty)
|
|
Parameters :
Name |
Optional |
Default value |
empty |
No
|
false
|
|
Public
confirmDialog
|
Type : MatDialog
|
|
displayedColumns
|
Type : []
|
Default value : ['checked', 'name', 'size', 'modificationTime']
|
|
fileNameAlias
|
Type : string
|
Default value : 'fileforvo'
|
|
fileStats
|
Type : FileStats
|
Default value : {
total: {
count: 0,
invalid: 0
},
invalid: {},
testtakers: 0
}
|
|
fileTypes
|
Default value : IQBFileTypes
|
|
lastSort
|
Type : Sort
|
Default value : {
active: 'name',
direction: 'asc'
}
|
|
Public
messageDialog
|
Type : MatDialog
|
|
Public
snackBar
|
Type : MatSnackBar
|
|
typeLabels
|
Type : object
|
Default value : {
Testtakers: 'Teilnehmerlisten',
Booklet: 'Testhefte',
SysCheck: 'System-Check-Definitionen',
Resource: 'Ressourcen',
Unit: 'Units'
}
|
|
Public
veronaPlayerApiVersionMax
|
Type : number
|
Decorators :
@Inject('VERONA_PLAYER_API_VERSION_MAX')
|
|
Public
veronaPlayerApiVersionMin
|
Type : number
|
Decorators :
@Inject('VERONA_PLAYER_API_VERSION_MIN')
|
|
import {
Component, OnInit, Inject, ViewChild
} from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import { Sort } from '@angular/material/sort';
import { saveAs } from 'file-saver';
import {
ConfirmDialogComponent, ConfirmDialogData, MessageDialogComponent,
MessageDialogData, MessageType
} from 'iqb-components';
import { map } from 'rxjs/operators';
import { WorkspaceDataService } from '../workspacedata.service';
import {
IQBFileType, GetFileResponseData, IQBFile, IQBFileTypes
} from '../workspace.interfaces';
import { BackendService } from '../backend.service';
import { MainDataService } from '../../maindata.service';
import { IqbFilesUploadQueueComponent } from './iqb-files-upload-queue/iqb-files-upload-queue.component';
import { FileDeletionReport } from './files.interfaces';
interface FileStats {
invalid: {
[type in IQBFileType]?: number;
}
total: {
count: number;
invalid: number;
};
testtakers: number;
}
@Component({
templateUrl: './files.component.html',
styleUrls: ['./files.component.css']
})
export class FilesComponent implements OnInit {
files: { [type in IQBFileType]?: MatTableDataSource<IQBFile> } = {};
fileTypes = IQBFileTypes;
displayedColumns = ['checked', 'name', 'size', 'modificationTime'];
fileNameAlias = 'fileforvo';
lastSort:Sort = {
active: 'name',
direction: 'asc'
};
typeLabels = {
Testtakers: 'Teilnehmerlisten',
Booklet: 'Testhefte',
SysCheck: 'System-Check-Definitionen',
Resource: 'Ressourcen',
Unit: 'Units'
};
fileStats: FileStats = {
total: {
count: 0,
invalid: 0
},
invalid: {},
testtakers: 0
};
@ViewChild('fileUploadQueue', { static: true }) uploadQueue: IqbFilesUploadQueueComponent;
constructor(
@Inject('SERVER_URL') private serverUrl: string,
@Inject('VERONA_PLAYER_API_VERSION_MIN') public veronaPlayerApiVersionMin: number,
@Inject('VERONA_PLAYER_API_VERSION_MAX') public veronaPlayerApiVersionMax: number,
private bs: BackendService,
public wds: WorkspaceDataService,
public confirmDialog: MatDialog,
public messageDialog: MatDialog,
private mds: MainDataService,
public snackBar: MatSnackBar
) { }
ngOnInit(): void {
// this.uploadUrl = `${this.serverUrl}workspace/${this.wds.wsId}/file`;
setTimeout(() => {
this.mds.setSpinnerOn();
this.updateFileList();
});
}
checkAll(isChecked: boolean, type: IQBFileType): void {
this.files[type].data = this.files[type].data.map(file => {
file.isChecked = isChecked;
return file;
});
}
deleteFiles(): void {
if (this.wds.wsRole !== 'RW') {
return;
}
const filesToDelete = [];
Object.keys(this.files).forEach(type => {
this.files[type].data.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
}
});
dialogRef.afterClosed().subscribe(result => {
if (result !== false) {
this.mds.setSpinnerOn();
this.bs.deleteFiles(this.wds.wsId, 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.`);
}
if (fileDeletionReport.was_used.length > 0) {
message.push(`${fileDeletionReport.was_used.length} Dateien werden von anderen verwendet
und wurden nicht gelöscht.`);
}
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
}
});
}
}
updateFileList(empty = false): void {
if (empty) {
this.files = {};
this.mds.setSpinnerOff();
} else {
this.bs.getFiles(this.wds.wsId)
.pipe(map(fileList => this.addFrontendChecksToFiles(fileList)))
.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 {
const stats: FileStats = {
total: {
count: 0,
invalid: 0
},
invalid: {},
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 {
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: IQBFile): IQBFile {
if (typeof file.info['verona-version'] !== 'undefined') {
const fileMayor = parseInt(file.info['verona-version'].toString().split('.').shift(), 10);
if (typeof file.report.error === 'undefined') {
// eslint-disable-next-line no-param-reassign
file.report.error = [];
}
if (fileMayor < this.veronaPlayerApiVersionMin || fileMayor > this.veronaPlayerApiVersionMax) {
file.report.error.push(`Verona Version \`${fileMayor}\` is not supported
(only versions between \`${this.veronaPlayerApiVersionMin}\` and \`${this.veronaPlayerApiVersionMax}\`)`);
}
}
return file;
}
download(file: IQBFile): void {
this.mds.setSpinnerOn();
this.bs.downloadFile(this.wds.wsId, file.type, file.name)
.subscribe(
(fileData: Blob|boolean) => {
this.mds.setSpinnerOff();
if (fileData !== false) {
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')));
});
}
}
<div class="columnhost">
<div class="filelist">
<mat-accordion class="example-headers-align" multi="true">
<ng-container *ngFor="let type of fileTypes">
<mat-expansion-panel [expanded]="true" *ngIf="files && files[type]">
<mat-expansion-panel-header>
<mat-panel-title>{{typeLabels[type]}}</mat-panel-title>
<mat-panel-description>
<span>{{files[type].data.length}} Datei{{files[type].data.length === 1 ? '' : 'en'}}</span>
<span *ngIf="fileStats.invalid[type]">, davon {{fileStats.invalid[type]}} Fehlerhaft</span>
<span *ngIf="type=='Testtakers'">, {{fileStats.testtakers}} Teilnehmer</span>
</mat-panel-description>
</mat-expansion-panel-header>
<mat-table [dataSource]="files[type]" matSort (matSortChange)="setTableSorting($event)">
<ng-container matColumnDef="checked">
<mat-header-cell *matHeaderCellDef class="checkboxcell">
<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="name">
<mat-header-cell *matHeaderCellDef mat-sort-header class="namecell">Dateiname</mat-header-cell>
<mat-cell *matCellDef="let element" class="namecell">
<div class="file-report">
<button mat-button (click)="download(element)">{{element.name}}</button>
<span class="vertical-align-middle">
<ng-container *ngIf="element.report.error && element.report.error?.length; else: noError">
<mat-icon class="report-error">error</mat-icon>
</ng-container>
<ng-template #noError>
<ng-container *ngIf="element.report.warning && element.report.warning?.length">
<mat-icon class="report-warning">warning</mat-icon>
</ng-container>
</ng-template>
</span>
<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>
<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> 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-expansion-panel>
</ng-container>
</mat-accordion>
</div>
<div class="sidebar">
<div class="buttons">
<button mat-raised-button (click)="deleteFiles()" matTooltip="Markierte Dateien löschen" matTooltipPosition="above" [disabled]="wds.wsRole !== 'RW'">
<mat-icon>delete</mat-icon>
</button>
<button mat-raised-button (click)="hiddenfileinput.click()" matTooltip="Dateien hochladen/aktualisieren" matTooltipPosition="above" [disabled]="wds.wsRole !== 'RW'">
<mat-icon>cloud_upload</mat-icon>
</button>
</div>
<input #hiddenfileinput type="file" name="fileforvo" multiple [iqbFilesUploadInputFor]="fileUploadQueue" [hidden]="true"/>
<iqb-files-upload-queue #fileUploadQueue
[fileAlias]="fileNameAlias"
[folderName]="'ws'"
[folder]="'workspace'"
(uploadCompleteEvent)="updateFileList()">
</iqb-files-upload-queue>
<div *ngIf="!uploadQueue.files?.length" class="workspace-report">
<alert *ngIf="fileStats.total.invalid; else: workspaceValid" 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>
<ng-template #workspaceValid>
<alert level="success" *ngIf="fileStats.total.count" text="Alle
{{fileStats.total.count > 1 ? fileStats.total.count : ''}} Dateien im Workspace sind Valide."></alert>
</ng-template>
</div>
</div>
</div>
.columnhost {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: left;
}
.filelist {
flex: 10 0 400px;
margin-top: 0.7em;
}
mat-expansion-panel {
overflow: visible;
}
mat-cell:first-of-type, mat-header-cell:first-of-type, mat-footer-cell:first-of-type {
padding-left: 0
}
.checkboxcell {
overflow: visible;
flex: 0 0 30px;
}
.namecell {
flex: 3 3 60px;
}
.namecell.mat-header-cell {
padding-left: 16px;
}
.datecell {
flex: 1 1 5px;
}
.sidebar {
flex: 10 0 200px;
padding-left: 1em;
}
.sidebar .buttons {
margin-top: 0.7em;
margin-bottom: 0.7em;
display: inline-flex;
vertical-align: middle;
align-items: center;
}
.checkerror, .checkwarning, .checkinfo {
margin: 20px;
font-size: 0.8em;
}
.report-error {
color: #821324;
}
.report-warning {
color: silver;
}
.report-info {
color: blue;
}
.mat-raised-button {
min-width: 100px;
margin: 2px;
}
.file-report {
cursor: pointer
}
.full-file-report {
display: none;
}
.file-report:hover .full-file-report {
display: block;
position: absolute ;
background: white;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
z-index: 1000;
cursor: default;
margin: 2px;
}
.full-file-report .mat-card-title,
.full-file-report .mat-card-subtitle {
margin-left: -16px;
}
.full-file-report mat-card-content {
max-height: 15em;
overflow-y: auto
}
Legend
Html element with directive