refactor dossier listing and dossier overview, working filters
This commit is contained in:
parent
204cdc7787
commit
8f22539cfe
@ -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"
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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'"
|
||||
|
||||
@ -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>
|
||||
) {}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -12,7 +12,7 @@ enum ButtonType {
|
||||
styleUrls: ['./scroll-button.component.scss']
|
||||
})
|
||||
export class ScrollButtonComponent {
|
||||
ButtonType = ButtonType;
|
||||
buttonType = ButtonType;
|
||||
|
||||
@Input()
|
||||
scrollViewport: CdkVirtualScrollViewport;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = [
|
||||
{
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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>) {}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)], []);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
};
|
||||
|
||||
@ -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]
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user