Merge pull request #4384 from tsiegleauq/mediafiles-on-fly

Add mediafiles upload from motion form
This commit is contained in:
Emanuel Schütze 2019-02-21 22:16:43 +01:00 committed by GitHub
commit 4c4a2c600b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 525 additions and 381 deletions

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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();
}
}
}

View File

@ -82,6 +82,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.
@ -202,7 +203,8 @@ import { CountdownTimeComponent } from './components/contdown-time/countdown-tim
SlideContainerComponent, SlideContainerComponent,
OwlDateTimeModule, OwlDateTimeModule,
OwlNativeDateTimeModule, OwlNativeDateTimeModule,
CountdownTimeComponent CountdownTimeComponent,
MediaUploadContentComponent
], ],
declarations: [ declarations: [
PermsDirective, PermsDirective,
@ -228,7 +230,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 },

View File

@ -1,6 +1,6 @@
<os-head-bar [nav]="false"> <os-head-bar [nav]="false">
<!-- Title --> <!-- Title -->
<div class="title-slot"> <h2 translate>Upload files</h2> </div> <div class="title-slot"><h2 translate>Upload files</h2></div>
<!-- Menu --> <!-- Menu -->
<div class="menu-slot"> <div class="menu-slot">
@ -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>

View File

@ -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;
}
}

View File

@ -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>(); this.router.navigate(['../'], { relativeTo: this.route });
} }
/** /**
* Converts given FileData into FormData format and hands it over to the repository * Handler for upload errors
* to upload
* *
* @param fileData the file to upload to the server, should fit to the FileData interface * @param error
*/ */
public async uploadFile(fileData: FileData): Promise<void> { public showError(error: string): void {
const input = new FormData(); this.raiseError(error);
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 });
} else {
this.table.renderRows();
}
}
}
/**
* 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.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();
}
} }
/** /**

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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.