Add NGrid UI for MediaFiles

This commit is contained in:
Sean Engelhardt 2019-07-12 13:09:07 +02:00
parent 56c1da352e
commit e2adc8911f
10 changed files with 282 additions and 134 deletions

View File

@ -1,4 +1,4 @@
<os-head-bar [nav]="false">
<os-head-bar [nav]="false" [goBack]="true">
<!-- Title -->
<div class="title-slot"><h2 translate>Upload files</h2></div>

View File

@ -1,11 +1,12 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Location } from '@angular/common';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view';
import { Router, ActivatedRoute } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
/**
@ -38,7 +39,7 @@ export class MediaUploadComponent extends BaseViewComponent implements OnInit {
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private router: Router,
private location: Location,
private route: ActivatedRoute,
private repo: MediafileRepositoryService
) {
@ -55,7 +56,7 @@ export class MediaUploadComponent extends BaseViewComponent implements OnInit {
* Handler for successful uploads
*/
public uploadSuccess(): void {
this.router.navigate(['../'], { relativeTo: this.route });
this.location.back();
}
/**

View File

@ -21,102 +21,108 @@
</div>-->
</os-head-bar>
<!-- TODO: Sort bar -->
<!-- Folder navigation bar -->
<div>
<button mat-button (click)="changeDirectory(null)">
<span translate>Base folder</span>
<div class="folder-nav-bar">
<button class="folder" mat-button (click)="changeDirectory(null)">
<mat-icon class="file-icon">home</mat-icon>
</button>
<span *ngFor="let directory of directoryChain; let last=last">
<span *ngFor="let directory of directoryChain; let last = last">
<div class="arrow">
<mat-icon>chevron_right</mat-icon>
<button *ngIf="!last" mat-button (click)="changeDirectory(directory.id)">
</div>
<button class="folder" mat-button (click)="changeDirectory(directory.id)" *ngIf="!last">
<span class="folder-text">
{{ directory.title }}
</span>
</button>
<button *ngIf="last" mat-button (click)="onEditFile(directory)">
<button
class="folder"
mat-button
[matMenuTriggerFor]="singleMediafileMenu"
[matMenuTriggerData]="{ mediafile: directory }"
*ngIf="last"
>
<os-icon-container icon="arrow_drop_down" swap="true" size="large">
{{ directory.title }}
<mat-icon>edit</mat-icon>
</os-icon-container>
</button>
</span>
<button mat-icon-button *ngIf="directory" (click)="changeDirectory(directory.parent_id)">
<mat-icon>arrow_upward</mat-icon>
</button>
</div>
<div *ngIf="directory && directory.inherited_access_groups_id !== true">
<span class="visibility" *ngIf="directory && directory.inherited_access_groups_id !== true">
<span translate>Visibility of this directory:</span>
<span *ngIf="directory.inherited_access_groups_id === false" translate>No one</span>
<span *ngIf="directory.has_inherited_access_groups" translate>
<span class="visible-for" *ngIf="directory.inherited_access_groups_id === false" translate>No one</span>
<span class="visible-for" *ngIf="directory.has_inherited_access_groups" translate>
<os-icon-container icon="group">{{ directory.inherited_access_groups }}</os-icon-container>
</span>
</span>
</div>
<mat-divider></mat-divider>
</div>
<mat-table [dataSource]="dataSource" class="os-listview-table">
<!-- Projector button -->
<ng-container matColumnDef="projector">
<td mat-cell *matCellDef="let mediafile">
<os-projector-button *ngIf="mediafile.isProjectable()" class="projector-button" [object]="mediafile"></os-projector-button>
</td>
</ng-container>
<!-- the actual file manager -->
<pbl-ngrid class="file-manager-table" showHeader="false" vScrollAuto [dataSource]="dataSource" [columns]="columnSet">
<!-- Icon column -->
<div *pblNgridCellDef="'icon'; row as mediafile" class="fill clickable">
<a class="detail-link" target="_blank" [routerLink]="mediafile.url" *ngIf="mediafile.is_file"> </a>
<a class="detail-link" (click)="changeDirectory(mediafile.id)" *ngIf="mediafile.is_directory"> </a>
<mat-icon class="file-icon">{{ mediafile.getIcon() }}</mat-icon>
</div>
<ng-container matColumnDef="icon">
<td mat-cell *matCellDef="let mediafile">
<mat-icon>{{ mediafile.getIcon() }}</mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="title">
<td mat-cell *matCellDef="let mediafile">
<a target="_blank" [routerLink]="mediafile.url" *ngIf="mediafile.is_file">
<!-- Title column -->
<div *pblNgridCellDef="'title'; row as mediafile" class="fill clickable">
<a class="detail-link" target="_blank" [routerLink]="mediafile.url" *ngIf="mediafile.is_file"> </a>
<a class="detail-link" (click)="changeDirectory(mediafile.id)" *ngIf="mediafile.is_directory"> </a>
<div class="innerTable">
<div class="file-title ellipsis-overflow">
{{ mediafile.title }}
</a>
<a (click)="changeDirectory(mediafile.id)" *ngIf="mediafile.is_directory">
{{ mediafile.title }}
</a>
</td>
</ng-container>
</div>
<div class="info-text" *ngIf="mediafile.is_file">
<span> {{ getDateFromTimestamp(mediafile.timestamp) }} · {{ mediafile.size }} </span>
</div>
</div>
</div>
<ng-container matColumnDef="info">
<td mat-cell *matCellDef="let mediafile">
<os-icon-container *ngIf="mediafile.is_file" icon="data_usage">{{ mediafile.size }}</os-icon-container>
<os-icon-container *ngIf="mediafile.access_groups.length" icon="group">{{ mediafile.access_groups }}</os-icon-container>
</td>
</ng-container>
<!-- Info column -->
<div *pblNgridCellDef="'info'; row as mediafile" class="fill clickable" (click)="onEditFile(mediafile)">
<os-icon-container *ngIf="mediafile.access_groups.length" icon="group">
<span translate>
{{ mediafile.access_groups }}
</span>
</os-icon-container>
</div>
<ng-container matColumnDef="indicator">
<td mat-cell *matCellDef="let mediafile">
<!-- Indicator column -->
<div *pblNgridCellDef="'indicator'; row as mediafile" class="fill">
<div
*ngIf="getFileSettings(mediafile).length > 0"
[matMenuTriggerFor]="singleMediafileMenu"
[matMenuTriggerData]="{ file: file }"
[matMenuTriggerData]="{ mediafile: mediafile }"
[matTooltip]="formatIndicatorTooltip(mediafile)"
>
<mat-icon *ngIf="mediafile.isFont()">text_fields</mat-icon>
<mat-icon *ngIf="mediafile.isImage()">insert_photo</mat-icon>
<mat-icon class="file-icon" *ngIf="mediafile.isFont()">text_fields</mat-icon>
<mat-icon class="file-icon" *ngIf="mediafile.isImage()">insert_photo</mat-icon>
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="menu">
<td mat-cell *matCellDef="let mediafile">
<!-- Indicator column -->
<div *pblNgridCellDef="'menu'; row as mediafile" class="fill">
<button
mat-icon-button
[matMenuTriggerFor]="singleMediafileMenu"
[matMenuTriggerData]="{ mediafile: mediafile }"
>
<!-- TODO: [disabled]="isMultiSelect" -->
<mat-icon>more_vert</mat-icon>
</button>
</td>
</ng-container>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</mat-table>
</div>
</pbl-ngrid>
<!-- Template for the managing buttons -->
<ng-template #manageButton let-mediafile="mediafile" let-action="action">
<button mat-menu-item (click)="onManageButton($event, mediafile, action)">
<mat-icon color="accent"> {{ isUsedAs(mediafile, action) ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
<mat-icon color="accent">
{{ isUsedAs(mediafile, action) ? 'check_box' : 'check_box_outline_blank' }}
</mat-icon>
<span>{{ getNameOfAction(action) }}</span>
</button>
</ng-template>
@ -127,20 +133,28 @@
<!-- Exclusive for images -->
<div *ngIf="mediafile.isImage()">
<div *ngFor="let action of logoActions">
<ng-container *ngTemplateOutlet="manageButton; context: { mediafile: mediafile, action: action }"></ng-container>
<ng-container
*ngTemplateOutlet="manageButton; context: { mediafile: mediafile, action: action }"
></ng-container>
</div>
</div>
<!-- Exclusive for fonts -->
<div *ngIf="mediafile.isFont()">
<div *ngFor="let action of fontActions">
<ng-container *ngTemplateOutlet="manageButton; context: { mediafile: mediafile, action: action }"></ng-container>
<ng-container
*ngTemplateOutlet="manageButton; context: { mediafile: mediafile, action: action }"
></ng-container>
</div>
</div>
<!-- Edit and delete for all images -->
<mat-divider *ngIf="mediafile.isFont() || mediafile.isImage()"></mat-divider>
<os-projector-button
*ngIf="mediafile.isProjectable()"
[object]="mediafile"
[menuItem]="true"
></os-projector-button>
<os-speaker-button [object]="mediafile" [menuItem]="true"></os-speaker-button>
<button mat-menu-item (click)="onEditFile(mediafile)">
<mat-icon>edit</mat-icon>
@ -188,10 +202,11 @@
</div>
</mat-menu>-->
<!-- File edit dialog -->
<ng-template #fileEditDialog>
<h1 mat-dialog-title>{{ 'Edit details for' | translate }}</h1>
<div class="os-form-card-mobile" mat-dialog-content>
<form class="edit-file-form" [formGroup]="fileEditForm" (keydown)="keyDownFunction($event)">
<form class="edit-file-form" [formGroup]="fileEditForm">
<mat-form-field>
<input
type="text"
@ -231,14 +246,14 @@
<!-- New folder dialog -->
<ng-template #newFolderDialog>
<h1 mat-dialog-title>
<span translate>Create new directory</span>
</h1>
<div mat-dialog-content>
<h1 mat-dialog-title>{{ 'Create new directory' | translate }}</h1>
<div class="os-form-card-mobile" mat-dialog-content>
<form class="edit-file-form" [formGroup]="newDirectoryForm">
<p translate>Please enter a name for the new directory:</p>
<mat-form-field [formGroup]="newDirectoryForm">
<input matInput osAutofocus formControlName="title" required/>
<mat-form-field>
<input matInput osAutofocus formControlName="title" required />
</mat-form-field>
<os-search-value-selector
ngDefaultControl
[formControl]="newDirectoryForm.get('access_groups_id')"
@ -246,9 +261,10 @@
listname="{{ 'Access groups' | translate }}"
[InputListValues]="groupsBehaviorSubject"
></os-search-value-selector>
</form>
</div>
<div mat-dialog-actions>
<button type="submit" mat-button color="primary" [mat-dialog-close]="true">
<button type="submit" mat-button [disabled]="!newDirectoryForm.valid" color="primary" [mat-dialog-close]="true">
<span translate>Save</span>
</button>
<button type="button" mat-button [mat-dialog-close]="null">
@ -267,7 +283,6 @@
<os-search-value-selector
ngDefaultControl
[formControl]="moveForm.get('directory_id')"
[multiple]="false"
[includeNone]="true"
[noneTitle]="'Base folder'"
listname="{{ 'Parent directory' | translate }}"

View File

@ -4,3 +4,70 @@
::ng-deep .mat-tooltip {
white-space: pre-line !important;
}
.folder-nav-bar {
$size: 40px;
position: relative;
display: flex;
line-height: $size;
background-color: white; // TODO: theme
.arrow {
height: $size;
float: left;
.mat-icon {
line-height: $size;
}
}
.folder {
height: $size;
}
.folder-text {
font-size: 16px;
font-weight: 500;
margin: auto 5px;
text-overflow: ellipsis;
overflow: hidden;
}
.visibility {
display: flex;
position: absolute;
right: 10px;
.visible-for {
margin-left: 10px;
display: inherit;
}
}
}
.file-manager-table {
.file-title {
font-weight: 500;
font-size: 16px;
}
.info-text {
font-size: 90%;
}
height: calc(100vh - 170px);
.pbl-ngrid-row {
$size: 60px;
height: $size !important;
.pbl-ngrid-cell {
height: $size !important;
}
}
// For some reason, hiding the table header adds an empty meta bar.
.pbl-ngrid-container {
> div {
display: none;
}
}
}

View File

@ -0,0 +1,13 @@
@import '~@angular/material/theming';
@mixin os-mediafile-list-theme($theme) {
$foreground: map-get($theme, foreground);
.file-icon {
color: mat-color($foreground, icon);
}
.info-text {
color: mat-color($foreground, icon);
}
}

View File

@ -1,13 +1,13 @@
import { Component, OnInit, ViewChild, TemplateRef, OnDestroy } from '@angular/core';
import { Component, OnInit, ViewChild, TemplateRef, OnDestroy, ViewEncapsulation } from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { PblDataSource, columnFactory, createDS } from '@pebula/ngrid';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
@ -27,10 +27,14 @@ import { BaseViewComponent } from 'app/site/base/base-view';
@Component({
selector: 'os-mediafile-list',
templateUrl: './mediafile-list.component.html',
styleUrls: ['./mediafile-list.component.scss']
styleUrls: ['./mediafile-list.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MediafileListComponent extends BaseViewComponent implements OnInit, OnDestroy {
public readonly dataSource: MatTableDataSource<ViewMediafile> = new MatTableDataSource<ViewMediafile>();
/**
* Data source for the files
*/
public dataSource: PblDataSource<ViewMediafile>;
/**
* Holds the actions for logos. Updated via an observable
@ -48,9 +52,7 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
public fileToEdit: ViewMediafile;
public newDirectoryForm: FormGroup;
public moveForm: FormGroup;
public directoryBehaviorSubject: BehaviorSubject<ViewMediafile[]>;
public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>;
@ -80,10 +82,42 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
@ViewChild('fileEditDialog', { static: true })
public fileEditDialog: TemplateRef<string>;
public displayedColumns = ['projector', 'icon', 'title', 'info', 'indicator', 'menu'];
/**
* Create the column set
*/
public columnSet = columnFactory()
.table(
{
prop: 'icon',
label: '',
width: '40px'
},
{
prop: 'title',
label: this.translate.instant('Title'),
width: 'auto',
minWidth: 60
},
{
prop: 'info',
label: this.translate.instant('Info'),
width: '20%',
minWidth: 60
},
{
prop: 'indicator',
label: '',
width: '40px'
},
{
prop: 'menu',
label: '',
width: '40px'
}
)
.build();
public isMultiselect = false; // TODO
private folderSubscription: Subscription;
private directorySubscription: Subscription;
public directory: ViewMediafile | null;
@ -157,12 +191,24 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
});
}
public ngOnDestroy(): void {
super.ngOnDestroy();
this.clearSubscriptions();
}
public getDateFromTimestamp(timestamp: string): string {
return new Date(timestamp).toLocaleString(this.translate.currentLang);
}
public changeDirectory(directoryId: number | null): void {
this.clearSubscriptions();
this.folderSubscription = this.repo.getListObservableDirectory(directoryId).subscribe(mediafiles => {
this.dataSource.data = [];
this.dataSource.data = mediafiles;
if (mediafiles) {
this.dataSource = createDS<ViewMediafile>()
.onTrigger(() => mediafiles)
.create();
}
});
if (directoryId) {
@ -353,9 +399,4 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
this.directorySubscription = null;
}
}
public ngOnDestroy(): void {
super.ngOnDestroy();
this.clearSubscriptions();
}
}

