merged with master

This commit is contained in:
Timo Bejan 2020-10-08 11:34:34 +03:00
parent 4f486aa4b4
commit bbee5cadeb
23 changed files with 800 additions and 403 deletions

View File

@ -1,129 +1,144 @@
import {BrowserModule} from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import {APP_INITIALIZER, NgModule} from '@angular/core'; import { APP_INITIALIZER, NgModule } from '@angular/core';
import {AppComponent} from './app.component'; import { AppComponent } from './app.component';
import {RouterModule} from '@angular/router'; import { RouterModule } from '@angular/router';
import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {ReactiveFormsModule} from "@angular/forms"; import { ReactiveFormsModule } from '@angular/forms';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from "@angular/common/http"; import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http';
import {BaseScreenComponent} from './screens/base-screen/base-screen.component'; import { BaseScreenComponent } from './screens/base-screen/base-screen.component';
import {ProjectListingScreenComponent} from './screens/project-listing-screen/project-listing-screen.component'; import { ProjectListingScreenComponent } from './screens/project-listing-screen/project-listing-screen.component';
import {ProjectOverviewScreenComponent} from './screens/project-overview-screen/project-overview-screen.component'; import { ProjectOverviewScreenComponent } from './screens/project-overview-screen/project-overview-screen.component';
import {MatToolbarModule} from "@angular/material/toolbar"; import { MatToolbarModule } from '@angular/material/toolbar';
import {ApiModule} from "@redaction/red-ui-http"; import { ApiModule } from '@redaction/red-ui-http';
import {ApiPathInterceptorService} from "./interceptor/api-path-interceptor.service"; import { ApiPathInterceptorService } from './interceptor/api-path-interceptor.service';
import {MatButtonModule} from "@angular/material/button"; import { MatButtonModule } from '@angular/material/button';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import {TranslateHttpLoader} from '@ngx-translate/http-loader'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import {MatMenuModule} from "@angular/material/menu"; import { MatMenuModule } from '@angular/material/menu';
import {languageInitializer} from "./i18n/language.initializer"; import { languageInitializer } from './i18n/language.initializer';
import {LanguageService} from "./i18n/language.service"; import { LanguageService } from './i18n/language.service';
import {MatIconModule} from "@angular/material/icon"; import { MatIconModule } from '@angular/material/icon';
import {IconsModule} from "./icons/icons.module"; import { IconsModule } from './icons/icons.module';
import {AddEditProjectDialogComponent} from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component'; import { AddEditProjectDialogComponent } from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
import {MatDialogModule} from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import {MatSnackBarModule} from "@angular/material/snack-bar"; import { MatSnackBarModule } from '@angular/material/snack-bar';
import {MatTooltipModule} from "@angular/material/tooltip"; import { MatTooltipModule } from '@angular/material/tooltip';
import {ConfirmationDialogComponent} from './common/confirmation-dialog/confirmation-dialog.component'; import { ConfirmationDialogComponent } from './common/confirmation-dialog/confirmation-dialog.component';
import {FilePreviewScreenComponent} from './screens/file/file-preview-screen/file-preview-screen.component'; import { FilePreviewScreenComponent } from './screens/file/file-preview-screen/file-preview-screen.component';
import {PdfViewerComponent} from './screens/file/pdf-viewer/pdf-viewer.component'; import { PdfViewerComponent } from './screens/file/pdf-viewer/pdf-viewer.component';
import {MatTabsModule} from "@angular/material/tabs"; import { MatTabsModule } from '@angular/material/tabs';
import {MatButtonToggleModule} from "@angular/material/button-toggle"; import { MatButtonToggleModule } from '@angular/material/button-toggle';
import {NgpSortModule} from "ngp-sort-pipe"; import { NgpSortModule } from 'ngp-sort-pipe';
import {MatFormFieldModule} from "@angular/material/form-field"; import { MatFormFieldModule } from '@angular/material/form-field';
import {MatSelectModule} from "@angular/material/select"; import { MatSelectModule } from '@angular/material/select';
import {NgxDropzoneModule} from "ngx-dropzone"; import { MatSidenavModule } from '@angular/material/sidenav';
import {MatSidenavModule} from "@angular/material/sidenav"; import { FileDetailsDialogComponent } from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component';
import {FileDetailsDialogComponent} from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component'; import { ToastrModule } from 'ngx-toastr';
import {ToastrModule} from "ngx-toastr"; import { ServiceWorkerModule } from '@angular/service-worker';
import {ServiceWorkerModule} from '@angular/service-worker'; import { environment } from '../environments/environment';
import {environment} from '../environments/environment'; import { ProjectDetailsDialogComponent } from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component';
import {ProjectDetailsDialogComponent} from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component'; import { AuthModule } from './auth/auth.module';
import {AuthModule} from "./auth/auth.module"; import { AuthGuard } from './auth/auth.guard';
import {AuthGuard} from "./auth/auth.guard"; import { FileUploadModule } from './upload/file-upload.module';
import {FileUploadModule} from "./upload/file-upload.module";
import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component'; import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component';
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { InitialsAvatarComponent } from './common/initials-avatar/initials-avatar.component';
import { StatusBarComponent } from './components/status-bar/status-bar.component';
export function HttpLoaderFactory(httpClient: HttpClient) { export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json'); return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
} }
@NgModule({ @NgModule({
declarations: [AppComponent, BaseScreenComponent, ProjectListingScreenComponent, ProjectOverviewScreenComponent, AddEditProjectDialogComponent, ConfirmationDialogComponent, FilePreviewScreenComponent, PdfViewerComponent, FileDetailsDialogComponent, ProjectDetailsDialogComponent, FullPageLoadingIndicatorComponent], declarations: [
imports: [ AppComponent,
BrowserModule, BaseScreenComponent,
BrowserAnimationsModule, ProjectListingScreenComponent,
ReactiveFormsModule, ProjectOverviewScreenComponent,
HttpClientModule, AddEditProjectDialogComponent,
AuthModule, ConfirmationDialogComponent,
IconsModule, FilePreviewScreenComponent,
ApiModule, PdfViewerComponent,
MatDialogModule, FileDetailsDialogComponent,
TranslateModule.forRoot({ ProjectDetailsDialogComponent,
loader: { FullPageLoadingIndicatorComponent,
provide: TranslateLoader, InitialsAvatarComponent,
useFactory: HttpLoaderFactory, StatusBarComponent
deps: [HttpClient] ],
} imports: [
}), BrowserModule,
RouterModule.forRoot([ BrowserAnimationsModule,
{ ReactiveFormsModule,
path: '', HttpClientModule,
redirectTo: 'ui/projects', AuthModule,
pathMatch: 'full', IconsModule,
}, ApiModule,
{ MatDialogModule,
path: 'ui', TranslateModule.forRoot({
component: BaseScreenComponent, loader: {
children: [ provide: TranslateLoader,
{ useFactory: HttpLoaderFactory,
path: 'projects', deps: [HttpClient]
component: ProjectListingScreenComponent, }
canActivate: [AuthGuard] }),
}, RouterModule.forRoot([
{ {
path: 'projects/:projectId', path: '',
component: ProjectOverviewScreenComponent, redirectTo: 'ui/projects',
canActivate: [AuthGuard] pathMatch: 'full'
}, },
{ {
path: 'projects/:projectId/file/:fileId', path: 'ui',
component: FilePreviewScreenComponent, component: BaseScreenComponent,
canActivate: [AuthGuard] children: [
} {
] path: 'projects',
} component: ProjectListingScreenComponent,
canActivate: [AuthGuard]
},
{
path: 'projects/:projectId',
component: ProjectOverviewScreenComponent,
canActivate: [AuthGuard]
},
{
path: 'projects/:projectId/file/:fileId',
component: FilePreviewScreenComponent,
canActivate: [AuthGuard]
}
]
}
]), ]),
NgpSortModule, NgpSortModule,
MatToolbarModule, MatToolbarModule,
MatButtonModule, MatButtonModule,
MatMenuModule, MatMenuModule,
MatIconModule, MatIconModule,
MatTooltipModule, MatTooltipModule,
MatSnackBarModule, MatSnackBarModule,
MatTabsModule, MatTabsModule,
MatButtonToggleModule, MatButtonToggleModule,
MatFormFieldModule, MatFormFieldModule,
ToastrModule.forRoot(), ToastrModule.forRoot(),
MatSelectModule, MatSelectModule,
MatSidenavModule, MatSidenavModule,
FileUploadModule, FileUploadModule,
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}), ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
MatProgressSpinnerModule MatProgressSpinnerModule
], ],
providers: [ { providers: [{
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
multi: true, multi: true,
useClass: ApiPathInterceptorService, useClass: ApiPathInterceptorService
}, { }, {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
multi: true, multi: true,
useFactory: languageInitializer, useFactory: languageInitializer,
deps: [LanguageService] deps: [LanguageService]
}], }],
bootstrap: [AppComponent], bootstrap: [AppComponent]
}) })
export class AppModule { export class AppModule {
} }

