Merge pull request #4384 from tsiegleauq/mediafiles-on-fly
Add mediafiles upload from motion form
This commit is contained in:
commit
4c4a2c600b
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -82,6 +82,7 @@ import { OpenSlidesTranslateModule } from '../core/translate/openslides-translat
|
||||
import { ProjectorComponent } from './components/projector/projector.component';
|
||||
import { SlideContainerComponent } from './components/slide-container/slide-container.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.
|
||||
@ -202,7 +203,8 @@ import { CountdownTimeComponent } from './components/contdown-time/countdown-tim
|
||||
SlideContainerComponent,
|
||||
OwlDateTimeModule,
|
||||
OwlNativeDateTimeModule,
|
||||
CountdownTimeComponent
|
||||
CountdownTimeComponent,
|
||||
MediaUploadContentComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -228,7 +230,8 @@ import { CountdownTimeComponent } from './components/contdown-time/countdown-tim
|
||||
MetaTextBlockComponent,
|
||||
ProjectorComponent,
|
||||
SlideContainerComponent,
|
||||
CountdownTimeComponent
|
||||
CountdownTimeComponent,
|
||||
MediaUploadContentComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||
|
@ -1,6 +1,6 @@
|
||||
<os-head-bar [nav]="false">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"> <h2 translate>Upload files</h2> </div>
|
||||
<div class="title-slot"><h2 translate>Upload files</h2></div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
@ -11,95 +11,17 @@
|
||||
</os-head-bar>
|
||||
|
||||
<mat-card class="os-card">
|
||||
<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>
|
||||
<os-media-upload-content
|
||||
[parallel]="parallel"
|
||||
(uploadSuccessEvent)="uploadSuccess()"
|
||||
(errorEvent)="showError($event)"
|
||||
></os-media-upload-content>
|
||||
</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">
|
||||
<!-- Select upload strategy -->
|
||||
<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>
|
||||
</button>
|
||||
</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 { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Component } from '@angular/core';
|
||||
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 { BaseViewComponent } from 'app/site/base/base-view';
|
||||
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;
|
||||
}
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Handle file uploads from user
|
||||
@ -29,44 +15,13 @@ interface FileData {
|
||||
templateUrl: './media-upload.component.html',
|
||||
styleUrls: ['./media-upload.component.scss']
|
||||
})
|
||||
export class MediaUploadComponent extends BaseViewComponent implements OnInit {
|
||||
/**
|
||||
* 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;
|
||||
|
||||
export class MediaUploadComponent extends BaseViewComponent {
|
||||
/**
|
||||
* Determine if uploading should happen parallel or synchronously.
|
||||
* Synchronous uploading might be necessary if we see that stuff breaks
|
||||
*/
|
||||
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
|
||||
*
|
||||
@ -75,201 +30,31 @@ export class MediaUploadComponent extends BaseViewComponent implements OnInit {
|
||||
* @param matSnackBar showing errors and information
|
||||
* @param router Angulars own router
|
||||
* @param route Angulars activated route
|
||||
* @param repo the mediafile repository
|
||||
* @param op the operator, to check who was the uploader
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private repo: MediafileRepositoryService,
|
||||
private op: OperatorService
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init
|
||||
* Creates a new uploadList as consumable data source
|
||||
* Handler for successful uploads
|
||||
*/
|
||||
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(
|
||||
() => {
|
||||
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) {
|
||||
public uploadSuccess(): void {
|
||||
this.router.navigate(['../'], { relativeTo: this.route });
|
||||
} else {
|
||||
this.table.renderRows();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the progress to display in the progress bar
|
||||
* Only used in synchronous upload since parallel upload
|
||||
* Handler for upload errors
|
||||
*
|
||||
* @returns the upload progress in percent.
|
||||
* @param error
|
||||
*/
|
||||
public calcUploadProgress(): number {
|
||||
if (this.filesUploaded && this.uploadList.data) {
|
||||
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();
|
||||
}
|
||||
public showError(error: string): void {
|
||||
this.raiseError(error);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -303,7 +303,12 @@
|
||||
<div *ngIf="!perms.isAllowed('change_metadata', motion) && recommendationLabel">
|
||||
<mat-basic-chip class="bluegrey"> {{ recommendationLabel }} </mat-basic-chip>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
@ -671,8 +676,9 @@
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
<div *ngIf="editMotion">
|
||||
<div *ngIf="editMotion" class="shortened-selector">
|
||||
<os-search-value-selector
|
||||
class="selector"
|
||||
ngDefaultControl
|
||||
[form]="contentForm"
|
||||
[formControl]="contentForm.get('attachments_id')"
|
||||
@ -680,6 +686,9 @@
|
||||
listname="{{ 'Attachments' | translate }}"
|
||||
[InputListValues]="mediafilesObserver"
|
||||
></os-search-value-selector>
|
||||
<button type="button" mat-icon-button (click)="onUploadAttachmentsButton(uploadDialog)">
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -868,3 +877,14 @@
|
||||
Final print template
|
||||
</button>
|
||||
</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;
|
||||
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 { 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 { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||
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 personalNoteService: personal comments and favorite marker
|
||||
* @param linenumberingService The line numbering service
|
||||
* @param categoryRepo
|
||||
* @param userRepo
|
||||
* @param categoryRepo Repository for categories
|
||||
* @param userRepo Repository for users
|
||||
*/
|
||||
public constructor(
|
||||
title: Title,
|
||||
@ -1257,6 +1257,39 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
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,
|
||||
* if the user is editing a motion.
|
||||
|
Loading…
Reference in New Issue
Block a user