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",
|
"core-js": "^2.5.4",
|
||||||
"file-saver": "^2.0.0-rc.3",
|
"file-saver": "^2.0.0-rc.3",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
|
"ngx-file-drop": "^5.0.0",
|
||||||
"ngx-mat-select-search": "^1.4.0",
|
"ngx-mat-select-search": "^1.4.0",
|
||||||
"po2json": "^1.0.0-alpha",
|
"po2json": "^1.0.0-alpha",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
|
||||||
|
|
||||||
// Core Services, Directives
|
// Core Services, Directives
|
||||||
import { AuthGuard } from './services/auth-guard.service';
|
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 { DataStoreService } from './services/data-store.service';
|
||||||
import { OperatorService } from './services/operator.service';
|
import { OperatorService } from './services/operator.service';
|
||||||
import { WebsocketService } from './services/websocket.service';
|
import { WebsocketService } from './services/websocket.service';
|
||||||
import { AddHeaderInterceptor } from './http-interceptor';
|
|
||||||
import { DataSendService } from './services/data-send.service';
|
import { DataSendService } from './services/data-send.service';
|
||||||
import { ViewportService } from './services/viewport.service';
|
import { ViewportService } from './services/viewport.service';
|
||||||
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
||||||
@ -31,12 +29,7 @@ import { HttpService } from './services/http.service';
|
|||||||
HttpService,
|
HttpService,
|
||||||
OperatorService,
|
OperatorService,
|
||||||
ViewportService,
|
ViewportService,
|
||||||
WebsocketService,
|
WebsocketService
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: AddHeaderInterceptor,
|
|
||||||
multi: true
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
entryComponents: [PromptDialogComponent]
|
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 { 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';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,13 +20,22 @@ export enum HTTPMethod {
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class HttpService {
|
export class HttpService {
|
||||||
|
/**
|
||||||
|
* http headers used by most requests
|
||||||
|
*/
|
||||||
|
private defaultHeaders: HttpHeaders;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a HttpService
|
* Construct a HttpService
|
||||||
*
|
*
|
||||||
|
* Sets the default headers to application/json
|
||||||
|
*
|
||||||
* @param http The HTTP Client
|
* @param http The HTTP Client
|
||||||
* @param translate
|
* @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.
|
* 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 url the target url, usually starting with /rest
|
||||||
* @param method the required HTTP method (i.e get, post, put)
|
* @param method the required HTTP method (i.e get, post, put)
|
||||||
* @param data optional, if sending a data body is required
|
* @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('/')) {
|
if (!url.endsWith('/')) {
|
||||||
url += '/';
|
url += '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
body: data
|
body: data,
|
||||||
|
headers: customHeader ? customHeader : this.defaultHeaders
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
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,
|
* Errors from the servers may be string or array of strings. This function joins the strings together,
|
||||||
* if an array is send.
|
* if an array is send.
|
||||||
* @param str a string or a string array to join together.
|
* @param str a string or a string array to join together.
|
||||||
|
* @returns Error text(s) as single string
|
||||||
*/
|
*/
|
||||||
private processErrorTexts(str: string | string[]): string {
|
private processErrorTexts(str: string | string[]): string {
|
||||||
if (str instanceof Array) {
|
if (str instanceof Array) {
|
||||||
@ -109,44 +122,54 @@ export class HttpService {
|
|||||||
* Exectures a get on a url with a certain object
|
* Exectures a get on a url with a certain object
|
||||||
* @param url The url to send the request to.
|
* @param url The url to send the request to.
|
||||||
* @param data An optional payload for the request.
|
* @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> {
|
public async get<T>(url: string, data?: any, header?: HttpHeaders): Promise<T> {
|
||||||
return await this.send<T>(url, HTTPMethod.GET, data);
|
return await this.send<T>(url, HTTPMethod.GET, data, header);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exectures a post on a url with a certain object
|
* Exectures a post on a url with a certain object
|
||||||
* @param url string of the url to send semothing to
|
* @param url string of the url to send semothing to
|
||||||
* @param data The data to send
|
* @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> {
|
public async post<T>(url: string, data: any, header?: HttpHeaders): Promise<T> {
|
||||||
return await this.send<T>(url, HTTPMethod.POST, data);
|
return await this.send<T>(url, HTTPMethod.POST, data, header);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exectures a put on a url with a certain object
|
* Exectures a put on a url with a certain object
|
||||||
* @param url string of the url to send semothing to
|
* @param url string of the url to send semothing to
|
||||||
* @param data the object that should be send
|
* @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> {
|
public async patch<T>(url: string, data: any, header?: HttpHeaders): Promise<T> {
|
||||||
return await this.send<T>(url, HTTPMethod.PATCH, data);
|
return await this.send<T>(url, HTTPMethod.PATCH, data, header);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exectures a put on a url with a certain object
|
* Exectures a put on a url with a certain object
|
||||||
* @param url the url that should be called
|
* @param url the url that should be called
|
||||||
* @param data: The data to send
|
* @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> {
|
public async put<T>(url: string, data: any, header?: HttpHeaders): Promise<T> {
|
||||||
return await this.send<T>(url, HTTPMethod.PUT, data);
|
return await this.send<T>(url, HTTPMethod.PUT, data, header);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a delete request.
|
* Makes a delete request.
|
||||||
* @param url the url that should be called
|
* @param url the url that should be called
|
||||||
* @param data An optional data to send in the requestbody.
|
* @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> {
|
public async delete<T>(url: string, data?: any, header?: HttpHeaders): Promise<T> {
|
||||||
return await this.send<T>(url, HTTPMethod.DELETE, data);
|
return await this.send<T>(url, HTTPMethod.DELETE, data, header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
|
MatProgressBarModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
MatSnackBarModule,
|
MatSnackBarModule,
|
||||||
@ -22,7 +23,7 @@ import {
|
|||||||
DateAdapter,
|
DateAdapter,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
MatBadgeModule
|
MatBadgeModule,
|
||||||
} from '@angular/material';
|
} from '@angular/material';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatChipsModule } from '@angular/material';
|
import { MatChipsModule } from '@angular/material';
|
||||||
@ -39,6 +40,9 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
|||||||
// ngx-translate
|
// ngx-translate
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
// ngx-file-drop
|
||||||
|
import { FileDropModule } from 'ngx-file-drop';
|
||||||
|
|
||||||
// directives
|
// directives
|
||||||
import { PermsDirective } from './directives/perms.directive';
|
import { PermsDirective } from './directives/perms.directive';
|
||||||
import { DomChangeDirective } from './directives/dom-change.directive';
|
import { DomChangeDirective } from './directives/dom-change.directive';
|
||||||
@ -82,6 +86,7 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
|
|||||||
MatTableModule,
|
MatTableModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
|
MatProgressBarModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
MatListModule,
|
MatListModule,
|
||||||
@ -101,7 +106,8 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
|
|||||||
DragDropModule,
|
DragDropModule,
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
RouterModule,
|
RouterModule,
|
||||||
NgxMatSelectSearchModule
|
NgxMatSelectSearchModule,
|
||||||
|
FileDropModule
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@ -118,6 +124,7 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
|
|||||||
MatTableModule,
|
MatTableModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
|
MatProgressBarModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
MatListModule,
|
MatListModule,
|
||||||
@ -133,6 +140,7 @@ import { SpeakerListComponent } from 'app/site/agenda/components/speaker-list/sp
|
|||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
NgxMatSelectSearchModule,
|
NgxMatSelectSearchModule,
|
||||||
|
FileDropModule,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
DomChangeDirective,
|
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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { MediafileListComponent } from './mediafile-list.component';
|
import { MediafileListComponent } from './mediafile-list.component';
|
||||||
import { E2EImportsModule } from '../../../../e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
describe('MediafileListComponent', () => {
|
describe('MediafileListComponent', () => {
|
||||||
let component: 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 { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
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({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class MediafilesRoutingModule {}
|
export class MediafilesRoutingModule {}
|
||||||
|
@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common';
|
|||||||
|
|
||||||
import { MediafilesRoutingModule } from './mediafiles-routing.module';
|
import { MediafilesRoutingModule } from './mediafiles-routing.module';
|
||||||
import { SharedModule } from '../../shared/shared.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({
|
@NgModule({
|
||||||
imports: [CommonModule, MediafilesRoutingModule, SharedModule],
|
imports: [CommonModule, MediafilesRoutingModule, SharedModule],
|
||||||
declarations: [MediafileListComponent]
|
declarations: [MediafileListComponent, MediaUploadComponent]
|
||||||
})
|
})
|
||||||
export class MediafilesModule {}
|
export class MediafilesModule {}
|
||||||
|
@ -34,6 +34,10 @@ export class ViewMediafile extends BaseViewModel {
|
|||||||
return this.mediafile ? this.mediafile.media_url_prefix : null;
|
return this.mediafile ? this.mediafile.media_url_prefix : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get hidden(): boolean {
|
||||||
|
return this.mediafile ? this.mediafile.hidden : null;
|
||||||
|
}
|
||||||
|
|
||||||
public get fileName(): string {
|
public get fileName(): string {
|
||||||
return this.mediafile && this.mediafile.mediafile ? this.mediafile.mediafile.name : null;
|
return this.mediafile && this.mediafile.mediafile ? this.mediafile.mediafile.name : null;
|
||||||
}
|
}
|
||||||
@ -52,6 +56,63 @@ export class ViewMediafile extends BaseViewModel {
|
|||||||
return this.title;
|
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 {
|
public updateValues(update: Mediafile): void {
|
||||||
if (this.mediafile.id === update.id) {
|
if (this.mediafile.id === update.id) {
|
||||||
this._mediafile = update;
|
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 { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { MediafileRepositoryService } from './mediafile-repository.service';
|
import { MediafileRepositoryService } from './mediafile-repository.service';
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
describe('FileRepositoryService', () => {
|
describe('FileRepositoryService', () => {
|
||||||
beforeEach(() => TestBed.configureTestingModule({}));
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: MediafileRepositoryService = TestBed.get(MediafileRepositoryService);
|
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 { DataStoreService } from '../../../core/services/data-store.service';
|
||||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||||
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class MediafileRepositoryService extends BaseRepository<ViewMediafile, Mediafile> {
|
export class MediafileRepositoryService extends BaseRepository<ViewMediafile, Mediafile> {
|
||||||
/**
|
/**
|
||||||
* Consturctor for the file repo
|
* Constructor for the mediafile repository
|
||||||
* @param DS the DataStore
|
* @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]);
|
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> {
|
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> {
|
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> {
|
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 {
|
public createViewModel(file: Mediafile): ViewMediafile {
|
||||||
const uploader = this.DS.get(User, file.uploader_id);
|
const uploader = this.DS.get(User, file.uploader_id);
|
||||||
return new ViewMediafile(file, uploader);
|
return new ViewMediafile(file, uploader);
|
||||||
|
@ -150,3 +150,10 @@ mat-expansion-panel {
|
|||||||
mat-panel-title mat-icon {
|
mat-panel-title mat-icon {
|
||||||
padding-right: 30px;
|
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