Pull request #200: Quick filters

Merge in RED/ui from RED-1548 to master

* commit 'da5360bacfec21a8b43527e78f693a3123927458':
  RED-1549: Dossier overview quick filters
  Dossier listing quick filters
This commit is contained in:
Timo Bejan 2021-06-07 14:04:35 +02:00
commit d3ebe3d9c4
32 changed files with 320 additions and 113 deletions

View File

@ -21,9 +21,9 @@ export class ConfirmDeleteUsersDialogComponent {
private readonly _appStateService: AppStateService,
public dialogRef: MatDialogRef<ConfirmDeleteUsersDialogComponent>
) {
this.dossiersCount = this._appStateService.allDossiers.filter(pw => {
this.dossiersCount = this._appStateService.allDossiers.filter(dw => {
for (const user of this.users) {
if (pw.memberIds.indexOf(user.userId) !== -1) {
if (dw.memberIds.indexOf(user.userId) !== -1) {
return true;
}
}

View File

@ -7,8 +7,8 @@ import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver';
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { DictionaryManagerComponent } from '../../../shared/components/dictionary-manager/dictionary-manager.component';
import { DictionarySaveService } from '../../../shared/services/dictionary-save.service';
import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component';
import { DictionarySaveService } from '@shared/services/dictionary-save.service';
import { TypeValueWrapper } from '../../../../models/file/type-value.wrapper';
@Component({

View File

@ -17,6 +17,7 @@ export enum AppConfigKey {
ADMIN_CONTACT_URL = 'ADMIN_CONTACT_URL',
AUTO_READ_TIME = 'AUTO_READ_TIME',
MAX_FILE_SIZE_MB = 'MAX_FILE_SIZE_MB',
RECENT_PERIOD_IN_HOURS = 'RECENT_PERIOD_IN_HOURS',
DELETE_RETENTION_HOURS = 'DELETE_RETENTION_HOURS',
APP_NAME = 'APP_NAME',

View File

@ -3,11 +3,11 @@ import { AppStateService } from '@state/app-state.service';
import { groupBy } from '@utils/functions';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { Router } from '@angular/router';
import { FilterModel } from '@shared/components/filter/model/filter.model';
import { PermissionsService } from '@services/permissions.service';
import { TranslateChartService } from '@services/translate-chart.service';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
@Component({
selector: 'redaction-dossier-details',

View File

@ -36,8 +36,8 @@ export class DossierListingActionsComponent {
});
}
getDossierStatusConfig(pw: DossierWrapper) {
const obj = pw.files.reduce((acc, file) => {
getDossierStatusConfig(dw: DossierWrapper) {
const obj = dw.files.reduce((acc, file) => {
const status = file.status;
if (!acc[status]) {
acc[status] = 1;

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { AppStateService } from '@state/app-state.service';
import { FilterModel } from '@shared/components/filter/model/filter.model';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
@Component({
selector: 'redaction-dossier-listing-details',

View File

@ -6,14 +6,14 @@
class="all-caps-label primary pointer"
translate="file-preview.tabs.annotations.select"
></div>
<redaction-filter
<redaction-popup-filter
(filtersChanged)="filtersChanged($event)"
[actionsTemplate]="annotationFilterActionTemplate"
[chevron]="true"
[filterTemplate]="annotationFilterTemplate"
[primaryFilters]="primaryFilters"
[secondaryFilters]="secondaryFilters"
></redaction-filter>
></redaction-popup-filter>
</div>
</div>
<div class="right-content">

View File

@ -9,13 +9,13 @@ import {
TemplateRef,
ViewChild
} from '@angular/core';
import { FilterModel } from '@shared/components/filter/model/filter.model';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import scrollIntoView from 'scroll-into-view-if-needed';
import { debounce } from '@utils/debounce';
import { FileDataModel } from '@models/file/file-data.model';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];

View File

@ -92,11 +92,11 @@ export class TeamMembersManagerComponent implements OnInit {
const ownerId = this.selectedOwnerId;
const memberIds = this.selectedMembersList;
const approverIds = this.selectedApproversList;
const pw = Object.assign({}, this.dossierWrapper);
pw.dossier.memberIds = memberIds;
pw.dossier.approverIds = approverIds;
pw.dossier.ownerId = ownerId;
result = await this._appStateService.addOrUpdateDossier(pw.dossier);
const dw = Object.assign({}, this.dossierWrapper);
dw.dossier.memberIds = memberIds;
dw.dossier.approverIds = approverIds;
dw.dossier.ownerId = ownerId;
result = await this._appStateService.addOrUpdateDossier(dw.dossier);
this.save.emit(result);
} catch (error) {
this._notificationService.showToastNotification(

View File

@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { FilterModel } from '@shared/components/filter/model/filter.model';
import { AppStateService } from '@state/app-state.service';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
@Component({
selector: 'redaction-type-filter',

View File

@ -1,8 +1,8 @@
import { Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DossierWrapper } from '../../../../state/model/dossier.wrapper';
import { DictionaryManagerComponent } from '../../../shared/components/dictionary-manager/dictionary-manager.component';
import { DictionarySaveService } from '../../../shared/services/dictionary-save.service';
import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component';
import { DictionarySaveService } from '@shared/services/dictionary-save.service';
import { AppStateService } from '../../../../state/app-state.service';
import { PermissionsService } from '../../../../services/permissions.service';

View File

@ -2,9 +2,9 @@ import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core
import { AppStateService } from '../../../../../state/app-state.service';
import { DossierWrapper } from '../../../../../state/model/dossier.wrapper';
import { EditDossierSectionInterface } from '../edit-dossier-section.interface';
import { DictionarySaveService } from '../../../../shared/services/dictionary-save.service';
import { DictionaryManagerComponent } from '../../../../shared/components/dictionary-manager/dictionary-manager.component';
import { PermissionsService } from '../../../../../services/permissions.service';
import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component';
import { DictionarySaveService } from '@shared/services/dictionary-save.service';
@Component({
selector: 'redaction-edit-dossier-dictionary',

View File

@ -2,36 +2,36 @@
<div class="page-header">
<div class="filters">
<div translate="filters.filter-by"></div>
<redaction-filter
<redaction-popup-filter
#statusFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.status'"
[icon]="'red:status'"
[primaryFilters]="statusFilters"
></redaction-filter>
<redaction-filter
></redaction-popup-filter>
<redaction-popup-filter
#peopleFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.people'"
[icon]="'red:user'"
[primaryFilters]="peopleFilters"
></redaction-filter>
<redaction-filter
></redaction-popup-filter>
<redaction-popup-filter
#needsWorkFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.needs-work'"
[filterTemplate]="needsWorkTemplate"
[icon]="'red:needs-work'"
[primaryFilters]="needsWorkFilters"
></redaction-filter>
<redaction-filter
></redaction-popup-filter>
<redaction-popup-filter
#dossierTemplateFilter
(filtersChanged)="filtersChanged()"
*ngIf="dossierTemplateFilters.length > 1"
[filterLabel]="'filters.dossier-templates'"
[icon]="'red:template'"
[primaryFilters]="dossierTemplateFilters"
></redaction-filter>
></redaction-popup-filter>
<redaction-search-input
[form]="searchForm"
[placeholder]="'dossier-listing.search'"
@ -63,6 +63,11 @@
| translate: { length: displayedEntities.length || 0 }
}}
</span>
<redaction-quick-filters
(filtersChanged)="filtersChanged()"
[filters]="quickFilters"
></redaction-quick-filters>
</div>
<div class="table-header" redactionSyncWidth="table-item">
@ -107,63 +112,63 @@
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="
let pw of displayedEntities
let dw of displayedEntities
| sortBy: sortingOption.order:sortingOption.column
"
[class.pointer]="canOpenDossier(pw)"
[class.pointer]="canOpenDossier(dw)"
[routerLink]="[
canOpenDossier(pw) ? '/main/dossiers/' + pw.dossier.dossierId : []
canOpenDossier(dw) ? '/main/dossiers/' + dw.dossier.dossierId : []
]"
class="table-item"
>
<div class="filename">
<div class="table-item-title heading">
{{ pw.dossier.dossierName }}
{{ dw.dossier.dossierName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ getDossierTemplate(pw).name }}
{{ getDossierTemplate(dw).name }}
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ documentCount(pw) }}
{{ documentCount(dw) }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ pw.totalNumberOfPages }}
{{ dw.totalNumberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ userCount(pw) }}
{{ userCount(dw) }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ pw.dossier.date | date: 'mediumDate' }}
{{ dw.dossier.date | date: 'mediumDate' }}
</div>
<div *ngIf="pw.dossier.dueDate">
<div *ngIf="dw.dossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ pw.dossier.dueDate | date: 'mediumDate' }}
{{ dw.dossier.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
<div>
<redaction-needs-work-badge
[needsWorkInput]="pw"
[needsWorkInput]="dw"
></redaction-needs-work-badge>
</div>
<div class="user-column">
<redaction-initials-avatar
[userId]="pw.dossier.ownerId"
[userId]="dw.dossier.ownerId"
[withName]="true"
></redaction-initials-avatar>
</div>
<div class="status-container">
<redaction-dossier-listing-actions
(actionPerformed)="actionPerformed()"
[dossier]="pw"
[dossier]="dw"
></redaction-dossier-listing-actions>
</div>
<div class="scrollbar-placeholder"></div>

View File

@ -4,14 +4,6 @@ import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { groupBy } from '@utils/functions';
import { FilterModel } from '@shared/components/filter/model/filter.model';
import {
annotationFilterChecker,
dossierMemberChecker,
dossierStatusChecker,
dossierTemplateChecker,
processFilters
} from '@shared/components/filter/utils/filter-utils';
import { TranslateService } from '@ngx-translate/core';
import { PermissionsService } from '@services/permissions.service';
import { DossierWrapper } from '@state/model/dossier.wrapper';
@ -21,11 +13,20 @@ import { TranslateChartService } from '@services/translate-chart.service';
import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { FilterComponent } from '@shared/components/filter/filter.component';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import {
annotationFilterChecker,
dossierMemberChecker,
dossierStatusChecker,
dossierTemplateChecker,
processFilters
} from '@shared/components/filters/popup-filter/utils/filter-utils';
import { QuickFiltersComponent } from '../../../shared/components/filters/quick-filters/quick-filters.component';
@Component({
selector: 'redaction-dossier-listing-screen',
@ -47,17 +48,19 @@ export class DossierListingScreenComponent
} = {
statusFilters: []
};
quickFilters: FilterModel[];
readonly itemSize = 85;
@ViewChild(CdkVirtualScrollViewport) scrollBar: CdkVirtualScrollViewport;
protected readonly _searchKey = 'name';
protected readonly _sortKey = 'dossier-listing';
@ViewChild(QuickFiltersComponent) protected _quickFiltersComponent: QuickFiltersComponent;
private _dossierAutoUpdateTimer: Subscription;
private _lastScrollPosition: number;
@ViewChild('statusFilter') private _statusFilterComponent: FilterComponent;
@ViewChild('peopleFilter') private _peopleFilterComponent: FilterComponent;
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: FilterComponent;
@ViewChild('dossierTemplateFilter') private _dossierTemplateFilterComponent: FilterComponent;
@ViewChild('statusFilter') private _statusFilterComponent: PopupFilterComponent;
@ViewChild('peopleFilter') private _peopleFilterComponent: PopupFilterComponent;
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: PopupFilterComponent;
@ViewChild('dossierTemplateFilter')
private _dossierTemplateFilterComponent: PopupFilterComponent;
private _routerEventsScrollPositionSub: Subscription;
private _fileChangedSub: Subscription;
@ -92,12 +95,13 @@ export class DossierListingScreenComponent
return this.allEntities.length - this.activeDossiersCount;
}
protected get _filterComponents(): FilterComponent[] {
protected get _filterComponents(): (PopupFilterComponent | QuickFiltersComponent)[] {
return [
this._statusFilterComponent,
this._peopleFilterComponent,
this._needsWorkFilterComponent,
this._dossierTemplateFilterComponent
this._dossierTemplateFilterComponent,
this._quickFiltersComponent
];
}
@ -116,7 +120,12 @@ export class DossierListingScreenComponent
matchAll: true,
checkerArgs: this.permissionsService
},
{ values: this.dossierTemplateFilters, checker: dossierTemplateChecker }
{ values: this.dossierTemplateFilters, checker: dossierTemplateChecker },
{
values: this.quickFilters,
checker: (dw: DossierWrapper) =>
this.quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
}
];
}
@ -174,12 +183,12 @@ export class DossierListingScreenComponent
return dossier.numberOfMembers;
}
canOpenDossier(pw: DossierWrapper): boolean {
return !!pw;
canOpenDossier(dw: DossierWrapper): boolean {
return !!dw;
}
getDossierTemplate(pw: DossierWrapper): DossierTemplateModel {
return this._appStateService.getDossierTemplateById(pw.dossier.dossierTemplateId);
getDossierTemplate(dw: DossierWrapper): DossierTemplateModel {
return this._appStateService.getDossierTemplateById(dw.dossier.dossierTemplateId);
}
openAddDossierDialog(): void {
@ -303,5 +312,28 @@ export class DossierListingScreenComponent
this.dossierTemplateFilters,
dossierTemplateFilters
);
this.quickFilters = [
{
key: this.user.id,
label: 'dossier-listing.quick-filters.my-dossiers',
checker: (dw: DossierWrapper) => dw.ownerId === this.user.id
},
{
key: this.user.id,
label: 'dossier-listing.quick-filters.to-approve',
checker: (dw: DossierWrapper) => dw.approverIds.includes(this.user.id)
},
{
key: this.user.id,
label: 'dossier-listing.quick-filters.to-review',
checker: (dw: DossierWrapper) => dw.memberIds.includes(this.user.id)
},
{
key: this.user.id,
label: 'dossier-listing.quick-filters.other',
checker: (dw: DossierWrapper) => !dw.memberIds.includes(this.user.id)
}
];
}
}

View File

@ -2,28 +2,28 @@
<div class="page-header">
<div class="filters">
<div translate="filters.filter-by"></div>
<redaction-filter
<redaction-popup-filter
#statusFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.status'"
[icon]="'red:status'"
[primaryFilters]="statusFilters"
></redaction-filter>
<redaction-filter
></redaction-popup-filter>
<redaction-popup-filter
#peopleFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.assigned-people'"
[icon]="'red:user'"
[primaryFilters]="peopleFilters"
></redaction-filter>
<redaction-filter
></redaction-popup-filter>
<redaction-popup-filter
#needsWorkFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.needs-work'"
[filterTemplate]="needsWorkTemplate"
[icon]="'red:needs-work'"
[primaryFilters]="needsWorkFilters"
></redaction-filter>
></redaction-popup-filter>
<redaction-search-input
[form]="searchForm"
@ -106,6 +106,11 @@
(reload)="bulkActionPerformed()"
[selectedFileIds]="selectedEntitiesIds"
></redaction-dossier-overview-bulk-actions>
<redaction-quick-filters
(filtersChanged)="filtersChanged()"
[filters]="quickFilters"
></redaction-quick-filters>
</div>
<div

View File

@ -15,15 +15,9 @@ import { FileUploadModel } from '@upload-download/model/file-upload.model';
import { FileUploadService } from '@upload-download/services/file-upload.service';
import { StatusOverlayService } from '@upload-download/services/status-overlay.service';
import { TranslateService } from '@ngx-translate/core';
import { FilterModel } from '@shared/components/filter/model/filter.model';
import * as moment from 'moment';
import { DossierDetailsComponent } from '../../components/dossier-details/dossier-details.component';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import {
annotationFilterChecker,
keyChecker,
processFilters
} from '@shared/components/filter/utils/filter-utils';
import { PermissionsService } from '@services/permissions.service';
import { UserService } from '@services/user.service';
import { FileStatus, UserPreferenceControllerService } from '@redaction/red-ui-http';
@ -32,12 +26,20 @@ import { filter, tap } from 'rxjs/operators';
import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { convertFiles, handleFileDrop } from '@utils/file-drop-utils';
import { FilterComponent } from '@shared/components/filter/filter.component';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import {
annotationFilterChecker,
keyChecker,
processFilters
} from '@shared/components/filters/popup-filter/utils/filter-utils';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import { QuickFiltersComponent } from '../../../shared/components/filters/quick-filters/quick-filters.component';
import { AppConfigService } from '../../../app-config/app-config.service';
@Component({
selector: 'redaction-dossier-overview-screen',
@ -57,7 +59,9 @@ export class DossierOverviewScreenComponent
statusFilters: FilterModel[];
} = { needsWorkFilters: [], statusFilters: [] };
readonly itemSize = 80;
quickFilters: FilterModel[];
@ViewChild(CdkVirtualScrollViewport) scrollBar: CdkVirtualScrollViewport;
@ViewChild(QuickFiltersComponent) protected _quickFiltersComponent: QuickFiltersComponent;
protected readonly _searchKey = 'searchField';
protected readonly _selectionKey = 'fileId';
protected readonly _sortKey = 'dossier-overview';
@ -68,9 +72,9 @@ export class DossierOverviewScreenComponent
private _fileChangedSub: Subscription;
private _lastScrollPosition: number;
private _lastOpenedFileId = '';
@ViewChild('statusFilter') private _statusFilterComponent: FilterComponent;
@ViewChild('peopleFilter') private _peopleFilterComponent: FilterComponent;
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: FilterComponent;
@ViewChild('statusFilter') private _statusFilterComponent: PopupFilterComponent;
@ViewChild('peopleFilter') private _peopleFilterComponent: PopupFilterComponent;
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: PopupFilterComponent;
@ViewChild('fileInput') private _fileInput: ElementRef;
constructor(
@ -85,6 +89,7 @@ export class DossierOverviewScreenComponent
private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _appStateService: AppStateService,
private readonly _userPreferenceControllerService: UserPreferenceControllerService,
private readonly _appConfigService: AppConfigService,
protected readonly _injector: Injector
) {
super(_injector);
@ -95,11 +100,24 @@ export class DossierOverviewScreenComponent
return this._appStateService.activeDossier;
}
protected get _filterComponents(): FilterComponent[] {
get user() {
return this._userService.user;
}
get checkedRequiredFilters() {
return this.quickFilters.filter(f => f.required && f.checked);
}
get checkedNotRequiredFilters() {
return this.quickFilters.filter(f => !f.required && f.checked);
}
protected get _filterComponents(): (PopupFilterComponent | QuickFiltersComponent)[] {
return [
this._statusFilterComponent,
this._peopleFilterComponent,
this._needsWorkFilterComponent
this._needsWorkFilterComponent,
this._quickFiltersComponent
];
}
@ -117,6 +135,16 @@ export class DossierOverviewScreenComponent
checker: annotationFilterChecker,
matchAll: true,
checkerArgs: this.permissionsService
},
{
values: this.quickFilters,
checker: (file: FileStatusWrapper) =>
this.checkedRequiredFilters.reduce((acc, f) => acc && f.checker(file), true) &&
(this.checkedNotRequiredFilters.length === 0 ||
this.checkedNotRequiredFilters.reduce(
(acc, f) => acc || f.checker(file),
false
))
}
];
}
@ -286,6 +314,11 @@ export class DossierOverviewScreenComponent
this.collapsedDetails = !this.collapsedDetails;
}
recentlyModifiedChecker = (file: FileStatusWrapper) =>
moment(file.lastUpdated)
.add(this._appConfigService.getConfig('RECENT_PERIOD_IN_HOURS'), 'hours')
.isAfter(moment());
protected _preFilter() {
this.detailsContainerFilters = {
needsWorkFilters: this.needsWorkFilters.map(f => ({ ...f })),
@ -377,5 +410,41 @@ export class DossierOverviewScreenComponent
(a, b) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
);
this.needsWorkFilters = processFilters(this.needsWorkFilters, needsWorkFilters);
this._computeQuickFilters();
}
private _computeQuickFilters() {
if (this.allEntities.filter(this.recentlyModifiedChecker).length > 0) {
this.quickFilters = [
{
key: this.user.id,
label: 'dossier-overview.quick-filters.recent',
required: true,
checker: this.recentlyModifiedChecker
}
];
} else {
this.quickFilters = [];
}
this.quickFilters = [
...this.quickFilters,
{
key: this.user.id,
label: 'dossier-overview.quick-filters.assigned-to-me',
checker: (file: FileStatusWrapper) => file.currentReviewer === this.user.id
},
{
key: this.user.id,
label: 'dossier-overview.quick-filters.unassigned',
checker: (file: FileStatusWrapper) => !file.currentReviewer
},
{
key: this.user.id,
label: 'dossier-overview.quick-filters.assigned-to-others',
checker: (file: FileStatusWrapper) => file.currentReviewer !== this.user.id
}
];
}
}

View File

@ -20,13 +20,11 @@ import { AnnotationData, FileDataModel } from '@models/file/file-data.model';
import { FileActionService } from '../../services/file-action.service';
import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { FilterModel } from '@shared/components/filter/model/filter.model';
import { tap } from 'rxjs/operators';
import { NotificationService } from '@services/notification.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { PermissionsService } from '@services/permissions.service';
import { Subscription, timer } from 'rxjs';
import { handleFilterDelta, processFilters } from '@shared/components/filter/utils/filter-utils';
import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { FormBuilder, FormGroup } from '@angular/forms';
@ -41,6 +39,11 @@ import { ViewMode } from '@models/file/view-mode';
import { FileWorkloadComponent } from '../../components/file-workload/file-workload.component';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import {
handleFilterDelta,
processFilters
} from '@shared/components/filters/popup-filter/utils/filter-utils';
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { FilterModel } from '@shared/components/filter/model/filter.model';
import { handleCheckedValue } from '@shared/components/filter/utils/filter-utils';
import { SuperTypeSorter } from '@utils/sorters/super-type-sorter';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { handleCheckedValue } from '@shared/components/filters/popup-filter/utils/filter-utils';
@Injectable()
export class AnnotationProcessingService {

View File

@ -1,10 +1,11 @@
import { ChangeDetectorRef, Component, Injector } from '@angular/core';
import { FilterModel } from '../components/filter/model/filter.model';
import { getFilteredEntities } from '../components/filter/utils/filter-utils';
import { FilterComponent } from '../components/filter/filter.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '@utils/debounce';
import { ScreenName, SortingOption, SortingService } from '@services/sorting.service';
import { FilterModel } from '../components/filters/popup-filter/model/filter.model';
import { PopupFilterComponent } from '../components/filters/popup-filter/popup-filter.component';
import { getFilteredEntities } from '../components/filters/popup-filter/utils/filter-utils';
import { QuickFiltersComponent } from '../components/filters/quick-filters/quick-filters.component';
// Functionalities: Filter, search, select, sort
@ -68,7 +69,7 @@ export abstract class BaseListingComponent<T = any> {
return [];
}
protected get _filterComponents(): FilterComponent[] {
protected get _filterComponents(): (PopupFilterComponent | QuickFiltersComponent)[] {
return [];
}

View File

@ -9,4 +9,5 @@ export interface FilterModel {
matches?: number;
filters?: FilterModel[];
checker?: (obj?) => boolean;
required?: boolean;
}

View File

@ -1,4 +1,4 @@
@import '../../../../../assets/styles/red-variables';
@import '../../../../../../assets/styles/red-variables';
.filter-menu-options,
.filter-menu-header {

View File

@ -12,9 +12,9 @@ import { handleCheckedValue } from './utils/filter-utils';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
@Component({
selector: 'redaction-filter',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.scss'],
selector: 'redaction-popup-filter',
templateUrl: './popup-filter.component.html',
styleUrls: ['./popup-filter.component.scss'],
providers: [
{
provide: MAT_CHECKBOX_DEFAULT_OPTIONS,
@ -25,7 +25,7 @@ import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
}
]
})
export class FilterComponent implements OnChanges {
export class PopupFilterComponent implements OnChanges {
@Output() filtersChanged = new EventEmitter<{
primary: FilterModel[];
secondary?: FilterModel[];

View File

@ -1,7 +1,7 @@
import { FilterModel } from '../model/filter.model';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { PermissionsService } from '@services/permissions.service';
import { FileStatusWrapper } from '../../../../../../models/file/file-status.wrapper';
import { DossierWrapper } from '../../../../../../state/model/dossier.wrapper';
import { PermissionsService } from '../../../../../../services/permissions.service';
export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) {
copySettings(oldFilters, newFilters);
@ -157,20 +157,23 @@ export const annotationFilterChecker = (
}
};
export const dossierStatusChecker = (pw: DossierWrapper, filter: FilterModel) =>
pw.hasStatus(filter.key);
export const dossierStatusChecker = (dw: DossierWrapper, filter: FilterModel) =>
dw.hasStatus(filter.key);
export const dossierMemberChecker = (pw: DossierWrapper, filter: FilterModel) =>
pw.hasMember(filter.key);
export const dossierMemberChecker = (dw: DossierWrapper, filter: FilterModel) =>
dw.hasMember(filter.key);
export const dossierTemplateChecker = (pw: DossierWrapper, filter: FilterModel) =>
pw.dossierTemplateId === filter.key;
export const dossierTemplateChecker = (dw: DossierWrapper, filter: FilterModel) =>
dw.dossierTemplateId === filter.key;
export const dueDateChecker = (pw: DossierWrapper, filter: FilterModel) =>
pw.dueDateMatches(filter.key);
export const dueDateChecker = (dw: DossierWrapper, filter: FilterModel) =>
dw.dueDateMatches(filter.key);
export const addedDateChecker = (pw: DossierWrapper, filter: FilterModel) =>
pw.addedDateMatches(filter.key);
export const addedDateChecker = (dw: DossierWrapper, filter: FilterModel) =>
dw.addedDateMatches(filter.key);
export const dossierApproverChecker = (dw: DossierWrapper, filter: FilterModel) =>
dw.approverIds.includes(filter.key);
export function getFilteredEntities(
entities: any[],

View File

@ -0,0 +1,8 @@
<div
(click)="toggle(filter)"
*ngFor="let filter of filters"
[class.active]="filter.checked"
class="quick-filter"
>
{{ filter.label | translate }}
</div>

View File

@ -0,0 +1,34 @@
@import '../../../../../../assets/styles/red-variables';
:host {
display: flex;
flex: 1;
justify-content: flex-end;
}
.quick-filter {
box-sizing: border-box;
border: 1px solid $grey-5;
border-radius: 17px;
background-color: $grey-6;
padding: 0 14px;
height: 34px;
display: flex;
align-items: center;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: $white;
}
&.active {
background-color: $white;
font-weight: 600;
border: none;
}
&:not(:last-child) {
margin-right: 8px;
}
}

View File

@ -0,0 +1,29 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FilterModel } from '../popup-filter/model/filter.model';
@Component({
selector: 'redaction-quick-filters',
templateUrl: './quick-filters.component.html',
styleUrls: ['./quick-filters.component.scss']
})
export class QuickFiltersComponent {
@Output() filtersChanged = new EventEmitter<FilterModel[]>();
@Input() filters: FilterModel[];
constructor() {}
get hasActiveFilters(): boolean {
return this.filters.filter(f => f.checked).length > 0;
}
deactivateAllFilters() {
for (const filter of this.filters) {
filter.checked = false;
}
}
toggle(filter: FilterModel) {
filter.checked = !filter.checked;
this.filtersChanged.emit(this.filters);
}
}

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { Color } from '@utils/types';
import { FilterModel } from '../filter/model/filter.model';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
export class DoughnutChartConfig {
value: number;

View File

@ -24,7 +24,6 @@ import { HasScrollbarDirective } from './directives/has-scrollbar.directive';
import { DictionaryAnnotationIconComponent } from './components/dictionary-annotation-icon/dictionary-annotation-icon.component';
import { HiddenActionComponent } from './components/hidden-action/hidden-action.component';
import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component';
import { FilterComponent } from './components/filter/filter.component';
import { EmptyStateComponent } from './components/empty-state/empty-state.component';
import { SortByPipe } from './components/sort-pipe/sort-by.pipe';
import { RoundCheckboxComponent } from './components/checkbox/round-checkbox.component';
@ -35,6 +34,8 @@ import { NavigateLastDossiersScreenDirective } from './directives/navigate-last-
import { DictionaryManagerComponent } from './components/dictionary-manager/dictionary-manager.component';
import { SideNavComponent } from '@shared/components/side-nav/side-nav.component';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { QuickFiltersComponent } from './components/filters/quick-filters/quick-filters.component';
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
const buttons = [
ChevronButtonComponent,
@ -55,7 +56,7 @@ const components = [
StatusBarComponent,
DictionaryAnnotationIconComponent,
HiddenActionComponent,
FilterComponent,
PopupFilterComponent,
ConfirmationDialogComponent,
EmptyStateComponent,
SortByPipe,
@ -84,9 +85,9 @@ const modules = [
];
@NgModule({
declarations: [...components, ...utils],
declarations: [...components, ...utils, QuickFiltersComponent],
imports: [CommonModule, ...modules, MonacoEditorModule],
exports: [...modules, ...components, ...utils],
exports: [...modules, ...components, ...utils, QuickFiltersComponent],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{

View File

@ -10,6 +10,7 @@
"LICENSE_START": "01-01-2021",
"LICENSE_END": "31-12-2021",
"LICENSE_PAGE_COUNT": 1000000,
"RECENT_PERIOD_IN_HOURS": 24,
"MAX_FILE_SIZE_MB": 100,
"DELETE_RETENTION_HOURS": 96
}

View File

@ -137,6 +137,12 @@
},
"no-match": {
"title": "No dossiers match your current filters."
},
"quick-filters": {
"my-dossiers": "My Dossiers",
"to-approve": "To Approve",
"to-review": "To Review",
"other": "Other"
}
},
"add-dossier-dialog": {
@ -327,6 +333,12 @@
"reanalyse-dossier": {
"success": "Files scheduled for reanalysis.",
"error": "Failed to schedule files for reanalysis. Please try again."
},
"quick-filters": {
"recent": "Recent",
"assigned-to-me": "Assigned to me",
"unassigned": "Unassigned",
"assigned-to-others": "Assigned to others"
}
},
"file-preview": {

View File

@ -10,6 +10,7 @@ ADMIN_CONTACT_URL="${ADMIN_CONTACT_URL:-}"
AUTO_READ_TIME="${AUTO_READ_TIME:-1.5}"
MAX_FILE_SIZE_MB="${MAX_FILE_SIZE_MB:-50}"
DELETE_RETENTION_HOURS="${DELETE_RETENTION_HOURS:-96}"
RECENT_PERIOD_IN_HOURS="${RECENT_PERIOD_IN_HOURS:-24}"
BACKEND_APP_VERSION="${BACKEND_APP_VERSION:-4.7.0}"
@ -35,6 +36,7 @@ echo '{
"APP_NAME":"'"$APP_NAME"'",
"AUTO_READ_TIME":'"$AUTO_READ_TIME"',
"MAX_FILE_SIZE_MB":"'"$MAX_FILE_SIZE_MB"'",
"RECENT_PERIOD_IN_HOURS":"'"RECENT_PERIOD_IN_HOURS"'",
"DELETE_RETENTION_HOURS":"'"$DELETE_RETENTION_HOURS"'",
"API_URL":"'"$API_URL"'"
}' > /usr/share/nginx/html/ui/assets/config/config.json