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:
Timo Bejan 2020-11-02 10:06:47 +01:00
commit 1eee96023b
17 changed files with 461 additions and 164 deletions

View File

@ -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">

View File

@ -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;
});
});

View File

@ -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

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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 }}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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)"

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -10,6 +10,10 @@
font-size: 13px;
color: $accent;
display: flex;
align-items: center;
gap: 8px;
.arrow-wrapper {
width: 16px;
margin-right: 8px;

View File

@ -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;
}