View File

@ -4,6 +4,11 @@ import { MediafileListComponent } from './components/mediafile-list/mediafile-li
import { MediaUploadComponent } from './components/media-upload/media-upload.component';
const routes: Routes = [
{
path: '',
redirectTo: 'files',
pathMatch: 'full'
},
{
path: 'files',
children: [{ path: '**', component: MediafileListComponent }],

View File

@ -91,6 +91,10 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
return this.mediafile.mediafile ? this.mediafile.mediafile.pages : null;
}
public get timestamp(): string {
return this.mediafile.create_timestamp ? this.mediafile.create_timestamp : null;
}
public constructor(
mediafile: Mediafile,
listOfSpeakers?: ViewListOfSpeakers,

View File

@ -3,12 +3,6 @@
// Determine the distance between the top edge to the start of the table content
$text-margin-top: 10px;
/** css hacks https://codepen.io/edge0703/pen/iHJuA */
.innerTable {
display: inline-block;
line-height: 150%;
}
.mat-button-toggle-group {
line-height: normal;
vertical-align: middle;

View File

@ -20,6 +20,7 @@
@import './app/shared/components/block-tile/block-tile.component.scss';
@import './app/shared/components/icon-container/icon-container.component.scss';
@import './app/site/common/components/start/start.component.scss';
@import './app/site/mediafiles/components/mediafile-list/mediafile-list.component.scss-theme.scss';
/** fonts */
@import './assets/styles/fonts.scss';
@ -37,6 +38,7 @@
@include os-sorting-tree-style($theme);
@include os-global-spinner-theme($theme);
@include os-tile-style($theme);
@include os-mediafile-list-theme($theme);
}
/** Load projector specific SCSS values */
@ -631,6 +633,12 @@ button.mat-menu-item.selected {
height: calc(100vh - 128px);
}
/** css hacks https://codepen.io/edge0703/pen/iHJuA */
.innerTable {
display: inline-block;
line-height: 150%;
}
.virtual-scroll-with-head-bar {
height: calc(100vh - 189px);