View File

@ -0,0 +1,4 @@
<div class="flex-row">
<div [className]="color + ' oval ' + size">{{initials}}</div>
<div *ngIf="withName" class="clamp-2">{{username || ('initials-avatar.unassigned.label' | translate)}}</div>
</div>

View File

@ -0,0 +1,11 @@
@import "../../../assets/styles/red-variables";
* {
font-size: 13px;
line-height: 16px;
}
.flex-row {
flex-wrap: wrap;
gap: 12px;
}

View File

@ -0,0 +1,37 @@
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'redaction-initials-avatar',
templateUrl: './initials-avatar.component.html',
styleUrls: ['./initials-avatar.component.scss']
})
export class InitialsAvatarComponent implements OnInit {
@Input()
public username: string;
@Input()
public color: 'gray' | 'red' = 'gray';
@Input()
public size: 'small' | 'large' = 'small';
@Input()
public withName = false;
constructor() { }
ngOnInit(): void {
}
public get initials(): string {
if (!this.username) {
return '?'
}
return this.username
.split(' ')
.filter((value, idx) => idx < 2)
.map((str) => str[0])
.join('');
}
}

View File

@ -0,0 +1,6 @@
<div class="rectangle-container">
<div *ngFor="let rect of config" [className]="'section-wrapper flex-' + rect.length">
<div *ngIf="rect.title" class="subtitle">{{ rect.title }}</div>
<div [className]="'rectangle ' + rect.color "></div>
</div>
</div>

View File

