diff --git a/client/src/app/shared/components/media-upload-content/media-upload-content.component.html b/client/src/app/shared/components/media-upload-content/media-upload-content.component.html
new file mode 100644
index 000000000..f90f4e59c
--- /dev/null
+++ b/client/src/app/shared/components/media-upload-content/media-upload-content.component.html
@@ -0,0 +1,80 @@
+
+
+
+
+
+ Drop files into this area OR select files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/app/shared/components/media-upload-content/media-upload-content.component.scss b/client/src/app/shared/components/media-upload-content/media-upload-content.component.scss
new file mode 100644
index 000000000..2ebb80aad
--- /dev/null
+++ b/client/src/app/shared/components/media-upload-content/media-upload-content.component.scss
@@ -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;
+ }
+}
diff --git a/client/src/app/shared/components/media-upload-content/media-upload-content.component.spec.ts b/client/src/app/shared/components/media-upload-content/media-upload-content.component.spec.ts
new file mode 100644
index 000000000..1bbab2ec6
--- /dev/null
+++ b/client/src/app/shared/components/media-upload-content/media-upload-content.component.spec.ts
@@ -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;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [E2EImportsModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MediaUploadContentComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/shared/components/media-upload-content/media-upload-content.component.ts b/client/src/app/shared/components/media-upload-content/media-upload-content.component.ts
new file mode 100644
index 000000000..4f69b2216
--- /dev/null
+++ b/client/src/app/shared/components/media-upload-content/media-upload-content.component.ts
@@ -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;
+
+ /**
+ * 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;
+
+ /**
+ * Emits an event on success
+ */
+ @Output()
+ public uploadSuccessEvent = new EventEmitter();
+
+ /**
+ * Emits an error event
+ */
+ @Output()
+ public errorEvent = new EventEmitter();
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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 {
+ 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 {
+ 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();
+ }
+ }
+}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index c7c06a309..3d185da14 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -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 },
diff --git a/client/src/app/site/mediafiles/components/media-upload/media-upload.component.html b/client/src/app/site/mediafiles/components/media-upload/media-upload.component.html
index 535f79368..c41f3ebfd 100644
--- a/client/src/app/site/mediafiles/components/media-upload/media-upload.component.html
+++ b/client/src/app/site/mediafiles/components/media-upload/media-upload.component.html
@@ -1,6 +1,6 @@
-
Upload files
+ Upload files
@@ -671,8 +676,9 @@
-
@@ -868,3 +877,14 @@
Final print template
+
+
+
+
+ Upload Files
+
+
+
diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss
index eb35c9829..a9605a1f9 100644
--- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss
+++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss
@@ -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;
+ }
+}
diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts
index d18c25cfc..e85b793a5 100644
--- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts
+++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts
@@ -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): 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.