Merge pull request #4019 from tsiegleauq/mediafile-upload
Upload media files
This commit is contained in:
commit
7ae95b9208
@ -33,6 +33,7 @@
|
||||
"core-js": "^2.5.4",
|
||||
"file-saver": "^2.0.0-rc.3",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"ngx-file-drop": "^5.0.0",
|
||||
"ngx-mat-select-search": "^1.4.0",
|
||||
"po2json": "^1.0.0-alpha",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
|
||||
// Core Services, Directives
|
||||
import { AuthGuard } from './services/auth-guard.service';
|
||||
@ -10,7 +9,6 @@ import { AutoupdateService } from './services/autoupdate.service';
|
||||
import { DataStoreService } from './services/data-store.service';
|
||||
import { OperatorService } from './services/operator.service';
|
||||
import { WebsocketService } from './services/websocket.service';
|
||||
import { AddHeaderInterceptor } from './http-interceptor';
|
||||
import { DataSendService } from './services/data-send.service';
|
||||
import { ViewportService } from './services/viewport.service';
|
||||
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
||||
@ -31,12 +29,7 @@ import { HttpService } from './services/http.service';
|
||||
HttpService,
|
||||
OperatorService,
|
||||
ViewportService,
|
||||
WebsocketService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AddHeaderInterceptor,
|
||||
multi: true
|
||||
}
|
||||
WebsocketService
|
||||
],
|
||||
entryComponents: [PromptDialogComponent]
|
||||
})
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Interceptor class for HTTP requests. Replaces all 'httpOptions' in all http.get or http.post requests.
|
||||
*
|
||||
* Should not need further adjustment.
|
||||
*/
|
||||
export class AddHeaderInterceptor implements HttpInterceptor {
|
||||
/**
|
||||
* Normal HttpInterceptor usage
|
||||
*
|
||||
* @param req Will clone the request and intercept it with our desired headers
|
||||
* @param next HttpHandler will catch the response and forwards it to the original instance
|
||||
*/
|
||||
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
const clonedRequest = req.clone({
|
||||
withCredentials: true,
|
||||
headers: req.headers.set('Content-Type', 'application/json')
|
||||
});
|
||||
|
||||
return next.handle(clonedRequest);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
@ -20,13 +20,22 @@ export enum HTTPMethod {
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HttpService {
|
||||
/**
|
||||
* http headers used by most requests
|
||||
*/
|
||||
private defaultHeaders: HttpHeaders;
|
||||
|
||||
/**
|
||||
* Construct a HttpService
|
||||
*
|
||||
* Sets the default headers to application/json
|
||||
*
|
||||
* @param http The HTTP Client
|
||||
* @param translate
|
||||
*/
|
||||
public constructor(private http: HttpClient, private translate: TranslateService) {}
|
||||
public constructor(private http: HttpClient, private translate: TranslateService) {
|
||||
this.defaultHeaders = new HttpHeaders().set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the a http request the the given URL.
|
||||
@ -35,14 +44,17 @@ export class HttpService {
|
||||
* @param url the target url, usually starting with /rest
|
||||
* @param method the required HTTP method (i.e get, post, put)
|
||||
* @param data optional, if sending a data body is required
|
||||
* @param customHeader optional custom HTTP header of required
|
||||
* @returns a promise containing a generic
|
||||
*/
|
||||
private async send<T>(url: string, method: HTTPMethod, data?: any): Promise<T> {
|
||||
private async send<T>(url: string, method: HTTPMethod, data?: any, customHeader?: HttpHeaders): Promise<T> {
|
||||
if (!url.endsWith('/')) {
|
||||
url += '/';
|
||||
}
|
||||
|
||||
const options = {
|
||||
body: data
|
||||
body: data,
|
||||
headers: customHeader ? customHeader : this.defaultHeaders
|
||||
};
|
||||
|
||||
try {
|
||||
@ -96,6 +108,7 @@ export class HttpService {
|
||||
* Errors from the servers may be string or array of strings. This function joins the strings together,
|
||||
* if an array is send.
|
||||
* @param str a string or a string array to join together.
|
||||
* @returns Error text(s) as single string
|
||||
*/
|
||||
private processErrorTexts(str: string | string[]): string {
|
||||
if (str instanceof Array) {
|
||||
@ -109,44 +122,54 @@ export class HttpService {
|
||||
* Exectures a get on a url with a certain object
|
||||
* @param url The url to send the request to.
|
||||
* @param data An optional payload for the request.
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async get<T>(url: string, data?: any): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.GET, data);
|
||||
public async get<T>(url: string, data?: any, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.GET, data, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exectures a post on a url with a certain object
|
||||
* @param url string of the url to send semothing to
|
||||
* @param data The data to send
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async post<T>(url: string, data: any): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.POST, data);
|
||||
public async post<T>(url: string, data: any, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.POST, data, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exectures a put on a url with a certain object
|
||||
* @param url string of the url to send semothing to
|
||||
* @param data the object that should be send
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async patch<T>(url: string, data: any): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.PATCH, data);
|
||||
public async patch<T>(url: string, data: any, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.PATCH, data, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exectures a put on a url with a certain object
|
||||
* @param url the url that should be called
|
||||
* @param data: The data to send
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async put<T>(url: string, data: any): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.PUT, data);
|
||||
public async put<T>(url: string, data: any, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.PUT, data, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a delete request.
|
||||
* @param url the url that should be called
|
||||
* @param data An optional data to send in the requestbody.
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async delete<T>(url: string, data?: any): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.DELETE, data);
|
||||
public async delete<T>(url: string, data?: any, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(url, HTTPMethod.DELETE, data, header);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
MatToolbarModule,
|
||||
MatCardModule,
|
||||
MatInputModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSidenavModule,
|
||||
MatSnackBarModule,
|
||||
@ -22,7 +23,7 @@ import {
|
||||
DateAdapter,
|
||||
MatIconModule,
|
||||
MatButtonToggleModule,
|
||||
MatBadgeModule
|
||||
MatBadgeModule,
|
||||
} from '@angular/material';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material';
|
||||
@ -39,6 +40,9 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
// ngx-translate
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
// ngx-file-drop
|
||||
import { FileDropModule } from 'ngx-file-drop';
|
||||
|
||||
// directives
|
||||
import { PermsDirective } from './directives/perms.directive';
|
||||
import { DomChangeDirective } from './directives/dom-change.directive';
|
||||
@ -82,6 +86,7 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatPaginatorModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSidenavModule,
|
||||
MatListModule,
|
||||
@ -101,7 +106,8 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
|
||||
DragDropModule,
|
||||
TranslateModule.forChild(),
|
||||
RouterModule,
|
||||
NgxMatSelectSearchModule
|
||||
NgxMatSelectSearchModule,
|
||||
FileDropModule
|
||||
],
|
||||
exports: [
|
||||
FormsModule,
|
||||
@ -118,6 +124,7 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatPaginatorModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSidenavModule,
|
||||
MatListModule,
|
||||
@ -133,6 +140,7 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
|
||||
MatButtonToggleModule,
|
||||
DragDropModule,
|
||||
NgxMatSelectSearchModule,
|
||||
FileDropModule,
|
||||
TranslateModule,
|
||||
PermsDirective,
|
||||
DomChangeDirective,
|
||||
|
@ -0,0 +1,105 @@
|
||||
<os-head-bar [nav]="false">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"> <h2 translate>Upload files</h2> </div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="uploadMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
<span translate>Parallel upload</span>
|
||||
</button>
|
||||
</mat-menu>
|
@ -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,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MediaUploadComponent } from './media-upload.component';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('MediaUploadComponent', () => {
|
||||
let component: MediaUploadComponent;
|
||||
let fixture: ComponentFixture<MediaUploadComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [MediaUploadComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MediaUploadComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,283 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { MatTableDataSource, MatTable, 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/services/operator.service';
|
||||
import { MediafileRepositoryService } from '../../services/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
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-media-upload',
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param titleService set the browser title
|
||||
* @param translate the translation service
|
||||
* @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
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
() => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the upload strategy between synchronous and parallel
|
||||
*
|
||||
* @param isParallel true or false, whether parallel upload is required or not
|
||||
*/
|
||||
public setUploadStrategy(isParallel: boolean): void {
|
||||
this.parallel = isParallel;
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
<os-head-bar [mainButton]="true" (mainEvent)="onMainEvent()" [editMode]="editFile" (saveEvent)="onSaveEditedFile()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 *ngIf="!editFile" translate>Files</h2>
|
||||
|
||||
<form
|
||||
class="edit-file-form"
|
||||
*ngIf="editFile"
|
||||
[formGroup]="fileEditForm"
|
||||
(ngSubmit)="onSaveEditedFile()"
|
||||
(keydown)="keyDownFunction($event)"
|
||||
>
|
||||
<mat-form-field>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
osAutofocus
|
||||
required
|
||||
formControlName="title"
|
||||
placeholder="{{ 'New file name' | translate}}"
|
||||
/>
|
||||
<mat-error *ngIf="fileEditForm.invalid" translate>Required</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-select formControlName="hidden" placeholder="{{ 'Visibility' | translate}}">
|
||||
<mat-option [value]="true"> <span translate>Hidden</span> </mat-option>
|
||||
<mat-option [value]="false"><span translate>Visible</span></mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- Filename -->
|
||||
<ng-container matColumnDef="title">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let file" (click)="download(file)">{{ file.title }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Info -->
|
||||
<ng-container matColumnDef="info">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
||||
<mat-cell *matCellDef="let file" (click)="download(file)">
|
||||
<div class="file-info-cell">
|
||||
<span> <mat-icon [inline]="true">insert_drive_file</mat-icon> {{ file.type }} </span>
|
||||
<span> <mat-icon [inline]="true">data_usage</mat-icon> {{ file.size }} </span>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- indicator -->
|
||||
<ng-container matColumnDef="indicator">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Indicator</mat-header-cell>
|
||||
<mat-cell *matCellDef="let file">
|
||||
<!-- check if the file is managed -->
|
||||
|
||||
<div
|
||||
*ngIf="getFileSettings(file).length > 0"
|
||||
[matMenuTriggerFor]="singleFileMenu"
|
||||
[matMenuTriggerData]="{ file: file }"
|
||||
[matTooltip]="formatIndicatorTooltip(file)"
|
||||
>
|
||||
<mat-icon *ngIf="file.isFont()">text_fields</mat-icon>
|
||||
<mat-icon *ngIf="file.isImage()">insert_photo</mat-icon>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- menu -->
|
||||
<ng-container matColumnDef="menu">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Menu</mat-header-cell>
|
||||
<mat-cell *matCellDef="let file">
|
||||
<button mat-icon-button [matMenuTriggerFor]="singleFileMenu" [matMenuTriggerData]="{ file: file }">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: getColumnDefinition()"></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
|
||||
|
||||
<mat-menu #singleFileMenu="matMenu">
|
||||
<ng-template matMenuContent let-file="file">
|
||||
<!-- Exclusive for images -->
|
||||
<div *ngIf="file.isImage()">
|
||||
<div *ngFor="let action of logoActions">
|
||||
<ng-container *ngTemplateOutlet="manageButton; context: { file: file, action: action }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Exclusive for fonts -->
|
||||
<div *ngIf="file.isFont()">
|
||||
<div *ngFor="let action of fontActions">
|
||||
<ng-container *ngTemplateOutlet="manageButton; context: { file: file, action: action }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit and delete for all images -->
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="onEditFile(file)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span translate>Edit</span>
|
||||
</button>
|
||||
<button mat-menu-item class="red-warning-text" (click)="onDelete(file)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
|
||||
<!-- Template for the managing buttons -->
|
||||
<ng-template #manageButton let-file="file" let-action="action">
|
||||
<button mat-menu-item (click)="onManageButton($event, file, action)">
|
||||
<mat-icon color="accent"> {{ isUsedAs(file, action) ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
|
||||
<span>{{ getNameOfAction(action) }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<!-- Menu for Mediafiles -->
|
||||
<mat-menu #mediafilesMenu="matMenu">
|
||||
<!-- Delete all files - later replaced with multi-select function -->
|
||||
<button mat-menu-item class="red-warning-text" (click)="onDeleteAllFiles()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete all files</span>
|
||||
</button>
|
||||
</mat-menu>
|
@ -0,0 +1,46 @@
|
||||
.os-listview-table {
|
||||
/** Title */
|
||||
.mat-column-title {
|
||||
flex: 2 0 50px;
|
||||
}
|
||||
|
||||
/** Info */
|
||||
.mat-column-info {
|
||||
width: 100%;
|
||||
flex: 1 0 40px;
|
||||
}
|
||||
|
||||
/** Indicator */
|
||||
.mat-column-indicator {
|
||||
flex: 1 0 30px;
|
||||
}
|
||||
|
||||
/** Menu */
|
||||
.mat-column-menu {
|
||||
flex: 0 0 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// multi line tooltip
|
||||
::ng-deep .mat-tooltip {
|
||||
white-space: pre-line !important;
|
||||
}
|
||||
|
||||
|
||||
.edit-file-form {
|
||||
mat-form-field + mat-form-field {
|
||||
margin: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
// duplicate. Put into own file
|
||||
.file-info-cell {
|
||||
display: grid;
|
||||
margin: 0;
|
||||
|
||||
span {
|
||||
.mat-icon {
|
||||
font-size: 130%;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MediafileListComponent } from './mediafile-list.component';
|
||||
import { E2EImportsModule } from '../../../../e2e-imports.module';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('MediafileListComponent', () => {
|
||||
let component: MediafileListComponent;
|
@ -0,0 +1,282 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
||||
import { ViewMediafile } from '../../models/view-mediafile';
|
||||
import { MediafileRepositoryService } from '../../services/mediafile-repository.service';
|
||||
import { MediaManageService } from '../../services/media-manage.service';
|
||||
import { PromptService } from 'app/core/services/prompt.service';
|
||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||
import { ViewportService } from 'app/core/services/viewport.service';
|
||||
|
||||
/**
|
||||
* Lists all the uploaded files.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-mediafile-list',
|
||||
templateUrl: './mediafile-list.component.html',
|
||||
styleUrls: ['./mediafile-list.component.scss'],
|
||||
})
|
||||
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile> implements OnInit {
|
||||
/**
|
||||
* Holds the actions for logos. Updated via an observable
|
||||
*/
|
||||
public logoActions: string[];
|
||||
|
||||
/**
|
||||
* Holds the actions for fonts. Update via an observable
|
||||
*/
|
||||
public fontActions: string[];
|
||||
|
||||
/**
|
||||
* Columns to display in Mediafile table when fill width is available
|
||||
*/
|
||||
public displayedColumnsDesktop: string[] = ['title', 'info', 'indicator', 'menu'];
|
||||
|
||||
/**
|
||||
* Columns to display in Mediafile table when fill width is available
|
||||
*/
|
||||
public displayedColumnsMobile: string[] = ['title', 'menu'];
|
||||
|
||||
/**
|
||||
* Show or hide the edit mode
|
||||
*/
|
||||
public editFile = false;
|
||||
|
||||
/**
|
||||
* Holds the file to edit
|
||||
*/
|
||||
public fileToEdit: ViewMediafile;
|
||||
|
||||
/**
|
||||
* The form to edit Files
|
||||
*/
|
||||
@ViewChild('fileEditForm')
|
||||
public fileEditForm: FormGroup;
|
||||
|
||||
/**
|
||||
* Constructs the component
|
||||
*
|
||||
* @param titleService sets the browser title
|
||||
* @param translate translation for the parent
|
||||
* @param matSnackBar showing errors and sucsess messages
|
||||
* @param router angulars router
|
||||
* @param route anduglars ActivatedRoute
|
||||
* @param repo the repository for mediafiles
|
||||
* @param mediaManage service to manage media files (setting images as logos)
|
||||
* @param promptService prevent deletion by accident
|
||||
* @param vp viewport Service to check screen size
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
matSnackBar: MatSnackBar,
|
||||
protected translate: TranslateService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private repo: MediafileRepositoryService,
|
||||
private mediaManage: MediaManageService,
|
||||
private promptService: PromptService,
|
||||
public vp: ViewportService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
* Set the title, make the edit Form and observe Mediafiles
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Files');
|
||||
this.initTable();
|
||||
|
||||
this.fileEditForm = new FormGroup({
|
||||
title: new FormControl('', Validators.required),
|
||||
hidden: new FormControl(),
|
||||
});
|
||||
|
||||
this.repo.getViewModelListObservable().subscribe(newFiles => {
|
||||
this.dataSource.data = newFiles;
|
||||
});
|
||||
|
||||
// Observe the logo actions
|
||||
this.mediaManage.getLogoActions().subscribe(action => {
|
||||
this.logoActions = action;
|
||||
});
|
||||
|
||||
// Observe the font actions
|
||||
this.mediaManage.getFontActions().subscribe(action => {
|
||||
this.fontActions = action;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the main Event.
|
||||
* In edit mode, this abandons the changes
|
||||
* Without edit mode, this will navigate to the upload page
|
||||
*/
|
||||
public onMainEvent(): void {
|
||||
if (!this.editFile) {
|
||||
this.router.navigate(['./upload'], { relativeTo: this.route });
|
||||
} else {
|
||||
this.editFile = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the edit button in the file menu
|
||||
*
|
||||
* @param file the selected file
|
||||
*/
|
||||
public onEditFile(file: ViewMediafile): void {
|
||||
console.log('edit file ', file);
|
||||
this.fileToEdit = file;
|
||||
|
||||
this.editFile = true;
|
||||
this.fileEditForm.setValue({ title: this.fileToEdit.title, hidden: this.fileToEdit.hidden });
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the save button in edit mode
|
||||
*/
|
||||
public onSaveEditedFile(): void {
|
||||
if (!this.fileEditForm.value || !this.fileEditForm.valid) {
|
||||
return;
|
||||
}
|
||||
const updateData = new Mediafile({
|
||||
title: this.fileEditForm.value.title,
|
||||
hidden: this.fileEditForm.value.hidden,
|
||||
});
|
||||
|
||||
this.repo.update(updateData, this.fileToEdit).then(() => {
|
||||
this.editFile = false;
|
||||
}, this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a delete request to the repository.
|
||||
*
|
||||
* @param file the file to delete
|
||||
*/
|
||||
public async onDelete(file: ViewMediafile): Promise<void> {
|
||||
const content = this.translate.instant('Delete a file');
|
||||
if (await this.promptService.open('Are you sure?', content)) {
|
||||
this.repo.delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* triggers a routine to delete all MediaFiles
|
||||
* TODO: Remove after Multiselect
|
||||
*
|
||||
* @deprecated to be removed once multi selection is implemented
|
||||
*/
|
||||
public async onDeleteAllFiles(): Promise<void> {
|
||||
const content = this.translate.instant('This will delete all files.');
|
||||
if (await this.promptService.open('Are you sure?', content)) {
|
||||
const viewMediafiles = this.dataSource.data;
|
||||
viewMediafiles.forEach(file => {
|
||||
this.repo.delete(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name of an action
|
||||
*
|
||||
* @param mediaFileAction Logo or font action
|
||||
* @returns the display name of the selected action
|
||||
*/
|
||||
public getNameOfAction(mediaFileAction: string): string {
|
||||
return this.translate.instant(this.mediaManage.getMediaConfig(mediaFileAction).display_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formated string for the tooltip containing all the action names.
|
||||
*
|
||||
* @param file the target file where the tooltip should be shown
|
||||
* @returns getNameOfAction with formated strings.
|
||||
*/
|
||||
public formatIndicatorTooltip(file: ViewMediafile): string {
|
||||
const settings = this.getFileSettings(file);
|
||||
const actionNames = settings.map(option => this.getNameOfAction(option));
|
||||
return actionNames.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given file is used in the following action
|
||||
* i.e checks if a image is used as projector logo
|
||||
*
|
||||
* @param mediaFileAction the action to check for
|
||||
* @param media the mediafile to check
|
||||
* @returns whether the file is used
|
||||
*/
|
||||
public isUsedAs(file: ViewMediafile, mediaFileAction: string): boolean {
|
||||
const config = this.mediaManage.getMediaConfig(mediaFileAction);
|
||||
return config.path === file.downloadUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the managed options for the given file
|
||||
*
|
||||
* @param file the file to look up
|
||||
* @returns array of actions
|
||||
*/
|
||||
public getFileSettings(file: ViewMediafile): string[] {
|
||||
let uses = [];
|
||||
if (file) {
|
||||
if (file.isFont()) {
|
||||
uses = this.fontActions.filter(action => this.isUsedAs(file, action));
|
||||
} else if (file.isImage()) {
|
||||
uses = this.logoActions.filter(action => this.isUsedAs(file, action));
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given image as the given option
|
||||
*
|
||||
* @param event The fired event after clicking the button
|
||||
* @param file the selected file
|
||||
* @param action the action that should be executed
|
||||
*/
|
||||
public onManageButton(event: any, file: ViewMediafile, action: string): void {
|
||||
// prohibits automatic closing
|
||||
event.stopPropagation();
|
||||
this.mediaManage.setAs(file, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the ViewportService to determine which column definition to use
|
||||
*
|
||||
* @returns the column definition for the screen size
|
||||
*/
|
||||
public getColumnDefinition(): string[] {
|
||||
return this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly downloads a mediafile
|
||||
*
|
||||
* @param file the select file to download
|
||||
*/
|
||||
public download(file: ViewMediafile): void {
|
||||
window.open(file.downloadUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicking escape while in editFileForm should deactivate edit mode.
|
||||
*
|
||||
* @param event The key that was pressed
|
||||
*/
|
||||
public keyDownFunction(event: KeyboardEvent): void {
|
||||
if (event.key === 'Escape') {
|
||||
this.editFile = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<os-head-bar plusButton=true (plusButtonClicked)=onPlusButton()>
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Files</h2>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="mediafilesMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- name column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell (click)="selectFile(file)" *matCellDef="let file">{{ file.title }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- prefix column -->
|
||||
<ng-container matColumnDef="info">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
||||
<mat-cell (click)="selectFile(file)" *matCellDef="let file">
|
||||
{{ file.type }}
|
||||
<br>
|
||||
{{ file.size }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- prefix column -->
|
||||
<ng-container matColumnDef="download">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Download</mat-header-cell>
|
||||
<mat-cell (click)="download(file)" *matCellDef="let file">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="['title', 'info', 'download']"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: ['title', 'info', 'download']"></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
|
||||
|
||||
<mat-menu #mediafilesMenu="matMenu">
|
||||
<button mat-menu-item class="red-warning-text" (click)="deleteAllFiles()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete all files</span>
|
||||
</button>
|
||||
</mat-menu>
|
@ -1,81 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ViewMediafile } from '../models/view-mediafile';
|
||||
import { MediafileRepositoryService } from '../services/mediafile-repository.service';
|
||||
import { ListViewBaseComponent } from '../../base/list-view-base';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
|
||||
/**
|
||||
* Lists all the uploaded files.
|
||||
*
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-mediafile-list',
|
||||
templateUrl: './mediafile-list.component.html',
|
||||
styleUrls: ['./mediafile-list.component.css']
|
||||
})
|
||||
export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile> implements OnInit {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param repo the repository for files
|
||||
* @param titleService
|
||||
* @param translate
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private repo: MediafileRepositoryService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
* Set the title
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Files');
|
||||
this.initTable();
|
||||
this.repo.getViewModelListObservable().subscribe(newUsers => {
|
||||
this.dataSource.data = newUsers;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the plus button delegated from head-bar
|
||||
*/
|
||||
public onPlusButton(): void {
|
||||
console.log('clicked plus (mediafile)');
|
||||
}
|
||||
|
||||
/**
|
||||
* function to Download all files
|
||||
* (serves as example to use functions on head bar)
|
||||
*
|
||||
* TODO: Not yet implemented, might not even be required
|
||||
*/
|
||||
public deleteAllFiles(): void {
|
||||
console.log('do download');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicking on a list row
|
||||
* @param file the selected file
|
||||
*/
|
||||
public selectFile(file: ViewMediafile): void {
|
||||
console.log('The file: ', file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly download a mediafile using the download button on the table
|
||||
* @param file
|
||||
*/
|
||||
public download(file: ViewMediafile): void {
|
||||
window.open(file.downloadUrl);
|
||||
}
|
||||
}
|
@ -1,11 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { MediafileListComponent } from './mediafile-list/mediafile-list.component';
|
||||
import { MediafileListComponent } from './components/mediafile-list/mediafile-list.component';
|
||||
import { MediaUploadComponent } from './components/media-upload/media-upload.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: MediafileListComponent }];
|
||||
const routes: Routes = [
|
||||
{ path: '', component: MediafileListComponent },
|
||||
{ path: 'upload', component: MediaUploadComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class MediafilesRoutingModule {}
|
||||
|
@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common';
|
||||
|
||||
import { MediafilesRoutingModule } from './mediafiles-routing.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { MediafileListComponent } from './mediafile-list/mediafile-list.component';
|
||||
import { MediafileListComponent } from './components/mediafile-list/mediafile-list.component';
|
||||
import { MediaUploadComponent } from './components/media-upload/media-upload.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MediafilesRoutingModule, SharedModule],
|
||||
declarations: [MediafileListComponent]
|
||||
declarations: [MediafileListComponent, MediaUploadComponent]
|
||||
})
|
||||
export class MediafilesModule {}
|
||||
|
@ -34,6 +34,10 @@ export class ViewMediafile extends BaseViewModel {
|
||||
return this.mediafile ? this.mediafile.media_url_prefix : null;
|
||||
}
|
||||
|
||||
public get hidden(): boolean {
|
||||
return this.mediafile ? this.mediafile.hidden : null;
|
||||
}
|
||||
|
||||
public get fileName(): string {
|
||||
return this.mediafile && this.mediafile.mediafile ? this.mediafile.mediafile.name : null;
|
||||
}
|
||||
@ -52,6 +56,63 @@ export class ViewMediafile extends BaseViewModel {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the file is an image
|
||||
*
|
||||
* @returns true or false
|
||||
*/
|
||||
public isImage(): boolean {
|
||||
return ['image/png', 'image/jpeg', 'image/gif'].includes(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the file is a font
|
||||
*
|
||||
* @returns true or false
|
||||
*/
|
||||
public isFont(): boolean {
|
||||
return ['font/ttf', 'font/woff'].includes(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the file is a pdf
|
||||
*
|
||||
* @returns true or false
|
||||
*/
|
||||
public isPdf(): boolean {
|
||||
return ['application/pdf'].includes(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the file is a video
|
||||
*
|
||||
* @returns true or false
|
||||
*/
|
||||
public isVideo(): boolean {
|
||||
return [
|
||||
'video/quicktime',
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/x-flv',
|
||||
'application/x-mpegURL',
|
||||
'video/MP2T',
|
||||
'video/3gpp',
|
||||
'video/x-msvideo',
|
||||
'video/x-ms-wmv',
|
||||
'video/x-matroska',
|
||||
].includes(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the file is presentable
|
||||
*
|
||||
* @returns true or false
|
||||
*/
|
||||
public isPresentable(): boolean {
|
||||
return this.isPdf() || this.isImage() || this.isVideo();
|
||||
}
|
||||
|
||||
public updateValues(update: Mediafile): void {
|
||||
if (this.mediafile.id === update.id) {
|
||||
this._mediafile = update;
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MediaManageService } from './media-manage.service';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('MediaManageService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: MediaManageService = TestBed.get(MediaManageService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
109
client/src/app/site/mediafiles/services/media-manage.service.ts
Normal file
109
client/src/app/site/mediafiles/services/media-manage.service.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpService } from 'app/core/services/http.service';
|
||||
import { ConfigService } from 'app/core/services/config.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ViewMediafile } from '../models/view-mediafile';
|
||||
|
||||
/**
|
||||
* The structure of an image config object
|
||||
*/
|
||||
interface ImageConfigObject {
|
||||
display_name: string
|
||||
key: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The structure of a font config
|
||||
*/
|
||||
interface FontConfigObject {
|
||||
display_name: string
|
||||
default: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the required structure of the manage payload
|
||||
*/
|
||||
interface ManagementPayload {
|
||||
id: number,
|
||||
key?: string,
|
||||
default?: string,
|
||||
value: ImageConfigObject | FontConfigObject
|
||||
}
|
||||
|
||||
/**
|
||||
* The service to manage Mediafiles.
|
||||
*
|
||||
* Declaring images as logos (web, projector, pdf, ...) is handles here.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MediaManageService {
|
||||
/**
|
||||
* Constructor for the MediaManage service
|
||||
*
|
||||
* @param httpService OpenSlides own HttpService
|
||||
*/
|
||||
public constructor(private config: ConfigService, private httpService: HttpService) {}
|
||||
|
||||
/**
|
||||
* Sets the given Mediafile to using the given management option
|
||||
* i.e: setting another projector logo
|
||||
*
|
||||
* TODO: Feels overly complicated. However, the server seems to requires a strictly shaped payload
|
||||
*
|
||||
* @param file the selected Mediafile
|
||||
* @param action determines the action
|
||||
*/
|
||||
public async setAs(file: ViewMediafile, action: string): Promise<void> {
|
||||
const restPath = `rest/core/config/${action}`;
|
||||
|
||||
const config = this.getMediaConfig(action);
|
||||
const path = (config.path !== file.downloadUrl) ? file.downloadUrl : '';
|
||||
|
||||
// Create the payload that the server requires to manage a mediafile
|
||||
const payload: ManagementPayload = {
|
||||
id: file.id,
|
||||
key: (config as ImageConfigObject).key,
|
||||
default: (config as FontConfigObject).default,
|
||||
value: {
|
||||
display_name: config.display_name,
|
||||
key: (config as ImageConfigObject).key,
|
||||
default: (config as FontConfigObject).default,
|
||||
path: path
|
||||
}
|
||||
}
|
||||
|
||||
return this.httpService.put<void>(restPath, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all actions that can be executed on images
|
||||
*
|
||||
* @returns observable array of strings with the actions for images
|
||||
*/
|
||||
public getLogoActions(): Observable<string[]> {
|
||||
return this.config.get('logos_available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all actions that can be executed on fonts
|
||||
*
|
||||
* @returns observable array of string with the actions for fonts
|
||||
*/
|
||||
public getFontActions(): Observable<string[]> {
|
||||
return this.config.get('fonts_available');
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the config object to a given action
|
||||
*
|
||||
* @param action the logo action or font action
|
||||
* @returns A media config object containing the requested values
|
||||
*/
|
||||
public getMediaConfig(action: string): ImageConfigObject | FontConfigObject {
|
||||
return this.config.instant(action);
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MediafileRepositoryService } from './mediafile-repository.service';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('FileRepositoryService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: MediafileRepositoryService = TestBed.get(MediafileRepositoryService);
|
||||
|
@ -7,49 +7,86 @@ import { User } from '../../../shared/models/users/user';
|
||||
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
||||
import { DataSendService } from 'app/core/services/data-send.service';
|
||||
import { HttpService } from 'app/core/services/http.service';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
/**
|
||||
* Repository for files
|
||||
* Repository for MediaFiles
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MediafileRepositoryService extends BaseRepository<ViewMediafile, Mediafile> {
|
||||
/**
|
||||
* Consturctor for the file repo
|
||||
* @param DS the DataStore
|
||||
* Constructor for the mediafile repository
|
||||
* @param DS Data store
|
||||
* @param mapperService OpenSlides class mapping service
|
||||
* @param dataSend sending data to the server
|
||||
* @param httpService OpenSlides own http service
|
||||
*/
|
||||
public constructor(DS: DataStoreService, mapperService: CollectionStringModelMapperService) {
|
||||
public constructor(
|
||||
DS: DataStoreService,
|
||||
mapperService: CollectionStringModelMapperService,
|
||||
private dataSend: DataSendService,
|
||||
private httpService: HttpService,
|
||||
) {
|
||||
super(DS, mapperService, Mediafile, [User]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a config value.
|
||||
* Alter a given mediaFile
|
||||
* Usually just i.e change the name and the hidden flag.
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
* @param file contains the new values
|
||||
* @param viewFile the file that should be updated
|
||||
*/
|
||||
public async update(file: Partial<Mediafile>, viewFile: ViewMediafile): Promise<void> {
|
||||
return null;
|
||||
const updateFile = new Mediafile();
|
||||
updateFile.patchValues(viewFile.mediafile);
|
||||
updateFile.patchValues(file);
|
||||
await this.dataSend.updateModel(updateFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a config value.
|
||||
* Deletes the given file from the server
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
* @param file the file to delete
|
||||
*/
|
||||
public async delete(file: ViewMediafile): Promise<void> {
|
||||
return null;
|
||||
return await this.dataSend.deleteModel(file.mediafile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a config value.
|
||||
* Mediafiles are uploaded using FormData objects and (usually) not created locally.
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
* @param file a new mediafile
|
||||
* @returns the ID as a promise
|
||||
*/
|
||||
public async create(file: Mediafile): Promise<Identifiable> {
|
||||
return null;
|
||||
return await this.dataSend.createModel(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to the server.
|
||||
* The HttpHeader should be Application/FormData, the empty header will
|
||||
* set the the required boundary automatically
|
||||
*
|
||||
* @param file created UploadData, containing a file
|
||||
* @returns the promise to a new mediafile.
|
||||
*/
|
||||
public async uploadFile(file: FormData): Promise<Identifiable> {
|
||||
const restPath = `rest/mediafiles/mediafile/`;
|
||||
const emptyHeader = new HttpHeaders();
|
||||
return this.httpService.post<Identifiable>(restPath, file, emptyHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates mediafile ViewModels out of given mediafile objects
|
||||
*
|
||||
* @param file mediafile to convert
|
||||
* @returns a new mediafile ViewModel
|
||||
*/
|
||||
public createViewModel(file: Mediafile): ViewMediafile {
|
||||
const uploader = this.DS.get(User, file.uploader_id);
|
||||
return new ViewMediafile(file, uploader);
|
||||
|
@ -150,3 +150,10 @@ mat-expansion-panel {
|
||||
mat-panel-title mat-icon {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
// ngx-file-drop requires the custom style in the global css file
|
||||
.file-drop-style {
|
||||
margin: auto;
|
||||
height: 100px;
|
||||
border: 2px dotted #0782d0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user