@ -0,0 +1,49 @@
@import '../../../assets/styles/red-variables';
.rectangle-container {
flex: 1;
display: flex;
width: 100%;
.subtitle {
margin-bottom: 8px;
}
.section-wrapper:first-child {
.rectangle {
border-radius: 6px 0 0 6px;
}
}
.section-wrapper:last-child {
.rectangle {
border-radius: 0 6px 6px 0;
}
&:first-child {
.rectangle {
border-radius: 6px;
}
}
}
.rectangle {
height: 4px;
&.grey {
background-color: $grey-4;
}
&.yellow {
background-color: $yellow-1;
}
&.blue {
background-color: $blue-1;
}
&.green {
background-color: $green-1;
}
}
}

View File

@ -0,0 +1,22 @@
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'redaction-status-bar',
templateUrl: './status-bar.component.html',
styleUrls: ['./status-bar.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class StatusBarComponent implements OnInit {
@Input()
public config: {
length: number,
color: 'green' | 'blue' | 'red' | 'grey' | 'yellow',
title?: string,
}[] = [];
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -8,25 +8,31 @@
<button mat-menu-item routerLink="/ui/projects" <button mat-menu-item routerLink="/ui/projects"
translate="top-bar.navigation-items.projects.label"> translate="top-bar.navigation-items.projects.label">
</button> </button>
<button *ngIf="appStateService.activeProject" [routerLink]="'/ui/projects/'+appStateService.activeProject.projectId" <button *ngIf="appStateService.activeProject"
[routerLink]="'/ui/projects/'+appStateService.activeProject.projectId"
mat-menu-item>{{appStateService.activeProject.projectName}}</button> mat-menu-item>{{appStateService.activeProject.projectName}}</button>
<button *ngIf="appStateService.activeFile" [routerLink]="'/ui/projects/'+appStateService.activeProject.projectId+'/file/'+appStateService.activeFile.fileId" <button *ngIf="appStateService.activeFile"
[routerLink]="'/ui/projects/'+appStateService.activeProject.projectId+'/file/'+appStateService.activeFile.fileId"
mat-menu-item>{{appStateService.activeFile.filename}}</button> mat-menu-item>{{appStateService.activeFile.filename}}</button>
</mat-menu> </mat-menu>
</div> </div>
<div class="menu left visible-lg"> <div class="menu left visible-lg">
<button color="primary" mat-flat-button routerLink="/ui/projects" <a class="breadcrumb" routerLink="/ui/projects"
translate="top-bar.navigation-items.projects.label"></button> translate="top-bar.navigation-items.projects.label"></a>
<div *ngIf="appStateService.activeProject" class="breadcrumb"> <div *ngIf="appStateService.activeProject" class="breadcrumb">
<mat-icon svgIcon="red:chevron-right"></mat-icon> <mat-icon svgIcon="red:chevron-right"></mat-icon>
</div> </div>
<button *ngIf="appStateService.activeProject" [routerLink]="'/ui/projects/'+appStateService.activeProject.projectId" color="accent" <a *ngIf="appStateService.activeProject" class="breadcrumb"
mat-flat-button>{{appStateService.activeProject.projectName}}</button> [routerLink]="'/ui/projects/'+appStateService.activeProject.projectId">
{{appStateService.activeProject.projectName}}
</a>
<div *ngIf="appStateService.activeFile" class="breadcrumb"> <div *ngIf="appStateService.activeFile" class="breadcrumb">
<mat-icon svgIcon="red:chevron-right"></mat-icon> <mat-icon svgIcon="red:chevron-right"></mat-icon>
</div> </div>
<button *ngIf="appStateService.activeFile" [routerLink]="'/ui/projects/'+appStateService.activeProject.projectId+'/file/'+appStateService.activeFile.fileId" color="accent" <a *ngIf="appStateService.activeFile" class="breadcrumb"
mat-flat-button>{{appStateService.activeFile.filename}}</button> [routerLink]="'/ui/projects/'+appStateService.activeProject.projectId+'/file/'+appStateService.activeFile.fileId">
{{appStateService.activeFile.filename}}
</a>
</div> </div>
<div class="menu right"> <div class="menu right">
<button [matMenuTriggerFor]="menu" mat-button translate="top-bar.navigation-items.my-account.label"></button> <button [matMenuTriggerFor]="menu" mat-button translate="top-bar.navigation-items.my-account.label"></button>
@ -52,7 +58,5 @@
<div class="divider"></div> <div class="divider"></div>
</div> </div>
<div class="red-content"> <div class="red-content">
<div class="red-content-inner"> <router-outlet></router-outlet>
<router-outlet></router-outlet>
</div>
</div> </div>

View File

@ -1,35 +1,68 @@
<section class="center-section" *ngIf="viewReady"> <section class="center-section" *ngIf="viewReady">
<div class="page-header"> <div class="page-header">
<div class="heading-xl" translate="projects.header.label"></div> <div class="filters flex-row">
<button (click)="openAddProjectDialog()" color="accent" mat-flat-button translate="projects.add-new.label"></button> <span translate="filters.filter-by.label"></span>
<div translate="filters.status.label"></div>
<div translate="filters.people.label"></div>
<div translate="filters.due-date.label"></div>
<div translate="filters.project.label"></div>
<div translate="filters.document.label"></div>
</div>
<button (click)="openAddProjectDialog()" color="warn" mat-flat-button translate="projects.add-new.label"></button>
</div> </div>
<div *ngIf="appStateService.allProjects?.length === 0 " translate="projects.no-projects.label"></div> <div *ngIf="appStateService.allProjects?.length === 0 " translate="projects.no-projects.label"></div>
<div class="listing">
<div *ngFor="let project of appStateService.allProjects" [routerLink]="'/ui/projects/'+project.projectId" <div class="flex red-content-inner">
class="list-entry clickable"> <div class="table-container">
<div class="list-entry-content"> <div class="table-header">
<div class="listing-title"> <span class="subheading">
{{project.projectName}} {{'projects.table-header.title.label'| translate:{ length: appStateService.allProjects?.length || 0 } }}
</div> </span>
<div class="listing-subtitle"> <div class="actions">
{{project.description}} <div translate="projects.table-header.bulk-select.label"></div>
</div> <div translate="projects.table-header.recent.label"></div>
<div class="listing-subtitle">
{{project.date | date:'short'}}
</div> </div>
</div> </div>
<div class="list-entry-actions">
<button (click)="editProject($event,project)" mat-icon-button <div class="table-content">
[matTooltip]="'projects.edit.action.label'|translate"> <div *ngFor="let project of appStateService.allProjects"
<mat-icon svgIcon="red:edit"></mat-icon> [routerLink]="'/ui/projects/'+project.projectId"
</button> class="table-item"
<button (click)="deleteProject($event,project)" color="warn" mat-icon-button >
[matTooltip]="'projects.delete.action.label'|translate"> <div class="flex-2">
<mat-icon svgIcon="red:delete"></mat-icon> <div class="table-item-title table-item-title--large">
</button> {{project.projectName}}
</div>
<div class="subtitle stats-subtitle">
<div>12</div>
<div>9</div>
<div>25 Dec. 2020</div>
</div>
</div>
<div class="flex-1">
<redaction-initials-avatar username="Timo Bejan"
withName="true"
></redaction-initials-avatar>
</div>
<div class="stats-bar">
<redaction-status-bar
[config]="[{ color: 'yellow', length: 2}, { length: 1, color: 'green'}]"
></redaction-status-bar>
</div>
<div class="on-hover-wrapper">
<div class="on-hover">
<div>d</div>
<div>s</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="right-fixed-container"></div>
</div> </div>
</section> </section>

View File

@ -1,7 +1,9 @@
@import "../../../assets/styles/red-mixins"; @import "../../../assets/styles/red-mixins";
:host { .stats-subtitle {
max-width: 1100px; margin-top: 6px;
margin: 0 auto; }
padding: 20px;
.stats-bar {
width: 160px;
} }

View File

@ -5,66 +5,130 @@
<div *ngIf="appStateService.activeProject" class="page-header"> <div *ngIf="appStateService.activeProject" class="page-header">
<div class="heading-xl clamp-1">{{appStateService.activeProject.projectName}}</div> <div class="filters flex-row">
<button (click)="fileInput.click()" color="accent" mat-flat-button <span translate="filters.filter-by.label"></span>
translate="project-overview.upload-files.label"></button> <div translate="filters.status.label"></div>
<div translate="filters.people.label"></div>
<div translate="filters.due-date.label"></div>
<div translate="filters.document.label"></div>
</div>
<button (click)="fileInput.click()" color="warn" mat-flat-button
translate="project-overview.upload-document.label"></button>
<input #fileInput (change)="uploadFiles($event.target.files)" class="file-upload-input" multiple="true" <input #fileInput (change)="uploadFiles($event.target.files)" class="file-upload-input" multiple="true"
type="file"> type="file">
</div> </div>
<div class="flex-row">
<div class="heading-l clamp-2">{{appStateService.activeProject?.description}}</div> <div class="flex red-content-inner">
<mat-form-field *ngIf="appStateService.projectFiles && appStateService.projectFiles.length > 0"> <div class="table-container">
<mat-label>{{'project-overview.sorting.label' | translate}}</mat-label> <div class="table-header">
<mat-select (valueChange)="sortingChanged($event)" [value]="sorting" color="primary"> <span class="subheading">
<mat-option *ngFor="let option of sortOptions" [value]="option.value"> {{'project-overview.table-header.title.label'| translate:{ length: appStateService.projectFiles?.length || 0 } }}
<mat-icon [svgIcon]="option.icon"></mat-icon> </span>
{{option.label | translate}} <div class="actions">
</mat-option> <div translate="project-overview.table-header.bulk-select.label"></div>
</mat-select> <div translate="project-overview.table-header.recent.label"></div>
</mat-form-field>
</div>
<div class="break-20"></div>
<div class="listing">
<div *ngFor="let fileStatus of appStateService.projectFiles | sortBy: sorting.order:sorting.name; trackBy:fileId"
[class.clickable]="fileStatus.status === 'PROCESSED'"
[routerLink]="fileStatus.status === 'PROCESSED' ? ['/ui/projects/'+projectId+'/file/'+fileStatus.fileId] : []"
class="list-entry xl">
<div class="list-entry-content">
<div class="listing-title one-line slim break-all">
{{fileStatus.filename}}
</div>
<div class="listing-subtitle">
{{'project-overview.file-listing.file-entry.status.label'| translate:fileStatus}}
</div>
<div class="listing-subtitle">
{{'project-overview.file-listing.file-entry.number-of-pages.label'| translate:fileStatus}}
</div>
<div class="listing-subtitle">
{{'project-overview.file-listing.file-entry.number-of-analyses.label'| translate:fileStatus}}
</div>
<div class="listing-subtitle">
{{'project-overview.file-listing.file-entry.added.label'| translate:{added: fileStatus.added | date:'short'} }}
</div>
<div *ngIf="fileStatus.lastUpdated" class="listing-subtitle">
{{'project-overview.file-listing.file-entry.last-updated.label'| translate:{lastUpdated: fileStatus.lastUpdated | date:'short'} }}
</div> </div>
</div> </div>
<div class="list-entry-actions">
<button (click)="deleteFile($event,fileStatus)" color="warn" mat-icon-button <div class="table-header">
[matTooltip]="'project-overview.delete.action.label'|translate"> <redaction-status-bar [config]="[{ length: 2, color: 'yellow', title: 'text 1'}, { length: 1, color: 'green', title: 'text 2'}]"></redaction-status-bar>
<mat-icon svgIcon="red:delete"></mat-icon> </div>
</button>
<button (click)="reanalyseFile($event,fileStatus)" color="primary" mat-icon-button <div class="table-col-names">
[matTooltip]="'project-overview.reanalyse.action.label'|translate"> <div class="flex-3 subtitle min-width" translate="project-overview.table-col-names.name.label"></div>
<mat-icon svgIcon="red:refresh"></mat-icon> <div class="flex-2 subtitle min-width" translate="project-overview.table-col-names.added-on.label"></div>
</button> <div class="flex-1 subtitle min-width" translate="project-overview.table-col-names.added-by.label"></div>
<div class="flex-1 subtitle min-width" translate="project-overview.table-col-names.assigned-to.label"></div>
<div class="subtitle status-container" translate="project-overview.table-col-names.status.label"></div>
</div>
<div class="table-item"
*ngFor="let fileStatus of appStateService.projectFiles | sortBy: sorting.order:sorting.name; trackBy:fileId"
[routerLink]="fileStatus.status === 'PROCESSED' ? ['/ui/projects/'+projectId+'/file/'+fileStatus.fileId] : []">
<div class="flex-3 table-item-title min-width">
{{ fileStatus.filename }}
</div>
<div class="subtitle flex-2 min-width">
{{ fileStatus.added | date:'d MMM. yyyy, hh:mm a' }}
</div>
<div class="subtitle flex-1 min-width">
Timo Bejan
</div>
<div class="subtitle flex-1 min-width">
<redaction-initials-avatar
withName="true"
></redaction-initials-avatar>
</div>
<div class=" status-container">
<div class="status-bar-wrapper">
<redaction-status-bar
[config]="[{ color: 'yellow', length: 1 }]"
></redaction-status-bar>
</div>
</div>
<div class="on-hover-wrapper">
<div class="on-hover">
<div>d</div>
<div>s</div>
<div>v</div>
</div>
</div>
</div>
</div>
<div class="project-details-container right-fixed-container">
<div class="actions-row">
<div>Edit</div>
<div>Delete</div>
<div>View</div>
</div>
<div class="subtitle stats-subtitle mt-20">
<div>
{{ appStateService.projectFiles.length }}
</div>
<div>9</div>
<div>
{{ appStateService.activeProject.date | date:'d MMM. yyyy' }}
</div>
</div>
<div class="heading-xl mt-20">
{{ appStateService.activeProject.projectName }}
</div>
<div class="owner flex-row mt-20">
<redaction-initials-avatar username="Timo Bejan"
size="large"
withName="true"
></redaction-initials-avatar>
</div>
<div class="description mt-20">
{{ appStateService.activeProject.description }}
</div>
<div class="project-team mt-20">
<div class="subheading" translate="project-overview.project-details.project-team.label"></div>
<div class="flex mt-20">
<div *ngFor="let username of ['S H', 'D O', 'E G', 'D V', 'J A', 'T H', 'P B']" class="member">
<redaction-initials-avatar [username]="username" size="large"></redaction-initials-avatar>
</div>
<div class="member">
<div class="oval large">+2</div>
</div>
<div class="member">
<div class="oval red large">+</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<button (click)="showDetailsDialog($event)" aria-label="details" class="details-button" color="primary" mat-fab>
<mat-icon svgIcon="red:info"></mat-icon>
</button>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -1,14 +1,45 @@
@import "../../../assets/styles/red-variables";
.listing {
position: relative;
height: calc(100vh - (61px + 80px + 70px + 80px));
overflow: auto;
}
.file-upload-input { .file-upload-input {
display: none; display: none;
} }
.min-width {
min-width: 60px;
}
.status-container {
display: flex;
justify-content: flex-end;
width: 40px;
.status-bar-wrapper {
width: 40px;
}
}
.table-header redaction-status-bar {
width: 100%;
padding-bottom: 10px;
}
.project-details-container {
.actions-row {
display: flex;
> div {
padding: 10px;
}
}
.description {
font-size: 13px;
line-height: 18px;
}
.project-team {
.member:not(:last-child) {
margin-right: 5px;
}
}
}

View File

@ -1,19 +1,19 @@
import {ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router"; import { ActivatedRoute, Router } from '@angular/router';
import { import {
FileStatus, FileStatus,
FileUploadControllerService, FileUploadControllerService,
ProjectControllerService, ProjectControllerService,
ReanalysisControllerService, ReanalysisControllerService,
StatusControllerService StatusControllerService
} from "@redaction/red-ui-http"; } from '@redaction/red-ui-http';
import {NotificationService, NotificationType} from "../../notification/notification.service"; import { NotificationService, NotificationType } from '../../notification/notification.service';
import {TranslateService} from "@ngx-translate/core"; import { TranslateService } from '@ngx-translate/core';
import {ConfirmationDialogComponent} from "../../common/confirmation-dialog/confirmation-dialog.component"; import { ConfirmationDialogComponent } from '../../common/confirmation-dialog/confirmation-dialog.component';
import {MatDialog} from "@angular/material/dialog"; import { MatDialog } from '@angular/material/dialog';
import {AppStateService} from "../../state/app-state.service"; import { AppStateService } from '../../state/app-state.service';
import {ProjectDetailsDialogComponent} from "./project-details-dialog/project-details-dialog.component"; import { ProjectDetailsDialogComponent } from './project-details-dialog/project-details-dialog.component';
import {FileDropOverlayService} from "../../upload/file-drop/service/file-drop-overlay.service"; import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service';
@Component({ @Component({
@ -25,24 +25,26 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
viewReady = false; viewReady = false;
@ViewChild('dropzoneComponent', {static: true}) dropZoneComponent; @ViewChild('dropzoneComponent', { static: true }) dropZoneComponent;
dragActive = false; dragActive = false;
sortOptions: any[] = [{ sortOptions: any[] = [{
value: {name: 'lastUpdated', order: 'desc'}, value: { name: 'lastUpdated', order: 'desc' },
label: 'project-overview.sorting.last-updated-desc.label', label: 'project-overview.sorting.last-updated-desc.label',
icon: 'red:sort-desc' icon: 'red:sort-desc'
}, { }, {
value: {name: 'lastUpdated', order: 'asc'}, value: { name: 'lastUpdated', order: 'asc' },
label: 'project-overview.sorting.last-updated-asc.label', label: 'project-overview.sorting.last-updated-asc.label',
icon: 'red:sort-asc' icon: 'red:sort-asc'
}, { }, {
value: {name: 'filename', order: 'desc'}, value: { name: 'filename', order: 'desc' },
label: 'project-overview.sorting.file-name-desc.label', label: 'project-overview.sorting.file-name-desc.label',
icon: 'red:sort-desc' icon: 'red:sort-desc'
}, { }, {
value: {name: 'filename', order: 'asc'}, label: 'project-overview.sorting.file-name-asc.label', icon: 'red:sort-asc' value: { name: 'filename', order: 'asc' },
label: 'project-overview.sorting.file-name-asc.label',
icon: 'red:sort-asc'
}]; }];
sorting: any = this.sortOptions[0].value; sorting: any = this.sortOptions[0].value;
projectId: string; projectId: string;
@ -78,7 +80,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
const dialogRef = this._dialog.open(ConfirmationDialogComponent, { const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
width: '400px', width: '400px',
maxWidth: '90vw', maxWidth: '90vw',
autoFocus: false, autoFocus: false
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@ -116,7 +118,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this._fileDropOverlayService.cleanupFileDropHandling(); this._fileDropOverlayService.cleanupFileDropHandling();
if (this._fileStatusInterval) { if (this._fileStatusInterval) {
this._fileStatusInterval = null; this._fileStatusInterval = null;
clearInterval(this._fileStatusInterval) clearInterval(this._fileStatusInterval);
} }
} }

View File

@ -68,7 +68,38 @@
} }
} }
}, },
"filters": {
"filter-by": {
"label": "Filter by:"
},
"status": {
"label": "Status"
},
"people": {
"label": "People"
},
"due-date": {
"label": "Due Date"
},
"project": {
"label": "Project"
},
"document": {
"label": "Document"
}
},
"projects": { "projects": {
"table-header": {
"title": {
"label": "{{length}} active projects"
},
"bulk-select": {
"label": "Bulk select"
},
"recent": {
"label": "Recent"
}
},
"add-edit-dialog": { "add-edit-dialog": {
"header-new": { "header-new": {
"label": "New Project" "label": "New Project"
@ -146,6 +177,37 @@
} }
}, },
"project-overview": { "project-overview": {
"table-header": {
"title": {
"label": "{{length}} documents"
},
"bulk-select": {
"label": "Bulk select"
},
"recent": {
"label": "Recent"
}
},
"table-col-names": {
"name": {
"label": "Name"
},
"added-on": {
"label": "Added on"
},
"added-by": {
"label": "Added by"
},
"assigned-to": {
"label": "Assigned to"
},
"status": {
"label": "Status"
}
},
"sorting": { "sorting": {
"label": "Sorting", "label": "Sorting",
"last-updated-desc": { "last-updated-desc": {
@ -196,14 +258,24 @@
} }
} }
}, },
"project-details": {
"project-team": {
"label": "Project team"
}
},
"header": { "header": {
"label": "Project Overview" "label": "Project Overview"
}, },
"upload-files": { "upload-document": {
"label": "Upload Files" "label": "Upload Document"
}, },
"no-project": { "no-project": {
"label": "Requested project: {{projectId}} does not exist! <a href='/ui/projects'>Back to Project Listing. <a/>" "label": "Requested project: {{projectId}} does not exist! <a href='/ui/projects'>Back to Project Listing. <a/>"
} }
},
"initials-avatar": {
"unassigned": {
"label": "Unassigned"
}
} }
} }

