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 --> <!-- Title -->
<div class="title-slot"><h2 translate>Upload files</h2></div> <div class="title-slot"><h2 translate>Upload files</h2></div>

View File

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

View File

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

View File

@ -4,3 +4,70 @@
::ng-deep .mat-tooltip { ::ng-deep .mat-tooltip {
white-space: pre-line !important; 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 { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; 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 { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service'; import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
@ -27,10 +27,14 @@ import { BaseViewComponent } from 'app/site/base/base-view';
@Component({ @Component({
selector: 'os-mediafile-list', selector: 'os-mediafile-list',
templateUrl: './mediafile-list.component.html', 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 { 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 * Holds the actions for logos. Updated via an observable
@ -48,9 +52,7 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
public fileToEdit: ViewMediafile; public fileToEdit: ViewMediafile;
public newDirectoryForm: FormGroup; public newDirectoryForm: FormGroup;
public moveForm: FormGroup; public moveForm: FormGroup;
public directoryBehaviorSubject: BehaviorSubject<ViewMediafile[]>; public directoryBehaviorSubject: BehaviorSubject<ViewMediafile[]>;
public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>; public groupsBehaviorSubject: BehaviorSubject<ViewGroup[]>;
@ -80,10 +82,42 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
@ViewChild('fileEditDialog', { static: true }) @ViewChild('fileEditDialog', { static: true })
public fileEditDialog: TemplateRef<string>; 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 public isMultiselect = false; // TODO
private folderSubscription: Subscription; private folderSubscription: Subscription;
private directorySubscription: Subscription; private directorySubscription: Subscription;
public directory: ViewMediafile | null; 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 { public changeDirectory(directoryId: number | null): void {
this.clearSubscriptions(); this.clearSubscriptions();
this.folderSubscription = this.repo.getListObservableDirectory(directoryId).subscribe(mediafiles => { this.folderSubscription = this.repo.getListObservableDirectory(directoryId).subscribe(mediafiles => {
this.dataSource.data = []; if (mediafiles) {
this.dataSource.data = mediafiles; this.dataSource = createDS<ViewMediafile>()
.onTrigger(() => mediafiles)
.create();
}
}); });
if (directoryId) { if (directoryId) {
@ -353,9 +399,4 @@ export class MediafileListComponent extends BaseViewComponent implements OnInit,
this.directorySubscription = null; 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'; import { MediaUploadComponent } from './components/media-upload/media-upload.component';
const routes: Routes = [ const routes: Routes = [
{
path: '',
redirectTo: 'files',
pathMatch: 'full'
},
{ {
path: 'files', path: 'files',
children: [{ path: '**', component: MediafileListComponent }], 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; 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( public constructor(
mediafile: Mediafile, mediafile: Mediafile,
listOfSpeakers?: ViewListOfSpeakers, listOfSpeakers?: ViewListOfSpeakers,

View File

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

View File

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