refactor dossier listing and dossier overview, working filters

This commit is contained in:
Dan Percic 2021-07-14 01:40:41 +03:00
parent 204cdc7787
commit 8f22539cfe
33 changed files with 545 additions and 550 deletions

View File

@ -19,7 +19,8 @@
<span class="all-caps-label">
{{
'trash.table-header.title' | translate: { length: displayedEntities.length }
'trash.table-header.title'
| translate: { length: (displayedEntities$ | async)?.length }
}}
</span>
@ -40,7 +41,7 @@
</div>
<div
[class.no-data]="!allEntities.length"
[class.no-data]="(allEntities$ | async)?.length === 0"
class="table-header"
redactionSyncWidth="table-item"
>
@ -75,13 +76,13 @@
</div>
<redaction-empty-state
*ngIf="!allEntities.length"
*ngIf="(allEntities$ | async)?.length === 0"
icon="red:template"
screen="trash"
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
*ngIf="(allEntities$ | async)?.length && (displayedEntities$ | async)?.length === 0"
screen="trash"
type="no-match"
></redaction-empty-state>
@ -89,7 +90,8 @@
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="
let entity of displayedEntities
let entity of displayedEntities$
| async
| sortBy: sortingOption.order:sortingOption.column;
trackBy: trackById
"
@ -148,14 +150,14 @@
</div>
<div class="action-buttons">
<redaction-circle-button
(action)="restore(entity)"
(action)="restore(entity.dossierId)"
icon="red:undo"
[tooltip]="'trash.action.restore' | translate"
type="dark-bg"
></redaction-circle-button>
<redaction-circle-button
(action)="hardDelete(entity)"
(action)="hardDelete(entity.dossierId)"
icon="red:trash"
[tooltip]="'trash.action.delete' | translate"
type="dark-bg"

View File

