work in progress ui fine tune

This commit is contained in:
Timo Bejan 2020-10-12 13:37:05 +03:00
parent e6db65da9d
commit ffa6dbd969
49 changed files with 957 additions and 316 deletions

View File

@ -1,36 +1,36 @@
{
"/project": {
"target": "http://ingress.redaction-timo-dev-latest.178.63.47.73.xip.io",
"target": "https://timo-redaction-dev.iqser.cloud/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/reanalyse": {
"target": "http://ingress.redaction-timo-dev-latest.178.63.47.73.xip.io",
"target": "https://timo-redaction-dev.iqser.cloud/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/upload": {
"target": "http://ingress.redaction-timo-dev-latest.178.63.47.73.xip.io",
"target": "https://timo-redaction-dev.iqser.cloud/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/download": {
"target": "http://ingress.redaction-timo-dev-latest.178.63.47.73.xip.io",
"target": "https://timo-redaction-dev.iqser.cloud/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/delete": {
"target": "http://ingress.redaction-timo-dev-latest.178.63.47.73.xip.io",
"target": "https://timo-redaction-dev.iqser.cloud/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/status": {
"target": "http://ingress.redaction-timo-dev-latest.178.63.47.73.xip.io",
"target": "https://timo-redaction-dev.iqser.cloud/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"

View File

@ -1 +1,2 @@
<router-outlet></router-outlet>
<redaction-full-page-loading-indicator [displayed]="appLoadStateService.loading | async"></redaction-full-page-loading-indicator>

View File

@ -1,4 +1,5 @@
import {Component} from '@angular/core';
import {AppLoadStateService} from "./utils/app-load-state.service";
@Component({
selector: 'redaction-root',
@ -7,5 +8,8 @@ import {Component} from '@angular/core';
})
export class AppComponent {
constructor(public appLoadStateService: AppLoadStateService){
}
}

View File

@ -1,50 +1,56 @@
import { BrowserModule } from '@angular/platform-browser';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {APP_INITIALIZER, NgModule} from '@angular/core';
import { AppComponent } from './app.component';
import { RouterModule } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http';
import { BaseScreenComponent } from './screens/base-screen/base-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 { MatToolbarModule } from '@angular/material/toolbar';
import { ApiModule } from '@redaction/red-ui-http';
import { ApiPathInterceptorService } from './interceptor/api-path-interceptor.service';
import { MatButtonModule } from '@angular/material/button';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { MatMenuModule } from '@angular/material/menu';
import { languageInitializer } from './i18n/language.initializer';
import { LanguageService } from './i18n/language.service';
import { MatIconModule } from '@angular/material/icon';
import { IconsModule } from './icons/icons.module';
import { AddEditProjectDialogComponent } from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ConfirmationDialogComponent } from './common/confirmation-dialog/confirmation-dialog.component';
import { FilePreviewScreenComponent } from './screens/file/file-preview-screen/file-preview-screen.component';
import { PdfViewerComponent } from './screens/file/pdf-viewer/pdf-viewer.component';
import { MatTabsModule } from '@angular/material/tabs';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { NgpSortModule } from 'ngp-sort-pipe';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { FileDetailsDialogComponent } from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component';
import { ToastrModule } from 'ngx-toastr';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { ProjectDetailsDialogComponent } from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component';
import { AuthModule } from './auth/auth.module';
import { AuthGuard } from './auth/auth.guard';
import { FileUploadModule } from './upload/file-upload.module';
import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component';
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';
import {AppComponent} from './app.component';
import {RouterModule} from '@angular/router';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ReactiveFormsModule} from '@angular/forms';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http';
import {BaseScreenComponent} from './screens/base-screen/base-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 {MatToolbarModule} from '@angular/material/toolbar';
import {ApiModule} from '@redaction/red-ui-http';
import {ApiPathInterceptorService} from './interceptor/api-path-interceptor.service';
import {MatButtonModule} from '@angular/material/button';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {TranslateHttpLoader} from '@ngx-translate/http-loader';
import {MatMenuModule} from '@angular/material/menu';
import {languageInitializer} from './i18n/language.initializer';
import {LanguageService} from './i18n/language.service';
import {MatIconModule} from '@angular/material/icon';
import {IconsModule} from './icons/icons.module';
import {AddEditProjectDialogComponent} from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
import {MatDialogModule} from '@angular/material/dialog';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatTooltipModule} from '@angular/material/tooltip';
import {ConfirmationDialogComponent} from './common/confirmation-dialog/confirmation-dialog.component';
import {FilePreviewScreenComponent} from './screens/file/file-preview-screen/file-preview-screen.component';
import {PdfViewerComponent} from './screens/file/pdf-viewer/pdf-viewer.component';
import {MatTabsModule} from '@angular/material/tabs';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {NgpSortModule} from 'ngp-sort-pipe';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatSelectModule} from '@angular/material/select';
import {MatSidenavModule} from '@angular/material/sidenav';
import {FileDetailsDialogComponent} from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component';
import {ToastrModule} from 'ngx-toastr';
import {ServiceWorkerModule} from '@angular/service-worker';
import {environment} from '../environments/environment';
import {ProjectDetailsDialogComponent} from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component';
import {AuthModule} from './auth/auth.module';
import {AuthGuard} from './auth/auth.guard';
import {FileUploadModule} from './upload/file-upload.module';
import {FullPageLoadingIndicatorComponent} from './utils/full-page-loading-indicator/full-page-loading-indicator.component';
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';
import {LogoComponent} from './logo/logo.component';
import {AuthInterceptorService} from "./interceptor/auth-interceptor.service";
import {CompositeRouteGuard} from "./utils/composite-route.guard";
import {AppStateGuard} from "./state/app-state.guard";
import {ChartsModule} from "ng2-charts";
import { SimpleDoughnutChartComponent } from './simple-doughnut-chart/simple-doughnut-chart.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@ -64,11 +70,14 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
ProjectDetailsDialogComponent,
FullPageLoadingIndicatorComponent,
InitialsAvatarComponent,
StatusBarComponent
StatusBarComponent,
LogoComponent,
SimpleDoughnutChartComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
ChartsModule,
ReactiveFormsModule,
HttpClientModule,
AuthModule,
@ -95,17 +104,26 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
{
path: 'projects',
component: ProjectListingScreenComponent,
canActivate: [AuthGuard]
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
}
},
{
path: 'projects/:projectId',
component: ProjectOverviewScreenComponent,
canActivate: [AuthGuard]
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
}
},
{
path: 'projects/:projectId/file/:fileId',
component: FilePreviewScreenComponent,
canActivate: [AuthGuard]
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
}
}
]
}
@ -125,13 +143,17 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
MatSelectModule,
MatSidenavModule,
FileUploadModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}),
MatProgressSpinnerModule
],
providers: [{
provide: HTTP_INTERCEPTORS,
multi: true,
useClass: ApiPathInterceptorService
}, {
provide: HTTP_INTERCEPTORS,
multi: true,
useClass: AuthInterceptorService
}, {
provide: APP_INITIALIZER,
multi: true,

View File

@ -5,14 +5,19 @@ import {AuthConfig, OAuthService} from "angular-oauth2-oidc";
import {AppConfigKey, AppConfigService} from "../app-config/app-config.service";
import {map} from "rxjs/operators";
import {JwksValidationHandler} from "angular-oauth2-oidc-jwks";
import {UserService} from "../user/user.service";
@Injectable()
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
private _configured = false;
constructor(private readonly _oauthService: OAuthService, private readonly _appConfigService: AppConfigService) {
constructor(private readonly _oauthService: OAuthService,
private readonly _userService: UserService,
private readonly _appConfigService: AppConfigService) {
}
private async _configure() {
@ -32,12 +37,16 @@ export class AuthGuard implements CanActivate {
return this._checkToken();
}
private _checkToken() {
private async _checkToken() {
const expired = this._oauthService.getAccessTokenExpiration() - new Date().getTime() < 0;
if (!this._oauthService.getAccessToken() || expired) {
this._oauthService.initLoginFlow();
return false;
}
if (!this._userService.user) {
await this._userService.loadCurrentUser();
return true;
}
return true;
}

View File

@ -1,4 +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 *ngIf="withName" class="name clamp-2">{{username || ('initials-avatar.unassigned.label' | translate)}}</div>
</div>

View File

@ -1,6 +1,6 @@
@import "../../../assets/styles/red-variables";
* {
.name {
font-size: 13px;
line-height: 16px;
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'redaction-initials-avatar',
@ -10,7 +10,7 @@ export class InitialsAvatarComponent implements OnInit {
public username: string;
@Input()
public color: 'gray' | 'red' = 'gray';
public color: 'red-white' | 'gray-red' | 'gray-dark' = 'gray-dark';
@Input()
public size: 'small' | 'large' = 'small';
@ -18,7 +18,8 @@ export class InitialsAvatarComponent implements OnInit {
@Input()
public withName = false;
constructor() { }
constructor() {
}
ngOnInit(): void {
}

View File

@ -13,6 +13,31 @@ export class IconsModule {
private iconRegistry: MatIconRegistry,
private sanitizer: DomSanitizer
) {
iconRegistry.addSvgIconInNamespace(
'red',
'calendar',
sanitizer.bypassSecurityTrustResourceUrl('/assets/icons/general/calendar.svg')
);
iconRegistry.addSvgIconInNamespace(
'red',
'files',
sanitizer.bypassSecurityTrustResourceUrl('/assets/icons/general/files.svg')
);
iconRegistry.addSvgIconInNamespace(
'red',
'user',
sanitizer.bypassSecurityTrustResourceUrl('/assets/icons/general/user.svg')
);
iconRegistry.addSvgIconInNamespace(
'red',
'stats',
sanitizer.bypassSecurityTrustResourceUrl('/assets/icons/general/stats.svg')
);
iconRegistry.addSvgIconInNamespace(
'red',
'drop-down',
sanitizer.bypassSecurityTrustResourceUrl('/assets/icons/general/drop-down-arrow.svg')
);
iconRegistry.addSvgIconInNamespace(
'red',
'plus',

View File

@ -0,0 +1,24 @@
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {AppConfigService} from "../app-config/app-config.service";
import {catchError} from "rxjs/operators";
import {OAuthService} from "angular-oauth2-oidc";
@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
constructor(private readonly _appConfigService: AppConfigService, private readonly _oauthService: OAuthService) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(catchError((err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
this._oauthService.initLoginFlow();
}
}
return of(err);
}));
}
}

View File

@ -0,0 +1,4 @@
<div class="redacto-logo">
<div class="line-1"></div>
<div class="line-2"></div>
</div>

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'redaction-logo',
templateUrl: './logo.component.html',
styleUrls: ['./logo.component.scss']
})
export class LogoComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -9,33 +9,43 @@
translate="top-bar.navigation-items.projects.label">
</button>
<button *ngIf="appStateService.activeProject"
[routerLink]="'/ui/projects/'+appStateService.activeProject.projectId"
mat-menu-item>{{appStateService.activeProject.projectName}}</button>
[routerLink]="'/ui/projects/'+appStateService.activeProject.project.projectId"
mat-menu-item>{{appStateService.activeProject.project.projectName}}</button>
<button *ngIf="appStateService.activeFile"
[routerLink]="'/ui/projects/'+appStateService.activeProject.projectId+'/file/'+appStateService.activeFile.fileId"
[routerLink]="'/ui/projects/'+appStateService.activeProject.project.projectId+'/file/'+appStateService.activeFile.fileId"
mat-menu-item>{{appStateService.activeFile.filename}}</button>
</mat-menu>
</div>
<div class="menu left visible-lg">
<a class="breadcrumb" routerLink="/ui/projects"
translate="top-bar.navigation-items.projects.label"></a>
translate="top-bar.navigation-items.projects.label"></a>
<div *ngIf="appStateService.activeProject" class="breadcrumb">
<mat-icon svgIcon="red:chevron-right"></mat-icon>
</div>
<a *ngIf="appStateService.activeProject" class="breadcrumb"
[routerLink]="'/ui/projects/'+appStateService.activeProject.projectId">
{{appStateService.activeProject.projectName}}
[routerLink]="'/ui/projects/'+appStateService.activeProject.project.projectId">
{{appStateService.activeProject.project.projectName}}
</a>
<div *ngIf="appStateService.activeFile" class="breadcrumb">
<mat-icon svgIcon="red:chevron-right"></mat-icon>
</div>
<a *ngIf="appStateService.activeFile" class="breadcrumb"
[routerLink]="'/ui/projects/'+appStateService.activeProject.projectId+'/file/'+appStateService.activeFile.fileId">
[routerLink]="'/ui/projects/'+appStateService.activeProject.project.projectId+'/file/'+appStateService.activeFile.fileId">
{{appStateService.activeFile.filename}}
</a>
</div>
<div class="center">
<redaction-logo></redaction-logo>
<div class="app-name" translate="app-name.label"></div>
</div>
<div class="menu right">
<button [matMenuTriggerFor]="menu" mat-button translate="top-bar.navigation-items.my-account.label"></button>
<button [matMenuTriggerFor]="menu" mat-button>
<div class="account-button-wrapper">
<redaction-initials-avatar color="red-white" size="small" [username]="user?.name"></redaction-initials-avatar>
<span>{{user?.name}}</span>
<mat-icon svgIcon="red:drop-down"></mat-icon>
</div>
</button>
<mat-menu #menu="matMenu">
<button [matMenuTriggerFor]="language" mat-menu-item

View File

@ -1 +1,17 @@
@import "../../../assets/styles/red-variables";
.account-button-wrapper {
display: flex;
justify-content: center;
align-items: center;
redaction-initials-avatar{
margin-right: 6px;
}
mat-icon {
width: 10px;
margin-left: 6px;
}
}

View File

@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core';
import {Component} from '@angular/core';
import {UserService} from "../../user/user.service";
import {AppStateService} from "../../state/app-state.service";
import {LanguageService} from "../../i18n/language.service";
@ -8,17 +8,17 @@ import {LanguageService} from "../../i18n/language.service";
templateUrl: './base-screen.component.html',
styleUrls: ['./base-screen.component.scss']
})
export class BaseScreenComponent implements OnInit {
export class BaseScreenComponent {
constructor(
public readonly appStateService: AppStateService,
private readonly _languageService: LanguageService,
private readonly _userService: UserService) {
private readonly _userService: UserService) {
}
ngOnInit(): void {
get user() {
return this._userService.user;
}
logout() {

View File

@ -37,8 +37,7 @@ export class FilePreviewScreenComponent implements OnInit {
this._activatedRoute.params.subscribe(params => {
this.projectId = params.projectId;
this.fileId = params.fileId;
this.appStateService.activateFile(this.projectId, this.fileId).subscribe(() => {
});
this.appStateService.activateFile(this.projectId, this.fileId)
});
}

View File

@ -1,9 +1,8 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {Project, ProjectControllerService} from "@redaction/red-ui-http";
import {Project} from "@redaction/red-ui-http";
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {NotificationService, NotificationType} from "../../../notification/notification.service";
import {TranslateService} from "@ngx-translate/core";
import {AppStateService} from "../../../state/app-state.service";
@Component({
selector: 'redaction-add-edit-project-dialog',
@ -15,9 +14,7 @@ export class AddEditProjectDialogComponent implements OnInit {
projectForm: FormGroup;
constructor(
private readonly _projectControllerService: ProjectControllerService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<AddEditProjectDialogComponent>,
@Inject(MAT_DIALOG_DATA) public project: Project) {
@ -31,21 +28,11 @@ export class AddEditProjectDialogComponent implements OnInit {
}
saveProject() {
async saveProject() {
const project: Project = this._formToObject();
if (this.project?.projectId) {
this._projectControllerService.updateProject(project, this.project.projectId).subscribe(() => {
this.dialogRef.close();
}, () => {
this._notificationService.showToastNotification(this._translateService.instant('projects.add-edit-dialog.errors.save'), null, NotificationType.ERROR);
})
} else {
this._projectControllerService.createProject(project).subscribe(() => {
this.dialogRef.close();
}, () => {
this._notificationService.showToastNotification(this._translateService.instant('projects.add-edit-dialog.errors.save'), null, NotificationType.ERROR);
})
}
project.projectId = this.project?.projectId;
await this._appStateService.addOrUpdateProject(project);
this.dialogRef.close();
}
private _formToObject(): Project {

View File

@ -1,69 +1,81 @@
<section *ngIf="viewReady">
<div class="page-header">
<div class="filters flex-row">
<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 class="page-header">
<div class="filters flex-row">
<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 class="add-project-btn">
<mat-icon svgIcon="red:plus">
</mat-icon>
<span translate="projects.add-new.label"></span>
</button>
</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="flex red-content-inner">
<div class="left-container">
<div class="table-header">
<div class="flex red-content-inner">
<div class="left-container">
<div class="table-header">
<span class="subheading">
{{'projects.table-header.title.label'| translate:{ length: appStateService.allProjects?.length || 0 } }}
{{'projects.table-header.title.label'| translate:{length: appStateService.allProjects?.length || 0} }}
</span>
<div class="actions">
<div translate="projects.table-header.bulk-select.label"></div>
<div translate="projects.table-header.recent.label"></div>
</div>
<div class="actions">
<div translate="projects.table-header.bulk-select.label"></div>
<div translate="projects.table-header.recent.label"></div>
</div>
</div>
<div class="table-content">
<div *ngFor="let project of appStateService.allProjects"
[routerLink]="'/ui/projects/'+project.projectId"
class="table-item"
>
<div class="flex-2">
<div class="table-item-title table-item-title--large">
{{project.projectName}}
</div>
<div class="subtitle stats-subtitle">
<div>12</div>
<div>9</div>
<div>25 Dec. 2020</div>
</div>
<div class="table-content">
<div *ngFor="let pw of appStateService.allProjects | sortBy:'desc':'projectDate'"
[routerLink]="'/ui/projects/'+pw.project.projectId"
class="table-item"
>
<div class="flex-2">
<div class="table-item-title table-item-title--large">
{{pw.project.projectName}}
</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="subtitle stats-subtitle">
<div><mat-icon svgIcon="red:files"></mat-icon>{{documentCount(pw)}}</div>
<div><mat-icon svgIcon="red:user"></mat-icon>{{userCount(pw)}}</div>
<div><mat-icon svgIcon="red:calendar"></mat-icon>{{pw.project.date | date:'mediumDate'}}</div>
</div>
</div>
<div class="flex-1">
<redaction-initials-avatar [username]="user.name"
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 class="on-hover-wrapper">
<div class="on-hover">
<div (click)="deleteProject($event,pw.project)">
<mat-icon svgIcon="red:delete"></mat-icon>
</div>
<div (click)="editProject($event,pw.project)">
<mat-icon svgIcon="red:edit"></mat-icon>
</div>
<div (click)="showDetailsDialog($event,pw)">
<mat-icon svgIcon="red:stats"></mat-icon>
</div>
</div>
</div>
</div>
</div>
<div class="right-fixed-container"></div>
</div>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>
<div class="right-fixed-container">
<!-- <redaction-simple-doughnut-chart [initialValues]="[120,26]">-->
<!-- </redaction-simple-doughnut-chart>-->
</div>
</div>

View File

@ -2,8 +2,22 @@
.stats-subtitle {
margin-top: 6px;
mat-icon {
width: 12px;
height: 12px;
margin-right: 4px;
}
}
.stats-bar {
width: 160px;
}
.add-project-btn {
mat-icon {
width: 14px;
margin-right: 10px;
color: $white;
}
}

View File

@ -4,8 +4,10 @@ import {MatDialog} from "@angular/material/dialog";
import {AddEditProjectDialogComponent} from "./add-edit-project-dialog/add-edit-project-dialog.component";
import {ConfirmationDialogComponent} from "../../common/confirmation-dialog/confirmation-dialog.component";
import {TranslateService} from "@ngx-translate/core";
import {NotificationService, NotificationType} from "../../notification/notification.service";
import {AppStateService} from "../../state/app-state.service";
import {NotificationService} from "../../notification/notification.service";
import {AppStateService, ProjectWrapper} from "../../state/app-state.service";
import {UserService} from "../../user/user.service";
import {ProjectDetailsDialogComponent} from "../project-overview-screen/project-details-dialog/project-details-dialog.component";
@Component({
selector: 'redaction-project-listing-screen',
@ -14,30 +16,29 @@ import {AppStateService} from "../../state/app-state.service";
})
export class ProjectListingScreenComponent implements OnInit {
viewReady = false;
constructor(
public readonly appStateService: AppStateService,
private readonly _userService: UserService,
private readonly _projectControllerService: ProjectControllerService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _dialog: MatDialog) {
}
get user() {
return this._userService.user;
}
ngOnInit(): void {
this._reloadProjects(true);
this.appStateService.reset();
}
openAddProjectDialog(project?: Project): void {
const dialogRef = this._dialog.open(AddEditProjectDialogComponent, {
this._dialog.open(AddEditProjectDialogComponent, {
width: '400px',
maxWidth: '90vw',
data: project
});
dialogRef.afterClosed().subscribe(result => {
this._reloadProjects();
});
}
editProject($event: MouseEvent, project: Project) {
@ -54,21 +55,26 @@ export class ProjectListingScreenComponent implements OnInit {
dialogRef.afterClosed().subscribe(result => {
if (result) {
this._projectControllerService.deleteProject(project.projectId).subscribe(() => {
this._reloadProjects();
}, () => {
this._notificationService.showToastNotification(this._translateService.instant('projects.delete.delete-failed.label', project), null, NotificationType.ERROR)
});
this.appStateService.deleteProject(project);
}
});
}
private _reloadProjects(initial: boolean = false) {
this.appStateService.reset();
this.appStateService.loadAllProjects().subscribe(() => {
if (initial) {
this.viewReady = true;
}
showDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
$event.stopPropagation();
this._dialog.open(ProjectDetailsDialogComponent, {
width: '600px',
maxWidth: '90vw',
data: project
});
}
documentCount(project: ProjectWrapper) {
return project.files.length;
}
userCount(project: ProjectWrapper) {
return 1;
}
}

View File

@ -1,12 +1,8 @@
import {Component, Inject, OnInit} from '@angular/core';
import {FileStatus, FileUploadControllerService, Project, ReanalysisControllerService} from "@redaction/red-ui-http";
import {FileUploadControllerService, ReanalysisControllerService} from "@redaction/red-ui-http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {download} from "../../../utils/file-download-utils";
export interface ProjectDetails {
project: Project;
files: FileStatus[];
}
import {ProjectWrapper} from "../../../state/app-state.service";
@Component({
selector: 'redaction-project-details-dialog',
@ -19,7 +15,7 @@ export class ProjectDetailsDialogComponent implements OnInit {
private readonly _fileUploadControllerService: FileUploadControllerService,
private readonly _reanalysisControllerService: ReanalysisControllerService,
public dialogRef: MatDialogRef<ProjectDetailsDialogComponent>,
@Inject(MAT_DIALOG_DATA) public projectDetails: ProjectDetails) {
@Inject(MAT_DIALOG_DATA) public projectDetails: ProjectWrapper) {
}
ngOnInit(): void {

View File

@ -1,4 +1,3 @@
<section *ngIf="viewReady">
<div *ngIf="!appStateService.activeProject"
[innerHTML]="'project-overview.no-project.label' | translate:{projectId: projectId}"
class="heading-l"></div>
@ -22,7 +21,7 @@
<div class="left-container">
<div class="table-header">
<span class="subheading">
{{'project-overview.table-header.title.label'| translate:{ length: appStateService.projectFiles?.length || 0 } }}
{{'project-overview.table-header.title.label'| translate:{ length: appStateService.activeProject?.files.length || 0 } }}
</span>
<div class="actions">
<div translate="project-overview.table-header.bulk-select.label"></div>
@ -43,7 +42,7 @@
</div>
<div class="table-item"
*ngFor="let fileStatus of appStateService.projectFiles | sortBy: sorting.order:sorting.name; trackBy:fileId"
*ngFor="let fileStatus of appStateService.activeProject.files | 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 }}
@ -83,23 +82,29 @@
<div class="project-details-container right-fixed-container">
<div class="actions-row">
<div>Edit</div>
<div>Delete</div>
<div>View</div>
<div (click)="deleteProject($event)">
<mat-icon svgIcon="red:delete"></mat-icon>
</div>
<div (click)="editProject($event)">
<mat-icon svgIcon="red:edit"></mat-icon>
</div>
<div (click)="showDetailsDialog($event)">
<mat-icon svgIcon="red:stats"></mat-icon>
</div>
</div>
<div class="subtitle stats-subtitle mt-20">
<div>
{{ appStateService.projectFiles.length }}
{{ appStateService.activeProject.files.length }}
</div>
<div>9</div>
<div>
{{ appStateService.activeProject.date | date:'d MMM. yyyy' }}
{{ appStateService.activeProject.project.date | date:'d MMM. yyyy' }}
</div>
</div>
<div class="heading-xl mt-20">
{{ appStateService.activeProject.projectName }}
{{ appStateService.activeProject.project.projectName }}
</div>
<div class="owner flex-row mt-20">
@ -110,7 +115,7 @@
</div>
<div class="description mt-20">
{{ appStateService.activeProject.description }}
{{ appStateService.activeProject.project.description }}
</div>
<div class="project-team mt-20">
@ -129,6 +134,3 @@
</div>
</div>
</div>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {
FileStatus,
FileUploadControllerService,
@ -7,13 +7,17 @@ import {
ReanalysisControllerService,
StatusControllerService
} from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '../../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmationDialogComponent } from '../../common/confirmation-dialog/confirmation-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { AppStateService } from '../../state/app-state.service';
import { ProjectDetailsDialogComponent } from './project-details-dialog/project-details-dialog.component';
import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service';
import {NotificationService, NotificationType} from '../../notification/notification.service';
import {TranslateService} from '@ngx-translate/core';
import {ConfirmationDialogComponent} from '../../common/confirmation-dialog/confirmation-dialog.component';
import {MatDialog} from '@angular/material/dialog';
import {AppStateService} from '../../state/app-state.service';
import {ProjectDetailsDialogComponent} from './project-details-dialog/project-details-dialog.component';
import {FileDropOverlayService} from '../../upload/file-drop/service/file-drop-overlay.service';
import {FileUploadModel} from "../../upload/model/file-upload.model";
import {FileUploadService} from "../../upload/file-upload.service";
import {UploadStatusOverlayService} from "../../upload/upload-status-dialog/service/upload-status-overlay.service";
import {AddEditProjectDialogComponent} from "../project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component";
@Component({
@ -23,26 +27,25 @@ import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop
})
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
viewReady = false;
@ViewChild('dropzoneComponent', { static: true }) dropZoneComponent;
@ViewChild('dropzoneComponent', {static: true}) dropZoneComponent;
dragActive = false;
sortOptions: any[] = [{
value: { name: 'lastUpdated', order: 'desc' },
value: {name: 'lastUpdated', order: 'desc'},
label: 'project-overview.sorting.last-updated-desc.label',
icon: 'red:sort-desc'
}, {
value: { name: 'lastUpdated', order: 'asc' },
value: {name: 'lastUpdated', order: 'asc'},
label: 'project-overview.sorting.last-updated-asc.label',
icon: 'red:sort-asc'
}, {
value: { name: 'filename', order: 'desc' },
value: {name: 'filename', order: 'desc'},
label: 'project-overview.sorting.file-name-desc.label',
icon: 'red:sort-desc'
}, {
value: { name: 'filename', order: 'asc' },
value: {name: 'filename', order: 'asc'},
label: 'project-overview.sorting.file-name-asc.label',
icon: 'red:sort-asc'
}];
@ -57,6 +60,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _dialog: MatDialog,
private readonly _fileUploadService: FileUploadService,
private _uploadStatusOverlayService: UploadStatusOverlayService,
private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _router: Router,
private readonly _fileDropOverlayService: FileDropOverlayService,
@ -64,7 +69,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private readonly _projectControllerService: ProjectControllerService) {
this._activatedRoute.params.subscribe(params => {
this.projectId = params.projectId;
this._loadProject(true);
this.appStateService.activateProject(this.projectId);
});
}
@ -101,16 +106,10 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
showDetailsDialog($event: MouseEvent) {
$event.stopPropagation();
const dialogRef = this._dialog.open(ProjectDetailsDialogComponent, {
this._dialog.open(ProjectDetailsDialogComponent, {
width: '600px',
maxWidth: '90vw',
data: {
project: this.appStateService.activeProject,
files: this.appStateService.projectFiles
}
});
dialogRef.afterClosed().subscribe(result => {
this._getFileStatus();
data: this.appStateService.activeProject
});
}
@ -124,29 +123,61 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
reanalyseFile($event: MouseEvent, fileStatus: FileStatus) {
$event.stopPropagation();
this._reanalysisControllerService.reanalyseFile(this.appStateService.activeProject.projectId, fileStatus.fileId).subscribe(() => {
this._reanalysisControllerService.reanalyseFile(this.appStateService.activeProject.project.projectId, fileStatus.fileId).subscribe(() => {
this._getFileStatus();
});
}
private _loadProject(initial: boolean = false) {
this.appStateService.activateProject(this.projectId).subscribe(() => {
if (initial) {
this.viewReady = true;
}
});
}
private _getFileStatus() {
this.appStateService.getActiveProjectStatus(true).subscribe(() => {
});
this.appStateService.reloadActiveProjectFiles();
}
fileId(index, item) {
return item.fileId;
}
uploadFiles(files: any) {
uploadFiles(files: FileList | File[]) {
const uploadFiles: FileUploadModel[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
uploadFiles.push({
file: file,
progress: 0,
completed: false,
error: null
})
}
this._fileUploadService.uploadFiles(uploadFiles);
this._uploadStatusOverlayService.openStatusOverlay();
}
editProject($event: MouseEvent) {
$event.stopPropagation();
this._dialog.open(AddEditProjectDialogComponent, {
width: '400px',
maxWidth: '90vw',
data: this.appStateService.activeProject.project
});
}
deleteProject($event: MouseEvent) {
$event.stopPropagation();
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw',
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.appStateService.deleteProject(this.appStateService.activeProject.project);
this.ngOnDestroy();
this._router.navigate(['/ui/projects']);
console.log('navigate?');
}
});
}
}

View File

@ -0,0 +1,14 @@
<svg height="160" width="160" viewBox="0 0 160 160" class="donut-chart">
<g *ngFor="let val of chartData; let i = index">
<circle attr.cx="{{cx}}"
attr.cy="{{cy}}"
attr.r="{{radius}}"
attr.stroke="{{colors[i]}}"
attr.stroke-width="{{strokeWidth}}"
attr.stroke-dasharray="{{adjustedCircumference}}"
attr.stroke-dashoffset="calculateStrokeDashOffset(value, circumference)"
fill="transparent"
attr.transform="{{returnCircleTransformValue(i)}}}"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@ -0,0 +1,108 @@
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'redaction-simple-doughnut-chart',
templateUrl: './simple-doughnut-chart.component.html',
styleUrls: ['./simple-doughnut-chart.component.scss']
})
export class SimpleDoughnutChartComponent implements OnInit {
@Input() initialValues: [];
@Input() angleOffset: number = -90;
@Input() colors: string[] = ["#6495ED", "goldenrod", "#cd5c5c", "thistle", "lightgray"];
@Input() cx: number = 80;
@Input() cy: number = 80;
@Input() radius: number = 60;
@Input() strokeWidth: 30;
chartData: any[] = [];
sortedValues: number[] = [];
perimeter: number;
data: any[] = [];
constructor() {
}
ngOnInit() {
this.sortInitialValues();
this.calculateChartData();
}
get adjustedCircumference() {
return this.circumference - 2
};
get circumference() {
return 2 * Math.PI * this.radius
};
get dataTotal() {
return this.sortedValues.reduce((acc, val) => acc + val)
};
calculateChartData() {
this.sortedValues.forEach((dataVal, index) => {
const {x, y} = this.calculateTextCoords(dataVal, this.angleOffset)
// start at -90deg so that the largest segment is perpendicular to top
const data = {
degrees: this.angleOffset,
textX: x,
textY: y
}
this.chartData.push(data)
this.angleOffset = this.dataPercentage(dataVal) * 360 + this.angleOffset
})
}
calculateStrokeDashOffset(dataVal, circumference) {
const strokeDiff = this.dataPercentage(dataVal) * circumference
return circumference - strokeDiff
}
calculateTextCoords(dataVal, angleOffset) {
// t must be radians
// x(t) = r cos(t) + j
// y(t) = r sin(t) + j
const angle = (this.dataPercentage(dataVal) * 360) / 2 + angleOffset
const radians = this.degreesToRadians(angle)
const textCoords = {
x: this.radius * Math.cos(radians) + this.cx,
y: this.radius * Math.sin(radians) + this.cy
}
return textCoords
}
degreesToRadians(angle) {
return angle * (Math.PI / 180)
}
dataPercentage(dataVal) {
return dataVal / this.dataTotal
}
percentageString(dataVal) {
return `${Math.round(this.dataPercentage(dataVal) * 100)}%`
}
returnCircleTransformValue(index) {
return `rotate(${this.chartData[index].degrees}, ${this.cx}, ${this.cy})`
}
segmentBigEnough(dataVal) {
return Math.round(this.dataPercentage(dataVal) * 100) > 5
}
sortInitialValues() {
return this.sortedValues = this.initialValues.sort((a, b) => b - a)
}
}

View File

@ -0,0 +1,21 @@
import {Injectable} from "@angular/core";
import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree} from "@angular/router";
import {Observable} from "rxjs";
import {AppStateService} from "./app-state.service";
@Injectable({
providedIn: 'root'
})
export class AppStateGuard implements CanActivate {
constructor(private readonly _appStateService: AppStateService) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this._appStateService.loadAllProjects().then(t => {
return true;
})
}
}

View File

@ -1,72 +1,147 @@
import {Injectable} from "@angular/core";
import {FileStatus, Project, ProjectControllerService, StatusControllerService} from "@redaction/red-ui-http";
import {map, mergeMap, tap} from "rxjs/operators";
import {NotificationService, NotificationType} from "../notification/notification.service";
import {TranslateService} from "@ngx-translate/core";
import {Router} from "@angular/router";
export interface AppState {
projects: ProjectWrapper[];
activeProject: ProjectWrapper;
activeFile: FileStatus;
totalAnalysedPages?: number;
totalDocuments?: number;
}
export class ProjectWrapper {
constructor(public project: Project, public files: FileStatus[]) {
}
get projectDate() {
return this.project.date;
}
}
@Injectable({
providedIn: 'root'
})
export class AppStateService {
private _projects: Project[];
private _activeProjectFiles: FileStatus[];
private _activeProject: Project;
private _activeFile: FileStatus;
private _appState: AppState;
constructor(private readonly _projectControllerService: ProjectControllerService,
private readonly _statusControllerService: StatusControllerService) {
constructor(
private readonly _router: Router,
private readonly _projectControllerService: ProjectControllerService,
private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService,
private readonly _statusControllerService: StatusControllerService) {
this._appState = {
projects: [],
activeProject: null,
activeFile: null
}
}
get activeProject(): ProjectWrapper {
return this._appState.activeProject;
}
get activeProject(): Project {
return this._activeProject;
get allProjects(): ProjectWrapper[] {
return this._appState.projects;
}
get activeFile(): FileStatus {
return this._activeFile;
return this._appState.activeFile;
}
get allProjects(): Project[] {
return this._projects;
async loadAllProjects() {
const projects = await this._projectControllerService.getProjects().toPromise();
this._appState.projects = projects.map(p => {
return new ProjectWrapper(p, []);
});
for (let project of projects) {
await this.getFiles(project.projectId);
}
this._computeStats();
}
get projectFiles(): FileStatus[] {
return this._activeProjectFiles;
async getFiles(projectId: string) {
const files = await this._statusControllerService.getProjectStatus(projectId).toPromise();
const project = this._appState.projects.find(p => p.project.projectId === projectId);
project.files = files;
this._computeStats();
return files;
}
loadAllProjects() {
return this._projectControllerService.getProjects().pipe(tap((projects: Project[]) => {
this._projects = projects;
}));
}
activateProject(projectId: string) {
this._activeProjectFiles = null;
this._activeFile = null;
return this._projectControllerService.getProject(projectId).pipe(tap(project => {
this._activeProject = project;
})).pipe(mergeMap(() => {
return this.getActiveProjectStatus();
}));
}
getActiveProjectStatus(update: boolean = false) {
return this._statusControllerService.getProjectStatus(this._activeProject.projectId).pipe(tap((files: FileStatus[]) => {
this._activeProjectFiles = files;
}))
this._appState.activeFile = null;
this._appState.activeProject = this._appState.projects.find(p => p.project.projectId === projectId);
if (!this._appState.activeProject) {
this._router.navigate(['/ui/projects']);
}
return this._appState.activeProject;
}
activateFile(projectId: string, fileId: string) {
return this.activateProject(projectId).pipe(map((files: FileStatus[]) => {
const file = files.find(f => f.fileId === fileId);
this._activeFile = file;
return file;
}));
this._appState.activeFile = null;
this._appState.activeProject = this._appState.projects.find(p => p.project.projectId === projectId);
this._appState.activeFile = this._appState.activeProject.files.find(f => f.fileId === fileId);
}
reset() {
this._activeProject = null;
this._activeProjectFiles = null;
this._activeFile = null;
this._appState.activeFile = null;
this._appState.activeProject = null;
}
deleteProject(project: Project) {
this._projectControllerService.deleteProject(project.projectId).subscribe(() => {
const index = this._appState.projects.findIndex(p => p.project.projectId === project.projectId);
this._appState.projects.splice(index, 1);
this._appState.projects = [...this._appState.projects];
}, () => {
this._notificationService.showToastNotification(this._translateService.instant('projects.delete.delete-failed.label', project), null, NotificationType.ERROR)
});
}
async addOrUpdateProject(project: Project) {
try {
let updatedProject;
if (project?.projectId) {
updatedProject = await this._projectControllerService.updateProject(project, project.projectId).toPromise();
} else {
updatedProject = await this._projectControllerService.createProject(project).toPromise();
}
const foundProject = this._appState.projects.find(p => p.project.projectId === updatedProject.projectId);
if (foundProject) {
Object.assign(foundProject.project, updatedProject);
} else {
this._appState.projects.push(new ProjectWrapper(updatedProject, []));
}
this._appState.projects = [...this._appState.projects];
} catch (error) {
this._notificationService.showToastNotification(this._translateService.instant('projects.add-edit-dialog.errors.save'), null, NotificationType.ERROR);
}
}
private _computeStats() {
let totalAnalysedPages = 0;
let totalDocuments = 0;
this._appState.projects.forEach(p => {
totalDocuments += p.files.length;
p.files.forEach(f => {
totalAnalysedPages += f.numberOfPages;
})
})
this._appState.totalAnalysedPages = totalAnalysedPages;
this._appState.totalDocuments = totalDocuments;
}
async reloadActiveProjectFiles() {
await this.getFiles(this._appState.activeProject.project.projectId);
}
}

View File

@ -21,7 +21,7 @@ export class FileUploadService {
uploadFiles(files: FileUploadModel[]) {
this.files.push(...files);
files.forEach(newFile => {
this._fileUploadControllerService.uploadFileForm(newFile.file, this._appStateService.activeProject.projectId, 'events', true).subscribe((event) => {
this._fileUploadControllerService.uploadFileForm(newFile.file, this._appStateService.activeProject.project.projectId, 'events', true).subscribe((event) => {
if (event.type === HttpEventType.UploadProgress) {
newFile.progress = Math.round((event.loaded / (event.total || event.loaded) * 100));
}
@ -42,6 +42,6 @@ export class FileUploadService {
}
stopAllUploads() {
this.files = [];
}
}

View File

@ -1,11 +1,13 @@
import {Injectable} from "@angular/core";
import {OAuthService} from "angular-oauth2-oidc";
import {OAuthService, UserInfo} from "angular-oauth2-oidc";
@Injectable({
providedIn: 'root'
})
export class UserService {
private _currentUser: UserInfo;
constructor(private _oauthService: OAuthService) {
}
@ -13,4 +15,13 @@ export class UserService {
this._oauthService.logOut();
}
async loadCurrentUser() {
this._currentUser = await this._oauthService.loadUserProfile();
}
get user() {
return this._currentUser;
}
}

View File

@ -0,0 +1,17 @@
import { EventEmitter, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AppLoadStateService {
private _loadingEvent = new EventEmitter();
get loading(): Observable<boolean> {
return this._loadingEvent;
}
pushLoadingEvent(event: boolean) {
this._loadingEvent.next(event);
}
}

View File

@ -0,0 +1,49 @@
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Injectable, Injector } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, mergeMap, tap } from 'rxjs/operators';
import {AppLoadStateService} from "./app-load-state.service";
@Injectable({
providedIn: 'root'
})
export class CompositeRouteGuard implements CanActivate {
constructor(protected router: Router, protected injector: Injector, private appLoadStateService: AppLoadStateService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
this.appLoadStateService.pushLoadingEvent(true);
let compositeCanActivateObservable: Observable<boolean> = of(true);
const routeGuards = route.data.routeGuards;
if (routeGuards) {
for (let i = 0; i < routeGuards.length; i++) {
const routeGuard = this.injector.get(routeGuards[i]);
const canActivateObservable: Observable<boolean> = routeGuard.canActivate(route, state);
compositeCanActivateObservable = compositeCanActivateObservable.pipe(
mergeMap(bool => {
if (!bool) {
return of(false);
} else {
return canActivateObservable;
}
})
);
}
}
compositeCanActivateObservable = compositeCanActivateObservable.pipe(
tap(() => {
this.appLoadStateService.pushLoadingEvent(false);
}),
catchError(() => {
this.appLoadStateService.pushLoadingEvent(false);
return of(false);
})
);
return compositeCanActivateObservable;
}
}

View File

@ -1,4 +1,7 @@
{
"app-name": {
"label": "Redacto"
},
"common": {
"confirmation-dialog": {
"title": {

View File

@ -1,4 +1,7 @@
{
"app-name": {
"label": "Redacto"
},
"upload-status": {
"dialog": {
"title": {

View File

@ -0,0 +1,31 @@
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 309.49 309.49" style="enable-background:new 0 0 309.49 309.49;" xml:space="preserve">
<g>
<path d="M5.504,303.826h298.484c3.039,0,5.502-2.464,5.502-5.503V31.43c0-3.039-2.463-5.502-5.502-5.502h-60.576V11.166
c0-3.039-2.465-5.502-5.504-5.502c-3.039,0-5.502,2.463-5.502,5.502v14.762h-44.436V11.166c0-3.039-2.465-5.502-5.504-5.502
c-3.039,0-5.502,2.463-5.502,5.502v14.762h-44.436V11.166c0-3.039-2.463-5.502-5.504-5.502c-3.039,0-5.502,2.463-5.502,5.502
v14.762H77.084V11.166c0-3.039-2.463-5.502-5.502-5.502c-3.039,0-5.504,2.463-5.504,5.502v14.762H5.504
C2.465,25.928,0,28.391,0,31.43v266.894C0,301.362,2.465,303.826,5.504,303.826z M11.006,36.934h55.072v22.76
c0,3.039,2.465,5.503,5.504,5.503c3.039,0,5.502-2.464,5.502-5.503v-22.76h44.439v22.76c0,3.039,2.463,5.503,5.502,5.503
c3.041,0,5.504-2.464,5.504-5.503v-22.76h44.436v22.76c0,3.039,2.463,5.503,5.502,5.503c3.039,0,5.504-2.464,5.504-5.503v-22.76
h44.436v22.76c0,3.039,2.463,5.503,5.502,5.503c3.039,0,5.504-2.464,5.504-5.503v-22.76h55.072V292.82H11.006V36.934z"/>
<path d="M271.705,104.438H37.789c-3.039,0-5.502,2.463-5.502,5.502v163.799c0,3.039,2.463,5.503,5.502,5.503h233.916
c3.039,0,5.502-2.464,5.502-5.503V109.939C277.207,106.9,274.744,104.438,271.705,104.438z M266.201,268.235H43.293V115.443
h222.908V268.235z"/>
<path d="M56.451,225.957c-3.039,0-5.504,2.463-5.504,5.502c0,3.039,2.465,5.504,5.504,5.504h31.711v17.695
c0,3.039,2.465,5.502,5.504,5.502c3.039,0,5.502-2.463,5.502-5.502v-17.695h29.713v17.695c0,3.039,2.465,5.502,5.504,5.502
c3.039,0,5.502-2.463,5.502-5.502v-17.695h29.715v17.695c0,3.039,2.465,5.502,5.504,5.502c3.039,0,5.502-2.463,5.502-5.502v-17.695
h29.719v17.695c0,3.039,2.463,5.502,5.502,5.502c3.039,0,5.504-2.463,5.504-5.502v-17.695h31.707c3.039,0,5.504-2.465,5.504-5.504
c0-3.039-2.465-5.502-5.504-5.502h-31.707v-28.615h31.707c3.039,0,5.504-2.463,5.504-5.502c0-3.039-2.465-5.504-5.504-5.504
h-31.707v-28.614h31.707c3.039,0,5.504-2.464,5.504-5.503c0-3.039-2.465-5.503-5.504-5.503h-31.707v-17.691
c0-3.039-2.465-5.503-5.504-5.503c-3.039,0-5.502,2.464-5.502,5.503v17.691h-29.719v-17.691c0-3.039-2.463-5.503-5.502-5.503
c-3.039,0-5.504,2.464-5.504,5.503v17.691h-29.715v-17.691c0-3.039-2.463-5.503-5.502-5.503c-3.039,0-5.504,2.464-5.504,5.503
v17.691H99.168v-17.691c0-3.039-2.463-5.503-5.502-5.503c-3.039,0-5.504,2.464-5.504,5.503v17.691H56.451
c-3.039,0-5.504,2.464-5.504,5.503c0,3.039,2.465,5.503,5.504,5.503h31.711v28.614H56.451c-3.039,0-5.504,2.465-5.504,5.504
c0,3.039,2.465,5.502,5.504,5.502h31.711v28.615H56.451z M139.887,225.957v-28.615h29.715v28.615H139.887z M210.326,225.957
h-29.719v-28.615h29.719V225.957z M210.326,157.722v28.614h-29.719v-28.614H210.326z M169.602,157.722v28.614h-29.715v-28.614
H169.602z M99.168,157.722h29.713v28.614H99.168V157.722z M99.168,197.342h29.713v28.615H99.168V197.342z"/>
<path d="M71.582,90.321h166.326c3.039,0,5.504-2.464,5.504-5.503c0-3.039-2.465-5.503-5.504-5.503H71.582
c-3.039,0-5.504,2.464-5.504,5.503C66.078,87.857,68.543,90.321,71.582,90.321z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,9 @@
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="255px" height="255px" viewBox="0 0 255 255" style="enable-background:new 0 0 255 255;" xml:space="preserve">
<g>
<g id="arrow-drop-down">
<polygon points="0,63.75 127.5,191.25 255,63.75 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1,13 @@
<svg id="Layer_1" enable-background="new 0 0 510 510" height="512" viewBox="0 0 510 510" width="512"
xmlns="http://www.w3.org/2000/svg">
<g>
<path d="m120 255h270v30h-270z"/>
<path d="m180 180h210v30h-210z"/>
<path
d="m360 44.906v-44.906h-210v44.906h-90v465.094h390v-465.094zm-180-14.906h150v60h-150zm240 450h-330v-405.094h60v45.094h210v-45.094h60z"/>
<path d="m120.469 180h29.063v30h-29.063z"/>
<path d="m120 405h270v30h-270z"/>
<path d="m180 330h210v30h-210z"/>
<path d="m120.469 330h29.063v30h-29.063z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 563 B

View File

@ -10,35 +10,5 @@
C485.371,388.667,512,324.38,512,256S485.371,123.333,437.02,74.98z M256,70c30.327,0,55,24.673,55,55c0,30.327-24.673,55-55,55
c-30.327,0-55-24.673-55-55C201,94.673,225.673,70,256,70z M326,420H186v-30h30V240h-30v-30h110v180h30V420z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 955 B

After

Width:  |  Height:  |  Size: 790 B

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" version="1.1" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg"
>
<!-- Generator: Sketch 49 (51002) - http://www.bohemiancoding.com/sketch -->
<defs></defs>
<g fill="none" fill-rule="evenodd" id="Settings" stroke="none" stroke-width="1">
<g fill="currentColor" id="Data-Sources" transform="translate(-564.000000, -592.000000)">
<g id="Group-17" transform="translate(554.000000, 582.000000)">

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,14 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512"
xmlns="http://www.w3.org/2000/svg">
<g>
<circle cx="437" cy="86.281" r="15"/>
<circle cx="377" cy="86.281" r="15"/>
<circle cx="317" cy="86.281" r="15"/>
<path
d="m15 497h482c8.284 0 15-6.716 15-15v-332-120c0-8.284-6.716-15-15-15h-482c-8.284 0-15 6.716-15 15v120 332c0 8.284 6.716 15 15 15zm15-452h452v90h-452zm0 120h452v302h-452z"/>
<path
d="m437 407h-16v-91c0-8.284-6.716-15-15-15h-60c-8.284 0-15 6.716-15 15v91h-30v-197c0-8.284-6.716-15-15-15h-60c-8.284 0-15 6.716-15 15v197h-30v-148.764c0-8.284-6.716-15-15-15h-60c-8.284 0-15 6.716-15 15v148.764h-16c-8.284 0-15 6.716-15 15s6.716 15 15 15h362c8.284 0 15-6.716 15-15s-6.716-15-15-15zm-286 0h-30v-133.764h30zm120 0h-30v-182h30zm120 0h-30v-76h30z"/>
<path
d="m173.343 71.282h-98.343c-8.284 0-15 6.716-15 15s6.716 15 15 15h98.343c8.284 0 15-6.716 15-15s-6.716-15-15-15z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@ -0,0 +1,10 @@
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 19.738 19.738" style="enable-background:new 0 0 19.738 19.738;" xml:space="preserve">
<g>
<path style="fill:#010002;" d="M18.18,19.738h-2c0-3.374-2.83-6.118-6.311-6.118s-6.31,2.745-6.31,6.118h-2
c0-4.478,3.729-8.118,8.311-8.118C14.451,11.62,18.18,15.26,18.18,19.738z"/>
<path style="fill:#010002;" d="M9.87,10.97c-3.023,0-5.484-2.462-5.484-5.485C4.385,2.461,6.846,0,9.87,0
c3.025,0,5.486,2.46,5.486,5.485S12.895,10.97,9.87,10.97z M9.87,2C7.948,2,6.385,3.563,6.385,5.485S7.948,8.97,9.87,8.97
c1.923,0,3.486-1.563,3.486-3.485S11.791,2,9.87,2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 665 B

View File

@ -10,6 +10,11 @@
border-radius: 12px;
font-size: 10px;
border: 1px solid #E2E4E9;
font-family: Inter, sans-serif;
letter-spacing: 0;
line-height: 12px;
text-align: center;
&.large {
height: 32px;
@ -18,12 +23,17 @@
font-size: 13px;
}
&.gray {
&.gray-dark {
background-color: $grey-4;
border: none;
}
&.red {
&.gray-red {
background-color: $grey-4;
border: none;
}
&.red-white {
background-color: $red-1;
color: $white;
border: none;

View File

@ -34,3 +34,8 @@
}
}
.icon-10 {
width: 10px;
height: 10px;
}

View File

@ -0,0 +1,24 @@
@import "red-variables";
.redacto-logo {
height: 14px;
width: 22px;
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-direction: column;
.line-1 {
height: 6px;
width: 16px;
border-radius: 3px;
background-color: $red-1;
}
.line-2 {
height: 6px;
width: 22px;
border-radius: 6px;
background-color: $red-1;
}
}

View File

@ -41,18 +41,28 @@ html, body {
> div {
padding: 10px;
mat-icon {
cursor: pointer;
width: 20px;
height: 20px;
&:hover {
color: $primary;
}
}
}
}
}
.filters {
font-size: 13px;
line-height: 14px;
font-size: 13px;
line-height: 14px;
> div {
padding: 10px 14px;
}
}
> div {
padding: 10px 14px;
}
}
.flex-row {
display: flex;
@ -106,7 +116,7 @@ html, body {
font-size: 14px;
letter-spacing: 0;
line-height: 14px;
height: 18px;
padding: 4px;
}
.red-top-bar {
@ -122,6 +132,23 @@ html, body {
justify-content: space-between;
padding: 0 24px;
.center {
display: flex;
align-items: center;
justify-content: center;
}
.app-name {
margin-left: 16px;
height: 20px;
color: #283241;
font-family: Inter, sans-serif;
font-size: 16px;
font-weight: 600;
letter-spacing: 0;
line-height: 20px;
}
.menu {
display: flex;
align-items: center;

View File

@ -10,3 +10,4 @@
@import "red-tables";
@import "red-components";
@import "red-controls";
@import "red-logo";

View File

@ -45,7 +45,9 @@
"@pdftron/webviewer": "^7.0.1",
"angular-oauth2-oidc": "^10.0.3",
"angular-oauth2-oidc-jwks": "^9.0.0",
"chart.js": "^2.9.3",
"file-saver": "^2.0.2",
"ng2-charts": "^2.4.2",
"ng2-file-upload": "^1.4.0",
"ngp-sort-pipe": "^0.0.4",
"ngx-dropzone": "^2.2.2",

View File

@ -1808,6 +1808,13 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/chart.js@^2.9.24":
version "2.9.25"
resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.25.tgz#a22d18f6f7cb5b4499f39de600c4c520f635a58f"
integrity sha512-SPgPISpaGM42WL9Dezms4+8fInHxAXDzTs8DwPxhGuJT+jETXlVITTbe8bvK1n+sMmyyelR9B4w0lwMkA24oJg==
dependencies:
moment "^2.10.2"
"@types/color-name@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
@ -3203,6 +3210,29 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chart.js@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
chartjs-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
check-more-types@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
@ -3430,7 +3460,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^1.9.0, color-convert@^1.9.1:
color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@ -7142,6 +7172,11 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
@ -7554,6 +7589,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdir
dependencies:
minimist "^1.2.5"
moment@^2.10.2:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
moment@^2.27.0, moment@^2.28.0:
version "2.28.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.28.0.tgz#cdfe73ce01327cee6537b0fafac2e0f21a237d75"
@ -7694,6 +7734,15 @@ ng-packagr@^10.1.2:
stylus "^0.54.7"
terser "^5.0.0"
ng2-charts@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/ng2-charts/-/ng2-charts-2.4.2.tgz#6b2d0a1a66911247c2e8c3d8602ba9aa7339bf22"
integrity sha512-mY3C2uKCaApHCQizS2YxEOqQ7sSZZLxdV6N1uM9u/VvUgVtYvlPtdcXbKpN52ak93ZE22I73DiLWVDnDNG4/AQ==
dependencies:
"@types/chart.js" "^2.9.24"
lodash-es "^4.17.15"
tslib "^2.0.0"
ng2-file-upload@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.4.0.tgz#8dea28d573234c52af474ad2a4001b335271e5c4"