Pull request #25: Ui updates
Merge in RED/ui from ui-updates to master * commit 'a4bcded240e1d5caa8f3de1691396329663105da': Accept suggestion menu Fixes Filters in project overview Filter fixes Toggle bulk select and add on project listing Prevent default fix
This commit is contained in:
commit
1eee96023b
@ -29,7 +29,7 @@
|
||||
</div>
|
||||
<div *ngFor="let filter of filters">
|
||||
<div class="mat-menu-item flex" (click)="toggleFilterExpanded($event, filter)">
|
||||
<div class="arrow-wrapper" *ngIf="filter.filters">
|
||||
<div class="arrow-wrapper" *ngIf="filter.filters && filter.filters.length">
|
||||
<mat-icon *ngIf="filter.expanded" svgIcon="red:arrow-down" color="accent">
|
||||
</mat-icon>
|
||||
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="red:arrow-right">
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
SimpleChanges,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import { ManualRedactions } from '@redaction/red-ui-http';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
import { FilterModel } from './model/filter.model';
|
||||
import { handleCheckedValue } from './utils/filter-utils';
|
||||
@ -42,11 +41,11 @@ export class FilterComponent implements OnChanges {
|
||||
}
|
||||
|
||||
activateAllFilters() {
|
||||
this._setAlLFilters(true);
|
||||
this._setAllFilters(true);
|
||||
}
|
||||
|
||||
deactivateAllFilters() {
|
||||
this._setAlLFilters(false);
|
||||
this._setAllFilters(false);
|
||||
}
|
||||
|
||||
get hasActiveFilters(): boolean {
|
||||
@ -67,11 +66,11 @@ export class FilterComponent implements OnChanges {
|
||||
filter.expanded = !filter.expanded;
|
||||
}
|
||||
|
||||
private _setAlLFilters(value: boolean) {
|
||||
private _setAllFilters(value: boolean) {
|
||||
this.filters?.forEach((f) => {
|
||||
f.checked = value;
|
||||
f.indeterminate = value;
|
||||
f.filters.forEach((ff) => {
|
||||
f.indeterminate = false;
|
||||
f.filters?.forEach((ff) => {
|
||||
ff.checked = value;
|
||||
});
|
||||
});
|
||||
|
||||
@ -99,7 +99,7 @@ export class DialogService {
|
||||
return ref;
|
||||
}
|
||||
|
||||
public acceptSuggestion(
|
||||
public openAcceptSuggestionModal(
|
||||
$event: MouseEvent,
|
||||
annotation: AnnotationWrapper
|
||||
): MatDialogRef<ConfirmationDialogComponent> {
|
||||
@ -115,7 +115,7 @@ export class DialogService {
|
||||
return ref;
|
||||
}
|
||||
|
||||
public rejectSuggestion(
|
||||
public openRejectSuggestionModal(
|
||||
$event: MouseEvent,
|
||||
annotation: AnnotationWrapper,
|
||||
rejectCallback: () => void
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<section [class.hidden]="!viewReady">
|
||||
<div class="page-header">
|
||||
<div class="flex-1">
|
||||
<mat-slide-toggle color="primary" labelPosition="after" [(ngModel)]="redactedView">
|
||||
<mat-slide-toggle [(ngModel)]="redactedView" color="primary" labelPosition="after">
|
||||
{{ 'file-preview.view-toggle.label' | translate }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
@ -13,9 +13,9 @@
|
||||
<div class="flex-1 actions-container">
|
||||
<div class="actions-row">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="openDeleteFileDialog($event)"
|
||||
*ngIf="userService.isManager(user)"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
@ -23,49 +23,49 @@
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="assignReviewer()"
|
||||
*ngIf="appStateService.isActiveProjectMember"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="reanalyseFile($event)"
|
||||
*ngIf="userService.isManager(user)"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:refresh"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openFileDetailsDialog($event)">
|
||||
<button (click)="openFileDetailsDialog($event)" mat-icon-button>
|
||||
<mat-icon svgIcon="red:info"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
*ngIf="userService.isManager(user)"
|
||||
[matMenuTriggerFor]="downloadMenu"
|
||||
class="arrow-button"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
class="arrow-button"
|
||||
[matMenuTriggerFor]="downloadMenu"
|
||||
>
|
||||
<span translate="file-preview.download.label"></span>
|
||||
<mat-icon svgIcon="red:arrow-down"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #downloadMenu="matMenu" xPosition="before">
|
||||
<div
|
||||
(click)="downloadFile('ORIGINAL')"
|
||||
mat-menu-item
|
||||
translate="file-preview.download.dropdown.original.label"
|
||||
(click)="downloadFile('ORIGINAL')"
|
||||
></div>
|
||||
<div
|
||||
(click)="downloadFile('ANNOTATED')"
|
||||
mat-menu-item
|
||||
translate="file-preview.download.dropdown.annotated.label"
|
||||
(click)="downloadFile('ANNOTATED')"
|
||||
></div>
|
||||
<div
|
||||
(click)="downloadFile('REDACTED')"
|
||||
mat-menu-item
|
||||
translate="file-preview.download.dropdown.redacted.label"
|
||||
(click)="downloadFile('REDACTED')"
|
||||
></div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
@ -74,68 +74,68 @@
|
||||
<div class="flex red-content-inner">
|
||||
<div class="left-container">
|
||||
<redaction-pdf-viewer
|
||||
(annotationSelected)="handleAnnotationSelected($event)"
|
||||
(keyUp)="handleKeyEvent($event)"
|
||||
(manualAnnotationRequested)="openManualRedactionDialog($event)"
|
||||
(pageChanged)="viewerPageChanged($event)"
|
||||
(viewerReady)="viewerReady($event)"
|
||||
*ngIf="fileData"
|
||||
[fileData]="redactedView ? fileData.redactedFileData : fileData.annotatedFileData"
|
||||
[fileStatus]="appStateService.activeFile"
|
||||
(keyUp)="handleKeyEvent($event)"
|
||||
(pageChanged)="viewerPageChanged($event)"
|
||||
(manualAnnotationRequested)="openManualRedactionDialog($event)"
|
||||
(annotationSelected)="handleAnnotationSelected($event)"
|
||||
(viewerReady)="viewerReady($event)"
|
||||
></redaction-pdf-viewer>
|
||||
</div>
|
||||
|
||||
<div class="right-fixed-container">
|
||||
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
|
||||
<redaction-filter
|
||||
(filtersChanged)="filtersChanged($event)"
|
||||
[filterTemplate]="annotationFilterTemplate"
|
||||
[filters]="filters"
|
||||
(filtersChanged)="filtersChanged($event)"
|
||||
></redaction-filter>
|
||||
</div>
|
||||
|
||||
<div class="right-content">
|
||||
<div
|
||||
class="pages"
|
||||
[class.activePanel]="pagesPanelActive"
|
||||
tabindex="0"
|
||||
(keyup)="preventArrowDefault($event)"
|
||||
(keydown)="preventArrowDefault($event)"
|
||||
#quickNavigation
|
||||
(keydown)="preventArrowDefault($event)"
|
||||
(keyup)="preventArrowDefault($event)"
|
||||
[class.activePanel]="pagesPanelActive"
|
||||
class="pages"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="page-number pointer"
|
||||
[ngClass]="{ active: pageNumber === activeViewerPage }"
|
||||
(click)="selectPage(pageNumber)"
|
||||
*ngFor="let pageNumber of displayedPages"
|
||||
[id]="'quick-nav-page-' + pageNumber"
|
||||
(click)="selectPage(pageNumber)"
|
||||
[ngClass]="{ active: pageNumber === activeViewerPage }"
|
||||
class="page-number pointer"
|
||||
>
|
||||
{{ pageNumber }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="annotations"
|
||||
[class.activePanel]="!pagesPanelActive"
|
||||
#annotationsElement
|
||||
tabindex="1"
|
||||
(keyup)="preventArrowDefault($event)"
|
||||
(keydown)="preventArrowDefault($event)"
|
||||
(keyup)="preventArrowDefault($event)"
|
||||
[class.activePanel]="!pagesPanelActive"
|
||||
class="annotations"
|
||||
tabindex="1"
|
||||
>
|
||||
<div *ngFor="let page of displayedPages">
|
||||
<div class="page-separator" attr.anotation-page-header="{{ page }}">
|
||||
<div attr.anotation-page-header="{{ page }}" class="page-separator">
|
||||
<span class="all-caps-label"
|
||||
><span translate="page"></span> {{ page }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
(click)="selectAnnotation(annotation)"
|
||||
*ngFor="let annotation of displayedAnnotations[page].annotations"
|
||||
class="annotation"
|
||||
[ngClass]="{ active: selectedAnnotation?.id === annotation.id }"
|
||||
attr.annotation-id="{{ annotation.id }}"
|
||||
attr.annotation-page="{{ page }}"
|
||||
[ngClass]="{ active: selectedAnnotation?.id === annotation.id }"
|
||||
(click)="selectAnnotation(annotation)"
|
||||
class="annotation"
|
||||
>
|
||||
<div class="details">
|
||||
<redaction-annotation-icon
|
||||
@ -169,19 +169,53 @@
|
||||
></redaction-comments>
|
||||
|
||||
<div
|
||||
class="annotation-actions"
|
||||
*ngIf="annotation.superType === 'request'"
|
||||
[class.visible]="isSuggestionVisible(annotation)"
|
||||
class="annotation-actions"
|
||||
>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="acceptSuggestion($event, annotation)"
|
||||
(click)="openAcceptSuggestionMenu($event, annotation)"
|
||||
*ngIf="appStateService.isActiveProjectOwnerAndManager"
|
||||
[class.active]="isSuggestionVisible(annotation)"
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="confirm"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:check-alt"></mat-icon>
|
||||
</button>
|
||||
<mat-menu
|
||||
#menu="matMenu"
|
||||
(closed)="onSuggestionMenuClose()"
|
||||
xPosition="before"
|
||||
>
|
||||
<div
|
||||
(click)="acceptSuggestion($event, annotation)"
|
||||
mat-menu-item
|
||||
>
|
||||
<!-- TODO -->
|
||||
<redaction-annotation-icon
|
||||
[typeValue]="{ hexColor: '#5CE594', type: 'S' }"
|
||||
></redaction-annotation-icon>
|
||||
<div
|
||||
translate="file-preview.tabs.annotations.accept-suggestion.add-to-dict.label"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
(click)="acceptSuggestion($event, annotation)"
|
||||
mat-menu-item
|
||||
>
|
||||
<!-- TODO -->
|
||||
<redaction-annotation-icon
|
||||
[typeValue]="{ hexColor: '#5B97DB', type: 'S' }"
|
||||
></redaction-annotation-icon>
|
||||
<div
|
||||
translate="file-preview.tabs.annotations.accept-suggestion.only-here.label"
|
||||
></div>
|
||||
</div>
|
||||
</mat-menu>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="rejectSuggestion($event, annotation)"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
|
||||
@ -104,22 +104,7 @@ redaction-pdf-viewer {
|
||||
background-color: #f9fafb;
|
||||
|
||||
.annotation-actions {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
#f9fafb,
|
||||
#f9fafb,
|
||||
#f9fafb
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 120px;
|
||||
padding-right: 16px;
|
||||
|
||||
.confirm {
|
||||
color: $green-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +118,25 @@ redaction-pdf-viewer {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
width: 40px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 120px;
|
||||
padding-right: 16px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
#f9fafb,
|
||||
#f9fafb,
|
||||
#f9fafb
|
||||
);
|
||||
|
||||
.confirm.active {
|
||||
background-color: $grey-2;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ import { FileActionService } from '../service/file-action.service';
|
||||
import { AnnotationDrawService } from '../service/annotation-draw.service';
|
||||
import { AnnotationProcessingService } from '../service/annotation-processing.service';
|
||||
import { FilterModel } from '../../../common/filter/model/filter.model';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
|
||||
const KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
|
||||
@ -38,24 +39,6 @@ const KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
styleUrls: ['./file-preview-screen.component.scss']
|
||||
})
|
||||
export class FilePreviewScreenComponent implements OnInit {
|
||||
private projectId: string;
|
||||
private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED';
|
||||
private instance: WebViewerInstance;
|
||||
private _dialogRef: MatDialogRef<any>;
|
||||
|
||||
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
|
||||
@ViewChild('annotationsElement') private _annotationsElement: ElementRef;
|
||||
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
|
||||
|
||||
public fileData: FileDataModel;
|
||||
public fileId: string;
|
||||
public annotations: AnnotationWrapper[] = [];
|
||||
public displayedAnnotations: { [key: number]: { annotations: AnnotationWrapper[] } } = {};
|
||||
public selectedAnnotation: AnnotationWrapper;
|
||||
public pagesPanelActive = true;
|
||||
public viewReady = false;
|
||||
filters: FilterModel[];
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
public readonly userService: UserService,
|
||||
@ -90,6 +73,38 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
this._activeViewer = value ? 'REDACTED' : 'ANNOTATED';
|
||||
}
|
||||
|
||||
public get activeViewer() {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public get displayedPages(): number[] {
|
||||
return Object.keys(this.displayedAnnotations).map((key) => Number(key));
|
||||
}
|
||||
|
||||
get activeViewerPage() {
|
||||
return this.instance?.docViewer?.getCurrentPage();
|
||||
}
|
||||
private projectId: string;
|
||||
private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED';
|
||||
private instance: WebViewerInstance;
|
||||
private _dialogRef: MatDialogRef<any>;
|
||||
|
||||
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
|
||||
@ViewChild('annotationsElement') private _annotationsElement: ElementRef;
|
||||
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
|
||||
@ViewChild('menu') private _matMenuTrigger: MatMenuTrigger;
|
||||
|
||||
public fileData: FileDataModel;
|
||||
public fileId: string;
|
||||
public annotations: AnnotationWrapper[] = [];
|
||||
public displayedAnnotations: { [key: number]: { annotations: AnnotationWrapper[] } } = {};
|
||||
public selectedAnnotation: AnnotationWrapper;
|
||||
public pagesPanelActive = true;
|
||||
public viewReady = false;
|
||||
public filters: FilterModel[];
|
||||
|
||||
private _activeSuggestion: AnnotationWrapper;
|
||||
|
||||
public ngOnInit(): void {
|
||||
this._loadFileData();
|
||||
this.appStateService.fileStatusChanged.subscribe((fileStatus) => {
|
||||
@ -152,14 +167,6 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
this._fileActionService.assignProjectReviewer();
|
||||
}
|
||||
|
||||
public get activeViewer() {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public get displayedPages(): number[] {
|
||||
return Object.keys(this.displayedAnnotations).map((key) => Number(key));
|
||||
}
|
||||
|
||||
public handleAnnotationSelected(annotationId: string) {
|
||||
this.selectedAnnotation = this.annotations.find((a) => a.id === annotationId);
|
||||
this.scrollToSelectedAnnotation();
|
||||
@ -197,10 +204,6 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
get activeViewerPage() {
|
||||
return this.instance?.docViewer?.getCurrentPage();
|
||||
}
|
||||
|
||||
@debounce()
|
||||
private _scrollViews() {
|
||||
this._scrollQuickNavigation();
|
||||
@ -242,18 +245,36 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public isSuggestionVisible(annotation: AnnotationWrapper) {
|
||||
return annotation.id === this._activeSuggestion?.id;
|
||||
}
|
||||
|
||||
public acceptSuggestion($event: MouseEvent, annotation: AnnotationWrapper) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.acceptSuggestion($event, annotation);
|
||||
this._dialogRef = this._dialogService.openAcceptSuggestionModal($event, annotation);
|
||||
});
|
||||
}
|
||||
|
||||
public openAcceptSuggestionMenu($event: MouseEvent, annotation: AnnotationWrapper) {
|
||||
$event.preventDefault();
|
||||
this._activeSuggestion = annotation;
|
||||
this._matMenuTrigger.openMenu();
|
||||
}
|
||||
|
||||
public onSuggestionMenuClose() {
|
||||
this._activeSuggestion = null;
|
||||
}
|
||||
|
||||
public rejectSuggestion($event: MouseEvent, annotation: AnnotationWrapper) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.rejectSuggestion($event, annotation, () => {
|
||||
// TODO DELETE ANNOTATIOn
|
||||
//this.activeViewer.annotManager.deleteAnnotation(annotation, false, true);
|
||||
});
|
||||
this._dialogRef = this._dialogService.openRejectSuggestionModal(
|
||||
$event,
|
||||
annotation,
|
||||
() => {
|
||||
// TODO DELETE ANNOTATIOn
|
||||
//this.activeViewer.annotManager.deleteAnnotation(annotation, false, true);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -418,6 +439,8 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
preventArrowDefault($event: KeyboardEvent) {
|
||||
$event.preventDefault();
|
||||
if (KEY_ARRAY.includes($event.key)) {
|
||||
$event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,14 +52,28 @@
|
||||
<div class="flex red-content-inner">
|
||||
<div class="left-container">
|
||||
<div class="table-header">
|
||||
<span class="all-caps-label">
|
||||
{{
|
||||
'project-listing.table-header.title.label'
|
||||
| translate: { length: appStateService.allProjects?.length || 0 }
|
||||
}}
|
||||
</span>
|
||||
<div class="select-all-container">
|
||||
<div
|
||||
*ngIf="bulkSelectActive"
|
||||
class="select-oval"
|
||||
[class.active]="areAllProjectsSelected()"
|
||||
(click)="toggleSelectAll()"
|
||||
></div>
|
||||
<span class="all-caps-label">
|
||||
{{
|
||||
'project-listing.table-header.title.label'
|
||||
| translate: { length: appStateService.allProjects?.length || 0 }
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div translate="project-listing.table-header.bulk-select.label"></div>
|
||||
<div
|
||||
translate="project-listing.table-header.bulk-select.label"
|
||||
class="pointer"
|
||||
[class.active]="bulkSelectActive"
|
||||
(click)="toggleBulkSelect()"
|
||||
></div>
|
||||
<mat-form-field appearance="none" class="red-select">
|
||||
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
||||
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
||||
@ -70,7 +84,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<div class="grid-container" [class.bulk-select]="bulkSelectActive">
|
||||
<div class="table-col-name" *ngIf="bulkSelectActive"></div>
|
||||
|
||||
<div class="table-col-name">
|
||||
<span
|
||||
class="small-label"
|
||||
@ -104,6 +120,14 @@
|
||||
class="table-item"
|
||||
[class.pointer]="canOpenProject(pw)"
|
||||
>
|
||||
<div class="pr-0" *ngIf="bulkSelectActive">
|
||||
<div
|
||||
class="select-oval"
|
||||
[class.active]="isProjectSelected(pw)"
|
||||
(click)="toggleProjectSelected($event, pw)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="table-item-title">
|
||||
{{ pw.project.projectName }}
|
||||
|
||||
@ -13,6 +13,10 @@
|
||||
|
||||
.grid-container {
|
||||
grid-template-columns: 2fr 1fr auto;
|
||||
|
||||
&.bulk-select {
|
||||
grid-template-columns: auto 2fr 1fr auto;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-subtitle {
|
||||
@ -22,6 +26,13 @@
|
||||
.status-container {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
.active {
|
||||
font-weight: 600;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-fixed-container {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Project } from '@redaction/red-ui-http';
|
||||
import { FileStatus, Project } from '@redaction/red-ui-http';
|
||||
import { AppStateService, ProjectWrapper } from '../../state/app-state.service';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
@ -15,6 +15,8 @@ import * as moment from 'moment';
|
||||
styleUrls: ['./project-listing-screen.component.scss']
|
||||
})
|
||||
export class ProjectListingScreenComponent implements OnInit {
|
||||
private _selectedProjectIds: string[] = [];
|
||||
|
||||
public projectsChartData: DoughnutChartConfig[] = [];
|
||||
public documentsChartData: DoughnutChartConfig[] = [];
|
||||
public sortingOptions: SortingOption[] = [
|
||||
@ -26,6 +28,7 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
}
|
||||
];
|
||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
||||
public bulkSelectActive = false;
|
||||
|
||||
statusFilters: FilterModel[];
|
||||
dueDateFilters: FilterModel[];
|
||||
@ -118,6 +121,10 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
this._dialogService.openAssignProjectMembersAndOwnerDialog($event, project);
|
||||
}
|
||||
|
||||
public toggleBulkSelect() {
|
||||
this.bulkSelectActive = !this.bulkSelectActive;
|
||||
}
|
||||
|
||||
public getProjectStatusConfig(pw: ProjectWrapper) {
|
||||
const obj = pw.files.reduce((acc, file) => {
|
||||
const status = file.status;
|
||||
@ -248,4 +255,35 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
}
|
||||
return filterMatched;
|
||||
}
|
||||
|
||||
public toggleProjectSelected($event: MouseEvent, pw: ProjectWrapper) {
|
||||
$event.stopPropagation();
|
||||
const idx = this._selectedProjectIds.indexOf(pw.project.projectId);
|
||||
if (idx === -1) {
|
||||
this._selectedProjectIds.push(pw.project.projectId);
|
||||
} else {
|
||||
this._selectedProjectIds.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public toggleSelectAll() {
|
||||
if (this.areAllProjectsSelected()) {
|
||||
this._selectedProjectIds = [];
|
||||
} else {
|
||||
this._selectedProjectIds = this.appStateService.allProjects.map(
|
||||
(pw) => pw.project.projectId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public areAllProjectsSelected(): boolean {
|
||||
return (
|
||||
this.appStateService.allProjects.length !== 0 &&
|
||||
this._selectedProjectIds.length === this.appStateService.allProjects.length
|
||||
);
|
||||
}
|
||||
|
||||
public isProjectSelected(pw: ProjectWrapper): boolean {
|
||||
return this._selectedProjectIds.indexOf(pw.project.projectId) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,24 +9,36 @@
|
||||
<div *ngIf="appStateService.activeProject" class="page-header">
|
||||
<div class="filters flex-row">
|
||||
<div translate="filters.filter-by.label"></div>
|
||||
<button mat-button translate="filters.status.label">
|
||||
<mat-icon svgIcon="red:status"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.people.label">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.due-date.label">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.created-on.label">
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.project.label">
|
||||
<mat-icon svgIcon="red:folder"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.document.label">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
</button>
|
||||
<redaction-filter
|
||||
[filters]="statusFilters"
|
||||
[filterLabel]="'filters.status.label'"
|
||||
[hasArrow]="false"
|
||||
[icon]="'red:status'"
|
||||
(filtersChanged)="filtersChanged()"
|
||||
></redaction-filter>
|
||||
<redaction-filter
|
||||
[filters]="peopleFilters"
|
||||
[filterLabel]="'filters.people.label'"
|
||||
[hasArrow]="false"
|
||||
[icon]="'red:user'"
|
||||
(filtersChanged)="filtersChanged()"
|
||||
></redaction-filter>
|
||||
<!-- <button mat-button translate="filters.due-date.label">-->
|
||||
<!-- <mat-icon svgIcon="red:lightning"></mat-icon>-->
|
||||
<!-- </button>-->
|
||||
<redaction-filter
|
||||
[filters]="addedDateFilters"
|
||||
[filterLabel]="'filters.created-on.label'"
|
||||
[hasArrow]="false"
|
||||
[icon]="'red:calendar'"
|
||||
(filtersChanged)="filtersChanged()"
|
||||
></redaction-filter>
|
||||
<!-- <button mat-button translate="filters.project.label">-->
|
||||
<!-- <mat-icon svgIcon="red:folder"></mat-icon>-->
|
||||
<!-- </button>-->
|
||||
<!-- <button mat-button translate="filters.document.label">-->
|
||||
<!-- <mat-icon svgIcon="red:document"></mat-icon>-->
|
||||
<!-- </button>-->
|
||||
</div>
|
||||
<button
|
||||
(click)="fileInput.click()"
|
||||
@ -48,6 +60,7 @@
|
||||
<div class="table-header">
|
||||
<div class="select-all-container">
|
||||
<div
|
||||
*ngIf="bulkSelectActive"
|
||||
class="select-oval"
|
||||
[class.active]="areAllFilesSelected()"
|
||||
(click)="toggleSelectAll()"
|
||||
@ -61,7 +74,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div translate="project-overview.table-header.bulk-select.label"></div>
|
||||
<div
|
||||
translate="project-overview.table-header.bulk-select.label"
|
||||
class="pointer"
|
||||
[class.active]="bulkSelectActive"
|
||||
(click)="toggleBulkSelect()"
|
||||
></div>
|
||||
<mat-form-field appearance="none" class="red-select">
|
||||
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
||||
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
||||
@ -72,9 +90,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<div class="grid-container" [class.bulk-select]="bulkSelectActive">
|
||||
<!-- Table column names-->
|
||||
<div class="table-col-name"></div>
|
||||
<div class="table-col-name" *ngIf="bulkSelectActive"></div>
|
||||
|
||||
<div class="table-col-name">
|
||||
<span
|
||||
@ -121,11 +139,17 @@
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="displayedFiles?.length === 0"
|
||||
class="no-data"
|
||||
translate="project-overview.no-files.label"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="table-item"
|
||||
[class.pointer]="canOpenFile(fileStatus)"
|
||||
*ngFor="
|
||||
let fileStatus of appStateService.activeProject.files
|
||||
let fileStatus of displayedFiles
|
||||
| sortBy: sortingOption.order:sortingOption.column;
|
||||
trackBy: fileId
|
||||
"
|
||||
@ -135,7 +159,7 @@
|
||||
: []
|
||||
"
|
||||
>
|
||||
<div class="pr-0">
|
||||
<div class="pr-0" *ngIf="bulkSelectActive">
|
||||
<div
|
||||
class="select-oval"
|
||||
[class.active]="isFileSelected(fileStatus)"
|
||||
|
||||
@ -4,39 +4,19 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select-all-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.select-oval {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pr-0 {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.select-oval {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $grey-5;
|
||||
background-color: $white;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background-color: $primary;
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
visibility: hidden;
|
||||
.actions {
|
||||
.active {
|
||||
font-weight: 600;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
grid-template-columns: auto 3fr 2fr 1fr 2fr auto;
|
||||
grid-template-columns: 3fr 2fr 1fr 2fr auto;
|
||||
|
||||
&.bulk-select {
|
||||
grid-template-columns: auto 3fr 2fr 1fr 2fr auto;
|
||||
}
|
||||
|
||||
.table-item {
|
||||
.disabled {
|
||||
|
||||
@ -15,10 +15,12 @@ import { UploadStatusOverlayService } from '../../upload/upload-status-dialog/se
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { SortingOption } from '../../utils/types';
|
||||
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { groupBy } from '../../utils/functions';
|
||||
import { groupBy, humanize } from '../../utils/functions';
|
||||
import { DialogService } from '../../dialogs/dialog.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { FileActionService } from '../file/service/file-action.service';
|
||||
import { FilterModel } from '../../common/filter/model/filter.model';
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-project-overview-screen',
|
||||
@ -49,6 +51,13 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
];
|
||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
||||
public documentsChartData: DoughnutChartConfig[] = [];
|
||||
public bulkSelectActive = false;
|
||||
|
||||
public statusFilters: FilterModel[];
|
||||
public peopleFilters: FilterModel[];
|
||||
public addedDateFilters: FilterModel[];
|
||||
|
||||
public displayedFiles: FileStatus[] = [];
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
@ -71,13 +80,13 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.appStateService.fileStatusChanged.subscribe(() => {
|
||||
this._calculateChartConfig();
|
||||
this._calculateData();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._fileDropOverlayService.initFileDropHandling();
|
||||
this._calculateChartConfig();
|
||||
this._calculateData();
|
||||
this._displayNewRuleToast();
|
||||
}
|
||||
|
||||
@ -117,6 +126,10 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
public toggleBulkSelect() {
|
||||
this.bulkSelectActive = !this.bulkSelectActive;
|
||||
}
|
||||
|
||||
private _displayNewRuleToast() {
|
||||
// @ts-ignore
|
||||
if (!this.appStateService.activeProject.files.filter((file) => file.newRule).length) {
|
||||
@ -157,10 +170,16 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
|
||||
private _reloadProjects() {
|
||||
this.appStateService.loadAllProjects().then(() => {
|
||||
this._calculateChartConfig();
|
||||
this._calculateData();
|
||||
});
|
||||
}
|
||||
|
||||
private _calculateData(): void {
|
||||
this._computeAllFilters();
|
||||
this._filterFiles();
|
||||
this._calculateChartConfig();
|
||||
}
|
||||
|
||||
private _calculateChartConfig() {
|
||||
if (this.appStateService.activeProject) {
|
||||
const groups = groupBy(this.appStateService.activeProject?.files, 'status');
|
||||
@ -208,7 +227,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
fileStatus.projectId,
|
||||
fileStatus.fileId,
|
||||
() => {
|
||||
this._calculateChartConfig();
|
||||
this._calculateData();
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -279,6 +298,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
this._fileUploadService.uploadFiles(uploadFiles);
|
||||
this._uploadStatusOverlayService.openStatusOverlay();
|
||||
}
|
||||
|
||||
public canOpenFile(fileStatus: FileStatus): boolean {
|
||||
// TODO check correct condition for this
|
||||
return !this.isError(fileStatus) && !this.isProcessing(fileStatus);
|
||||
@ -288,4 +308,99 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
const sortedByRecent: boolean = this.sortingOption === this.sortingOptions[0];
|
||||
this.sortingOption = sortedByRecent ? this.sortingOptions[1] : this.sortingOptions[0];
|
||||
}
|
||||
|
||||
private _computeAllFilters() {
|
||||
const allDistinctFileStatus = new Set<string>();
|
||||
const allDistinctPeople = new Set<string>();
|
||||
const allDistinctAddedDates = new Set<string>();
|
||||
|
||||
// All people
|
||||
this.appStateService.activeProject.project.memberIds.forEach((memberId) =>
|
||||
allDistinctPeople.add(memberId)
|
||||
);
|
||||
|
||||
// File statuses
|
||||
this.appStateService.activeProject.files.forEach((file) =>
|
||||
allDistinctFileStatus.add(file.status)
|
||||
);
|
||||
|
||||
// Added dates
|
||||
this.appStateService.activeProject.files.forEach((file) =>
|
||||
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'))
|
||||
);
|
||||
|
||||
this.statusFilters = [];
|
||||
allDistinctFileStatus.forEach((status) => {
|
||||
this.statusFilters.push({
|
||||
key: status,
|
||||
label: humanize(status)
|
||||
});
|
||||
});
|
||||
|
||||
this.peopleFilters = [];
|
||||
allDistinctPeople.forEach((userId) => {
|
||||
this.peopleFilters.push({
|
||||
key: userId,
|
||||
label: this.userService.getNameForId(userId)
|
||||
});
|
||||
});
|
||||
|
||||
this.addedDateFilters = [];
|
||||
allDistinctAddedDates.forEach((date) => {
|
||||
this.addedDateFilters.push({
|
||||
key: date,
|
||||
label: date
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
filtersChanged() {
|
||||
this._filterFiles();
|
||||
}
|
||||
|
||||
private _filterFiles() {
|
||||
const filteredFiles = [];
|
||||
|
||||
for (const file of this.appStateService.activeProject.files) {
|
||||
const statusFilterMatched = this._checkFilter(
|
||||
file,
|
||||
this.statusFilters,
|
||||
(f: FileStatus, filter: FilterModel) => f.status === filter.key
|
||||
);
|
||||
|
||||
const peopleFilterMatched = this._checkFilter(
|
||||
file,
|
||||
this.peopleFilters,
|
||||
(f: FileStatus, filter: FilterModel) => f.currentReviewer === filter.key
|
||||
);
|
||||
|
||||
const addedFilterMatched = this._checkFilter(
|
||||
file,
|
||||
this.addedDateFilters,
|
||||
(f: FileStatus, filter: FilterModel) =>
|
||||
moment(f.added).format('DD/MM/YYYY') === filter.key
|
||||
);
|
||||
|
||||
if (statusFilterMatched && peopleFilterMatched && addedFilterMatched) {
|
||||
filteredFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
this.displayedFiles = filteredFiles;
|
||||
}
|
||||
|
||||
private _checkFilter(file: FileStatus, filters: FilterModel[], validate: Function) {
|
||||
const hasChecked = filters.find((f) => f.checked);
|
||||
if (!hasChecked) {
|
||||
return true;
|
||||
}
|
||||
let filterMatched = false;
|
||||
for (const filter of filters) {
|
||||
if (filter.checked && validate(file, filter)) {
|
||||
filterMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return filterMatched;
|
||||
}
|
||||
}
|
||||
|
||||
@ -478,7 +478,15 @@
|
||||
"label": "Quick Navigation"
|
||||
},
|
||||
"annotations": {
|
||||
"label": "Annotations"
|
||||
"label": "Annotations",
|
||||
"accept-suggestion": {
|
||||
"add-to-dict": {
|
||||
"label": "Approve and add to dictionary"
|
||||
},
|
||||
"only-here": {
|
||||
"label": "Approve only here"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"label": "Info",
|
||||
|
||||
@ -43,3 +43,10 @@
|
||||
width: 9px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.mat-icon-button {
|
||||
transition: background-color 0.25s ease-in-out;
|
||||
&:hover {
|
||||
background-color: $grey-2;
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,3 +131,16 @@
|
||||
stroke: rgba($red-1, 0.1);
|
||||
background-color: rgba($red-1, 0.1);
|
||||
}
|
||||
|
||||
.select-oval {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $grey-5;
|
||||
background-color: $white;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,10 @@
|
||||
font-size: 13px;
|
||||
color: $accent;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.arrow-wrapper {
|
||||
width: 16px;
|
||||
margin-right: 8px;
|
||||
|
||||
@ -181,3 +181,17 @@ body {
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select-all-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.select-oval {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pr-0 {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user