@ -1,85 +1,88 @@
import { Component, Injector, OnInit } from '@angular/core';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { Dossier, DossierControllerService, DossierTemplateModel } from '@redaction/red-ui-http';
import { Dossier, DossierTemplateModel } from '@redaction/red-ui-http';
import { LoadingService } from '../../../../services/loading.service';
import { AppConfigKey, AppConfigService } from '../../../app-config/app-config.service';
import * as moment from 'moment';
import { TranslateService } from '@ngx-translate/core';
import { FilterService } from '../../../shared/services/filter.service';
import { SearchService } from '../../../shared/services/search.service';
import { ScreenStateService } from '../../../shared/services/screen-state.service';
import { ScreenNames, SortingService } from '../../../../services/sorting.service';
import { NewBaseListingComponent } from '../../../shared/base/new-base-listing.component';
import { DossiersService } from '../../../dossier/services/dossiers.service';
@Component({
templateUrl: './trash-screen.component.html',
styleUrls: ['./trash-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
providers: [FilterService, SearchService, ScreenStateService, SortingService, DossiersService]
})
export class TrashScreenComponent extends BaseListingComponent<Dossier> implements OnInit {
export class TrashScreenComponent extends NewBaseListingComponent<Dossier> implements OnInit {
readonly itemSize = 85;
private readonly _deleteRetentionHours = this._appConfigService.getConfig(
AppConfigKey.DELETE_RETENTION_HOURS
);
protected readonly _searchKey = 'dossierName';
protected readonly _selectionKey = 'dossierId';
constructor(
private readonly _appStateService: AppStateService,
readonly permissionsService: PermissionsService,
protected readonly _injector: Injector,
private readonly _dossierControllerService: DossierControllerService,
private readonly _dossiersService: DossiersService,
private readonly _loadingService: LoadingService,
private readonly _appConfigService: AppConfigService,
private readonly _translateService: TranslateService
private readonly _appConfigService: AppConfigService
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DOSSIER_LISTING);
this._searchService.setSearchKey('dossierName');
this._screenStateService.setSelectionKey('dossierId');
this._screenStateService.setIdKey('dossierId');
}
async ngOnInit(): Promise<void> {
this._loadingService.start();
await this.loadDossierTemplatesData();
this._filterService.setFilters(this._filters);
this._filterService.filterEntities();
this.filterService.filterEntities();
this._loadingService.stop();
}
async loadDossierTemplatesData(): Promise<void> {
this.allEntities = await this._dossierControllerService.getDeletedDossiers().toPromise();
console.log(this.allEntities);
this._executeSearchImmediately();
this._screenStateService.setEntities(await this._dossiersService.getDeletedDossiers());
}
getDossierTemplate(dossierTemplateId: string): DossierTemplateModel {
return this._appStateService.getDossierTemplateById(dossierTemplateId);
}
getRestoreDate(softDeletedTime: string) {
return moment(softDeletedTime).add(this._deleteRetentionHours, 'hours').format();
getRestoreDate(softDeletedTime: string): string {
return moment(softDeletedTime).add(this._deleteRetentionHours, 'hours').toISOString();
}
async restore(dossier: Dossier) {
this._loadingService.start();
await this._dossierControllerService.restoreDossiers([dossier.dossierId]).toPromise();
this.allEntities = this.allEntities.filter(e => e !== dossier);
this._loadingService.stop();
restore(dossierId: string): void {
this._loadingService.loadWhile(this._restore(dossierId));
}
async hardDelete(dossier: Dossier) {
this._loadingService.start();
await this._dossierControllerService.hardDeleteDossiers([dossier.dossierId]).toPromise();
this.allEntities = this.allEntities.filter(e => e !== dossier);
this._loadingService.stop();
hardDelete(dossierId: string): void {
this._loadingService.loadWhile(this._hardDelete(dossierId));
}
trackById(index: number, dossier: Dossier) {
trackById(index: number, dossier: Dossier): string {
return dossier.dossierId;
}
private async _restore(dossierId: string): Promise<void> {
await this._dossiersService.restore(dossierId);
this._removeFromList(dossierId);
}
private async _hardDelete(dossierId: string): Promise<void> {
await this._dossiersService.hardDelete(dossierId);
this._removeFromList(dossierId);
}
private _removeFromList(dossierId: string): void {
const entities = this._screenStateService.entities.filter(e => e.dossierId !== dossierId);
this._screenStateService.setEntities(entities);
this.filterService.filterEntities();
}
}

View File

@ -45,9 +45,9 @@
<div *ngIf="hasFiles" class="mt-24">
<redaction-simple-doughnut-chart
(toggleFilter)="toggleFilter('statusFilters', $event)"
(toggleFilter)="filterService.filterEntities()"
[config]="documentsChartData"
[filter]="filters.statusFilters"
[filter]="filterService.getFilter$('statusFilters') | async"
[radius]="63"
[strokeWidth]="15"
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier'"
@ -57,9 +57,9 @@
<div *ngIf="hasFiles" class="mt-24 legend pb-32">
<div
(click)="toggleFilter('needsWorkFilters', filter.key)"
*ngFor="let filter of filters.needsWorkFilters"
[class.active]="filter.checked"
(click)="filterService.toggleFilter('needsWorkFilters', filter.key)"
*ngFor="let filter of filterService.getFilter$('needsWorkFilters') | async"
[class.active]="filterService.filterChecked$('needsWorkFilters', filter.key) | async"
>
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</div>

View File

@ -1,14 +1,15 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
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 { PermissionsService } from '@services/permissions.service';
import { TranslateChartService } from '@services/translate-chart.service';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { UserService } from '@services/user.service';
import { User } from '@redaction/red-ui-http';
import { NotificationService } from '@services/notification.service';
import { FilterService } from '../../../shared/services/filter.service';
import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper';
@Component({
selector: 'redaction-dossier-details',
@ -19,8 +20,6 @@ export class DossierDetailsComponent implements OnInit {
documentsChartData: DoughnutChartConfig[] = [];
owner: User;
editingOwner = false;
@Input() filters: { needsWorkFilters: FilterModel[]; statusFilters: FilterModel[] };
@Output() filtersChanged = new EventEmitter();
@Output() openAssignDossierMembersDialog = new EventEmitter();
@Output() openDossierDictionaryDialog = new EventEmitter();
@Output() toggleCollapse = new EventEmitter();
@ -29,6 +28,7 @@ export class DossierDetailsComponent implements OnInit {
readonly appStateService: AppStateService,
readonly translateChartService: TranslateChartService,
readonly permissionsService: PermissionsService,
readonly filterService: FilterService<FileStatusWrapper>,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _userService: UserService,
private readonly _notificationService: NotificationService
@ -76,12 +76,6 @@ export class DossierDetailsComponent implements OnInit {
this._changeDetectorRef.detectChanges();
}
toggleFilter(filterType: 'needsWorkFilters' | 'statusFilters', key: string): void {
const filter = this.filters[filterType].find(f => f.key === key);
filter.checked = !filter.checked;
this.filtersChanged.emit(this.filters);
}
async assignOwner(user: User | string) {
this.owner = typeof user === 'string' ? this._userService.getRedUserById(user) : user;
const dw = Object.assign({}, this.appStateService.activeDossier);

View File

@ -26,9 +26,8 @@
</div>
<div>
<redaction-simple-doughnut-chart
(toggleFilter)="toggleFilter($event)"
[config]="documentsChartData"
[filter]="filters"
[filter]="filterService.getFilter$('statusFilters') | async"
[radius]="80"
[strokeWidth]="15"
[subtitle]="'dossier-listing.stats.charts.total-documents'"

View File

@ -1,24 +1,20 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } 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/filters/popup-filter/model/filter.model';
import { FilterService } from '../../../shared/services/filter.service';
@Component({
selector: 'redaction-dossier-listing-details',
templateUrl: './dossier-listing-details.component.html',
styleUrls: ['./dossier-listing-details.component.scss']
styleUrls: ['./dossier-listing-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DossierListingDetailsComponent {
export class DossierListingDetailsComponent<T> {
@Input() dossiersChartData: DoughnutChartConfig[];
@Input() documentsChartData: DoughnutChartConfig[];
@Input() filters: FilterModel[];
@Output() filtersChanged = new EventEmitter();
constructor(readonly appStateService: AppStateService) {}
toggleFilter(key: string): void {
const filter = this.filters.find(f => f.key === key);
filter.checked = !filter.checked;
this.filtersChanged.emit(this.filters);
}
constructor(
readonly appStateService: AppStateService,
readonly filterService: FilterService<T>
) {}
}

View File

@ -1,14 +1,14 @@
<button
(click)="scroll(ButtonType.TOP)"
[hidden]="!showScroll(ButtonType.TOP)"
(click)="scroll(buttonType.TOP)"
[hidden]="!showScroll(buttonType.TOP)"
class="scroll-button top pointer"
>
<mat-icon svgIcon="red:arrow-down-o"></mat-icon>
</button>
<button
(click)="scroll(ButtonType.BOTTOM)"
[hidden]="!showScroll(ButtonType.BOTTOM)"
(click)="scroll(buttonType.BOTTOM)"
[hidden]="!showScroll(buttonType.BOTTOM)"
class="scroll-button bottom pointer"
>
<mat-icon svgIcon="red:arrow-down-o"></mat-icon>

View File

@ -12,7 +12,7 @@ enum ButtonType {
styleUrls: ['./scroll-button.component.scss']
})
export class ScrollButtonComponent {
ButtonType = ButtonType;
buttonType = ButtonType;
@Input()
scrollViewport: CdkVirtualScrollViewport;

View File

@ -47,6 +47,7 @@ import { ScrollButtonComponent } from './components/scroll-button/scroll-button.
import { ChangeLegalBasisDialogComponent } from './dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component';
import { RecategorizeImageDialogComponent } from './dialogs/recategorize-image-dialog/recategorize-image-dialog.component';
import { DossiersService } from './services/dossiers.service';
const screens = [
DossierListingScreenComponent,
@ -97,6 +98,7 @@ const components = [
];
const services = [
DossiersService,
DossiersDialogService,
FileActionService,
AnnotationActionsService,

View File

@ -1,6 +1,5 @@
<section>
<redaction-page-header
[filterConfigs]="filterConfigs"
[buttonConfigs]="buttonConfigs"
[searchPlaceholder]="'dossier-listing.search' | translate"
></redaction-page-header>
@ -13,14 +12,11 @@
<span class="all-caps-label">
{{
'dossier-listing.table-header.title'
| translate: { length: (displayed$ | async)?.length || 0 }
| translate: { length: (displayedEntities$ | async)?.length || 0 }
}}
</span>
<redaction-quick-filters
(filtersChanged)="filtersChanged($event)"
[filters]="quickFilters"
></redaction-quick-filters>
<redaction-quick-filters></redaction-quick-filters>
</div>
<div class="table-header" redactionSyncWidth="table-item">
@ -50,14 +46,14 @@
<redaction-empty-state
(action)="openAddDossierDialog()"
*ngIf="(entities$ | async)?.length === 0"
*ngIf="(allEntities$ | async)?.length === 0"
[showButton]="permissionsService.isManager()"
icon="red:folder"
screen="dossier-listing"
></redaction-empty-state>
<redaction-empty-state
*ngIf="(entities$ | async)?.length && (displayed$ | async)?.length === 0"
*ngIf="(allEntities$ | async)?.length && (displayedEntities$ | async)?.length === 0"
screen="dossier-listing"
type="no-match"
></redaction-empty-state>
@ -65,7 +61,7 @@
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="
let dw of displayed$
let dw of displayedEntities$
| async
| sortBy: sortingOption.order:sortingOption.column
"
@ -90,7 +86,7 @@
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ filesCount(dw) }}
{{ dw.files.length }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
@ -123,7 +119,7 @@
</div>
<div class="status-container">
<redaction-dossier-listing-actions
(actionPerformed)="actionPerformed()"
(actionPerformed)="calculateData()"
[dossier]="dw"
></redaction-dossier-listing-actions>
</div>
@ -139,11 +135,9 @@
<div class="right-container" redactionHasScrollbar>
<redaction-dossier-listing-details
(filtersChanged)="filtersChanged($event)"
*ngIf="(entities$ | async)?.length !== 0"
*ngIf="(allEntities$ | async)?.length !== 0"
[documentsChartData]="documentsChartData"
[dossiersChartData]="dossiersChartData"
[filters]="detailsContainerFilters"
></redaction-dossier-listing-details>
</div>
</div>

View File

@ -1,4 +1,12 @@
import { Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
Injector,
OnDestroy,
OnInit,
TemplateRef,
ViewChild
} from '@angular/core';
import { Dossier, DossierTemplateModel } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
@ -12,7 +20,7 @@ import { filter, tap } from 'rxjs/operators';
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 { NavigationStart, Router } from '@angular/router';
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';
@ -20,22 +28,20 @@ import {
annotationFilterChecker,
dossierMemberChecker,
dossierStatusChecker,
dossierTemplateChecker,
processFilters
dossierTemplateChecker
} from '@shared/components/filters/popup-filter/utils/filter-utils';
import { UserPreferenceService } from '../../../../services/user-preference.service';
import { FilterConfig } from '../../../shared/components/page-header/models/filter-config.model';
import { ButtonConfig } from '../../../shared/components/page-header/models/button-config.model';
import { FilterService } from '../../../shared/services/filter.service';
import { SearchService } from '../../../shared/services/search.service';
import { ScreenStateService } from '../../../shared/services/screen-state.service';
import { NewBaseListingComponent } from '../../../shared/base/new-base-listing.component';
import { FilterWrapper } from '../../../shared/components/filters/popup-filter/model/filter-wrapper.model';
import { ScreenNames, SortingService } from '../../../../services/sorting.service';
@Component({
templateUrl: './dossier-listing-screen.component.html',
styleUrls: ['./dossier-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierListingScreenComponent
@ -44,13 +50,6 @@ export class DossierListingScreenComponent
{
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
statusFilters: FilterModel[];
peopleFilters: FilterModel[];
needsWorkFilters: FilterModel[];
dossierTemplateFilters: FilterModel[];
detailsContainerFilters: FilterModel[] = [];
quickFilters: FilterModel[];
filterConfigs: FilterConfig[];
buttonConfigs: ButtonConfig[] = [
{
label: this._translateService.instant('dossier-listing.add-new'),
@ -89,67 +88,27 @@ export class DossierListingScreenComponent
this._loadEntitiesFromState();
}
get activeDossiersCount(): number {
return this._screenStateService.entities.filter(
p => p.dossier.status === Dossier.StatusEnum.ACTIVE
).length;
}
get inactiveDossiersCount(): number {
return this._screenStateService.entities.length - this.activeDossiersCount;
}
get displayed$(): Observable<DossierWrapper[]> {
return this._screenStateService.displayedEntities$;
}
get entities$(): Observable<DossierWrapper[]> {
return this._screenStateService.entities$;
}
protected get _filters(): FilterWrapper[] {
return [
{ values: this.statusFilters, checker: dossierStatusChecker },
{ values: this.peopleFilters, checker: dossierMemberChecker },
{
values: this.needsWorkFilters,
checker: annotationFilterChecker,
matchAll: true,
checkerArgs: this.permissionsService
},
{ values: this.dossierTemplateFilters, checker: dossierTemplateChecker },
{
values: this.quickFilters,
checker: (dw: DossierWrapper) =>
this.quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
}
];
}
ngOnInit(): void {
this._calculateData();
this.calculateData();
this._dossierAutoUpdateTimer = timer(0, 10000)
.pipe(
tap(async () => {
await this._appStateService.loadAllDossiers();
this._loadEntitiesFromState();
this.calculateData();
})
)
.subscribe();
this._fileChangedSub = this._appStateService.fileChanged.subscribe(() => {
this._calculateData();
this.calculateData();
});
this._routerEventsScrollPositionSub = this._router.events
.pipe(
filter(
events => events instanceof NavigationStart || events instanceof NavigationEnd
)
)
.subscribe(event => {
if (event instanceof NavigationStart && event.url !== '/main/dossiers') {
.pipe(filter(event => event instanceof NavigationStart))
.subscribe((event: NavigationStart) => {
if (event.url !== '/main/dossiers') {
this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top');
}
});
@ -172,17 +131,12 @@ export class DossierListingScreenComponent
this._fileChangedSub.unsubscribe();
}
filesCount(dossier: DossierWrapper) {
return dossier.files.length;
}
getDossierTemplate(dw: DossierWrapper): DossierTemplateModel {
return this._appStateService.getDossierTemplateById(dw.dossier.dossierTemplateId);
}
openAddDossierDialog(): void {
this._dialogService.openAddDossierDialog(async addResponse => {
this._calculateData();
await this._router.navigate([`/main/dossiers/${addResponse.dossier.dossierId}`]);
if (addResponse.addMembers) {
this._dialogService.openDialog('editDossier', null, {
@ -193,16 +147,8 @@ export class DossierListingScreenComponent
});
}
actionPerformed() {
this._calculateData();
}
filtersChanged(event) {
this._filterService.filtersChanged(event);
}
protected _preFilter() {
this.detailsContainerFilters = this.statusFilters.map(f => ({ ...f }));
filtersChanged() {
this.filterService.filterEntities();
}
private _loadEntitiesFromState() {
@ -213,19 +159,26 @@ export class DossierListingScreenComponent
return this._userService.user;
}
private _calculateData() {
private get _activeDossiersCount(): number {
return this._screenStateService.entities.filter(
p => p.dossier.status === Dossier.StatusEnum.ACTIVE
).length;
}
private get _inactiveDossiersCount(): number {
return this._screenStateService.entities.length - this._activeDossiersCount;
}
calculateData() {
this._computeAllFilters();
this._filterService.setFilters(this._filters);
this._filterService.setPreFilters(() => this._preFilter());
this._filterService.filterEntities();
this.dossiersChartData = [
{ value: this.activeDossiersCount, color: 'ACTIVE', label: 'active' },
{ value: this.inactiveDossiersCount, color: 'DELETED', label: 'archived' }
{ value: this._activeDossiersCount, color: 'ACTIVE', label: 'active' },
{ value: this._inactiveDossiersCount, color: 'DELETED', label: 'archived' }
];
const groups = groupBy(this._appStateService.aggregatedFiles, 'status');
this.documentsChartData = [];
for (const key of Object.keys(groups)) {
this.documentsChartData.push({
value: groups[key].length,
@ -245,6 +198,7 @@ export class DossierListingScreenComponent
const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctDossierTemplates = new Set<string>();
this._screenStateService?.entities?.forEach(entry => {
// all people
entry.dossier.memberIds.forEach(f => allDistinctPeople.add(f));
@ -259,114 +213,105 @@ export class DossierListingScreenComponent
if (entry.hasNone) allDistinctNeedsWork.add('none');
});
// Rule set
allDistinctDossierTemplates.add(entry.dossierTemplateId);
});
const statusFilters = [];
allDistinctFileStatus.forEach(status => {
statusFilters.push({
key: status,
label: this._translateService.instant(status)
});
});
statusFilters.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]);
this.statusFilters = processFilters(this.statusFilters, statusFilters);
const statusFilters = [...allDistinctFileStatus].map<FilterModel>(status => ({
key: status,
label: this._translateService.instant(status)
}));
const peopleFilters = [];
allDistinctPeople.forEach(userId => {
peopleFilters.push({
key: userId,
label: this._userService.getNameForId(userId)
});
});
this.peopleFilters = processFilters(this.peopleFilters, peopleFilters);
const needsWorkFilters = [];
allDistinctNeedsWork.forEach(type => {
needsWorkFilters.push({
key: type,
label: `filter.${type}`
});
this.filterService.addFilter({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
values: statusFilters.sort(StatusSorter.byKey),
checker: dossierStatusChecker
});
needsWorkFilters.sort(
(a, b) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
);
this.needsWorkFilters = processFilters(this.needsWorkFilters, needsWorkFilters);
const peopleFilters = [...allDistinctPeople].map<FilterModel>(userId => ({
key: userId,
label: this._userService.getNameForId(userId)
}));
const dossierTemplateFilters = [];
allDistinctDossierTemplates.forEach(dossierTemplateId => {
dossierTemplateFilters.push({
key: dossierTemplateId,
label: this._appStateService.getDossierTemplateById(dossierTemplateId).name
});
this.filterService.addFilter({
slug: 'peopleFilters',
label: this._translateService.instant('filters.people'),
icon: 'red:user',
values: peopleFilters,
checker: dossierMemberChecker
});
this.dossierTemplateFilters = processFilters(
this.dossierTemplateFilters,
dossierTemplateFilters
);
this._createFilterConfigs();
this._createQuickFilters();
}
const needsWorkFilters = [...allDistinctNeedsWork].map<FilterModel>(type => ({
key: type,
label: `filter.${type}`
}));
private _createFilterConfigs() {
this.filterConfigs = [
{
label: this._translateService.instant('filters.status'),
primaryFilters: this.statusFilters,
icon: 'red:status'
},
{
label: this._translateService.instant('filters.people'),
primaryFilters: this.peopleFilters,
icon: 'red:user'
},
{
label: this._translateService.instant('filters.needs-work'),
primaryFilters: this.needsWorkFilters,
icon: 'red:needs-work',
filterTemplate: this._needsWorkTemplate
},
{
label: this._translateService.instant('filters.dossier-templates'),
primaryFilters: this.dossierTemplateFilters,
icon: 'red:template',
hide: this.dossierTemplateFilters.length <= 1
}
];
this.filterService.addFilter({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: this._needsWorkTemplate,
values: needsWorkFilters.sort(RedactionFilterSorter.byKey),
checker: annotationFilterChecker,
matchAll: true,
checkerArgs: this.permissionsService
});
const dossierTemplateFilters = [...allDistinctDossierTemplates].map<FilterModel>(id => ({
key: id,
label: this._appStateService.getDossierTemplateById(id).name
}));
this.filterService.addFilter({
slug: 'dossierTemplateFilters',
label: this._translateService.instant('filters.dossier-templates'),
icon: 'red:template',
hide: this.filterService.getFilter('dossierTemplateFilters')?.values?.length <= 1,
values: dossierTemplateFilters,
checker: dossierTemplateChecker
});
const quickFilters = this._createQuickFilters();
this.filterService.addFilter({
slug: 'quickFilters',
values: quickFilters,
checker: (dw: DossierWrapper) =>
quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
});
this.filterService.filterEntities();
}
private _createQuickFilters() {
const myDossiersLabel = this._translateService.instant(
'dossier-listing.quick-filters.my-dossiers'
);
const filters: FilterModel[] = [
{
key: this._user.id,
label: this._translateService.instant('dossier-listing.quick-filters.my-dossiers'),
key: 'my-dossiers',
label: myDossiersLabel,
checker: (dw: DossierWrapper) => dw.ownerId === this._user.id
},
{
key: this._user.id,
key: 'to-approve',
label: this._translateService.instant('dossier-listing.quick-filters.to-approve'),
checker: (dw: DossierWrapper) => dw.approverIds.includes(this._user.id)
},
{
key: this._user.id,
key: 'to-review',
label: this._translateService.instant('dossier-listing.quick-filters.to-review'),
checker: (dw: DossierWrapper) => dw.memberIds.includes(this._user.id)
},
{
key: this._user.id,
key: 'other',
label: this._translateService.instant('dossier-listing.quick-filters.other'),
checker: (dw: DossierWrapper) => !dw.memberIds.includes(this._user.id)
}
];
this.quickFilters = filters.filter(
f =>
f.label ===
this._translateService.instant('dossier-listing.quick-filters.my-dossiers') ||
this._userPreferenceService.areDevFeaturesEnabled
return filters.filter(
f => f.label === myDossiersLabel || this._userPreferenceService.areDevFeaturesEnabled
);
}
}

View File

@ -1,6 +1,5 @@
<section *ngIf="!!activeDossier">
<redaction-page-header
[filterConfigs]="filterConfigs"
[actionConfigs]="actionConfigs"
[showCloseButton]="true"
[searchPlaceholder]="'dossier-overview.search' | translate"
@ -8,7 +7,7 @@
<redaction-file-download-btn
[disabled]="areSomeEntitiesSelected"
[dossier]="activeDossier"
[file]="allEntities"
[file]="allEntities$ | async"
tooltipPosition="below"
></redaction-file-download-btn>
@ -49,23 +48,20 @@
<span class="all-caps-label">
{{
'dossier-overview.table-header.title'
| translate: { length: displayedEntities.length || 0 }
| translate: { length: (displayedEntities$ | async)?.length || 0 }
}}
</span>
<redaction-dossier-overview-bulk-actions
(reload)="bulkActionPerformed()"
[selectedFileIds]="selectedEntitiesIds"
[selectedFileIds]="selectedEntitiesIds$ | async"
></redaction-dossier-overview-bulk-actions>
<redaction-quick-filters
(filtersChanged)="filtersChanged($event)"
[filters]="quickFilters"
></redaction-quick-filters>
<redaction-quick-filters></redaction-quick-filters>
</div>
<div
[class.no-data]="!allEntities.length"
[class.no-data]="(allEntities$ | async).length === 0"
class="table-header"
redactionSyncWidth="table-item"
>
@ -122,14 +118,14 @@
<redaction-empty-state
(action)="fileInput.click()"
*ngIf="!allEntities.length"
*ngIf="(allEntities$ | async)?.length === 0"
buttonIcon="red:upload"
icon="red:document"
screen="dossier-overview"
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
*ngIf="(allEntities$ | async)?.length && (displayedEntities$ | async).length === 0"
screen="dossier-overview"
type="no-match"
></redaction-empty-state>
@ -137,9 +133,10 @@
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="
let fileStatus of displayedEntities
let fileStatus of displayedEntities$
| async
| sortBy: sortingOption.order:sortingOption.column;
trackBy: fileId
trackBy: trackByFileId
"
[class.disabled]="fileStatus.isExcluded"
[class.last-opened]="isLastOpenedFile(fileStatus)"
@ -268,11 +265,9 @@
<div [class.collapsed]="collapsedDetails" class="right-container" redactionHasScrollbar>
<redaction-dossier-details
(filtersChanged)="filtersChanged($event)"
(openAssignDossierMembersDialog)="openAssignDossierMembersDialog()"
(openDossierDictionaryDialog)="openDossierDictionaryDialog()"
(toggleCollapse)="toggleCollapsedDetails()"
[filters]="detailsContainerFilters"
></redaction-dossier-details>
</div>
</div>

View File

@ -1,4 +1,5 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
HostListener,
@ -28,13 +29,11 @@ import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { convertFiles, handleFileDrop } from '@utils/file-drop-utils';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
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
keyChecker
} from '@shared/components/filters/popup-filter/utils/filter-utils';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { AppConfigKey, AppConfigService } from '../../../app-config/app-config.service';
@ -44,6 +43,7 @@ import { FilterService } from '../../../shared/services/filter.service';
import { SearchService } from '../../../shared/services/search.service';
import { ScreenStateService } from '../../../shared/services/screen-state.service';
import { ScreenNames, SortingService } from '../../../../services/sorting.service';
import { NewBaseListingComponent } from '../../../shared/base/new-base-listing.component';
@Component({
templateUrl: './dossier-overview-screen.component.html',
@ -51,25 +51,14 @@ import { ScreenNames, SortingService } from '../../../../services/sorting.servic
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierOverviewScreenComponent
extends BaseListingComponent<FileStatusWrapper>
extends NewBaseListingComponent<FileStatusWrapper>
implements OnInit, OnDestroy, OnDetach, OnAttach
{
statusFilters: FilterModel[];
peopleFilters: FilterModel[];
needsWorkFilters: FilterModel[];
collapsedDetails = false;
detailsContainerFilters: {
needsWorkFilters: FilterModel[];
statusFilters: FilterModel[];
} = { needsWorkFilters: [], statusFilters: [] };
readonly itemSize = 80;
quickFilters: FilterModel[];
filterConfigs: FilterConfig[];
actionConfigs: ActionConfig[];
protected readonly _searchKey = 'searchField';
protected readonly _selectionKey = 'fileId';
@ViewChild(DossierDetailsComponent, { static: false })
private readonly _dossierDetailsComponent: DossierDetailsComponent;
private _filesAutoUpdateTimer: Subscription;
@ -95,10 +84,13 @@ export class DossierOverviewScreenComponent
private readonly _appStateService: AppStateService,
private readonly _userPreferenceControllerService: UserPreferenceControllerService,
private readonly _appConfigService: AppConfigService,
private readonly _changeDetectorRef: ChangeDetectorRef,
protected readonly _injector: Injector
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DOSSIER_OVERVIEW);
this._searchService.setSearchKey('searchField');
this._screenStateService.setIdKey('fileId');
this._loadEntitiesFromState();
}
@ -111,39 +103,15 @@ export class DossierOverviewScreenComponent
}
get checkedRequiredFilters() {
return this.quickFilters.filter(f => f.required && f.checked);
return this.filterService
.getFilter('quickFilters')
?.values.filter(f => f.required && f.checked);
}
get checkedNotRequiredFilters() {
return this.quickFilters.filter(f => !f.required && f.checked);
}
protected get _filters(): {
values: FilterModel[];
checker: Function;
matchAll?: boolean;
checkerArgs?: any;
}[] {
return [
{ values: this.statusFilters, checker: keyChecker('status') },
{ values: this.peopleFilters, checker: keyChecker('currentReviewer') },
{
values: this.needsWorkFilters,
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
))
}
];
return this.filterService
.getFilter('quickFilters')
?.values.filter(f => !f.required && f.checked);
}
isLastOpenedFile(fileStatus: FileStatusWrapper): boolean {
@ -241,17 +209,18 @@ export class DossierOverviewScreenComponent
}
calculateData(): void {
if (!this._appStateService.activeDossierId) {
return;
}
if (!this._appStateService.activeDossierId) return;
this._loadEntitiesFromState();
this._computeAllFilters();
this._filterEntities();
this.filterService.filterEntities();
this._dossierDetailsComponent?.calculateChartConfig();
this._changeDetectorRef.detectChanges();
}
fileId(index, item) {
trackByFileId(index: number, item: FileStatusWrapper) {
return item.fileId;
}
@ -278,7 +247,7 @@ export class DossierOverviewScreenComponent
}
bulkActionPerformed() {
this.selectedEntitiesIds = [];
this._screenStateService.selectedEntitiesIds$.next([]);
this.reloadDossiers();
}
@ -296,9 +265,7 @@ export class DossierOverviewScreenComponent
dossierWrapper: this.activeDossier,
section: 'members'
},
() => {
this.reloadDossiers();
}
() => this.reloadDossiers()
);
}
@ -317,15 +284,8 @@ export class DossierOverviewScreenComponent
.add(this._appConfigService.getConfig(AppConfigKey.RECENT_PERIOD_IN_HOURS), 'hours')
.isAfter(moment());
protected _preFilter() {
this.detailsContainerFilters = {
needsWorkFilters: this.needsWorkFilters.map(f => ({ ...f })),
statusFilters: this.statusFilters.map(f => ({ ...f }))
};
}
private _loadEntitiesFromState() {
if (this.activeDossier) this.allEntities = this.activeDossier.files;
if (this.activeDossier) this._screenStateService.setEntities(this.activeDossier.files);
}
private async _uploadFiles(files: FileUploadModel[]) {
@ -333,7 +293,7 @@ export class DossierOverviewScreenComponent
if (fileCount) {
this._statusOverlayService.openUploadStatusOverlay();
}
this._changeDetectorRef.detectChanges();
// this._changeDetectorRef.detectChanges();
}
private _computeAllFilters() {
@ -344,7 +304,7 @@ export class DossierOverviewScreenComponent
const allDistinctAddedDates = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
this.allEntities.forEach(file => {
this._screenStateService.entities.forEach(file => {
allDistinctPeople.add(file.currentReviewer);
allDistinctFileStatusWrapper.add(file.status);
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'));
@ -359,13 +319,18 @@ export class DossierOverviewScreenComponent
if (file.hasNone) allDistinctNeedsWork.add('none');
});
const statusFilters = [...allDistinctFileStatusWrapper].map(item => ({
const statusFilters = [...allDistinctFileStatusWrapper].map<FilterModel>(item => ({
key: item,
label: this._translateService.instant(item)
}));
statusFilters.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]);
this.statusFilters = processFilters(this.statusFilters, statusFilters);
this.filterService.addFilter({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
values: statusFilters.sort(StatusSorter.byKey),
checker: keyChecker('status')
});
const peopleFilters = [];
if (allDistinctPeople.has(undefined) || allDistinctPeople.has(null)) {
@ -382,30 +347,54 @@ export class DossierOverviewScreenComponent
label: this._userService.getNameForId(userId)
});
});
this.peopleFilters = processFilters(this.peopleFilters, peopleFilters);
this.filterService.addFilter({
slug: 'peopleFilters',
label: this._translateService.instant('filters.assigned-people'),
icon: 'red:user',
values: peopleFilters,
checker: keyChecker('currentReviewer')
});
const needsWorkFilters = [...allDistinctNeedsWork].map(item => ({
const needsWorkFilters = [...allDistinctNeedsWork].map<FilterModel>(item => ({
key: item,
label: this._translateService.instant('filter.' + item)
}));
needsWorkFilters.sort(
(a, b) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
);
this.needsWorkFilters = processFilters(this.needsWorkFilters, needsWorkFilters);
this._createQuickFilters();
this._createFilterConfigs();
this.filterService.addFilter({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: this._needsWorkTemplate,
values: needsWorkFilters.sort(RedactionFilterSorter.byKey),
checker: annotationFilterChecker,
matchAll: true,
checkerArgs: this.permissionsService
});
this.filterService.addFilter({
slug: 'quickFilters',
values: this._createQuickFilters(),
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
))
});
this._createActionConfigs();
}
private _createQuickFilters() {
if (this.allEntities.filter(this.recentlyModifiedChecker).length > 0) {
let quickFilters = [];
if (this._screenStateService.entities.filter(this.recentlyModifiedChecker).length > 0) {
const recentPeriod = this._appConfigService.getConfig(
AppConfigKey.RECENT_PERIOD_IN_HOURS
);
this.quickFilters = [
quickFilters = [
{
key: this.user.id,
key: 'recent',
label: this._translateService.instant('dossier-overview.quick-filters.recent', {
hours: recentPeriod
}),
@ -413,26 +402,24 @@ export class DossierOverviewScreenComponent
checker: this.recentlyModifiedChecker
}
];
} else {
this.quickFilters = [];
}
this.quickFilters = [
...this.quickFilters,
return [
...quickFilters,
{
key: this.user.id,
key: 'assigned-to-me',
label: this._translateService.instant(
'dossier-overview.quick-filters.assigned-to-me'
),
checker: (file: FileStatusWrapper) => file.currentReviewer === this.user.id
},
{
key: this.user.id,
key: 'unassigned',
label: this._translateService.instant('dossier-overview.quick-filters.unassigned'),
checker: (file: FileStatusWrapper) => !file.currentReviewer
},
{
key: this.user.id,
key: 'assigned-to-others',
label: this._translateService.instant(
'dossier-overview.quick-filters.assigned-to-others'
),
@ -442,27 +429,6 @@ export class DossierOverviewScreenComponent
];
}
private _createFilterConfigs() {
this.filterConfigs = [
{
label: this._translateService.instant('filters.status'),
primaryFilters: this.statusFilters,
icon: 'red:status'
},
{
label: this._translateService.instant('filters.assigned-people'),
primaryFilters: this.peopleFilters,
icon: 'red:user'
},
{
label: this._translateService.instant('filters.needs-work'),
primaryFilters: this.needsWorkFilters,
icon: 'red:needs-work',
filterTemplate: this._needsWorkTemplate
}
];
}
private _createActionConfigs() {
this.actionConfigs = [
{

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { DossierControllerService } from '@redaction/red-ui-http';
@Injectable()
export class DossiersService {
constructor(private readonly _dossierControllerService: DossierControllerService) {}
getDeletedDossiers() {
return this._dossierControllerService.getDeletedDossiers().toPromise();
}
restore(dossierIds: string | Array<string>): Promise<unknown> {
if (typeof dossierIds === 'string') dossierIds = [dossierIds];
return this._dossierControllerService.restoreDossiers(dossierIds).toPromise();
}
hardDelete(dossierIds: string | Array<string>): Promise<unknown> {
if (typeof dossierIds === 'string') dossierIds = [dossierIds];
return this._dossierControllerService.hardDeleteDossiers(dossierIds).toPromise();
}
}

View File

@ -9,6 +9,7 @@ import { SearchService } from '../services/search.service';
import { ScreenStateService } from '../services/screen-state.service';
import { getFilteredEntities } from '../components/filters/popup-filter/utils/filter-utils';
import { debounce } from '../../../utils/debounce';
import { FilterWrapper } from '../components/filters/popup-filter/model/filter-wrapper.model';
// Functionalities: Filter, search, select, sort
@ -37,7 +38,7 @@ export abstract class BaseListingComponent<T = any> {
protected readonly _selectionKey: string;
// Overwrite this in ngOnInit
@ViewChild(QuickFiltersComponent)
protected _quickFilters: QuickFiltersComponent;
protected _quickFilters: QuickFiltersComponent<T>;
private _searchValue = '';
@ -65,12 +66,7 @@ export abstract class BaseListingComponent<T = any> {
return this._sortingService.getSortingOption();
}
protected get _filters(): {
values: FilterModel[];
checker: Function;
matchAll?: boolean;
checkerArgs?: any;
}[] {
protected get _filters(): FilterWrapper[] {
return [];
}
@ -103,7 +99,6 @@ export abstract class BaseListingComponent<T = any> {
}
resetFilters() {
this._quickFilters.deactivateFilters();
this.showResetFilters = false;
this.filtersChanged();
}

View File

@ -1,47 +1,80 @@
import { Component, Injector, ViewChild } from '@angular/core';
import { ScreenName, SortingOption, SortingService } from '@services/sorting.service';
import { FilterModel } from '../components/filters/popup-filter/model/filter.model';
import { QuickFiltersComponent } from '../components/filters/quick-filters/quick-filters.component';
import { SortingOption, SortingService } from '@services/sorting.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { FilterService } from '../services/filter.service';
import { SearchService } from '../services/search.service';
import { ScreenStateService } from '../services/screen-state.service';
import { FilterWrapper } from '../components/filters/popup-filter/model/filter-wrapper.model';
import { Observable } from 'rxjs';
import { FilterModel } from '../components/filters/popup-filter/model/filter.model';
@Component({ template: '' })
export abstract class NewBaseListingComponent<T = any> {
@ViewChild(CdkVirtualScrollViewport) scrollViewport: CdkVirtualScrollViewport;
export abstract class NewBaseListingComponent<T> {
@ViewChild(CdkVirtualScrollViewport)
readonly scrollViewport: CdkVirtualScrollViewport;
readonly filterService: FilterService<T>;
protected readonly _sortingService: SortingService;
protected readonly _filterService: FilterService<T>;
protected readonly _searchService: SearchService<T>;
protected readonly _screenStateService: ScreenStateService<T>;
// Overwrite this in ngOnInit
@ViewChild(QuickFiltersComponent)
protected _quickFilters: QuickFiltersComponent;
protected constructor(protected readonly _injector: Injector) {
this.filterService = this._injector.get<FilterService<T>>(FilterService);
this._sortingService = this._injector.get<SortingService>(SortingService);
this._filterService = this._injector.get<FilterService<T>>(FilterService);
this._searchService = this._injector.get<SearchService<T>>(SearchService);
this._screenStateService = this._injector.get<ScreenStateService<T>>(ScreenStateService);
}
get selectedEntitiesIds$(): Observable<string[]> {
return this._screenStateService.selectedEntitiesIds$;
}
get displayedEntities$(): Observable<T[]> {
return this._screenStateService.displayedEntities$;
}
get allEntities$(): Observable<T[]> {
return this._screenStateService.entities$;
}
get areAllEntitiesSelected() {
return this._screenStateService.areAllEntitiesSelected;
}
get areSomeEntitiesSelected() {
return this._screenStateService.areSomeEntitiesSelected;
}
get sortingOption(): SortingOption {
return this._sortingService.getSortingOption();
}
getFilter$(slug: string): Observable<FilterModel[]> {
return this.filterService.getFilter$(slug);
}
protected get _filters(): FilterWrapper[] {
return [];
}
resetFilters() {
this._quickFilters.deactivateFilters();
this._filterService.reset();
this.filterService.reset();
}
toggleSort($event) {
this._sortingService.toggleSort($event);
}
toggleSelectAll() {
return this._screenStateService.toggleSelectAll();
}
toggleEntitySelected(event: MouseEvent, entity: T) {
event.stopPropagation();
return this._screenStateService.toggleEntitySelected(entity);
}
isSelected(entity: T) {
return this._screenStateService.isSelected(entity);
}
}

View File

@ -1,6 +1,12 @@
import { FilterModel } from './filter.model';
import { TemplateRef } from '@angular/core';
export interface FilterWrapper {
slug: string;
label?: string;
icon?: string;
filterTemplate?: TemplateRef<any>;
hide?: boolean;
values: FilterModel[];
checker: Function;
matchAll?: boolean;

View File

@ -1,5 +1,4 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
@ -17,7 +16,6 @@ import { TranslateService } from '@ngx-translate/core';
selector: 'redaction-popup-filter',
templateUrl: './popup-filter.component.html',
styleUrls: ['./popup-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: MAT_CHECKBOX_DEFAULT_OPTIONS,

View File

@ -1,6 +1,6 @@
<div
(click)="toggle(filter)"
*ngFor="let filter of filters"
(click)="filterService.toggleFilter('quickFilters', filter.key)"
*ngFor="let filter of filterService.getFilter$('quickFilters') | async"
[class.active]="filter.checked"
class="quick-filter"
>

View File

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

View File

@ -1,9 +1,10 @@
import { Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'redaction-full-page-loading-indicator',
templateUrl: './full-page-loading-indicator.component.html',
styleUrls: ['./full-page-loading-indicator.component.scss']
styleUrls: ['./full-page-loading-indicator.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FullPageLoadingIndicatorComponent {
@Input() displayed = false;

View File

@ -3,6 +3,7 @@ import { TemplateRef } from '@angular/core';
import { BaseHeaderConfig } from './base-config.model';
export interface FilterConfig extends BaseHeaderConfig {
primaryFilters: FilterModel[];
primaryFilters?: FilterModel[];
primaryFiltersLabel?: string;
filterTemplate?: TemplateRef<any>;
}

View File

@ -1,16 +1,16 @@
<div class="page-header">
<div *ngIf="pageLabel" class="breadcrumb">{{ pageLabel }}</div>
<div class="filters" *ngIf="filterConfigs">
<div class="filters" *ngIf="filters$ | async as filters">
<div translate="filters.filter-by"></div>
<ng-container *ngFor="let config of filterConfigs; trackBy: trackByLabel">
<ng-container *ngFor="let config of filters; trackBy: trackByLabel">
<redaction-popup-filter
(filtersChanged)="filterService.filtersChanged($event)"
(filtersChanged)="filterService.filterEntities()"
*ngIf="!config.hide"
[filterLabel]="config.label"
[icon]="config.icon"
[primaryFilters]="config.primaryFilters"
[primaryFilters]="config.values"
[filterTemplate]="config.filterTemplate"
></redaction-popup-filter>
</ng-container>
@ -23,7 +23,7 @@
<div
(click)="resetFilters()"
*ngIf="hasActiveFilters"
*ngIf="(filterService.showResetFilters$ | async) || searchService.searchValue"
class="reset-filters"
translate="reset-filters"
></div>

View File

@ -1,55 +1,39 @@
import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core';
import { Component, Input } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import { FilterConfig } from '@shared/components/page-header/models/filter-config.model';
import { ActionConfig } from '@shared/components/page-header/models/action-config.model';
import { ButtonConfig } from '@shared/components/page-header/models/button-config.model';
import { BaseHeaderConfig } from '@shared/components/page-header/models/base-config.model';
import { FilterService } from '@shared/services/filter.service';
import { SearchService } from '@shared/services/search.service';
import { map } from 'rxjs/operators';
@Component({
selector: 'redaction-page-header',
templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
styleUrls: ['./page-header.component.scss']
})
export class PageHeaderComponent<T> {
@Input() pageLabel: string;
@Input() showCloseButton: boolean;
@Input() filterConfigs: FilterConfig[];
@Input() actionConfigs: ActionConfig[];
@Input() buttonConfigs: ButtonConfig[];
@Input() searchPlaceholder: string;
@ViewChildren(PopupFilterComponent)
private readonly _filterComponents: QueryList<PopupFilterComponent>;
constructor(
readonly permissionsService: PermissionsService,
readonly filterService: FilterService<T>,
readonly searchService: SearchService<T>
) {}
get hasActiveFilters() {
const hasActiveFilters = this._filterComponents?.reduce(
(acc, component) => acc || component?.hasActiveFilters,
false
);
return (
hasActiveFilters ||
this.searchService.searchValue ||
this.filterService.showResetFilters
);
get filters$() {
return this.filterService.allFilters$.pipe(map(all => all.filter(f => f.icon)));
}
resetFilters() {
this.filterService.reset();
this._filterComponents.forEach(component => component?.deactivateFilters());
this.searchService.reset();
}
trackByLabel(index: number, item: BaseHeaderConfig) {
trackByLabel(index: number, item) {
return item.label;
}
}

View File

@ -6,7 +6,7 @@
attr.width="{{ size }}"
class="donut-chart"
>
<g *ngFor="let value of parsedConfig; let i = index">
<g *ngFor="let value of config; let i = index">
<circle
*ngIf="exists(i)"
[attr.stroke]="value.color.includes('#') ? value.color : ''"
@ -34,9 +34,9 @@
<div class="breakdown-container">
<div>
<div
(click)="selectValue(val)"
*ngFor="let val of parsedConfig"
[class.active]="val.checked"
(click)="selectValue(val.key)"
*ngFor="let val of config"
[class.active]="filterService.filterChecked$('statusFilters', val.key) | async"
[class.filter-disabled]="!filter"
>
<redaction-status-bar

View File

@ -1,8 +1,9 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { Color } from '@utils/types';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { FilterService } from '@shared/services/filter.service';
export class DoughnutChartConfig {
export interface DoughnutChartConfig {
value: number;
color: Color;
label: string;
@ -15,7 +16,7 @@ export class DoughnutChartConfig {
templateUrl: './simple-doughnut-chart.component.html',
styleUrls: ['./simple-doughnut-chart.component.scss']
})
export class SimpleDoughnutChartComponent implements OnChanges {
export class SimpleDoughnutChartComponent<T> implements OnChanges {
@Input() subtitle: string;
@Input() config: DoughnutChartConfig[] = [];
@Input() radius = 85;
@ -32,16 +33,8 @@ export class SimpleDoughnutChartComponent implements OnChanges {
cx = 0;
cy = 0;
size = 0;
parsedConfig: {
color: Color;
active?: boolean;
checked: boolean;
label: string;
value: number;
key?: string;
}[];
constructor() {}
constructor(readonly filterService: FilterService<T>) {}
get circumference() {
return 2 * Math.PI * this.radius;
@ -60,10 +53,6 @@ export class SimpleDoughnutChartComponent implements OnChanges {
this.cx = this.radius + this.strokeWidth / 2;
this.cy = this.radius + this.strokeWidth / 2;
this.size = this.strokeWidth + this.radius * 2;
this.parsedConfig = this.config.map(el => ({
...el,
checked: this.filter?.find(f => f.key === el.key)?.checked
}));
}
calculateChartData() {
@ -102,8 +91,10 @@ export class SimpleDoughnutChartComponent implements OnChanges {
: `${config.label} (${config.value} ${this.counterText})`;
}
selectValue(val: any) {
this.toggleFilter.emit(val.key);
selectValue(key: string) {
this.filterService.toggleFilter('statusFilters', key);
this.filterService.filterEntities();
this.toggleFilter.emit(key);
}
exists(index: number) {

View File

@ -1,15 +1,18 @@
import { ChangeDetectorRef, Injectable } from '@angular/core';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { getFilteredEntities } from '@shared/components/filters/popup-filter/utils/filter-utils';
import {
getFilteredEntities,
processFilters
} from '@shared/components/filters/popup-filter/utils/filter-utils';
import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
import { ScreenStateService } from '@shared/services/screen-state.service';
import { SearchService } from '@shared/services/search.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
@Injectable()
export class FilterService<T> {
showResetFilters = false;
_preFilter: () => void;
_filters: FilterWrapper[];
_allFilters$ = new BehaviorSubject<FilterWrapper[]>([]);
constructor(
private readonly _screenStateService: ScreenStateService<T>,
@ -17,35 +20,34 @@ export class FilterService<T> {
private readonly _changeDetector: ChangeDetectorRef
) {}
setFilters(value: FilterWrapper[]) {
this._filters = value;
get filters() {
return Object.values(this._allFilters$.getValue());
}
setPreFilters(value: () => void) {
this._preFilter = value;
get showResetFilters$() {
return this.allFilters$.pipe(
map(all => this._toFlatFilters(all)),
filter(f => !!f.find(el => el.checked)),
distinctUntilChanged()
);
}
get filters(): FilterWrapper[] {
if (this._filters === undefined || this._filters === null) throw new Error('Unset filters');
return this._filters;
filterChecked$(slug: string, key: string) {
const filters = this.getFilter$(slug);
return filters.pipe(map(all => all.find(f => f.key === key)?.checked));
}
filtersChanged(filters?: { [key: string]: FilterModel[] } | FilterModel[]): void {
if (filters instanceof Array) this.showResetFilters = !!filters.find(f => f.checked);
else
for (const key of Object.keys(filters ?? {})) {
for (let idx = 0; idx < this[key]?.length; ++idx) {
this[key][idx] = filters[key][idx];
}
}
toggleFilter(slug: string, key: string) {
const filters = this.filters.find(f => f.slug === slug);
let found = filters.values.find(f => f.key === key);
if (!found) found = filters.values.map(f => f.filters?.find(ff => ff.key === key))[0];
found.checked = !found.checked;
this._allFilters$.next(this.filters);
this.filterEntities();
}
filterEntities(): void {
if (this._preFilter) this._preFilter();
const filtered = getFilteredEntities(this._screenStateService.entities, this.filters);
this._screenStateService.setFilteredEntities(filtered);
this._searchService.executeSearchImmediately();
@ -53,8 +55,48 @@ export class FilterService<T> {
this._changeDetector.detectChanges();
}
addFilter(value: FilterWrapper): void {
const oldFilters = this.getFilter(value.slug)?.values;
if (!oldFilters) return this._allFilters$.next([...this.filters, value]);
value.values = processFilters(oldFilters, value.values);
this._allFilters$.next([...this.filters.filter(f => f.slug !== value.slug), value]);
}
getFilter(slug: string): FilterWrapper {
return this.filters.find(f => f?.slug === slug);
}
getFilter$(slug: string): Observable<FilterModel[]> {
return this.getFilterWrapper$(slug).pipe(map(f => f.values));
}
getFilterWrapper$(slug: string): Observable<FilterWrapper> {
return this.allFilters$.pipe(map(all => all.find(f => f.slug === slug)));
}
get allFilters$(): Observable<FilterWrapper[]> {
return this._allFilters$.asObservable();
}
reset(): void {
this.showResetFilters = false;
this.filtersChanged();
this.filters.forEach(item => {
item.values.forEach(child => {
child.checked = false;
child.indeterminate = false;
child.filters?.forEach(f => {
f.checked = false;
f.indeterminate = false;
});
});
});
this.filterEntities();
}
private _toFlatFilters(entities: FilterWrapper[]): FilterModel[] {
const flatChildren = (filters: FilterModel[]) =>
(filters ?? []).reduce((acc, f) => [...acc, ...(f?.filters ?? [])], []);
return entities.reduce((acc, f) => [...acc, ...f.values, ...flatChildren(f.values)], []);
}
}

View File

@ -1,14 +1,16 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
@Injectable()
export class ScreenStateService<T> {
entities$ = new BehaviorSubject<Array<T>>([]);
entities$ = new BehaviorSubject<T[]>([]);
filteredEntities$ = new BehaviorSubject<T[]>([]);
displayedEntities$ = new BehaviorSubject<T[]>([]);
selectedEntitiesIds: string[] = [];
selectedEntitiesIds$ = new BehaviorSubject<string[]>([]);
private _idKey: string;
private _setSelectionKey: string;
get entities(): T[] {
return Object.values(this.entities$.getValue());
}
@ -17,85 +19,94 @@ export class ScreenStateService<T> {
return Object.values(this.filteredEntities$.getValue());
}
get selectedEntitiesIds(): string[] {
return Object.values(this.selectedEntitiesIds$.getValue());
}
get displayedEntities(): T[] {
return Object.values(this.displayedEntities$.getValue());
}
//
// select<K>(mapFn: (state: T[]) => K): Observable<K> {
// return this.entities$.asObservable().pipe(
// map((state: T[]) => mapFn(state)),
// distinctUntilChanged()
// );
// }
setEntities(newEntities: Partial<T[]>) {
map<K>(func: (state: T[]) => K): Observable<K> {
return this.entities$.asObservable().pipe(
map((state: T[]) => func(state)),
distinctUntilChanged()
);
}
setEntities(newEntities: Partial<T[]>): void {
this.entities$.next(newEntities);
}
setFilteredEntities(newEntities: Partial<T[]>) {
setFilteredEntities(newEntities: Partial<T[]>): void {
this.filteredEntities$.next(newEntities);
}
setDisplayedEntities(newEntities: Partial<T[]>) {
setSelectedEntitiesIds(newEntities: Partial<string[]>): void {
this.selectedEntitiesIds$.next(newEntities);
}
setDisplayedEntities(newEntities: Partial<T[]>): void {
this.displayedEntities$.next(newEntities);
}
setSelectionKey(value: string) {
this._setSelectionKey = value;
setIdKey(value: string): void {
this._idKey = value;
}
get areAllEntitiesSelected() {
get areAllEntitiesSelected(): boolean {
return (
this.displayedEntities.length !== 0 &&
this.selectedEntitiesIds.length === this.displayedEntities.length
);
}
get areSomeEntitiesSelected() {
get areSomeEntitiesSelected(): boolean {
return this.selectedEntitiesIds.length > 0;
}
isSelected(entity: T) {
return this.selectedEntitiesIds.indexOf(entity[this._getSelectionKey]) !== -1;
isSelected(entity: T): boolean {
return this.selectedEntitiesIds.indexOf(entity[this._getIdKey]) !== -1;
}
toggleEntitySelected(entity: T) {
const idx = this.selectedEntitiesIds.indexOf(entity[this._getSelectionKey]);
if (idx === -1) {
this.selectedEntitiesIds.push(entity[this._getSelectionKey]);
} else {
this.selectedEntitiesIds.splice(idx, 1);
toggleEntitySelected(entity: T): void {
const currentEntityIdx = this.selectedEntitiesIds.indexOf(entity[this._getIdKey]);
if (currentEntityIdx === -1) {
const currentEntityId = entity[this._getIdKey];
return this.setSelectedEntitiesIds([...this.selectedEntitiesIds, currentEntityId]);
}
this.setSelectedEntitiesIds(this.selectedEntitiesIds.splice(currentEntityIdx, 1));
}
toggleSelectAll() {
if (this.areSomeEntitiesSelected) {
this.selectedEntitiesIds = [];
} else {
this.selectedEntitiesIds = this.displayedEntities.map(
entity => entity[this._getSelectionKey]
);
}
toggleSelectAll(): void {
if (this.areSomeEntitiesSelected) return this.selectedEntitiesIds$.next([]);
this.setSelectedEntitiesIds(this._displayedEntitiesIds);
}
updateSelection() {
if (this._setSelectionKey) {
this.selectedEntitiesIds = this.displayedEntities
.map(entity => entity[this._getSelectionKey])
.filter(id => this.selectedEntitiesIds.includes(id));
}
updateSelection(): void {
if (!this._idKey) return;
const ids = this._displayedEntitiesIds.filter(id => this.selectedEntitiesIds.includes(id));
this.setSelectedEntitiesIds(ids);
}
logCurrentState() {
logCurrentState(): void {
console.log('Entities', this.entities);
console.log('Displayed', this.displayedEntities$.getValue());
console.log('Filtered', this.filteredEntities$.getValue());
console.log('Displayed', this.displayedEntities);
console.log('Filtered', this.filteredEntities);
console.log('Selected', this.selectedEntitiesIds);
}
private get _getSelectionKey(): string {
if (!this._setSelectionKey) throw new Error('Not implemented');
private get _displayedEntitiesIds(): string[] {
return this.displayedEntities.map(entity => entity[this._getIdKey]);
}
return this._setSelectionKey;
private get _getIdKey(): string {
if (!this._idKey) throw new Error('Not implemented');
return this._idKey;
}
}

View File

@ -6,7 +6,7 @@ import { FormBuilder } from '@angular/forms';
@Injectable()
export class SearchService<T> {
private _searchValue = '';
private _setSearchKey: string;
private _searchKey: string;
readonly searchForm = this._formBuilder.group({
query: ['']
@ -21,37 +21,35 @@ export class SearchService<T> {
@debounce(200)
executeSearch(): void {
this._searchValue = this.searchValue;
return this.executeSearchImmediately();
this._searchValue = this.searchValue.toLowerCase();
this.executeSearchImmediately();
}
executeSearchImmediately(): void {
const displayed = (
this._screenStateService.filteredEntities ?? this._screenStateService.entities
).filter(entity =>
this._searchField(entity).toLowerCase().includes(this._searchValue.toLowerCase())
);
this._screenStateService.filteredEntities || this._screenStateService.entities
).filter(entity => this._searchField(entity).toLowerCase().includes(this._searchValue));
this._screenStateService.setDisplayedEntities(displayed);
this._screenStateService.updateSelection();
}
setSearchKey(value: string) {
this._setSearchKey = value;
setSearchKey(value: string): void {
this._searchKey = value;
}
get searchValue(): string {
return this.searchForm.get('query').value;
}
reset() {
reset(): void {
this.searchForm.reset({ query: '' });
}
private get _getSearchKey(): string {
if (!this._setSearchKey) throw new Error('Not implemented');
if (!this._searchKey) throw new Error('Not implemented');
return this._setSearchKey;
return this._searchKey;
}
protected _searchField(entity: T): string {

View File

@ -15,19 +15,32 @@ export class LoadingService {
}
start(): void {
this._loadingEvent.next(true);
// setTimeout is used so that value doesn't change after it was checked for changes
setTimeout(() => this._loadingEvent.next(true));
this._loadingStarted = new Date().getTime();
}
stop(): void {
const timeDelta = new Date().getTime() - this._loadingStarted;
if (timeDelta < MIN_LOADING_TIME) {
setTimeout(() => {
this._loadingEvent.next(false);
}, MIN_LOADING_TIME - timeDelta);
return;
}
const timeSinceStarted = new Date().getTime() - this._loadingStarted;
const remainingLoadingTime = MIN_LOADING_TIME - timeSinceStarted;
return remainingLoadingTime > 0 ? this._stopAfter(remainingLoadingTime) : this._stop();
}
loadWhile(func: Promise<void>) {
this.start();
func.then(
() => this.stop(),
() => this.stop()
);
}
private _stop() {
this._loadingEvent.next(false);
}
private _stopAfter(timeout: number) {
setTimeout(() => this._stop(), timeout);
}
}

View File

@ -5,5 +5,7 @@ export const RedactionFilterSorter = {
image: 3,
hint: 4,
suggestion: 5,
none: 6
none: 6,
byKey: (a: { key: string }, b: { key: string }) =>
RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
};

View File

@ -8,5 +8,6 @@ export const StatusSorter = {
UNASSIGNED: 10,
UNDER_REVIEW: 15,
UNDER_APPROVAL: 20,
APPROVED: 25
APPROVED: 25,
byKey: (a: { key: string }, b: { key: string }) => StatusSorter[a.key] - StatusSorter[b.key]
};

View File

@ -11,7 +11,8 @@
*/ /* tslint:disable:no-unused-variable member-ordering */
import { Inject, Injectable, Optional } from '@angular/core';
import { HttpClient, HttpEvent, HttpHeaders, HttpResponse } from '@angular/common/http';
import { HttpClient, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
import { CustomHttpUrlEncodingCodec } from '../encoder';
import { Observable } from 'rxjs';
@ -388,8 +389,13 @@ export class DossierControllerService {
headers = headers.set('Authorization', 'Bearer ' + accessToken);
}
let queryParameters = new HttpParams({ encoder: new CustomHttpUrlEncodingCodec() });
for (const dossierId of body) {
queryParameters = queryParameters.set('dossierId', dossierId);
}
// to determine the Accept header
const httpHeaderAccepts: string[] = [];
const httpHeaderAccepts: string[] = ['application/json'];
const httpHeaderAcceptSelected: string | undefined =
this.configuration.selectHeaderAccept(httpHeaderAccepts);
if (httpHeaderAcceptSelected !== undefined) {
@ -397,7 +403,7 @@ export class DossierControllerService {
}
// to determine the Content-Type header
const consumes: string[] = ['*/*'];
const consumes: string[] = ['application/json'];
const httpContentTypeSelected: string | undefined =
this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
@ -409,6 +415,7 @@ export class DossierControllerService {
`${this.basePath}/deleted-dossiers/hard-delete`,
{
body: body,
params: queryParameters,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
@ -461,6 +468,11 @@ export class DossierControllerService {
headers = headers.set('Authorization', 'Bearer ' + accessToken);
}
let queryParameters = new HttpParams({ encoder: new CustomHttpUrlEncodingCodec() });
for (const dossierId of body) {
queryParameters = queryParameters.set('dossierId', dossierId);
}
// to determine the Accept header
const httpHeaderAccepts: string[] = [];
const httpHeaderAcceptSelected: string | undefined =
@ -479,6 +491,7 @@ export class DossierControllerService {
return this.httpClient.request<any>('post', `${this.basePath}/deleted-dossiers/restore`, {
body: body,
params: queryParameters,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,