View File

@ -0,0 +1,38 @@
@import "red-variables";
.oval {
font-weight: 600;
display: flex;
justify-content: center;
align-items: center;
height: 24px;
width: 24px;
border-radius: 12px;
font-size: 10px;
border: 1px solid #E2E4E9;
&.large {
height: 32px;
width: 32px;
border-radius: 16px;
font-size: 13px;
}
&.gray {
background-color: $grey-4;
border: none;
}
&.red {
background-color: $red-1;
color: $white;
border: none;
}
}
.stats-subtitle {
display: flex;
> div:not(:last-child) {
margin-right: 12px;
}
}

View File

@ -10,7 +10,6 @@
label { label {
height: 14px; height: 14px;
opacity: 0.6; opacity: 0.6;
color: $grey-1;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
letter-spacing: 0; letter-spacing: 0;

View File

@ -6,4 +6,5 @@
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
} }

View File

@ -1,154 +1,81 @@
@import "red-variables"; @import "red-variables";
@import "red-mixins"; @import "red-mixins";
@media only screen and (max-width: 720px) {
.listing {
.list-entry {
width: 100% !important;
}
}
}
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100vh; height: 100vh;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
color: $grey-1;
} }
.page-header { .page-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-bottom: 20px; padding: 0 24px;
height: 50px;
&.slim { box-shadow: 0 2px 4px 0 $grey-4;
padding-bottom: 0; position: fixed;
} top: 61px;
width: 100vw;
box-sizing: border-box;
background-color: $white;
z-index: 1;
} }
.red-content-inner {
.listing { margin-top: 50px;
width: 100%;
display: inline-flex;
flex-wrap: wrap;
.list-entry {
box-sizing: border-box;
height: 122px;
width: 310px;
border: 1px solid $grey-1;
border-radius: 2px;
background-color: $white;
padding: 20px 24px;
margin-right: 22px;
margin-bottom: 22px;
display: flex;
flex-direction: row;
transition: background-color 0.35s ease-in-out;
&.xl {
width: 100%;
padding: 16px 24px;
}
&:hover {
background-color: $grey-2;
.list-entry-actions {
mat-icon {
opacity: 1;
}
}
}
&.clickable:hover {
background-color: $grey-1;
cursor: pointer;
color: $white;
.list-entry-actions {
button.mat-primary {
mat-icon {
color: $white;
}
}
}
.listing-title, .listing-subtitle {
color: $white;
}
}
.list-entry-content {
display: flex;
flex: 1;
flex-direction: column;
}
.list-entry-actions {
width: 18px;
margin-left: 22px;
display: flex;
flex-direction: column;
margin-top: -4px;
mat-icon {
opacity: 0;
transition: opacity 0.35s ease-in-out;
width: 20px;
height: 20px;
}
}
.listing-title {
height: 44px;
color: #283241;
font-family: Inter, sans-serf, serif;
font-size: 18px;
font-weight: bold;
letter-spacing: 0;
margin-bottom: 17px;
line-height: 22px;
@include line-clamp(2);
transition: color 0.35s ease-in-out;
&.one-line {
height: 22px;
@include line-clamp(1);
}
&.slim {
margin-bottom: 9px;
}
&.break-all {
word-break: break-all;
}
}
.listing-subtitle {
height: 12px;
opacity: 0.7;
color: #283241;
font-family: Inconsolata, monospace, monospace;
font-size: 12px;
letter-spacing: 0;
line-height: 12px;
@include line-clamp(1);
transition: color 0.35s ease-in-out;
}
}
} }
.right-fixed-container {
border-left: 1px solid $grey-4;
height: 100%;
width: 330px;
padding: 24px;
position: fixed;
right: 0;
}
.filters {
font-size: 13px;
line-height: 14px;
> div {
padding: 10px 14px;
}
}
.flex-row { .flex-row {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
} }
.flex {
display: flex;
}
.flex-1 {
flex: 1;
}
.flex-2 {
flex: 2;
}
.flex-3 {
flex: 3;
}
.mt-20 {
margin-top: 20px;
}
.table-container {
height: calc(100vh - 61px - 50px);
width: calc(100vw - 379px);
}
.break-20 { .break-20 {
height: 20px; height: 20px;
background: transparent; background: transparent;
@ -163,7 +90,6 @@ html, body {
.detail-row { .detail-row {
opacity: 1; opacity: 1;
color: $grey-1;
font-family: Inconsolata, monospace, monospace; font-family: Inconsolata, monospace, monospace;
font-size: 14px; font-size: 14px;
letter-spacing: 0; letter-spacing: 0;
@ -171,13 +97,6 @@ html, body {
height: 18px; height: 18px;
} }
.center-section {
max-width: 1100px;
margin: 0 auto;
padding: 20px;
}
.red-top-bar { .red-top-bar {
height: 61px; height: 61px;
width: 100%; width: 100%;
@ -189,8 +108,7 @@ html, body {
height: 60px; height: 60px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding-left: 60px; padding: 0 24px;
padding-right: 60px;
.menu { .menu {
display: flex; display: flex;
@ -199,15 +117,23 @@ html, body {
} }
.breadcrumb { .breadcrumb {
padding-left: 8px; text-decoration: none;
padding-right: 8px; color: $grey-1;
color: $yellow-1; font-size: 13px;
line-height: 18px;
font-weight: 600;
@include line-clamp(1);
max-width: 320px;
mat-icon { mat-icon {
height: 14px; height: 10px;
width: 14px; width: 10px;
padding: 0 8px;
} }
&:last-child {
color: $red-1;
}
} }
.divider { .divider {
@ -221,7 +147,6 @@ html, body {
width: 100vw; width: 100vw;
height: calc(100vh - 61px); height: calc(100vh - 61px);
overflow: auto; overflow: auto;
} }
.hidden { .hidden {

View File

@ -0,0 +1,88 @@
@import "red-variables";
@import "red-mixins";
.table-header {
background-color: rgba(226, 228, 233, 0.9);
padding: 7px 24px 9px;
display: flex;
justify-content: space-between;
align-items: center;
.actions {
display: flex;
font-size: 13px;
div {
padding: 10px 14px;
}
}
}
.table-col-names {
display: flex;
text-transform: uppercase;
> div {
padding: 8px 24px;
font-weight: 600;
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
}
}
.table-item {
display: flex;
align-items: center;
height: 80px;
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
> div:not(.on-hover) {
padding: 0 24px;
}
.table-item-title {
font-size: 13px;
font-weight: 600;
line-height: 18px;
@include line-clamp(1);
}
.table-item-title--large {
font-size: 16px;
line-height: 20px;
}
.on-hover-wrapper {
width: 0;
height: 0;
padding: 0 !important;
align-self: flex-start;
.on-hover {
position: relative;
right: 142px;
height: 80px;
width: 142px;
background: linear-gradient(90deg, rgba(249, 250, 251, 0) 0%, #F9FAFB 100%);
display: none;
justify-content: flex-end;
align-items: center;
div {
margin-right: 12px;
}
}
}
&:hover {
background-color: #F9FAFB;
.on-hover {
display: flex;
}
.rectangle {
background-color: $grey-4 !important;
}
}
}

View File

@ -2,8 +2,9 @@
@import "red-mixins"; @import "red-mixins";
button { button {
font-family: Inconsolata, monospace !important; font-family: Inter, sans-serif !important;
font-weight: 700 !important; font-weight: 400 !important;
border-radius: 17px !important;
} }
a { a {
@ -16,23 +17,34 @@ a {
} }
.heading-xl { .heading-xl {
color: $grey-1; font-size: 24px;
font-family: Inter, sans-serf; font-weight: 600;
font-size: 32px; line-height: 29px;
font-weight: bold;
letter-spacing: 0;
line-height: 39px;
} }
.heading-l { .heading-l {
color: #283241; color: #283241;
font-family: Inter, sans-serf; font-family: Inter, sans-serif;
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
letter-spacing: 0; letter-spacing: 0;
line-height: 22px; line-height: 22px;
} }
.subheading {
text-transform: uppercase;
opacity: 0.7;
font-size: 11px;
font-weight: 600;
letter-spacing: 0;
line-height: 14px;
}
.subtitle {
opacity: 0.7;
font-size: 11px;
line-height: 14px;
}
.clamp-1 { .clamp-1 {
@include line-clamp(1); @include line-clamp(1);

View File

@ -7,4 +7,6 @@
@import "red-dialog"; @import "red-dialog";
@import "red-input"; @import "red-input";
@import "red-media-queries"; @import "red-media-queries";
@import "red-tables";
@import "red-components";
@import "red-controls"; @import "red-controls";

View File

@ -9,9 +9,9 @@ $dark: #000;
$grey-1: #283241; $grey-1: #283241;
$grey-2: #ECECEE; $grey-2: #ECECEE;
$grey-3: #aaacb3; $grey-3: #aaacb3;
$grey-4: #E2E4E9;
$blue-1: #4875F7; $blue-1: #4875F7;
$red-1: #F65757; $red-1: #F65757;
$yellow-1: #FFB83B; $yellow-1: #FFB83B;
$green-1: #46CE7D; $green-1: #46CE7D;

View File

@ -2694,7 +2694,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base64-js@1.3.1, base64-js@^1.0.2: base64-js@^1.0.2:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
@ -6782,11 +6782,6 @@ jest@26.2.2:
import-local "^3.0.2" import-local "^3.0.2"
jest-cli "^26.2.2" jest-cli "^26.2.2"
js-sha256@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -6949,21 +6944,6 @@ karma-source-map-support@1.4.0:
dependencies: dependencies:
source-map-support "^0.5.5" source-map-support "^0.5.5"
keycloak-angular@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/keycloak-angular/-/keycloak-angular-8.0.1.tgz#29851e7aded21925faa051c69dfa5872bda6661f"
integrity sha512-q68vcaFiSYNjbzPM1v+6LohMpWUyus9hcQBi2rNz06xOtWuRU4U6t5vQgoim6bDhtkhWpR5+a3SYl0lzUJKyrw==
dependencies:
tslib "^2.0.0"
keycloak-js@10.0.2:
version "10.0.2"
resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-10.0.2.tgz#f0cf5b942627c5221f1466552c40e4624503b77b"
integrity sha512-7nkg4Ob1khHGcNbuK36AMndKUEuIQFpNlWU9ygWs7nSBPCI9VZ8dJjjXfKJHm0ewgcqLFGPIJ6bxxRlfcQ6sLg==
dependencies:
base64-js "1.3.1"
js-sha256 "0.9.0"
killable@^1.0.1: killable@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"