diff --git a/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.html index e8f0dd7dc..995d027c0 100644 --- a/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/trash/trash-screen.component.html @@ -19,7 +19,8 @@ {{ - 'trash.table-header.title' | translate: { length: displayedEntities.length } + 'trash.table-header.title' + | translate: { length: (displayedEntities$ | async)?.length } }} @@ -40,7 +41,7 @@
@@ -75,13 +76,13 @@
@@ -89,7 +90,8 @@
implements OnInit { +export class TrashScreenComponent extends NewBaseListingComponent 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 { this._loadingService.start(); await this.loadDossierTemplatesData(); - this._filterService.setFilters(this._filters); - this._filterService.filterEntities(); + this.filterService.filterEntities(); this._loadingService.stop(); } async loadDossierTemplatesData(): Promise { - 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 { + await this._dossiersService.restore(dossierId); + this._removeFromList(dossierId); + } + + private async _hardDelete(dossierId: string): Promise { + 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(); + } } diff --git a/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.html b/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.html index 760dddfcd..11992f128 100644 --- a/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.html @@ -45,9 +45,9 @@
diff --git a/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.ts b/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.ts index 4d0212f09..2f3e35e54 100644 --- a/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/dossier-details/dossier-details.component.ts @@ -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, 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); diff --git a/apps/red-ui/src/app/modules/dossier/components/dossier-listing-details/dossier-listing-details.component.html b/apps/red-ui/src/app/modules/dossier/components/dossier-listing-details/dossier-listing-details.component.html index 1487103d7..1bb665327 100644 --- a/apps/red-ui/src/app/modules/dossier/components/dossier-listing-details/dossier-listing-details.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/dossier-listing-details/dossier-listing-details.component.html @@ -26,9 +26,8 @@
{ @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 + ) {} } diff --git a/apps/red-ui/src/app/modules/dossier/components/scroll-button/scroll-button.component.html b/apps/red-ui/src/app/modules/dossier/components/scroll-button/scroll-button.component.html index 0ca366bbc..a0319cacd 100644 --- a/apps/red-ui/src/app/modules/dossier/components/scroll-button/scroll-button.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/scroll-button/scroll-button.component.html @@ -1,14 +1,14 @@
@@ -50,14 +46,14 @@ @@ -65,7 +61,7 @@
- {{ filesCount(dw) }} + {{ dw.files.length }}
@@ -123,7 +119,7 @@
@@ -139,11 +135,9 @@
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.ts index d4aefdd9c..0d8a7354d 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.ts @@ -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 { - return this._screenStateService.displayedEntities$; - } - - get entities$(): Observable { - 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(); const allDistinctNeedsWork = new Set(); const allDistinctDossierTemplates = new Set(); + 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(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(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(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(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 ); } } diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html index 726486399..fcf01bb2a 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html @@ -1,6 +1,5 @@
@@ -49,23 +48,20 @@ {{ 'dossier-overview.table-header.title' - | translate: { length: displayedEntities.length || 0 } + | translate: { length: (displayedEntities$ | async)?.length || 0 } }} - +
@@ -122,14 +118,14 @@ @@ -137,9 +133,10 @@
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.ts index d64614254..1f6d1a578 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.ts @@ -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 + extends NewBaseListingComponent 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(); const allDistinctNeedsWork = new Set(); - 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(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(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 = [ { diff --git a/apps/red-ui/src/app/modules/dossier/services/dossiers.service.ts b/apps/red-ui/src/app/modules/dossier/services/dossiers.service.ts new file mode 100644 index 000000000..554967f65 --- /dev/null +++ b/apps/red-ui/src/app/modules/dossier/services/dossiers.service.ts @@ -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): Promise { + if (typeof dossierIds === 'string') dossierIds = [dossierIds]; + return this._dossierControllerService.restoreDossiers(dossierIds).toPromise(); + } + + hardDelete(dossierIds: string | Array): Promise { + if (typeof dossierIds === 'string') dossierIds = [dossierIds]; + return this._dossierControllerService.hardDeleteDossiers(dossierIds).toPromise(); + } +} diff --git a/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts b/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts index 1aac87bc6..4592e307b 100644 --- a/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts +++ b/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts @@ -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 { protected readonly _selectionKey: string; // Overwrite this in ngOnInit @ViewChild(QuickFiltersComponent) - protected _quickFilters: QuickFiltersComponent; + protected _quickFilters: QuickFiltersComponent; private _searchValue = ''; @@ -65,12 +66,7 @@ export abstract class BaseListingComponent { 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 { } resetFilters() { - this._quickFilters.deactivateFilters(); this.showResetFilters = false; this.filtersChanged(); } diff --git a/apps/red-ui/src/app/modules/shared/base/new-base-listing.component.ts b/apps/red-ui/src/app/modules/shared/base/new-base-listing.component.ts index 62af13e62..2c2ac8e6c 100644 --- a/apps/red-ui/src/app/modules/shared/base/new-base-listing.component.ts +++ b/apps/red-ui/src/app/modules/shared/base/new-base-listing.component.ts @@ -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 { - @ViewChild(CdkVirtualScrollViewport) scrollViewport: CdkVirtualScrollViewport; +export abstract class NewBaseListingComponent { + @ViewChild(CdkVirtualScrollViewport) + readonly scrollViewport: CdkVirtualScrollViewport; + readonly filterService: FilterService; protected readonly _sortingService: SortingService; - protected readonly _filterService: FilterService; protected readonly _searchService: SearchService; protected readonly _screenStateService: ScreenStateService; - // Overwrite this in ngOnInit - @ViewChild(QuickFiltersComponent) - protected _quickFilters: QuickFiltersComponent; - protected constructor(protected readonly _injector: Injector) { + this.filterService = this._injector.get>(FilterService); this._sortingService = this._injector.get(SortingService); - this._filterService = this._injector.get>(FilterService); this._searchService = this._injector.get>(SearchService); this._screenStateService = this._injector.get>(ScreenStateService); } + get selectedEntitiesIds$(): Observable { + return this._screenStateService.selectedEntitiesIds$; + } + + get displayedEntities$(): Observable { + return this._screenStateService.displayedEntities$; + } + + get allEntities$(): Observable { + 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 { + 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); + } } diff --git a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts index 9e1a890c5..1b19edc1d 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts +++ b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts @@ -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; + hide?: boolean; values: FilterModel[]; checker: Function; matchAll?: boolean; diff --git a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/popup-filter.component.ts b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/popup-filter.component.ts index 0064a4f40..7cca79894 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/popup-filter.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/popup-filter.component.ts @@ -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, diff --git a/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.html b/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.html index 964328673..59f0d5177 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.html +++ b/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.html @@ -1,6 +1,6 @@
diff --git a/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.ts b/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.ts index fcb682f08..15fb86b2f 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/filters/quick-filters/quick-filters.component.ts @@ -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 { @Output() filtersChanged = new EventEmitter(); - @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) {} } diff --git a/apps/red-ui/src/app/modules/shared/components/full-page-loading-indicator/full-page-loading-indicator.component.ts b/apps/red-ui/src/app/modules/shared/components/full-page-loading-indicator/full-page-loading-indicator.component.ts index 6cdd92f19..d03e2ea20 100644 --- a/apps/red-ui/src/app/modules/shared/components/full-page-loading-indicator/full-page-loading-indicator.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/full-page-loading-indicator/full-page-loading-indicator.component.ts @@ -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; diff --git a/apps/red-ui/src/app/modules/shared/components/page-header/models/filter-config.model.ts b/apps/red-ui/src/app/modules/shared/components/page-header/models/filter-config.model.ts index 29b749703..a4474cde2 100644 --- a/apps/red-ui/src/app/modules/shared/components/page-header/models/filter-config.model.ts +++ b/apps/red-ui/src/app/modules/shared/components/page-header/models/filter-config.model.ts @@ -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; } diff --git a/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html b/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html index 3e960cc89..4e2d0ac7d 100644 --- a/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html +++ b/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html @@ -1,16 +1,16 @@