Add mediafiles upload from motion form
Refactors the media file upload component into a shared component to be used in both normal pages and dialogs. A dialog was addes into the media file "edit" view to upload and set attachments on the fly.
This commit is contained in:
parent
f4f8b8422f
commit
46a229bb67
@ -0,0 +1,80 @@
|
|||||||
|
<div class="upload-area">
|
||||||
|
<input hidden type="file" #fileInput (change)="onSelectFile($event)" multiple />
|
||||||
|
|
||||||
|
<div class="selection-area">
|
||||||
|
<file-drop (onFileDrop)="onDropFile($event)" (click)="fileInput.click()" customstyle="file-drop-style">
|
||||||
|
<span translate>Drop files into this area OR select files</span>
|
||||||
|
</file-drop>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container" *ngIf="uploadList.data.length > 0">
|
||||||
|
<table mat-table [dataSource]="uploadList" class="mat-elevation-z8">
|
||||||
|
<!-- Title -->
|
||||||
|
<ng-container matColumnDef="title" sticky>
|
||||||
|
<th mat-header-cell *matHeaderCellDef><span translate>Title</span></th>
|
||||||
|
<td mat-cell *matCellDef="let file">
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput [value]="file.title" (input)="onChangeTitle($event.target.value, file)" />
|
||||||
|
</mat-form-field>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Original file name -->
|
||||||
|
<ng-container matColumnDef="filename">
|
||||||
|
<th mat-header-cell *matHeaderCellDef><span translate>File name</span></th>
|
||||||
|
<td mat-cell *matCellDef="let file">{{ file.filename }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- File information -->
|
||||||
|
<ng-container matColumnDef="information">
|
||||||
|
<th mat-header-cell *matHeaderCellDef><span translate>File information</span></th>
|
||||||
|
<td mat-cell *matCellDef="let file">
|
||||||
|
<div class="file-info-cell">
|
||||||
|
<span> <mat-icon [inline]="true">insert_drive_file</mat-icon> {{ file.mediafile.type }} </span>
|
||||||
|
<span>
|
||||||
|
<mat-icon [inline]="true">data_usage</mat-icon>
|
||||||
|
{{ getReadableSize(file.mediafile.size) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Hidden -->
|
||||||
|
<ng-container matColumnDef="hidden">
|
||||||
|
<th mat-header-cell *matHeaderCellDef><span translate>Hidden</span></th>
|
||||||
|
<td mat-cell *matCellDef="let file">
|
||||||
|
<mat-checkbox
|
||||||
|
[checked]="file.hidden"
|
||||||
|
(change)="onChangeHidden($event.checked, file)"
|
||||||
|
></mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Remove Button -->
|
||||||
|
<ng-container matColumnDef="remove">
|
||||||
|
<th mat-header-cell *matHeaderCellDef><span translate>Remove</span></th>
|
||||||
|
<td mat-cell *matCellDef="let file">
|
||||||
|
<button mat-icon-button color="warn" (click)="onRemoveButton(file)">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload and clear buttons -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button type="button" mat-raised-button (click)="onUploadButton()" color="primary">
|
||||||
|
<span translate> Upload </span>
|
||||||
|
</button>
|
||||||
|
<button type="button" mat-raised-button (click)="onClearButton()"><span translate> Clear list </span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-card class="os-card" *ngIf="showProgress">
|
||||||
|
<mat-progress-bar *ngIf="!parallel" mode="determinate" [value]="calcUploadProgress()"></mat-progress-bar>
|
||||||
|
<mat-progress-bar *ngIf="parallel" mode="buffer"></mat-progress-bar>
|
||||||
|
</mat-card>
|
@ -0,0 +1,62 @@
|
|||||||
|
.table-container {
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
/** Title */
|
||||||
|
.mat-column-title {
|
||||||
|
min-width: 100px;
|
||||||
|
|
||||||
|
.mat-form-field {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filename */
|
||||||
|
.mat-column-filename {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Information */
|
||||||
|
.mat-column-information {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hidden */
|
||||||
|
.mat-column-hidden {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** remove */
|
||||||
|
.mat-column-remove {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info-cell {
|
||||||
|
display: grid;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
.mat-icon {
|
||||||
|
font-size: 130%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-area {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
button + button {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MediaUploadContentComponent } from './media-upload-content.component';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
describe('MediaUploadContentComponent', () => {
|
||||||
|
let component: MediaUploadContentComponent;
|
||||||
|
let fixture: ComponentFixture<MediaUploadContentComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MediaUploadContentComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,265 @@
|
|||||||
|
import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { MatTableDataSource, MatTable } from '@angular/material';
|
||||||
|
|
||||||
|
import { UploadEvent, FileSystemFileEntry } from 'ngx-file-drop';
|
||||||
|
|
||||||
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
|
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To hold the structure of files to upload
|
||||||
|
*/
|
||||||
|
interface FileData {
|
||||||
|
mediafile: File;
|
||||||
|
filename: string;
|
||||||
|
title: string;
|
||||||
|
uploader_id: number;
|
||||||
|
hidden: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-media-upload-content',
|
||||||
|
templateUrl: './media-upload-content.component.html',
|
||||||
|
styleUrls: ['./media-upload-content.component.scss']
|
||||||
|
})
|
||||||
|
export class MediaUploadContentComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* Columns to display in the upload-table
|
||||||
|
*/
|
||||||
|
public displayedColumns: string[] = ['title', 'filename', 'information', 'hidden', 'remove'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine wether to show the progress bar
|
||||||
|
*/
|
||||||
|
public showProgress = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumable data source for the table
|
||||||
|
*/
|
||||||
|
public uploadList: MatTableDataSource<FileData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the IDs of the uploaded files
|
||||||
|
*/
|
||||||
|
private filesUploadedIds: number[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if uploading should happen parallel or synchronously.
|
||||||
|
* Synchronous uploading might be necessary if we see that stuff breaks
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public parallel = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if an error was detected to prevent automatic navigation
|
||||||
|
*/
|
||||||
|
public errorMessage: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hold the mat table to manually render new rows
|
||||||
|
*/
|
||||||
|
@ViewChild(MatTable)
|
||||||
|
public table: MatTable<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event on success
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
public uploadSuccessEvent = new EventEmitter<number[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an error event
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
public errorEvent = new EventEmitter<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the media upload page
|
||||||
|
*
|
||||||
|
* @param repo the mediafile repository
|
||||||
|
* @param op the operator, to check who was the uploader
|
||||||
|
*/
|
||||||
|
public constructor(private repo: MediafileRepositoryService, private op: OperatorService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init
|
||||||
|
* Creates a new uploadList as consumable data source
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.uploadList = new MatTableDataSource<FileData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts given FileData into FormData format and hands it over to the repository
|
||||||
|
* to upload
|
||||||
|
*
|
||||||
|
* @param fileData the file to upload to the server, should fit to the FileData interface
|
||||||
|
*/
|
||||||
|
public async uploadFile(fileData: FileData): Promise<void> {
|
||||||
|
const input = new FormData();
|
||||||
|
input.set('mediafile', fileData.mediafile);
|
||||||
|
input.set('title', fileData.title);
|
||||||
|
input.set('uploader_id', '' + fileData.uploader_id);
|
||||||
|
input.set('hidden', '' + fileData.hidden);
|
||||||
|
|
||||||
|
// raiseError will automatically ignore existing files
|
||||||
|
await this.repo.uploadFile(input).then(
|
||||||
|
fileId => {
|
||||||
|
this.filesUploadedIds.push(fileId.id);
|
||||||
|
// remove the uploaded file from the array
|
||||||
|
this.onRemoveButton(fileData);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.errorMessage = error;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a file size in bit into human readable format
|
||||||
|
*
|
||||||
|
* @param bits file size in bits
|
||||||
|
* @returns a readable file size representation
|
||||||
|
*/
|
||||||
|
public getReadableSize(bits: number): string {
|
||||||
|
const unitLevel = Math.floor(Math.log(bits) / Math.log(1024));
|
||||||
|
const bytes = +(bits / Math.pow(1024, unitLevel)).toFixed(2);
|
||||||
|
return `${bytes} ${['B', 'kB', 'MB', 'GB', 'TB'][unitLevel]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change event to set a file to hidden or not
|
||||||
|
*
|
||||||
|
* @param hidden whether the file should be hidden
|
||||||
|
* @param file the given file
|
||||||
|
*/
|
||||||
|
public onChangeHidden(hidden: boolean, file: FileData): void {
|
||||||
|
file.hidden = hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change event to adjust the title
|
||||||
|
*
|
||||||
|
* @param newTitle the new title
|
||||||
|
* @param file the given file
|
||||||
|
*/
|
||||||
|
public onChangeTitle(newTitle: string, file: FileData): void {
|
||||||
|
file.title = newTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a file to list to upload later
|
||||||
|
*
|
||||||
|
* @param file the file to upload
|
||||||
|
*/
|
||||||
|
public addFile(file: File): void {
|
||||||
|
const newFile: FileData = {
|
||||||
|
mediafile: file,
|
||||||
|
filename: file.name,
|
||||||
|
title: file.name,
|
||||||
|
uploader_id: this.op.user.id,
|
||||||
|
hidden: false
|
||||||
|
};
|
||||||
|
this.uploadList.data.push(newFile);
|
||||||
|
|
||||||
|
if (this.table) {
|
||||||
|
this.table.renderRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the select file event
|
||||||
|
*
|
||||||
|
* @param $event holds the file. Triggered by changing the file input element
|
||||||
|
*/
|
||||||
|
public onSelectFile(event: any): void {
|
||||||
|
if (event.target.files && event.target.files.length > 0) {
|
||||||
|
// file list is a special kind of collection, so array.foreach does not apply
|
||||||
|
for (const addedFile of event.target.files) {
|
||||||
|
this.addFile(addedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the drop-file event
|
||||||
|
*
|
||||||
|
* @param event holds the file. Triggered by dropping in the area
|
||||||
|
*/
|
||||||
|
public onDropFile(event: UploadEvent): void {
|
||||||
|
for (const droppedFile of event.files) {
|
||||||
|
// Check if the dropped element is a file. "Else" would be a dir.
|
||||||
|
if (droppedFile.fileEntry.isFile) {
|
||||||
|
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
|
||||||
|
fileEntry.file((file: File) => {
|
||||||
|
this.addFile(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for the upload button.
|
||||||
|
* Iterate over the upload list and executes `uploadFile` on each element
|
||||||
|
*/
|
||||||
|
public async onUploadButton(): Promise<void> {
|
||||||
|
if (this.uploadList && this.uploadList.data.length > 0) {
|
||||||
|
this.errorMessage = '';
|
||||||
|
this.showProgress = true;
|
||||||
|
|
||||||
|
if (this.parallel) {
|
||||||
|
const promises = this.uploadList.data.map(file => this.uploadFile(file));
|
||||||
|
await Promise.all(promises);
|
||||||
|
} else {
|
||||||
|
for (const file of this.uploadList.data) {
|
||||||
|
await this.uploadFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.showProgress = false;
|
||||||
|
|
||||||
|
if (this.errorMessage === '') {
|
||||||
|
this.uploadSuccessEvent.next(this.filesUploadedIds);
|
||||||
|
} else {
|
||||||
|
this.table.renderRows();
|
||||||
|
const filenames = this.uploadList.data.map(file => file.filename);
|
||||||
|
this.errorEvent.next(`${this.errorMessage}\n${filenames}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the progress to display in the progress bar
|
||||||
|
* Only used in synchronous upload since parallel upload
|
||||||
|
*
|
||||||
|
* @returns the upload progress in percent.
|
||||||
|
*/
|
||||||
|
public calcUploadProgress(): number {
|
||||||
|
if (this.filesUploadedIds && this.filesUploadedIds.length > 0 && this.uploadList.data) {
|
||||||
|
return 100 / (this.uploadList.data.length / this.filesUploadedIds.length);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given file from the upload table
|
||||||
|
*
|
||||||
|
* @param file the file to remove
|
||||||
|
*/
|
||||||
|
public onRemoveButton(file: FileData): void {
|
||||||
|
if (this.uploadList.data) {
|
||||||
|
this.uploadList.data.splice(this.uploadList.data.indexOf(file), 1);
|
||||||
|
this.table.renderRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for the clear button. Deletes the upload list
|
||||||
|
*/
|
||||||
|
public onClearButton(): void {
|
||||||
|
this.uploadList.data = [];
|
||||||
|
if (this.table) {
|
||||||
|
this.table.renderRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -83,6 +83,7 @@ import { OpenSlidesTranslateModule } from '../core/translate/openslides-translat
|
|||||||
import { ProjectorComponent } from './components/projector/projector.component';
|
import { ProjectorComponent } from './components/projector/projector.component';
|
||||||
import { SlideContainerComponent } from './components/slide-container/slide-container.component';
|
import { SlideContainerComponent } from './components/slide-container/slide-container.component';
|
||||||
import { CountdownTimeComponent } from './components/contdown-time/countdown-time.component';
|
import { CountdownTimeComponent } from './components/contdown-time/countdown-time.component';
|
||||||
|
import { MediaUploadContentComponent } from './components/media-upload-content/media-upload-content.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share Module for all "dumb" components and pipes.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -203,7 +204,8 @@ import { CountdownTimeComponent } from './components/contdown-time/countdown-tim
|
|||||||
SlideContainerComponent,
|
SlideContainerComponent,
|
||||||
OwlDateTimeModule,
|
OwlDateTimeModule,
|
||||||
OwlNativeDateTimeModule,
|
OwlNativeDateTimeModule,
|
||||||
CountdownTimeComponent
|
CountdownTimeComponent,
|
||||||
|
MediaUploadContentComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -230,7 +232,8 @@ import { CountdownTimeComponent } from './components/contdown-time/countdown-tim
|
|||||||
MetaTextBlockComponent,
|
MetaTextBlockComponent,
|
||||||
ProjectorComponent,
|
ProjectorComponent,
|
||||||
SlideContainerComponent,
|
SlideContainerComponent,
|
||||||
CountdownTimeComponent
|
CountdownTimeComponent,
|
||||||
|
MediaUploadContentComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||||
|
@ -11,95 +11,17 @@
|
|||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<mat-card class="os-card">
|
<mat-card class="os-card">
|
||||||
<div class="upload-area">
|
<os-media-upload-content
|
||||||
<input hidden type="file" #fileInput (change)="onSelectFile($event)" multiple />
|
[parallel]="parallel"
|
||||||
|
(uploadSuccessEvent)="uploadSuccess()"
|
||||||
<div class="selection-area">
|
(errorEvent)="showError($event)"
|
||||||
<file-drop (onFileDrop)="onDropFile($event)" (click)="fileInput.click()" customstyle="file-drop-style">
|
></os-media-upload-content>
|
||||||
<span translate>Drop files into this area OR select files</span>
|
|
||||||
</file-drop>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-container" *ngIf="uploadList.data.length > 0">
|
|
||||||
<table mat-table [dataSource]="uploadList" class="mat-elevation-z8">
|
|
||||||
<!-- Title -->
|
|
||||||
<ng-container matColumnDef="title" sticky>
|
|
||||||
<th mat-header-cell *matHeaderCellDef> <span translate>Title</span> </th>
|
|
||||||
<td mat-cell *matCellDef="let file">
|
|
||||||
<mat-form-field>
|
|
||||||
<input matInput [value]="file.title" (input)="onChangeTitle($event.target.value, file)" />
|
|
||||||
</mat-form-field>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Original file name -->
|
|
||||||
<ng-container matColumnDef="filename">
|
|
||||||
<th mat-header-cell *matHeaderCellDef> <span translate>File name</span> </th>
|
|
||||||
<td mat-cell *matCellDef="let file"> {{ file.filename }} </td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- File information -->
|
|
||||||
<ng-container matColumnDef="information">
|
|
||||||
<th mat-header-cell *matHeaderCellDef> <span translate>File information</span> </th>
|
|
||||||
<td mat-cell *matCellDef="let file">
|
|
||||||
<div class="file-info-cell">
|
|
||||||
<span>
|
|
||||||
<mat-icon [inline]="true">insert_drive_file</mat-icon> {{ file.mediafile.type }}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<mat-icon [inline]="true">data_usage</mat-icon>
|
|
||||||
{{ getReadableSize(file.mediafile.size) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Hidden -->
|
|
||||||
<ng-container matColumnDef="hidden">
|
|
||||||
<th mat-header-cell *matHeaderCellDef> <span translate>Hidden</span> </th>
|
|
||||||
<td mat-cell *matCellDef="let file">
|
|
||||||
<mat-checkbox
|
|
||||||
[checked]="file.hidden"
|
|
||||||
(change)="onChangeHidden($event.checked, file)"
|
|
||||||
></mat-checkbox>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Remove Button -->
|
|
||||||
<ng-container matColumnDef="remove">
|
|
||||||
<th mat-header-cell *matHeaderCellDef> <span translate>Remove</span> </th>
|
|
||||||
<td mat-cell *matCellDef="let file">
|
|
||||||
<button mat-icon-button color="warn" (click)="onRemoveButton(file)">
|
|
||||||
<mat-icon>close</mat-icon>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Upload and clear buttons -->
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button type="button" mat-raised-button (click)="onUploadButton()" color="primary">
|
|
||||||
<span translate> Upload </span>
|
|
||||||
</button>
|
|
||||||
<button type="button" mat-raised-button (click)="onClearButton()"> <span translate> Clear list </span> </button>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<mat-card class="os-card" *ngIf="showProgress">
|
|
||||||
<mat-progress-bar *ngIf="!parallel" mode="determinate" [value]="calcUploadProgress()"></mat-progress-bar>
|
|
||||||
<mat-progress-bar *ngIf="parallel" mode="buffer"></mat-progress-bar>
|
|
||||||
</mat-card>
|
|
||||||
|
|
||||||
|
|
||||||
<mat-menu #uploadMenu="matMenu">
|
<mat-menu #uploadMenu="matMenu">
|
||||||
<!-- Select upload strategy -->
|
<!-- Select upload strategy -->
|
||||||
<button mat-menu-item (click)="setUploadStrategy(!parallel)">
|
<button mat-menu-item (click)="setUploadStrategy(!parallel)">
|
||||||
<mat-icon color="accent">{{ parallel ? "check_box" : "check_box_outline_blank" }}</mat-icon>
|
<mat-icon color="accent">{{ parallel ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>
|
||||||
<span translate>Parallel upload</span>
|
<span translate>Parallel upload</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
.table-container {
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
/** Title */
|
|
||||||
.mat-column-title {
|
|
||||||
min-width: 100px;
|
|
||||||
|
|
||||||
.mat-form-field {
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Filename */
|
|
||||||
.mat-column-filename {
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Information */
|
|
||||||
.mat-column-information {
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hidden */
|
|
||||||
.mat-column-hidden {
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** remove */
|
|
||||||
.mat-column-remove {
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-info-cell {
|
|
||||||
display: grid;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
span {
|
|
||||||
.mat-icon {
|
|
||||||
font-size: 130%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-area {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
button + button {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +1,11 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { MatTableDataSource, MatTable, MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
import { UploadEvent, FileSystemFileEntry } from 'ngx-file-drop';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To hold the structure of files to upload
|
|
||||||
*/
|
|
||||||
interface FileData {
|
|
||||||
mediafile: File;
|
|
||||||
filename: string;
|
|
||||||
title: string;
|
|
||||||
uploader_id: number;
|
|
||||||
hidden: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle file uploads from user
|
* Handle file uploads from user
|
||||||
@ -29,44 +15,13 @@ interface FileData {
|
|||||||
templateUrl: './media-upload.component.html',
|
templateUrl: './media-upload.component.html',
|
||||||
styleUrls: ['./media-upload.component.scss']
|
styleUrls: ['./media-upload.component.scss']
|
||||||
})
|
})
|
||||||
export class MediaUploadComponent extends BaseViewComponent implements OnInit {
|
export class MediaUploadComponent extends BaseViewComponent {
|
||||||
/**
|
|
||||||
* Columns to display in the upload-table
|
|
||||||
*/
|
|
||||||
public displayedColumns: string[] = ['title', 'filename', 'information', 'hidden', 'remove'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine wheter to show the progress bar
|
|
||||||
*/
|
|
||||||
public showProgress = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumable data source for the table
|
|
||||||
*/
|
|
||||||
public uploadList: MatTableDataSource<FileData>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To count the files that report successful uploading
|
|
||||||
*/
|
|
||||||
public filesUploaded: number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if uploading should happen parallel or synchronously.
|
* Determine if uploading should happen parallel or synchronously.
|
||||||
* Synchronous uploading might be necessary if we see that stuff breaks
|
* Synchronous uploading might be necessary if we see that stuff breaks
|
||||||
*/
|
*/
|
||||||
public parallel = true;
|
public parallel = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* Set to true if an error was detected to prevent automatic navigation
|
|
||||||
*/
|
|
||||||
public error = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hold the mat table to manually render new rows
|
|
||||||
*/
|
|
||||||
@ViewChild(MatTable)
|
|
||||||
public table: MatTable<any>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the media upload page
|
* Constructor for the media upload page
|
||||||
*
|
*
|
||||||
@ -75,201 +30,31 @@ export class MediaUploadComponent extends BaseViewComponent implements OnInit {
|
|||||||
* @param matSnackBar showing errors and information
|
* @param matSnackBar showing errors and information
|
||||||
* @param router Angulars own router
|
* @param router Angulars own router
|
||||||
* @param route Angulars activated route
|
* @param route Angulars activated route
|
||||||
* @param repo the mediafile repository
|
|
||||||
* @param op the operator, to check who was the uploader
|
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
matSnackBar: MatSnackBar,
|
matSnackBar: MatSnackBar,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute
|
||||||
private repo: MediafileRepositoryService,
|
|
||||||
private op: OperatorService
|
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init
|
* Handler for successful uploads
|
||||||
* Creates a new uploadList as consumable data source
|
|
||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public uploadSuccess(): void {
|
||||||
this.uploadList = new MatTableDataSource<FileData>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts given FileData into FormData format and hands it over to the repository
|
|
||||||
* to upload
|
|
||||||
*
|
|
||||||
* @param fileData the file to upload to the server, should fit to the FileData interface
|
|
||||||
*/
|
|
||||||
public async uploadFile(fileData: FileData): Promise<void> {
|
|
||||||
const input = new FormData();
|
|
||||||
input.set('mediafile', fileData.mediafile);
|
|
||||||
input.set('title', fileData.title);
|
|
||||||
input.set('uploader_id', '' + fileData.uploader_id);
|
|
||||||
input.set('hidden', '' + fileData.hidden);
|
|
||||||
|
|
||||||
// raiseError will automatically ignore existing files
|
|
||||||
await this.repo.uploadFile(input).then(
|
|
||||||
() => {
|
|
||||||
this.filesUploaded++;
|
|
||||||
// remove the uploaded file from the array
|
|
||||||
this.onRemoveButton(fileData);
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
this.error = true;
|
|
||||||
this.raiseError(`${error} :"${fileData.title}"`);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a file size in bit into human readable format
|
|
||||||
*
|
|
||||||
* @param bits file size in bits
|
|
||||||
* @returns a readable file size representation
|
|
||||||
*/
|
|
||||||
public getReadableSize(bits: number): string {
|
|
||||||
const unitLevel = Math.floor(Math.log(bits) / Math.log(1024));
|
|
||||||
const bytes = +(bits / Math.pow(1024, unitLevel)).toFixed(2);
|
|
||||||
return `${bytes} ${['B', 'kB', 'MB', 'GB', 'TB'][unitLevel]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change event to set a file to hidden or not
|
|
||||||
*
|
|
||||||
* @param hidden whether the file should be hidden
|
|
||||||
* @param file the given file
|
|
||||||
*/
|
|
||||||
public onChangeHidden(hidden: boolean, file: FileData): void {
|
|
||||||
file.hidden = hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change event to adjust the title
|
|
||||||
*
|
|
||||||
* @param newTitle the new title
|
|
||||||
* @param file the given file
|
|
||||||
*/
|
|
||||||
public onChangeTitle(newTitle: string, file: FileData): void {
|
|
||||||
file.title = newTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a file to list to upload later
|
|
||||||
*
|
|
||||||
* @param file the file to upload
|
|
||||||
*/
|
|
||||||
public addFile(file: File): void {
|
|
||||||
const newFile: FileData = {
|
|
||||||
mediafile: file,
|
|
||||||
filename: file.name,
|
|
||||||
title: file.name,
|
|
||||||
uploader_id: this.op.user.id,
|
|
||||||
hidden: false
|
|
||||||
};
|
|
||||||
this.uploadList.data.push(newFile);
|
|
||||||
|
|
||||||
if (this.table) {
|
|
||||||
this.table.renderRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for the select file event
|
|
||||||
*
|
|
||||||
* @param $event holds the file. Triggered by changing the file input element
|
|
||||||
*/
|
|
||||||
public onSelectFile(event: any): void {
|
|
||||||
if (event.target.files && event.target.files.length > 0) {
|
|
||||||
// file list is a special kind of collection, so array.foreach does not apply
|
|
||||||
for (const addedFile of event.target.files) {
|
|
||||||
this.addFile(addedFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for the drop-file event
|
|
||||||
*
|
|
||||||
* @param event holds the file. Triggered by dropping in the area
|
|
||||||
*/
|
|
||||||
public onDropFile(event: UploadEvent): void {
|
|
||||||
for (const droppedFile of event.files) {
|
|
||||||
// Check if the dropped element is a file. "Else" would be a dir.
|
|
||||||
if (droppedFile.fileEntry.isFile) {
|
|
||||||
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
|
|
||||||
fileEntry.file((file: File) => {
|
|
||||||
this.addFile(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click handler for the upload button.
|
|
||||||
* Iterate over the upload list and executes `uploadFile` on each element
|
|
||||||
*/
|
|
||||||
public async onUploadButton(): Promise<void> {
|
|
||||||
if (this.uploadList && this.uploadList.data.length > 0) {
|
|
||||||
this.filesUploaded = 0;
|
|
||||||
this.error = false;
|
|
||||||
this.showProgress = true;
|
|
||||||
|
|
||||||
if (this.parallel) {
|
|
||||||
const promises = this.uploadList.data.map(file => this.uploadFile(file));
|
|
||||||
await Promise.all(promises);
|
|
||||||
} else {
|
|
||||||
for (const file of this.uploadList.data) {
|
|
||||||
await this.uploadFile(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.showProgress = false;
|
|
||||||
|
|
||||||
if (!this.error) {
|
|
||||||
this.router.navigate(['../'], { relativeTo: this.route });
|
this.router.navigate(['../'], { relativeTo: this.route });
|
||||||
} else {
|
|
||||||
this.table.renderRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the progress to display in the progress bar
|
* Handler for upload errors
|
||||||
* Only used in synchronous upload since parallel upload
|
|
||||||
*
|
*
|
||||||
* @returns the upload progress in percent.
|
* @param error
|
||||||
*/
|
*/
|
||||||
public calcUploadProgress(): number {
|
public showError(error: string): void {
|
||||||
if (this.filesUploaded && this.uploadList.data) {
|
this.raiseError(error);
|
||||||
return 100 / (this.uploadList.data.length / this.filesUploaded);
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the given file from the upload table
|
|
||||||
*
|
|
||||||
* @param file the file to remove
|
|
||||||
*/
|
|
||||||
public onRemoveButton(file: FileData): void {
|
|
||||||
if (this.uploadList.data) {
|
|
||||||
this.uploadList.data.splice(this.uploadList.data.indexOf(file), 1);
|
|
||||||
this.table.renderRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click handler for the clear button. Deletes the upload list
|
|
||||||
*/
|
|
||||||
public onClearButton(): void {
|
|
||||||
this.uploadList.data = [];
|
|
||||||
if (this.table) {
|
|
||||||
this.table.renderRows();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -303,7 +303,12 @@
|
|||||||
<div *ngIf="!perms.isAllowed('change_metadata', motion) && recommendationLabel">
|
<div *ngIf="!perms.isAllowed('change_metadata', motion) && recommendationLabel">
|
||||||
<mat-basic-chip class="bluegrey"> {{ recommendationLabel }} </mat-basic-chip>
|
<mat-basic-chip class="bluegrey"> {{ recommendationLabel }} </mat-basic-chip>
|
||||||
</div>
|
</div>
|
||||||
<button mat-stroked-button *ngIf="canFollowRecommendation()" (click)="onFollowRecButton()" class="spacer-top-10">
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
*ngIf="canFollowRecommendation()"
|
||||||
|
(click)="onFollowRecButton()"
|
||||||
|
class="spacer-top-10"
|
||||||
|
>
|
||||||
<span translate>Follow recommendation</span>
|
<span translate>Follow recommendation</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -671,8 +676,9 @@
|
|||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="editMotion">
|
<div *ngIf="editMotion" class="shortened-selector">
|
||||||
<os-search-value-selector
|
<os-search-value-selector
|
||||||
|
class="selector"
|
||||||
ngDefaultControl
|
ngDefaultControl
|
||||||
[form]="contentForm"
|
[form]="contentForm"
|
||||||
[formControl]="contentForm.get('attachments_id')"
|
[formControl]="contentForm.get('attachments_id')"
|
||||||
@ -680,6 +686,9 @@
|
|||||||
listname="{{ 'Attachments' | translate }}"
|
listname="{{ 'Attachments' | translate }}"
|
||||||
[InputListValues]="mediafilesObserver"
|
[InputListValues]="mediafilesObserver"
|
||||||
></os-search-value-selector>
|
></os-search-value-selector>
|
||||||
|
<button type="button" mat-icon-button (click)="onUploadAttachmentsButton(uploadDialog)">
|
||||||
|
<mat-icon>cloud_upload</mat-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -868,3 +877,14 @@
|
|||||||
Final print template
|
Final print template
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
|
<!-- upload file dialog -->
|
||||||
|
<ng-template #uploadDialog>
|
||||||
|
<h1 mat-dialog-title>
|
||||||
|
<span translate>Upload Files</span>
|
||||||
|
</h1>
|
||||||
|
<os-media-upload-content
|
||||||
|
(uploadSuccessEvent)="uploadSuccess($event)"
|
||||||
|
(errorEvent)="showUploadError($event)"
|
||||||
|
></os-media-upload-content>
|
||||||
|
</ng-template>
|
||||||
|
@ -177,3 +177,14 @@ span {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shortened-selector {
|
||||||
|
justify-content: space-between;
|
||||||
|
display: flex;
|
||||||
|
.selector {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
.mat-icon-button {
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Component, OnInit, ElementRef, HostListener } from '@angular/core';
|
import { Component, OnInit, ElementRef, HostListener, TemplateRef } from '@angular/core';
|
||||||
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
|
||||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||||
import { MatDialog, MatSnackBar, MatCheckboxChange, ErrorStateMatcher } from '@angular/material';
|
import { MatDialog, MatSnackBar, MatCheckboxChange, ErrorStateMatcher } from '@angular/material';
|
||||||
@ -344,8 +344,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
* @param pdfExport export the motion to pdf
|
* @param pdfExport export the motion to pdf
|
||||||
* @param personalNoteService: personal comments and favorite marker
|
* @param personalNoteService: personal comments and favorite marker
|
||||||
* @param linenumberingService The line numbering service
|
* @param linenumberingService The line numbering service
|
||||||
* @param categoryRepo
|
* @param categoryRepo Repository for categories
|
||||||
* @param userRepo
|
* @param userRepo Repository for users
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
title: Title,
|
title: Title,
|
||||||
@ -1257,6 +1257,39 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
|||||||
this.personalNoteService.setPersonalNoteStar(this.motion.motion, !this.motion.star);
|
this.personalNoteService.setPersonalNoteStar(this.motion.motion, !this.motion.star);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the upload attachments button
|
||||||
|
*/
|
||||||
|
public onUploadAttachmentsButton(templateRef: TemplateRef<string>): void {
|
||||||
|
this.dialogService.open(templateRef, {
|
||||||
|
maxHeight: '90vh',
|
||||||
|
width: '750px',
|
||||||
|
maxWidth: '90vw'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for successful uploads.
|
||||||
|
* Adds the IDs of the upload process to the mediafile selector
|
||||||
|
*
|
||||||
|
* @param fileIds the ids of the uploads if they were successful
|
||||||
|
*/
|
||||||
|
public uploadSuccess(fileIds: number[]): void {
|
||||||
|
const currentAttachments = this.contentForm.get('attachments_id').value as number[];
|
||||||
|
const newAttachments = [...currentAttachments, ...fileIds];
|
||||||
|
this.contentForm.get('attachments_id').setValue(newAttachments);
|
||||||
|
this.dialogService.closeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for upload errors
|
||||||
|
*
|
||||||
|
* @param error the error message passed by the upload component
|
||||||
|
*/
|
||||||
|
public showUploadError(error: string): void {
|
||||||
|
this.raiseError(error);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to prevent automatically closing the window/tab,
|
* Function to prevent automatically closing the window/tab,
|
||||||
* if the user is editing a motion.
|
* if the user is editing a motion.
|
||||||
|
Loading…
Reference in New Issue
Block a user