Pull request #240: RED-1564

Merge in RED/ui from RED-1564 to master

* commit '51417fb3ae9530c5acbad5c9dd5ee71c2e3c7875':
  temporary fix active filter
  fix rebase issues
  change base listing component
  update disctionary listing, fix toggle select
  update restore icon
  trash screen fixes + update other listing screens
  working trash screen
  refactor dossier listing and dossier overview, working filters
  update filter, sorting and search services
  fix error handler, change listing details filters
  refactor sorting service
  split listing component into services
  show time to restore
  add actions to page header, reset quick filters from page header
  fix rebase
  wip page header component
  fix rebase
  created trash screen with placeholders
  use querylist to get popup filters
  add padding to user dropdown actions
This commit is contained in:
Timo Bejan 2021-07-14 15:43:04 +02:00
commit b70d711f87
71 changed files with 1759 additions and 1074 deletions

View File

@ -94,30 +94,26 @@
*ngIf="userPreferenceService.areDevFeaturesEnabled"
class="mr-8"
></redaction-notifications>
<redaction-user-button
[matMenuTriggerFor]="userMenu"
[showDot]="showPendingDownloadsDot"
[user]="user"
></redaction-user-button>
<mat-menu #userMenu="matMenu" class="user-menu" xPosition="before">
<button
[routerLink]="'/main/my-profile'"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.my-profile"
></button>
<button
(click)="appStateService.reset()"
*ngIf="permissionsService.isManager() || permissionsService.isUserAdmin()"
[routerLink]="'/main/admin'"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.admin"
></button>
<button
*ngIf="permissionsService.isUser()"
[routerLink]="'/main/downloads'"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.downloads"
></button>
<mat-menu #userMenu="matMenu" xPosition="before">
<ng-container *ngFor="let item of userMenuItems; trackBy: trackByName">
<button
(click)="(item.action)"
*ngIf="item.show"
[routerLink]="item.routerLink"
mat-menu-item
translate
>
{{ item.name }}
</button>
</ng-container>
<button (click)="logout()" mat-menu-item>
<mat-icon svgIcon="red:logout"></mat-icon>
<span translate="top-bar.navigation-items.my-account.children.logout"> </span>

View File

@ -4,32 +4,58 @@ import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { Router } from '@angular/router';
import { AppConfigService } from '@app-config/app-config.service';
import { Title } from '@angular/platform-browser';
import { FileDownloadService } from '@upload-download/services/file-download.service';
import { StatusOverlayService } from '@upload-download/services/status-overlay.service';
import { TranslateService } from '@ngx-translate/core';
interface MenuItem {
name: string;
routerLink?: string;
show: boolean;
action?: () => void;
}
@Component({
selector: 'redaction-base-screen',
templateUrl: './base-screen.component.html',
styleUrls: ['./base-screen.component.scss']
})
export class BaseScreenComponent {
readonly userMenuItems: MenuItem[] = [
{
name: 'top-bar.navigation-items.my-account.children.my-profile',
routerLink: '/main/my-profile',
show: true
},
{
name: 'top-bar.navigation-items.my-account.children.admin',
routerLink: '/main/admin',
show: this.permissionsService.isManager() || this.permissionsService.isUserAdmin(),
action: this.appStateService.reset
},
{
name: 'top-bar.navigation-items.my-account.children.downloads',
routerLink: '/main/downloads',
show: this.permissionsService.isUser()
},
{
name: 'top-bar.navigation-items.my-account.children.trash',
routerLink: '/main/admin/trash',
show: this.permissionsService.isManager() || this.permissionsService.isUserAdmin()
}
];
constructor(
readonly appStateService: AppStateService,
readonly permissionsService: PermissionsService,
readonly userPreferenceService: UserPreferenceService,
readonly titleService: Title,
readonly fileDownloadService: FileDownloadService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _appConfigService: AppConfigService,
private readonly _router: Router,
private readonly _userService: UserService,
private readonly _translateService: TranslateService
) {
_router.events.subscribe(() => {
this._dossiersView = this._router.url.indexOf('/main/dossiers') === 0;
this._dossiersView = _router.url.indexOf('/main/dossiers') === 0;
});
}
@ -54,4 +80,8 @@ export class BaseScreenComponent {
logout() {
this._userService.logout();
}
trackByName(index: number, item: MenuItem) {
return item.name;
}
}

View File

@ -19,6 +19,7 @@ import { RouterModule } from '@angular/router';
import { SmtpConfigScreenComponent } from './screens/smtp-config/smtp-config-screen.component';
import { ReportsScreenComponent } from './screens/reports/reports-screen.component';
import { DossierAttributesListingScreenComponent } from './screens/dossier-attributes-listing/dossier-attributes-listing-screen.component';
import { TrashScreenComponent } from './screens/trash/trash-screen.component';
const routes = [
{ path: '', redirectTo: 'dossier-templates', pathMatch: 'full' },
@ -151,6 +152,14 @@ const routes = [
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'trash',
component: TrashScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
}
];

View File

@ -38,6 +38,7 @@ import { ResetPasswordComponent } from './dialogs/add-edit-user-dialog/reset-pas
import { UserDetailsComponent } from './dialogs/add-edit-user-dialog/user-details/user-details.component';
import { AddEditDossierAttributeDialogComponent } from './dialogs/add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component';
import { DossierAttributesListingScreenComponent } from './screens/dossier-attributes-listing/dossier-attributes-listing-screen.component';
import { TrashScreenComponent } from './screens/trash/trash-screen.component';
const dialogs = [
AddEditDossierTemplateDialogComponent,
@ -66,7 +67,8 @@ const screens = [
WatermarkScreenComponent,
SmtpConfigScreenComponent,
ReportsScreenComponent,
DossierAttributesListingScreenComponent
DossierAttributesListingScreenComponent,
TrashScreenComponent
];
const components = [

View File

@ -5,8 +5,7 @@
icon="red:trash"
tooltip="dossier-templates-listing.action.delete"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-circle-button
(action)="openEditDossierTemplateDialog($event)"
@ -14,6 +13,5 @@
icon="red:edit"
tooltip="dossier-templates-listing.action.edit"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
</div>

View File

@ -3,7 +3,7 @@
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="areSomeEntitiesSelected && !areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
@ -13,22 +13,20 @@
}}
</span>
<ng-container *ngIf="areSomeEntitiesSelected">
<ng-container *ngIf="areSomeEntitiesSelected$ | async">
<redaction-circle-button
[matMenuTriggerFor]="readOnlyMenu"
icon="red:read-only"
tooltip="file-attributes-csv-import.table-header.actions.read-only"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-circle-button
(action)="deactivateSelection()"
icon="red:trash"
tooltip="file-attributes-csv-import.table-header.actions.remove-selected"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<div class="separator"></div>
@ -92,7 +90,7 @@
</div>
<redaction-empty-state
*ngIf="!allEntities.length"
*ngIf="(allEntities$ | async)?.length === 0"
icon="red:attribute"
screen="file-attributes-csv-import"
></redaction-empty-state>
@ -102,7 +100,7 @@
<div
(mouseenter)="setHoveredColumn.emit(field.csvColumn)"
(mouseleave)="setHoveredColumn.emit()"
*cdkVirtualFor="let field of displayedEntities"
*cdkVirtualFor="let field of displayedEntities$ | async"
class="table-item"
>
<div (click)="toggleEntitySelected($event, field)" class="selection-column">
@ -127,23 +125,20 @@
icon="red:edit"
tooltip="file-attributes-csv-import.action.edit-name"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<ng-container *ngIf="field.editingName">
<redaction-circle-button
(action)="field.editingName = false; field.name = field.temporaryName"
icon="red:check"
tooltip="file-attributes-csv-import.action.save-name"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-circle-button
(action)="field.editingName = false; field.temporaryName = field.name"
icon="red:close"
tooltip="file-attributes-csv-import.action.cancel-edit-name"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
</ng-container>
</div>
<div>
@ -174,8 +169,7 @@
icon="red:trash"
tooltip="file-attributes-csv-import.action.remove"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>

View File

@ -7,18 +7,23 @@ import {
Output,
SimpleChanges
} from '@angular/core';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { Field } from '../file-attributes-csv-import-dialog.component';
import { FileAttributeConfig } from '@redaction/red-ui-http';
import { FilterService } from '../../../../shared/services/filter.service';
import { SearchService } from '../../../../shared/services/search.service';
import { ScreenStateService } from '../../../../shared/services/screen-state.service';
import { SortingService } from '../../../../../services/sorting.service';
import { BaseListingComponent } from '../../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-active-fields-listing',
templateUrl: './active-fields-listing.component.html',
styleUrls: ['./active-fields-listing.component.scss']
styleUrls: ['./active-fields-listing.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class ActiveFieldsListingComponent extends BaseListingComponent<Field> implements OnChanges {
@Input() allEntities: Field[];
@Output() allEntitiesChange = new EventEmitter<Field[]>();
@Input() entities: Field[];
@Output() entitiesChange = new EventEmitter<Field[]>();
@Output() setHoveredColumn = new EventEmitter<string>();
@Output() toggleFieldActive = new EventEmitter<Field>();
@ -28,16 +33,16 @@ export class ActiveFieldsListingComponent extends BaseListingComponent<Field> im
FileAttributeConfig.TypeEnum.DATE
];
protected readonly _selectionKey = 'csvColumn';
constructor(protected readonly _injector: Injector) {
super(_injector);
this._screenStateService.setIdKey('csvColumn');
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.allEntities) {
this.displayedEntities = this.allEntities;
this._updateSelection();
if (changes.entities) {
this._screenStateService.setEntities(this.entities);
this._screenStateService.setDisplayedEntities(this.entities);
this._screenStateService.updateSelection();
}
}
@ -45,13 +50,15 @@ export class ActiveFieldsListingComponent extends BaseListingComponent<Field> im
this.allEntities
.filter(field => this.isSelected(field))
.forEach(field => (field.primaryAttribute = false));
this.allEntities = [...this.allEntities.filter(field => !this.isSelected(field))];
this.allEntitiesChange.emit(this.allEntities);
this.selectedEntitiesIds = [];
this._screenStateService.setEntities([
...this.allEntities.filter(field => !this.isSelected(field))
]);
this.entitiesChange.emit(this.allEntities);
this._screenStateService.setSelectedEntitiesIds([]);
}
setAttributeForSelection(attribute: string, value: any) {
for (const csvColumn of this.selectedEntitiesIds) {
for (const csvColumn of this._screenStateService.selectedEntitiesIds) {
this.allEntities.find(f => f.csvColumn === csvColumn)[attribute] = value;
}
}

View File

@ -119,7 +119,7 @@
(click)="toggleFieldActive(field)"
(mouseenter)="setHoveredColumn(field.csvColumn)"
(mouseleave)="setHoveredColumn()"
*ngFor="let field of displayedEntities"
*ngFor="let field of displayedEntities$ | async"
class="csv-header-pill-wrapper"
>
<div [class.selected]="isActive(field)" class="csv-header-pill">
@ -177,7 +177,7 @@
<redaction-active-fields-listing
(setHoveredColumn)="setHoveredColumn($event)"
(toggleFieldActive)="toggleFieldActive($event)"
[(allEntities)]="activeFields"
[(entities)]="activeFields"
></redaction-active-fields-listing>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { Component, Inject, Injector } from '@angular/core';
import { AbstractControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { AppStateService } from '@state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as Papa from 'papaparse';
@ -10,9 +10,13 @@ import {
} from '@redaction/red-ui-http';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { NotificationService, NotificationType } from '@services/notification.service';
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 { SortingService } from '../../../../services/sorting.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
export interface Field {
id?: string;
@ -28,7 +32,8 @@ export interface Field {
@Component({
selector: 'redaction-file-attributes-csv-import-dialog',
templateUrl: './file-attributes-csv-import-dialog.component.html',
styleUrls: ['./file-attributes-csv-import-dialog.component.scss']
styleUrls: ['./file-attributes-csv-import-dialog.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class FileAttributesCsvImportDialogComponent extends BaseListingComponent<Field> {
csvFile: File;
@ -50,6 +55,7 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
private readonly _fileAttributesControllerService: FileAttributesControllerService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>,
protected readonly _injector: Injector,
@Inject(MAT_DIALOG_DATA)
@ -97,10 +103,10 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
this.parseResult.meta.fields = Object.keys(this.parseResult.data[0]);
}
this.allEntities = this.parseResult.meta.fields.map(field =>
this._buildAttribute(field)
this._screenStateService.setEntities(
this.parseResult.meta.fields.map(field => this._buildAttribute(field))
);
this.displayedEntities = [...this.allEntities];
this._screenStateService.setDisplayedEntities(this.allEntities);
this.activeFields = [];
for (const entity of this.allEntities) {
@ -170,9 +176,9 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
}
}
return count;
} else {
return 0;
}
return 0;
}
isActive(field: Field): boolean {

View File

@ -24,7 +24,7 @@
<span class="all-caps-label">
{{
'default-colors-screen.table-header.title'
| translate: { length: allEntities.length }
| translate: { length: (allEntities$ | async).length }
}}
</span>
</div>
@ -51,7 +51,9 @@
<!-- Table lines -->
<div
*cdkVirtualFor="
let color of allEntities | sortBy: sortingOption.order:sortingOption.column
let color of allEntities$
| async
| sortBy: sortingOption.order:sortingOption.column
"
class="table-item"
>
@ -77,8 +79,7 @@
icon="red:edit"
tooltip="default-colors-screen.action.edit"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>

View File

@ -4,13 +4,17 @@ import { Colors, DictionaryControllerService } from '@redaction/red-ui-http';
import { ActivatedRoute } from '@angular/router';
import { PermissionsService } from '@services/permissions.service';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { LoadingService } from '../../../../services/loading.service';
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 { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-default-colors-screen',
templateUrl: './default-colors-screen.component.html',
styleUrls: ['./default-colors-screen.component.scss']
styleUrls: ['./default-colors-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DefaultColorsScreenComponent
extends BaseListingComponent<{
@ -19,7 +23,6 @@ export class DefaultColorsScreenComponent
}>
implements OnInit
{
protected readonly _sortKey = 'default-colors';
private _colorsObj: Colors;
constructor(
@ -32,6 +35,7 @@ export class DefaultColorsScreenComponent
protected readonly _injector: Injector
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DEFAULT_COLORS);
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
@ -60,10 +64,12 @@ export class DefaultColorsScreenComponent
.getColors(this._appStateService.activeDossierTemplateId)
.toPromise();
this._colorsObj = data;
this.allEntities = Object.keys(data).map(key => ({
key,
value: data[key]
}));
this._screenStateService.setEntities(
Object.keys(data).map(key => ({
key,
value: data[key]
}))
);
this._loadingService.stop();
}
}

View File

@ -25,17 +25,22 @@
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="areSomeEntitiesSelected && !areAllEntitiesSelected"
[indeterminate]="
(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected
"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ tableHeader }}
{{
'dictionary-listing.table-header.title'
| translate: { length: (displayedEntities$ | async)?.length }
}}
</span>
<redaction-circle-button
(action)="openDeleteDictionariesDialog($event)"
*ngIf="areSomeEntitiesSelected && permissionsService.isAdmin()"
*ngIf="(areSomeEntitiesSelected$ | async) && permissionsService.isAdmin()"
icon="red:trash"
tooltip="dictionary-listing.bulk.delete"
type="dark-bg"
@ -100,7 +105,7 @@
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
*ngIf="allEntities.length && (displayedEntities$ | async)?.length === 0"
screen="dictionary-listing"
type="no-match"
></redaction-empty-state>
@ -108,7 +113,8 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div
*cdkVirtualFor="
let dict of displayedEntities
let dict of displayedEntities$
| async
| sortBy: sortingOption.order:sortingOption.column
"
[routerLink]="[dict.type]"

View File

@ -7,10 +7,14 @@ import { forkJoin, of } from 'rxjs';
import { PermissionsService } from '@services/permissions.service';
import { ActivatedRoute } from '@angular/router';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { TypeValueWrapper } from '../../../../models/file/type-value.wrapper';
import { TranslateService } from '@ngx-translate/core';
import { LoadingService } from '../../../../services/loading.service';
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 { BaseListingComponent } from '../../../shared/base/base-listing.component';
const toChartConfig = (dict: TypeValueWrapper): DoughnutChartConfig => ({
value: dict.entries ? dict.entries.length : 0,
@ -20,18 +24,15 @@ const toChartConfig = (dict: TypeValueWrapper): DoughnutChartConfig => ({
});
@Component({
selector: 'redaction-dictionary-listing-screen',
templateUrl: './dictionary-listing-screen.component.html',
styleUrls: ['./dictionary-listing-screen.component.scss']
styleUrls: ['./dictionary-listing-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DictionaryListingScreenComponent
extends BaseListingComponent<TypeValueWrapper>
implements OnInit
{
chartData: DoughnutChartConfig[] = [];
protected readonly _searchKey = 'label';
protected readonly _selectionKey = 'type';
protected readonly _sortKey = 'dictionary-listing';
constructor(
private readonly _dialogService: AdminDialogService,
@ -45,26 +46,26 @@ export class DictionaryListingScreenComponent
) {
super(_injector);
_loadingService.start();
this._sortingService.setScreenName(ScreenNames.DOSSIER_LISTING);
this._searchService.setSearchKey('label');
this._screenStateService.setIdKey('type');
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
get tableHeader(): string {
return this._translateService.instant('dictionary-listing.table-header.title', {
length: this.displayedEntities.length
});
}
ngOnInit(): void {
this._loadDictionaryData();
}
openDeleteDictionariesDialog($event?: MouseEvent, types = this.selectedEntitiesIds) {
openDeleteDictionariesDialog(
$event?: MouseEvent,
types = this._screenStateService.selectedEntitiesIds
) {
this._dialogService.openDialog('confirm', $event, null, async () => {
this._loadingService.start();
await this._dictionaryControllerService
.deleteTypes(types, this._appStateService.activeDossierTemplateId)
.toPromise();
this.selectedEntitiesIds = [];
this._screenStateService.setSelectedEntitiesIds([]);
await this._appStateService.loadDictionaryData();
this._loadDictionaryData(false);
this._calculateData();
@ -96,13 +97,15 @@ export class DictionaryListingScreenComponent
const entities = Object.values(appStateDictionaryData).filter(d => !d.virtual);
if (!loadEntries)
this.allEntities = entities.map(dict => {
dict.entries = this.allEntities.find(d => d.type === dict.type)?.entries || [];
return dict;
});
else this.allEntities = entities;
this._screenStateService.setEntities(
entities.map(dict => {
dict.entries = this.allEntities.find(d => d.type === dict.type)?.entries || [];
return dict;
})
);
else this._screenStateService.setEntities(entities);
this.displayedEntities = [...this.allEntities];
this._screenStateService.setDisplayedEntities(this.allEntities);
if (!loadEntries) return;

View File

@ -20,22 +20,22 @@
<redaction-admin-side-nav type="dossier-templates"></redaction-admin-side-nav>
<div class="content-container">
<div *ngIf="allEntities.length" class="header-item">
<div *ngIf="(allEntities$ | async)?.length" class="header-item">
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="areSomeEntitiesSelected && !areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'dossier-attributes-listing.table-header.title' | translate: { length: displayedEntities.length } }}
{{ 'dossier-attributes-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }}
</span>
<redaction-circle-button
(action)="openConfirmDeleteAttributeDialog($event)"
*ngIf="areSomeEntitiesSelected"
*ngIf="areSomeEntitiesSelected$ | async"
icon="red:trash"
tooltip="dossier-attributes-listing.bulk.delete"
type="dark-bg"
@ -57,7 +57,7 @@
</div>
</div>
<div *ngIf="allEntities.length" class="table-header" redactionSyncWidth="table-item">
<div *ngIf="(allEntities$ | async)?.length" class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
@ -81,21 +81,21 @@
<redaction-empty-state
(action)="openAddEditAttributeDialog($event)"
*ngIf="!allEntities.length"
*ngIf="(allEntities$ | async)?.length === 0"
[showButton]="true"
icon="red:attribute"
screen="dossier-attributes-listing"
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
*ngIf="(allEntities$ | async)?.length && (displayedEntities$ | async)?.length === 0"
screen="dossier-attributes-listing"
type="no-match"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar>
<div
*cdkVirtualFor="let attribute of displayedEntities | sortBy: sortingOption.order:sortingOption.column"
*cdkVirtualFor="let attribute of displayedEntities$ | async | sortBy: sortingOption.order:sortingOption.column"
class="table-item pointer"
>
<div (click)="toggleEntitySelected($event, attribute)" class="selection-column">

View File

@ -5,17 +5,17 @@ import { AppStateService } from '../../../../state/app-state.service';
import { ActivatedRoute } from '@angular/router';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { LoadingService } from '../../../../services/loading.service';
import { ScreenNames, SortingService } from '../../../../services/sorting.service';
import { FilterService } from '../../../shared/services/filter.service';
import { SearchService } from '../../../shared/services/search.service';
import { ScreenStateService } from '../../../shared/services/screen-state.service';
@Component({
selector: 'redaction-dossier-attributes',
templateUrl: './dossier-attributes-listing-screen.component.html',
styleUrls: ['./dossier-attributes-listing-screen.component.scss']
styleUrls: ['./dossier-attributes-listing-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierAttributesListingScreenComponent extends BaseListingComponent<DossierAttributeConfig> implements OnInit {
protected readonly _searchKey = 'label';
protected readonly _selectionKey = 'id';
protected readonly _sortKey = 'dossier-attributes-listing';
constructor(
protected readonly _injector: Injector,
private readonly _appStateService: AppStateService,
@ -25,7 +25,10 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen
private readonly _dossierAttributesService: DossierAttributesControllerService
) {
super(_injector);
this._appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
this._searchService.setSearchKey('label');
this._screenStateService.setIdKey('id');
this._sortingService.setScreenName(ScreenNames.DOSSIER_ATTRIBUTES_LISTING);
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
async ngOnInit() {
@ -35,7 +38,7 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen
openConfirmDeleteAttributeDialog($event: MouseEvent, dossierAttribute?: DossierAttributeConfig) {
this._dialogService.openDialog('confirm', $event, null, async () => {
this._loadingService.start();
const ids = dossierAttribute ? [dossierAttribute.id] : this.selectedEntitiesIds;
const ids = dossierAttribute ? [dossierAttribute.id] : this._screenStateService.selectedEntitiesIds;
await this._dossierAttributesService
.deleteDossierAttributesConfig(ids, this._appStateService.activeDossierTemplateId)
.toPromise();
@ -59,8 +62,8 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen
const response = await this._dossierAttributesService
.getDossierAttributesConfig(this._appStateService.activeDossierTemplateId)
.toPromise();
this.allEntities = response?.dossierAttributeConfigs || [];
this._executeSearchImmediately();
this._screenStateService.setEntities(response?.dossierAttributeConfigs || []);
this.filterService.filterEntities();
this._loadingService.stop();
}
}

View File

@ -23,18 +23,20 @@
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="areSomeEntitiesSelected && !areAllEntitiesSelected"
[indeterminate]="
(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected
"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{
'dossier-templates-listing.table-header.title'
| translate: { length: displayedEntities.length }
| translate: { length: (displayedEntities$ | async).length }
}}
</span>
<ng-container *ngIf="areSomeEntitiesSelected">
<ng-container *ngIf="areSomeEntitiesSelected$ | async">
<redaction-circle-button
(action)="openDeleteTemplatesDialog($event)"
*ngIf="permissionsService.isAdmin()"
@ -65,7 +67,7 @@
</div>
<div
[class.no-data]="!allEntities.length"
[class.no-data]="(allEntities$ | async)?.length === 0"
class="table-header"
redactionSyncWidth="table-item"
>
@ -100,13 +102,15 @@
</div>
<redaction-empty-state
*ngIf="!allEntities.length"
*ngIf="(allEntities$ | async)?.length === 0"
icon="red:template"
screen="dossier-templates-listing"
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
*ngIf="
(allEntities$ | async)?.length && (displayedEntities$ | async).length === 0
"
screen="dossier-templates-listing"
type="no-match"
></redaction-empty-state>
@ -114,7 +118,8 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div
*cdkVirtualFor="
let dossierTemplate of displayedEntities
let dossierTemplate of displayedEntities$
| async
| sortBy: sortingOption.order:sortingOption.column
"
[routerLink]="[dossierTemplate.dossierTemplateId, 'dictionaries']"

View File

@ -1,26 +1,27 @@
import { Component, Injector, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { DossierTemplateModelWrapper } from '../../../../models/file/dossier-template-model.wrapper';
import { LoadingService } from '../../../../services/loading.service';
import { DossierTemplateControllerService } from '@redaction/red-ui-http';
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 { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-dossier-templates-listing-screen',
templateUrl: './dossier-templates-listing-screen.component.html',
styleUrls: ['./dossier-templates-listing-screen.component.scss']
styleUrls: ['./dossier-templates-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierTemplatesListingScreenComponent
extends BaseListingComponent<DossierTemplateModelWrapper>
implements OnInit
{
protected readonly _searchKey = 'name';
protected readonly _selectionKey = 'dossierTemplateId';
protected readonly _sortKey = 'dossier-templates-listing';
constructor(
private readonly _dialogService: AdminDialogService,
private readonly _appStateService: AppStateService,
@ -31,6 +32,9 @@ export class DossierTemplatesListingScreenComponent
readonly userPreferenceService: UserPreferenceService
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DOSSIER_TEMPLATES_LISTING);
this._searchService.setSearchKey('name');
this._screenStateService.setIdKey('dossierTemplateId');
}
ngOnInit(): void {
@ -41,9 +45,9 @@ export class DossierTemplatesListingScreenComponent
return this._dialogService.openDialog('confirm', $event, null, async () => {
this._loadingService.start();
await this._dossierTemplateControllerService
.deleteDossierTemplates(this.selectedEntitiesIds)
.deleteDossierTemplates(this._screenStateService.selectedEntitiesIds)
.toPromise();
this.selectedEntitiesIds = [];
this._screenStateService.setSelectedEntitiesIds([]);
await this._appStateService.loadAllDossierTemplates();
await this._appStateService.loadDictionaryData();
this.loadDossierTemplatesData();
@ -53,8 +57,8 @@ export class DossierTemplatesListingScreenComponent
loadDossierTemplatesData() {
this._loadingService.start();
this._appStateService.reset();
this.allEntities = this._appStateService.dossierTemplates;
this._executeSearchImmediately();
this._screenStateService.setEntities(this._appStateService.dossierTemplates);
this.filterService.filterEntities();
this._loadDossierTemplateStats();
this._loadingService.stop();
}
@ -73,7 +77,7 @@ export class DossierTemplatesListingScreenComponent
}
private _loadDossierTemplateStats() {
this.allEntities.forEach(rs => {
this._screenStateService.entities.forEach(rs => {
const dictionaries = this._appStateService.dictionaryData[rs.dossierTemplateId];
if (dictionaries) {
rs.dictionariesCount = Object.keys(dictionaries)

View File

@ -25,17 +25,22 @@
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="areSomeEntitiesSelected && !areAllEntitiesSelected"
[indeterminate]="
(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected
"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'file-attributes-listing.table-header.title' | translate: { length: displayedEntities.length } }}
{{
'file-attributes-listing.table-header.title'
| translate: { length: (displayedEntities$ | async)?.length }
}}
</span>
<redaction-circle-button
(click)="openConfirmDeleteAttributeDialog($event)"
*ngIf="areSomeEntitiesSelected"
*ngIf="areSomeEntitiesSelected$ | async"
icon="red:trash"
tooltip="file-attributes-listing.bulk-actions.delete"
type="dark-bg"
@ -67,7 +72,11 @@
</div>
</div>
<div [class.no-data]="!allEntities.length" class="table-header" redactionSyncWidth="table-item">
<div
[class.no-data]="(allEntities$ | async)?.length === 0"
class="table-header"
redactionSyncWidth="table-item"
>
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
@ -110,13 +119,13 @@
</div>
<redaction-empty-state
*ngIf="!allEntities.length"
*ngIf="(allEntities$ | async)?.length === 0"
icon="red:attribute"
screen="file-attributes-listing"
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
*ngIf="(allEntities$ | async)?.length && (displayedEntities$ | async)?.length === 0"
screen="file-attributes-listing"
type="no-match"
></redaction-empty-state>
@ -124,7 +133,11 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines -->
<div
*cdkVirtualFor="let attribute of displayedEntities | sortBy: sortingOption.order:sortingOption.column"
*cdkVirtualFor="
let attribute of displayedEntities$
| async
| sortBy: sortingOption.order:sortingOption.column
"
class="table-item"
>
<div (click)="toggleEntitySelected($event, attribute)" class="selection-column">

View File

@ -1,23 +1,34 @@
import { Component, ElementRef, Injector, OnInit, ViewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Injector,
OnInit,
ViewChild
} from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { FileAttributeConfig, FileAttributesConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { ActivatedRoute } from '@angular/router';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { LoadingService } from '../../../../services/loading.service';
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 { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-file-attributes-listing-screen',
templateUrl: './file-attributes-listing-screen.component.html',
styleUrls: ['./file-attributes-listing-screen.component.scss']
styleUrls: ['./file-attributes-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class FileAttributesListingScreenComponent extends BaseListingComponent<FileAttributeConfig> implements OnInit {
protected readonly _searchKey = 'label';
protected readonly _selectionKey = 'id';
protected readonly _sortKey = 'file-attributes-listing';
export class FileAttributesListingScreenComponent
extends BaseListingComponent<FileAttributeConfig>
implements OnInit
{
private _existingConfiguration: FileAttributesConfig;
@ViewChild('fileInput') private _fileInput: ElementRef;
constructor(
@ -30,7 +41,10 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
protected readonly _injector: Injector
) {
super(_injector);
this._appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
this._sortingService.setScreenName(ScreenNames.FILE_ATTRIBUTES_LISTING);
this._searchService.setSearchKey('label');
this._screenStateService.setIdKey('id');
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
async ngOnInit() {
@ -61,7 +75,10 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
.toPromise();
} else {
await this._fileAttributesService
.deleteFileAttributes(this.selectedEntitiesIds, this._appStateService.activeDossierTemplateId)
.deleteFileAttributes(
this._screenStateService.selectedEntitiesIds,
this._appStateService.activeDossierTemplateId
)
.toPromise();
}
await this._loadData();
@ -80,9 +97,7 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
dossierTemplateId: this._appStateService.activeDossierTemplateId,
existingConfiguration: this._existingConfiguration
},
async () => {
await this._loadData();
}
async () => await this._loadData()
);
}
@ -93,10 +108,10 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
.getFileAttributesConfiguration(this._appStateService.activeDossierTemplateId)
.toPromise();
this._existingConfiguration = response;
this.allEntities = response?.fileAttributeConfigs || [];
this._screenStateService.setEntities(response?.fileAttributeConfigs || []);
} catch (e) {
} finally {
this._executeSearchImmediately();
this.filterService.filterEntities();
this._loadingService.stop();
}
}

View File

@ -0,0 +1,170 @@
<section>
<div class="overlay-shadow"></div>
<redaction-page-header
[pageLabel]="'trash.label' | translate"
[showCloseButton]="true"
></redaction-page-header>
<div class="red-content-inner">
<div class="content-container">
<div class="header-item">
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="
(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected
"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{
'trash.table-header.title'
| translate: { length: (displayedEntities$ | async)?.length }
}}
</span>
<redaction-circle-button
(action)="bulkRestore()"
*ngIf="areSomeEntitiesSelected$ | async"
icon="red:put-back"
[tooltip]="'trash.bulk.restore' | translate"
type="dark-bg"
></redaction-circle-button>
<redaction-circle-button
(action)="bulkDelete()"
*ngIf="areSomeEntitiesSelected$ | async"
icon="red:trash"
[tooltip]="'trash.bulk.delete' | translate"
type="dark-bg"
></redaction-circle-button>
</div>
<div
[class.no-data]="(allEntities$ | async)?.length === 0"
class="table-header"
redactionSyncWidth="table-item"
>
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[withSort]="true"
column="name"
[label]="'trash.table-col-names.name' | translate"
></redaction-table-col-name>
<redaction-table-col-name
class="user-column"
[label]="'trash.table-col-names.owner' | translate"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[withSort]="true"
column="dateDeleted"
[label]="'trash.table-col-names.deleted-on' | translate"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[withSort]="true"
column="timeToRestore"
[label]="'trash.table-col-names.time-to-restore' | translate"
></redaction-table-col-name>
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state
*ngIf="(allEntities$ | async)?.length === 0"
icon="red:template"
screen="trash"
></redaction-empty-state>
<redaction-empty-state
*ngIf="(allEntities$ | async)?.length && (displayedEntities$ | async)?.length === 0"
screen="trash"
type="no-match"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="
let entity of displayedEntities$
| async
| sortBy: sortingOption.order:sortingOption.column;
trackBy: trackById
"
class="table-item"
>
<div (click)="toggleEntitySelected($event, entity)" class="selection-column">
<redaction-round-checkbox
[active]="isSelected(entity)"
></redaction-round-checkbox>
</div>
<div class="filename">
<div
class="table-item-title heading"
[matTooltip]="entity.dossierName"
matTooltipPosition="above"
>
{{ entity.dossierName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ entity.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ entity.date | date: 'mediumDate' }}
</div>
<div *ngIf="entity.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ entity.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
<div class="user-column">
<redaction-initials-avatar
[userId]="entity.ownerId"
[withName]="true"
></redaction-initials-avatar>
</div>
<div class="small-label">
{{ entity.softDeletedTime | date: 'd MMM. yyyy, hh:mm a' }}
</div>
<div>
<div class="small-label">
{{ getRestoreDate(entity.softDeletedTime) | date: 'timeFromNow' }}
</div>
<div class="action-buttons">
<redaction-circle-button
(action)="bulkRestore([entity.dossierId])"
icon="red:put-back"
[tooltip]="'trash.action.restore' | translate"
type="dark-bg"
></redaction-circle-button>
<redaction-circle-button
(action)="bulkDelete([entity.dossierId])"
icon="red:trash"
[tooltip]="'trash.action.delete' | translate"
type="dark-bg"
></redaction-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
</div>
</div>
</section>

View File

@ -0,0 +1,45 @@
@import '../../../../../assets/styles/red-variables';
@import '../../../../../assets/styles/red-mixins';
.header-item {
padding: 0 24px 0 10px;
redaction-circle-button:not(:last-child) {
margin-right: 4px !important;
}
}
redaction-table-col-name::ng-deep {
> div {
padding-left: 10px !important;
}
}
.content-container {
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 1fr 1fr 1fr 1fr 11px;
.table-item {
> div:not(.scrollbar-placeholder) {
padding-left: 10px;
.table-item-title {
max-width: 100%;
}
}
> div {
height: 80px;
padding: 0 24px;
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 1fr 1fr 1fr 1fr;
}
}
}
}

View File

@ -0,0 +1,87 @@
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { Dossier, DossierTemplateModel, StatusControllerService } 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 { 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 { BaseListingComponent } from '../../../shared/base/base-listing.component';
import { DossiersService } from '../../../dossier/services/dossiers.service';
import { tap } from 'rxjs/operators';
@Component({
templateUrl: './trash-screen.component.html',
styleUrls: ['./trash-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService, DossiersService]
})
export class TrashScreenComponent extends BaseListingComponent<Dossier> implements OnInit {
readonly itemSize = 85;
private readonly _deleteRetentionHours = this._appConfigService.getConfig(
AppConfigKey.DELETE_RETENTION_HOURS
);
constructor(
private readonly _appStateService: AppStateService,
private readonly _statusControllerService: StatusControllerService,
readonly permissionsService: PermissionsService,
protected readonly _injector: Injector,
private readonly _dossiersService: DossiersService,
private readonly _loadingService: LoadingService,
private readonly _appConfigService: AppConfigService
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DOSSIER_LISTING);
this._screenStateService.setIdKey('dossierId');
}
async ngOnInit(): Promise<void> {
this._loadingService.start();
await this.loadDossierTemplatesData();
this.filterService.filterEntities();
this._loadingService.stop();
}
async loadDossierTemplatesData(): Promise<void> {
this._screenStateService.setEntities(await this._dossiersService.getDeletedDossiers());
}
getRestoreDate(softDeletedTime: string): string {
return moment(softDeletedTime).add(this._deleteRetentionHours, 'hours').toISOString();
}
trackById(index: number, dossier: Dossier): string {
return dossier.dossierId;
}
bulkDelete(dossierIds = this._screenStateService.selectedEntitiesIds) {
this._loadingService.loadWhile(this._hardDelete(dossierIds));
}
bulkRestore(dossierIds = this._screenStateService.selectedEntitiesIds) {
this._loadingService.loadWhile(this._restore(dossierIds));
}
private async _restore(dossierIds: string[]): Promise<void> {
await this._dossiersService.restore(dossierIds);
this._removeFromList(dossierIds);
}
private async _hardDelete(dossierIds: string[]): Promise<void> {
await this._dossiersService.hardDelete(dossierIds);
this._removeFromList(dossierIds);
}
private _removeFromList(ids: string[]): void {
const entities = this._screenStateService.entities.filter(e => !ids.includes(e.dossierId));
this._screenStateService.setEntities(entities);
this._screenStateService.setSelectedEntitiesIds([]);
this.filterService.filterEntities();
}
}

View File

@ -38,23 +38,25 @@
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="areSomeEntitiesSelected && !areAllEntitiesSelected"
[indeterminate]="
(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected
"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{
'user-listing.table-header.title'
| translate: { length: displayedEntities.length }
| translate: { length: (displayedEntities$ | async)?.length }
}}
</span>
<redaction-circle-button
(action)="bulkDelete()"
*ngIf="areSomeEntitiesSelected"
[disabled]="!canDeleteSelected"
*ngIf="areSomeEntitiesSelected$ | async"
[disabled]="(canDeleteSelected$ | async) === false"
[tooltip]="
canDeleteSelected
(canDeleteSelected$ | async)
? 'user-listing.bulk.delete'
: 'user-listing.bulk.delete-disabled'
"
@ -89,7 +91,7 @@
</div>
<redaction-empty-state
*ngIf="!displayedEntities.length"
*ngIf="(displayedEntities$ | async)?.length === 0"
screen="user-listing"
type="no-match"
></redaction-empty-state>
@ -97,7 +99,7 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines -->
<div
*cdkVirtualFor="let user of displayedEntities; trackBy: trackById"
*cdkVirtualFor="let user of displayedEntities$ | async; trackBy: trackById"
class="table-item"
>
<div (click)="toggleEntitySelected($event, user)" class="selection-column">
@ -146,7 +148,7 @@
<div [class.collapsed]="collapsedDetails" class="right-container" redactionHasScrollbar>
<redaction-users-stats
(toggleCollapse)="toggleCollapsedDetails()"
(toggleCollapse)="collapsedDetails = !collapsedDetails"
[chartData]="chartData"
></redaction-users-stats>
</div>

View File

@ -1,4 +1,11 @@
import { Component, Injector, OnInit, QueryList, ViewChildren } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
Injector,
OnInit,
QueryList,
ViewChildren
} from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { UserService } from '@services/user.service';
import { User, UserControllerService } from '@redaction/red-ui-http';
@ -6,18 +13,25 @@ import { AdminDialogService } from '../../services/admin-dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { TranslateChartService } from '@services/translate-chart.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { LoadingService } from '../../../../services/loading.service';
import { InitialsAvatarComponent } from '../../../shared/components/initials-avatar/initials-avatar.component';
import { FilterService } from '../../../shared/services/filter.service';
import { SearchService } from '../../../shared/services/search.service';
import { ScreenStateService } from '../../../shared/services/screen-state.service';
import { SortingService } from '../../../../services/sorting.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
templateUrl: './user-listing-screen.component.html',
styleUrls: ['./user-listing-screen.component.scss']
styleUrls: ['./user-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class UserListingScreenComponent extends BaseListingComponent<User> implements OnInit {
collapsedDetails = false;
chartData: DoughnutChartConfig[] = [];
protected readonly _selectionKey = 'userId';
@ViewChildren(InitialsAvatarComponent)
private readonly _avatars: QueryList<InitialsAvatarComponent>;
@ -32,10 +46,12 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
protected readonly _injector: Injector
) {
super(_injector);
this._screenStateService.setIdKey('userId');
}
get canDeleteSelected(): boolean {
return this.selectedEntitiesIds.indexOf(this.userService.userId) === -1;
get canDeleteSelected$(): Observable<boolean> {
const entities$ = this._screenStateService.selectedEntitiesIds$;
return entities$.pipe(map(all => all.indexOf(this.userService.userId) === -1));
}
async ngOnInit() {
@ -69,26 +85,22 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
this._avatars.find(item => item.userId === user.userId).detectChanges();
}
toggleCollapsedDetails() {
this.collapsedDetails = !this.collapsedDetails;
}
async bulkDelete() {
this.openDeleteUsersDialog(this.allEntities.filter(u => this.isSelected(u)));
bulkDelete() {
this.openDeleteUsersDialog(
this._screenStateService.entities.filter(u => this.isSelected(u))
);
}
trackById(index: number, user: User) {
return user.userId;
}
protected _searchField(user: any): string {
return this.userService.getName(user);
}
private async _loadData() {
this.allEntities = await this._userControllerService.getAllUsers().toPromise();
this._screenStateService.setEntities(
await this._userControllerService.getAllUsers().toPromise()
);
await this.userService.loadAllUsers();
this._executeSearchImmediately();
this.filterService.filterEntities();
this._computeStats();
this._loadingService.stop();
}

View File

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

View File

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

View File

@ -10,7 +10,7 @@
<div class="dossier-stats-item">
<mat-icon svgIcon="red:needs-work"></mat-icon>
<div>
<div class="heading">{{ totalPages | number }}</div>
<div class="heading">{{ appStateService.totalAnalysedPages | number }}</div>
<div translate="dossier-listing.stats.analyzed-pages"></div>
</div>
</div>
@ -18,7 +18,7 @@
<div class="dossier-stats-item">
<mat-icon svgIcon="red:user"></mat-icon>
<div>
<div class="heading">{{ totalPeople }}</div>
<div class="heading">{{ appStateService.totalPeople }}</div>
<div translate="dossier-listing.stats.total-people"></div>
</div>
</div>
@ -26,9 +26,8 @@
</div>
<div>
<redaction-simple-doughnut-chart
(toggleFilter)="toggleFilter('statusFilters', $event)"
[config]="documentsChartData"
[filter]="filters.statusFilters"
[filter]="filterService.getFilter$('statusFilters') | async"
[radius]="80"
[strokeWidth]="15"
[subtitle]="'dossier-listing.stats.charts.total-documents'"

View File

@ -1,32 +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: { statusFilters: FilterModel[] };
@Output() filtersChanged = new EventEmitter();
constructor(private readonly _appStateService: AppStateService) {}
get totalPages() {
return this._appStateService.totalAnalysedPages;
}
get totalPeople() {
return this._appStateService.totalPeople;
}
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);
}
constructor(
readonly appStateService: AppStateService,
readonly filterService: FilterService<T>
) {}
}

View File

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

View File

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

View File

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

View File

@ -1,57 +1,8 @@
<section>
<div class="page-header">
<div class="filters">
<div translate="filters.filter-by"></div>
<redaction-popup-filter
#statusFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.status'"
[icon]="'red:status'"
[primaryFilters]="statusFilters"
></redaction-popup-filter>
<redaction-popup-filter
#peopleFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.people'"
[icon]="'red:user'"
[primaryFilters]="peopleFilters"
></redaction-popup-filter>
<redaction-popup-filter
#needsWorkFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.needs-work'"
[filterTemplate]="needsWorkTemplate"
[icon]="'red:needs-work'"
[primaryFilters]="needsWorkFilters"
></redaction-popup-filter>
<redaction-popup-filter
#dossierTemplateFilter
(filtersChanged)="filtersChanged()"
*ngIf="dossierTemplateFilters.length > 1"
[filterLabel]="'filters.dossier-templates'"
[icon]="'red:template'"
[primaryFilters]="dossierTemplateFilters"
></redaction-popup-filter>
<redaction-input-with-action
[form]="searchForm"
placeholder="dossier-listing.search"
type="search"
></redaction-input-with-action>
<div
(click)="resetFilters()"
*ngIf="hasActiveFilters"
class="reset-filters"
translate="reset-filters"
></div>
</div>
<redaction-icon-button
(action)="openAddDossierDialog()"
*ngIf="permissionsService.isManager()"
icon="red:plus"
text="dossier-listing.add-new"
type="primary"
></redaction-icon-button>
</div>
<redaction-page-header
[buttonConfigs]="buttonConfigs"
[searchPlaceholder]="'dossier-listing.search' | translate"
></redaction-page-header>
<div class="overlay-shadow"></div>
@ -61,14 +12,11 @@
<span class="all-caps-label">
{{
'dossier-listing.table-header.title'
| translate: { length: displayedEntities.length || 0 }
| translate: { length: (displayedEntities$ | async)?.length || 0 }
}}
</span>
<redaction-quick-filters
(filtersChanged)="filtersChanged()"
[filters]="quickFilters"
></redaction-quick-filters>
<redaction-quick-filters></redaction-quick-filters>
</div>
<div class="table-header" redactionSyncWidth="table-item">
@ -98,14 +46,14 @@
<redaction-empty-state
(action)="openAddDossierDialog()"
*ngIf="!allEntities.length"
*ngIf="(allEntities$ | async)?.length === 0"
[showButton]="permissionsService.isManager()"
icon="red:folder"
screen="dossier-listing"
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
*ngIf="(allEntities$ | async)?.length && (displayedEntities$ | async)?.length === 0"
screen="dossier-listing"
type="no-match"
></redaction-empty-state>
@ -113,13 +61,12 @@
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="
let dw of displayedEntities
let dw of displayedEntities$
| async
| sortBy: sortingOption.order:sortingOption.column
"
[class.pointer]="canOpenDossier(dw)"
[routerLink]="[
canOpenDossier(dw) ? '/main/dossiers/' + dw.dossier.dossierId : []
]"
[class.pointer]="!!dw"
[routerLink]="[!!dw ? '/main/dossiers/' + dw.dossier.dossierId : []]"
class="table-item"
>
<div class="filename">
@ -139,7 +86,7 @@
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ documentCount(dw) }}
{{ dw.files.length }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
@ -147,7 +94,7 @@
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ userCount(dw) }}
{{ dw.numberOfMembers }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
@ -172,7 +119,7 @@
</div>
<div class="status-container">
<redaction-dossier-listing-actions
(actionPerformed)="actionPerformed()"
(actionPerformed)="calculateData()"
[dossier]="dw"
></redaction-dossier-listing-actions>
</div>
@ -188,11 +135,9 @@
<div class="right-container" redactionHasScrollbar>
<redaction-dossier-listing-details
(filtersChanged)="filtersChanged($event)"
*ngIf="allEntities.length"
*ngIf="(allEntities$ | async)?.length !== 0"
[documentsChartData]="documentsChartData"
[dossiersChartData]="dossiersChartData"
[filters]="detailsContainerFilters"
></redaction-dossier-listing-details>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { Component, Injector, OnDestroy, OnInit, 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,57 +12,52 @@ 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 { BaseListingComponent } from '@shared/base/base-listing.component';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import {
annotationFilterChecker,
dossierMemberChecker,
dossierStatusChecker,
dossierTemplateChecker,
processFilters
dossierTemplateChecker
} from '@shared/components/filters/popup-filter/utils/filter-utils';
import { QuickFiltersComponent } from '../../../shared/components/filters/quick-filters/quick-filters.component';
import { UserPreferenceService } from '../../../../services/user-preference.service';
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 { BaseListingComponent } from '../../../shared/base/base-listing.component';
import { ScreenNames, SortingService } from '../../../../services/sorting.service';
@Component({
selector: 'redaction-dossier-listing-screen',
templateUrl: './dossier-listing-screen.component.html',
styleUrls: ['./dossier-listing-screen.component.scss']
styleUrls: ['./dossier-listing-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierListingScreenComponent
extends BaseListingComponent<DossierWrapper>
implements OnInit, OnDestroy, OnAttach, OnDetach
{
export class DossierListingScreenComponent extends BaseListingComponent<DossierWrapper> implements OnInit, OnDestroy, OnAttach, OnDetach {
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
statusFilters: FilterModel[];
peopleFilters: FilterModel[];
needsWorkFilters: FilterModel[];
dossierTemplateFilters: FilterModel[];
detailsContainerFilters: {
statusFilters: FilterModel[];
} = {
statusFilters: []
};
quickFilters: FilterModel[];
buttonConfigs: ButtonConfig[] = [
{
label: this._translateService.instant('dossier-listing.add-new'),
action: () => this.openAddDossierDialog(),
hide: !this.permissionsService.isManager(),
icon: 'red:plus',
type: 'primary'
}
];
readonly itemSize = 85;
protected readonly _searchKey = 'name';
protected readonly _sortKey = 'dossier-listing';
@ViewChild(QuickFiltersComponent) protected _quickFiltersComponent: QuickFiltersComponent;
private _dossierAutoUpdateTimer: Subscription;
private _lastScrollPosition: number;
@ViewChild('statusFilter') private _statusFilterComponent: PopupFilterComponent;
@ViewChild('peopleFilter') private _peopleFilterComponent: PopupFilterComponent;
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: PopupFilterComponent;
@ViewChild('dossierTemplateFilter')
private _dossierTemplateFilterComponent: PopupFilterComponent;
private _routerEventsScrollPositionSub: Subscription;
private _fileChangedSub: Subscription;
@ViewChild('needsWorkTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkTemplate: TemplateRef<any>;
constructor(
readonly permissionsService: PermissionsService,
private readonly _translateChartService: TranslateChartService,
@ -75,84 +70,33 @@ export class DossierListingScreenComponent
protected readonly _injector: Injector
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DOSSIER_LISTING);
this._searchService.setSearchKey('name');
this._appStateService.reset();
this._loadEntitiesFromState();
}
get noData() {
return this.allEntities.length === 0;
}
get user() {
return this._userService.user;
}
get activeDossiersCount() {
return this.allEntities.filter(p => p.dossier.status === Dossier.StatusEnum.ACTIVE).length;
}
get inactiveDossiersCount() {
return this.allEntities.length - this.activeDossiersCount;
}
protected get _filterComponents(): (PopupFilterComponent | QuickFiltersComponent)[] {
return [
this._statusFilterComponent,
this._peopleFilterComponent,
this._needsWorkFilterComponent,
this._dossierTemplateFilterComponent,
this._quickFiltersComponent
];
}
protected get _filters(): {
values: FilterModel[];
checker: Function;
matchAll?: boolean;
checkerArgs?: any;
}[] {
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');
}
});
@ -175,25 +119,12 @@ export class DossierListingScreenComponent
this._fileChangedSub.unsubscribe();
}
documentCount(dossier: DossierWrapper) {
return dossier.files.length;
}
userCount(dossier: DossierWrapper) {
return dossier.numberOfMembers;
}
canOpenDossier(dw: DossierWrapper): boolean {
return !!dw;
}
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, {
@ -204,29 +135,32 @@ export class DossierListingScreenComponent
});
}
actionPerformed() {
this._calculateData();
}
protected _preFilter() {
this.detailsContainerFilters = {
statusFilters: this.statusFilters.map(f => ({ ...f }))
};
}
private _loadEntitiesFromState() {
this.allEntities = this._appStateService.allDossiers;
this._screenStateService.setEntities(this._appStateService.allDossiers);
}
private _calculateData() {
private get _user() {
return this._userService.user;
}
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._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,
@ -236,9 +170,7 @@ export class DossierListingScreenComponent
});
}
this.documentsChartData.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]);
this.documentsChartData = this._translateChartService.translateStatus(
this.documentsChartData
);
this.documentsChartData = this._translateChartService.translateStatus(this.documentsChartData);
}
private _computeAllFilters() {
@ -246,97 +178,114 @@ export class DossierListingScreenComponent
const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctDossierTemplates = new Set<string>();
this.allEntities.forEach(entry => {
// all people
entry.dossier.memberIds.forEach(memberId => allDistinctPeople.add(memberId));
// file statuses
entry.files.forEach(file => {
allDistinctFileStatus.add(file.status);
});
this._screenStateService?.entities?.forEach(entry => {
// all people
entry.dossier.memberIds.forEach(f => allDistinctPeople.add(f));
// Needs work
entry.files.forEach(file => {
if (this.permissionsService.fileRequiresReanalysis(file))
allDistinctNeedsWork.add('analysis');
allDistinctFileStatus.add(file.status);
if (this.permissionsService.fileRequiresReanalysis(file)) allDistinctNeedsWork.add('analysis');
if (entry.hintsOnly) allDistinctNeedsWork.add('hint');
if (entry.hasRedactions) allDistinctNeedsWork.add('redaction');
if (entry.hasRequests) allDistinctNeedsWork.add('suggestion');
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.quickFilters = [
const needsWorkFilters = [...allDistinctNeedsWork].map<FilterModel>(type => ({
key: type,
label: `filter.${type}`
}));
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: 'dossier-listing.quick-filters.my-dossiers',
checker: (dw: DossierWrapper) => dw.ownerId === this.user.id
key: 'my-dossiers',
label: myDossiersLabel,
checker: (dw: DossierWrapper) => dw.ownerId === this._user.id
},
{
key: this.user.id,
label: 'dossier-listing.quick-filters.to-approve',
checker: (dw: DossierWrapper) => dw.approverIds.includes(this.user.id)
key: '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,
label: 'dossier-listing.quick-filters.to-review',
checker: (dw: DossierWrapper) => dw.memberIds.includes(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,
label: 'dossier-listing.quick-filters.other',
checker: (dw: DossierWrapper) => !dw.memberIds.includes(this.user.id)
key: 'other',
label: this._translateService.instant('dossier-listing.quick-filters.other'),
checker: (dw: DossierWrapper) => !dw.memberIds.includes(this._user.id)
}
].filter(
f =>
f.label === 'dossier-listing.quick-filters.my-dossiers' ||
this._userPreferenceService.areDevFeaturesEnabled
);
];
return filters.filter(f => f.label === myDossiersLabel || this._userPreferenceService.areDevFeaturesEnabled);
}
}

View File

@ -1,87 +1,36 @@
<section *ngIf="!!activeDossier">
<div class="page-header">
<div class="filters">
<div translate="filters.filter-by"></div>
<redaction-popup-filter
#statusFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.status'"
[icon]="'red:status'"
[primaryFilters]="statusFilters"
></redaction-popup-filter>
<redaction-popup-filter
#peopleFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.assigned-people'"
[icon]="'red:user'"
[primaryFilters]="peopleFilters"
></redaction-popup-filter>
<redaction-popup-filter
#needsWorkFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.needs-work'"
[filterTemplate]="needsWorkTemplate"
[icon]="'red:needs-work'"
[primaryFilters]="needsWorkFilters"
></redaction-popup-filter>
<redaction-page-header
[actionConfigs]="actionConfigs"
[showCloseButton]="true"
[searchPlaceholder]="'dossier-overview.search' | translate"
>
<redaction-file-download-btn
[disabled]="areSomeEntitiesSelected$ | async"
[dossier]="activeDossier"
[file]="allEntities$ | async"
tooltipPosition="below"
></redaction-file-download-btn>
<redaction-input-with-action
[form]="searchForm"
placeholder="dossier-overview.search"
type="search"
></redaction-input-with-action>
<redaction-circle-button
(action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn()"
[disabled]="areSomeEntitiesSelected$ | async"
[tooltipClass]="'small ' + ((areSomeEntitiesSelected$ | async) ? '' : 'warn')"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
icon="red:refresh"
tooltipPosition="below"
type="warn"
></redaction-circle-button>
<div
(click)="resetFilters()"
*ngIf="hasActiveFilters"
class="reset-filters"
translate="reset-filters"
></div>
</div>
<div class="actions">
<redaction-circle-button
(action)="openEditDossierDialog($event)"
*ngIf="permissionsService.isManager()"
icon="red:edit"
tooltip="dossier-overview.header-actions.edit"
tooltipPosition="below"
></redaction-circle-button>
<redaction-file-download-btn
[disabled]="areSomeEntitiesSelected"
[dossier]="activeDossier"
[file]="allEntities"
tooltipPosition="below"
></redaction-file-download-btn>
<redaction-circle-button
(action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn()"
[disabled]="areSomeEntitiesSelected"
[tooltipClass]="'small ' + (areSomeEntitiesSelected ? '' : 'warn')"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all'"
icon="red:refresh"
tooltipPosition="below"
type="warn"
></redaction-circle-button>
<redaction-circle-button
(action)="fileInput.click()"
class="ml-14"
icon="red:upload"
tooltip="dossier-overview.header-actions.upload-document"
tooltipPosition="below"
type="primary"
></redaction-circle-button>
<redaction-circle-button
[routerLink]="['/main/dossiers/']"
class="ml-6"
icon="red:close"
tooltip="common.close"
tooltipPosition="below"
></redaction-circle-button>
</div>
</div>
<redaction-circle-button
(action)="fileInput.click()"
class="ml-14"
icon="red:upload"
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
tooltipPosition="below"
type="primary"
></redaction-circle-button>
</redaction-page-header>
<div class="overlay-shadow"></div>
@ -92,30 +41,29 @@
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="areSomeEntitiesSelected && !areAllEntitiesSelected"
[indeterminate]="
(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected
"
></redaction-round-checkbox>
</div>
<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()"
[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"
>
@ -172,14 +120,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>
@ -187,9 +135,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)"
@ -318,12 +267,9 @@
<div [class.collapsed]="collapsedDetails" class="right-container" redactionHasScrollbar>
<redaction-dossier-details
#dossierDetailsComponent
(filtersChanged)="filtersChanged($event)"
(openAssignDossierMembersDialog)="openAssignDossierMembersDialog()"
(openDossierDictionaryDialog)="openDossierDictionaryDialog()"
(toggleCollapse)="toggleCollapsedDetails()"
[filters]="detailsContainerFilters"
></redaction-dossier-details>
</div>
</div>

View File

@ -96,10 +96,6 @@ cdk-virtual-scroll-viewport {
}
}
.ml-6 {
margin-left: 6px;
}
.mr-4 {
margin-right: 4px;
}

View File

@ -1,10 +1,12 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
HostListener,
Injector,
OnDestroy,
OnInit,
TemplateRef,
ViewChild
} from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
@ -27,52 +29,46 @@ 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 { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import { QuickFiltersComponent } from '../../../shared/components/filters/quick-filters/quick-filters.component';
import { AppConfigService } from '../../../app-config/app-config.service';
import { AppConfigKey, AppConfigService } from '../../../app-config/app-config.service';
import { FilterConfig } from '@shared/components/page-header/models/filter-config.model';
import { ActionConfig } from '@shared/components/page-header/models/action-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 { ScreenNames, SortingService } from '../../../../services/sorting.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-dossier-overview-screen',
templateUrl: './dossier-overview-screen.component.html',
styleUrls: ['./dossier-overview-screen.component.scss']
styleUrls: ['./dossier-overview-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierOverviewScreenComponent
extends BaseListingComponent<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[];
@ViewChild(QuickFiltersComponent) protected _quickFiltersComponent: QuickFiltersComponent;
protected readonly _searchKey = 'searchField';
protected readonly _selectionKey = 'fileId';
protected readonly _sortKey = 'dossier-overview';
@ViewChild('dossierDetailsComponent', { static: false })
private _dossierDetailsComponent: DossierDetailsComponent;
filterConfigs: FilterConfig[];
actionConfigs: ActionConfig[];
@ViewChild(DossierDetailsComponent, { static: false })
private readonly _dossierDetailsComponent: DossierDetailsComponent;
private _filesAutoUpdateTimer: Subscription;
private _routerEventsScrollPositionSub: Subscription;
private _fileChangedSub: Subscription;
private _lastScrollPosition: number;
private _lastOpenedFileId = '';
@ViewChild('statusFilter') private _statusFilterComponent: PopupFilterComponent;
@ViewChild('peopleFilter') private _peopleFilterComponent: PopupFilterComponent;
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: PopupFilterComponent;
@ViewChild('needsWorkTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkTemplate: TemplateRef<any>;
@ViewChild('fileInput') private _fileInput: ElementRef;
constructor(
@ -88,9 +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();
}
@ -103,48 +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 _filterComponents(): (PopupFilterComponent | QuickFiltersComponent)[] {
return [
this._statusFilterComponent,
this._peopleFilterComponent,
this._needsWorkFilterComponent,
this._quickFiltersComponent
];
}
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 {
@ -223,10 +190,6 @@ export class DossierOverviewScreenComponent
});
}
isPending(fileStatusWrapper: FileStatusWrapper) {
return fileStatusWrapper.status === FileStatus.StatusEnum.UNPROCESSED;
}
isError(fileStatusWrapper: FileStatusWrapper) {
return fileStatusWrapper.status === FileStatus.StatusEnum.ERROR;
}
@ -246,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,17 +242,12 @@ export class DossierOverviewScreenComponent
fileLink(fileStatus: FileStatusWrapper) {
return this.permissionsService.canOpenFile(fileStatus)
? [
'/main/dossiers/' +
this.activeDossier.dossier.dossierId +
'/file/' +
fileStatus.fileId
]
? [`/main/dossiers/${this.activeDossier.dossierId}/file/${fileStatus.fileId}`]
: [];
}
bulkActionPerformed() {
this.selectedEntitiesIds = [];
this._screenStateService.selectedEntitiesIds$.next([]);
this.reloadDossiers();
}
@ -306,9 +265,7 @@ export class DossierOverviewScreenComponent
dossierWrapper: this.activeDossier,
section: 'members'
},
() => {
this.reloadDossiers();
}
() => this.reloadDossiers()
);
}
@ -324,18 +281,11 @@ export class DossierOverviewScreenComponent
recentlyModifiedChecker = (file: FileStatusWrapper) =>
moment(file.lastUpdated)
.add(this._appConfigService.getConfig('RECENT_PERIOD_IN_HOURS'), 'hours')
.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[]) {
@ -343,32 +293,22 @@ export class DossierOverviewScreenComponent
if (fileCount) {
this._statusOverlayService.openUploadStatusOverlay();
}
this._changeDetectorRef.detectChanges();
// this._changeDetectorRef.detectChanges();
}
private _computeAllFilters() {
if (!this.activeDossier) {
return;
}
if (!this.activeDossier) return;
const allDistinctFileStatusWrapper = new Set<string>();
const allDistinctPeople = new Set<string>();
const allDistinctAddedDates = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
// All people
this.allEntities.forEach(file => allDistinctPeople.add(file.currentReviewer));
this._screenStateService.entities.forEach(file => {
allDistinctPeople.add(file.currentReviewer);
allDistinctFileStatusWrapper.add(file.status);
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'));
// File statuses
this.allEntities.forEach(file => allDistinctFileStatusWrapper.add(file.status));
// Added dates
this.allEntities.forEach(file =>
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'))
);
// Needs work
this.allEntities.forEach(file => {
if (this.permissionsService.fileRequiresReanalysis(file))
allDistinctNeedsWork.add('analysis');
if (file.hintsOnly) allDistinctNeedsWork.add('hint');
@ -379,16 +319,18 @@ export class DossierOverviewScreenComponent
if (file.hasNone) allDistinctNeedsWork.add('none');
});
const statusFilters = [];
allDistinctFileStatusWrapper.forEach(status => {
statusFilters.push({
key: status,
label: this._translateService.instant(status)
});
});
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)) {
@ -405,57 +347,96 @@ export class DossierOverviewScreenComponent
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: 'peopleFilters',
label: this._translateService.instant('filters.assigned-people'),
icon: 'red:user',
values: peopleFilters,
checker: keyChecker('currentReviewer')
});
needsWorkFilters.sort(
(a, b) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
);
this.needsWorkFilters = processFilters(this.needsWorkFilters, needsWorkFilters);
this._computeQuickFilters();
const needsWorkFilters = [...allDistinctNeedsWork].map<FilterModel>(item => ({
key: item,
label: this._translateService.instant('filter.' + item)
}));
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 _computeQuickFilters() {
if (this.allEntities.filter(this.recentlyModifiedChecker).length > 0) {
const recentPeriodInHours = this._appConfigService.getConfig('RECENT_PERIOD_IN_HOURS');
this.quickFilters = [
private _createQuickFilters() {
let quickFilters = [];
if (this._screenStateService.entities.filter(this.recentlyModifiedChecker).length > 0) {
const recentPeriod = this._appConfigService.getConfig(
AppConfigKey.RECENT_PERIOD_IN_HOURS
);
quickFilters = [
{
key: this.user.id,
label: 'dossier-overview.quick-filters.recent',
labelParams: { hours: recentPeriodInHours },
key: 'recent',
label: this._translateService.instant('dossier-overview.quick-filters.recent', {
hours: recentPeriod
}),
required: true,
checker: this.recentlyModifiedChecker
}
];
} else {
this.quickFilters = [];
}
this.quickFilters = [
...this.quickFilters,
return [
...quickFilters,
{
key: this.user.id,
label: 'dossier-overview.quick-filters.assigned-to-me',
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,
label: 'dossier-overview.quick-filters.unassigned',
key: 'unassigned',
label: this._translateService.instant('dossier-overview.quick-filters.unassigned'),
checker: (file: FileStatusWrapper) => !file.currentReviewer
},
{
key: this.user.id,
label: 'dossier-overview.quick-filters.assigned-to-others',
key: 'assigned-to-others',
label: this._translateService.instant(
'dossier-overview.quick-filters.assigned-to-others'
),
checker: (file: FileStatusWrapper) =>
!!file.currentReviewer && file.currentReviewer !== this.user.id
}
];
}
private _createActionConfigs() {
this.actionConfigs = [
{
label: this._translateService.instant('dossier-overview.header-actions.edit'),
action: $event => this.openEditDossierDialog($event),
icon: 'red:edit',
hide: !this.permissionsService.isManager()
}
];
}
}

View File

@ -0,0 +1,19 @@
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: Array<string>): Promise<unknown> {
return this._dossierControllerService.restoreDossiers(dossierIds).toPromise();
}
hardDelete(dossierIds: Array<string>): Promise<unknown> {
return this._dossierControllerService.hardDeleteDossiers(dossierIds).toPromise();
}
}

View File

@ -62,6 +62,7 @@ export class IconsModule {
'pages',
'plus',
'preview',
'put-back',
'radio-indeterminate',
'radio-selected',
'read-only',

View File

@ -1,199 +1,83 @@
import { ChangeDetectorRef, Component, Injector, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '@utils/debounce';
import { ScreenName, SortingOption, SortingService } from '@services/sorting.service';
import { FilterModel } from '../components/filters/popup-filter/model/filter.model';
import { PopupFilterComponent } from '../components/filters/popup-filter/popup-filter.component';
import { getFilteredEntities } from '../components/filters/popup-filter/utils/filter-utils';
import { QuickFiltersComponent } from '../components/filters/quick-filters/quick-filters.component';
import { Component, Injector, ViewChild } from '@angular/core';
import { SortingOption, SortingService } from '@services/sorting.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
// Functionalities: Filter, search, select, sort
// Usage: overwrite necessary methods/members in your component
import { FilterService } from '../services/filter.service';
import { SearchService } from '../services/search.service';
import { ScreenStateService } from '../services/screen-state.service';
import { Observable } from 'rxjs';
import { FilterModel } from '../components/filters/popup-filter/model/filter.model';
@Component({ template: '' })
export abstract class BaseListingComponent<T = any> {
allEntities: T[] = [];
filteredEntities: T[] = [];
displayedEntities: T[] = [];
selectedEntitiesIds: string[] = [];
searchForm: FormGroup;
@ViewChild(CdkVirtualScrollViewport) scrollViewport: CdkVirtualScrollViewport;
export abstract class BaseListingComponent<T> {
@ViewChild(CdkVirtualScrollViewport)
readonly scrollViewport: CdkVirtualScrollViewport;
protected readonly _formBuilder: FormBuilder;
protected readonly _changeDetectorRef: ChangeDetectorRef;
readonly filterService: FilterService<T>;
protected readonly _sortingService: SortingService;
// ----
// Overwrite in child class:
protected readonly _searchKey: string;
protected readonly _selectionKey: string;
protected readonly _sortKey: ScreenName;
protected readonly _searchService: SearchService<T>;
protected readonly _screenStateService: ScreenStateService<T>;
protected constructor(protected readonly _injector: Injector) {
this._formBuilder = this._injector.get<FormBuilder>(FormBuilder);
this._changeDetectorRef = this._injector.get<ChangeDetectorRef>(ChangeDetectorRef);
this.filterService = this._injector.get<FilterService<T>>(FilterService);
this._sortingService = this._injector.get<SortingService>(SortingService);
this._initSearch();
this._searchService = this._injector.get<SearchService<T>>(SearchService);
this._screenStateService = this._injector.get<ScreenStateService<T>>(ScreenStateService);
}
get hasActiveFilters() {
return (
this._filterComponents
.filter(f => !!f)
.reduce((prev, component) => prev || component?.hasActiveFilters, false) ||
this.searchForm.get('query').value
);
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 allEntities(): T[] {
return this._screenStateService.entities;
}
get areAllEntitiesSelected() {
return (
this.displayedEntities.length !== 0 &&
this.selectedEntitiesIds.length === this.displayedEntities.length
);
return this._screenStateService.areAllEntitiesSelected;
}
get areSomeEntitiesSelected() {
return this.selectedEntitiesIds.length > 0;
get areSomeEntitiesSelected$() {
return this._screenStateService.areSomeEntitiesSelected$;
}
get sortingOption(): SortingOption {
return this._sortingService.getSortingOption(this._getSortKey);
return this._sortingService.getSortingOption();
}
protected get _filters(): {
values: FilterModel[];
checker: Function;
matchAll?: boolean;
checkerArgs?: any;
}[] {
return [];
getFilter$(slug: string): Observable<FilterModel[]> {
return this.filterService.getFilter$(slug);
}
protected get _filterComponents(): (PopupFilterComponent | QuickFiltersComponent)[] {
return [];
}
// ----
private get _getSearchKey(): string {
if (!this._searchKey) throw new Error('Not implemented');
return this._searchKey;
}
// Search
private get _getSelectionKey(): string {
if (!this._selectionKey) throw new Error('Not implemented');
return this._selectionKey;
}
private get _getSortKey(): ScreenName {
if (!this._sortKey) throw new Error('Not implemented');
return this._sortKey;
}
filtersChanged(filters?: { [key: string]: FilterModel[] }): void {
if (filters) {
for (const key of Object.keys(filters)) {
for (let idx = 0; idx < this[key].length; ++idx) {
this[key][idx] = filters[key][idx];
}
}
}
this._filterEntities();
get searchForm() {
return this._searchService.searchForm;
}
resetFilters() {
for (const filterComponent of this._filterComponents.filter(f => !!f)) {
filterComponent.deactivateAllFilters();
}
this.filtersChanged();
this.searchForm.reset({ query: '' });
}
// Filter
toggleEntitySelected($event: MouseEvent, entity: T) {
$event.stopPropagation();
const idx = this.selectedEntitiesIds.indexOf(entity[this._getSelectionKey]);
if (idx === -1) {
this.selectedEntitiesIds.push(entity[this._getSelectionKey]);
} else {
this.selectedEntitiesIds.splice(idx, 1);
}
}
toggleSelectAll() {
if (this.areSomeEntitiesSelected) {
this.selectedEntitiesIds = [];
} else {
this.selectedEntitiesIds = this.displayedEntities.map(
entity => entity[this._getSelectionKey]
);
}
}
isSelected(entity: T) {
return this.selectedEntitiesIds.indexOf(entity[this._getSelectionKey]) !== -1;
this.filterService.reset();
}
toggleSort($event) {
this._sortingService.toggleSort(this._getSortKey, $event);
this._sortingService.toggleSort($event);
}
// Selection
protected _preFilter() {
return;
toggleSelectAll() {
return this._screenStateService.toggleSelectAll();
}
protected _searchField(entity: T): string {
return entity[this._getSearchKey];
toggleEntitySelected(event: MouseEvent, entity: T) {
event.stopPropagation();
return this._screenStateService.toggleEntitySelected(entity);
}
@debounce(200)
protected _executeSearch() {
this._executeSearchImmediately();
}
protected _executeSearchImmediately() {
this.displayedEntities = (
this._filters.length ? this.filteredEntities : this.allEntities
).filter(entity =>
this._searchField(entity)
.toLowerCase()
.includes(this.searchForm.get('query').value.toLowerCase())
);
this._updateSelection();
}
protected _updateSelection() {
if (this._selectionKey) {
this.selectedEntitiesIds = this.displayedEntities
.map(entity => entity[this._getSelectionKey])
.filter(id => this.selectedEntitiesIds.includes(id));
}
}
// Sort
protected _filterEntities() {
this._preFilter();
this.filteredEntities = getFilteredEntities(this.allEntities, this._filters);
this._executeSearch();
this._changeDetectorRef.detectChanges();
}
private _initSearch() {
this.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe(() => this._executeSearch());
isSelected(entity: T) {
return this._screenStateService.isSelected(entity);
}
}

View File

@ -11,6 +11,7 @@
</div>
<redaction-circle-button
class="pl-1"
(action)="save.emit(value)"
icon="red:check"
tooltip="assign-user.save"

View File

@ -1,5 +1,12 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
export type IconButtonType = 'default' | 'show-bg' | 'primary';
export enum IconButtonTypes {
DEFAULT = 'default',
SHOW_BG = 'show-bg',
PRIMARY = 'primary'
}
@Component({
selector: 'redaction-icon-button',
templateUrl: './icon-button.component.html',
@ -11,6 +18,6 @@ export class IconButtonComponent {
@Input() text: string;
@Input() showDot = false;
@Input() disabled = false;
@Input() type: 'default' | 'show-bg' | 'primary' = 'default';
@Input() type: IconButtonType = IconButtonTypes.DEFAULT;
@Output() action = new EventEmitter<any>();
}

View File

@ -0,0 +1,14 @@
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;
checkerArgs?: any;
}

View File

@ -16,49 +16,52 @@
<mat-menu
#filterMenu="matMenu"
(closed)="applyFilters()"
[class]="secondaryFilters?.length > 0 ? 'padding-bottom-0' : ''"
[class.padding-bottom-0]="secondaryFilters?.length > 0"
xPosition="before"
>
<div (mouseenter)="filterMouseEnter()" (mouseleave)="filterMouseLeave()">
<ng-template matMenuContent>
<div class="filter-menu-header">
<div class="all-caps-label" translate="filter-menu.filter-types"></div>
<div class="actions">
<div
(click)="activateAllFilters(); $event.stopPropagation()"
(click)="activatePrimaryFilters(); $event.stopPropagation()"
class="all-caps-label primary pointer"
translate="actions.all"
></div>
<div
(click)="deactivateAllFilters(); $event.stopPropagation()"
(click)="deactivateFilters(); $event.stopPropagation()"
class="all-caps-label primary pointer"
translate="actions.none"
></div>
</div>
</div>
<div *ngFor="let filter of primaryFilters">
<ng-template
<ng-container
[ngTemplateOutlet]="defaultFilterTemplate"
[ngTemplateOutletContext]="{
filter: filter,
atLeastOneIsExpandable: atLeastOneFilterIsExpandable
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-template>
></ng-container>
</div>
<div *ngIf="secondaryFilters?.length > 0" class="filter-options">
<div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div>
<div *ngFor="let filter of secondaryFilters">
<ng-template
<ng-container
[ngTemplateOutlet]="defaultFilterTemplate"
[ngTemplateOutletContext]="{
filter: filter,
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-template>
></ng-container>
</div>
</div>
</div>
</ng-template>
</mat-menu>
<ng-template #defaultFilterLabelTemplate let-filter="filter">
@ -88,16 +91,17 @@
[indeterminate]="_(filter).indeterminate"
class="filter-menu-checkbox"
>
<ng-template
<ng-container
[ngTemplateOutlet]="filterTemplate ?? defaultFilterLabelTemplate"
[ngTemplateOutletContext]="{ filter: filter }"
[ngTemplateOutlet]="filterTemplate ? filterTemplate : defaultFilterLabelTemplate"
></ng-template>
></ng-container>
</mat-checkbox>
<ng-template
<ng-container
[ngTemplateOutlet]="actionsTemplate ?? null"
[ngTemplateOutletContext]="{ filter: filter }"
[ngTemplateOutlet]="actionsTemplate ? actionsTemplate : null"
></ng-template>
></ng-container>
</div>
<div *ngIf="_(filter).filters?.length && _(filter).expanded">
<div
(click)="$event.stopPropagation()"
@ -108,17 +112,16 @@
(click)="filterCheckboxClicked($event, subFilter, filter)"
[checked]="subFilter.checked"
>
<ng-template
<ng-container
[ngTemplateOutlet]="filterTemplate ?? defaultFilterLabelTemplate"
[ngTemplateOutletContext]="{ filter: subFilter }"
[ngTemplateOutlet]="
filterTemplate ? filterTemplate : defaultFilterLabelTemplate
"
></ng-template>
></ng-container>
</mat-checkbox>
<ng-template
<ng-container
[ngTemplateOutlet]="actionsTemplate ?? null"
[ngTemplateOutletContext]="{ filter: subFilter }"
[ngTemplateOutlet]="actionsTemplate ? actionsTemplate : null"
></ng-template>
></ng-container>
</div>
</div>
</ng-template>

View File

@ -10,6 +10,7 @@ import {
import { FilterModel } from './model/filter.model';
import { handleCheckedValue } from './utils/filter-utils';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'redaction-popup-filter',
@ -34,25 +35,20 @@ export class PopupFilterComponent implements OnChanges {
@Input() actionsTemplate: TemplateRef<any>;
@Input() primaryFilters: FilterModel[] = [];
@Input() secondaryFilters: FilterModel[] = [];
@Input() filterLabel = 'filter-menu.label';
@Input() filterLabel = this._translateService.instant('filter-menu.label');
@Input() icon: string;
@Input() chevron = false;
mouseOver = true;
mouseOverTimeout: number;
atLeastOneFilterIsExpandable = false;
atLeastOneSecondaryFilterIsExpandable = false;
constructor(private readonly _changeDetectorRef: ChangeDetectorRef) {}
constructor(
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _translateService: TranslateService
) {}
get hasActiveFilters(): boolean {
for (const filter of this._allFilters) {
if (filter.checked || filter.indeterminate) {
return true;
}
}
return false;
return !!this._allFilters.find(f => f.checked || f.indeterminate);
}
private get _allFilters(): FilterModel[] {
@ -60,41 +56,34 @@ export class PopupFilterComponent implements OnChanges {
}
ngOnChanges(): void {
this.atLeastOneFilterIsExpandable = false;
this.atLeastOneSecondaryFilterIsExpandable = false;
this.primaryFilters?.forEach(f => {
this.atLeastOneFilterIsExpandable =
this.atLeastOneFilterIsExpandable || this.isExpandable(f);
});
this.secondaryFilters?.forEach(f => {
this.atLeastOneSecondaryFilterIsExpandable =
this.atLeastOneSecondaryFilterIsExpandable || this.isExpandable(f);
});
this.atLeastOneFilterIsExpandable = !!this.primaryFilters?.find(f => this.isExpandable(f));
this.atLeastOneSecondaryFilterIsExpandable = !!this.secondaryFilters?.find(f =>
this.isExpandable(f)
);
}
filterCheckboxClicked($event: any, filter: FilterModel, parent?: FilterModel) {
$event.stopPropagation();
filter.checked = !filter.checked;
if (parent) {
handleCheckedValue(parent);
} else {
if (filter.indeterminate) {
filter.checked = false;
}
if (filter.indeterminate) filter.checked = false;
filter.indeterminate = false;
filter.filters?.forEach(f => (f.checked = filter.checked));
}
this._changeDetectorRef.detectChanges();
this.applyFilters();
}
activateAllFilters() {
this._setAllFilters(true);
activatePrimaryFilters() {
this._setFilters(true);
}
deactivateAllFilters() {
this._setAllFilters(false);
deactivateFilters() {
this._setFilters();
}
applyFilters() {
@ -102,6 +91,7 @@ export class PopupFilterComponent implements OnChanges {
primary: this.primaryFilters,
secondary: this.secondaryFilters
});
this._changeDetectorRef.detectChanges();
}
toggleFilterExpanded($event: MouseEvent, filter: FilterModel) {
@ -109,36 +99,21 @@ export class PopupFilterComponent implements OnChanges {
filter.expanded = !filter.expanded;
}
filterMouseEnter() {
this.mouseOver = true;
if (this.mouseOverTimeout) {
clearTimeout(this.mouseOverTimeout);
}
}
filterMouseLeave() {
this.mouseOver = false;
this.mouseOverTimeout = setTimeout(() => {
// this.trigger.closeMenu();
}, 1000);
}
isExpandable(filter: FilterModel) {
return filter.filters && filter.filters.length > 0;
return filter?.filters?.length > 0;
}
_(obj): FilterModel {
return obj as FilterModel;
}
private _setAllFilters(value: boolean) {
const filters = value ? this.primaryFilters : this._allFilters;
private _setFilters(onlyPrimaryFilters = false) {
const filters = onlyPrimaryFilters ? this.primaryFilters : this._allFilters;
filters.forEach(f => {
f.checked = value;
f.checked = onlyPrimaryFilters;
f.indeterminate = false;
f.filters?.forEach(ff => {
ff.checked = value;
});
f.filters?.forEach(ff => (ff.checked = onlyPrimaryFilters));
});
this.applyFilters();
}
}

View File

@ -1,7 +1,8 @@
import { FilterModel } from '../model/filter.model';
import { FileStatusWrapper } from '../../../../../../models/file/file-status.wrapper';
import { DossierWrapper } from '../../../../../../state/model/dossier.wrapper';
import { PermissionsService } from '../../../../../../services/permissions.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { PermissionsService } from '@services/permissions.service';
import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) {
copySettings(oldFilters, newFilters);
@ -175,11 +176,8 @@ export const addedDateChecker = (dw: DossierWrapper, filter: FilterModel) =>
export const dossierApproverChecker = (dw: DossierWrapper, filter: FilterModel) =>
dw.approverIds.includes(filter.key);
export function getFilteredEntities(
entities: any[],
filters: { values: FilterModel[]; checker: Function; matchAll?: boolean; checkerArgs?: any }[]
) {
const filteredEntities = [];
export function getFilteredEntities<T>(entities: T[], filters: FilterWrapper[]) {
const filteredEntities: T[] = [];
for (const entity of entities) {
let add = true;
for (const filter of filters) {

View File

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

View File

@ -1,29 +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[];
constructor() {}
get hasActiveFilters(): boolean {
return this.filters.filter(f => f.checked).length > 0;
}
deactivateAllFilters() {
for (const filter of this.filters) {
filter.checked = false;
}
}
toggle(filter: FilterModel) {
filter.checked = !filter.checked;
this.filtersChanged.emit(this.filters);
}
constructor(readonly filterService: FilterService<T>) {}
}

View File

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

View File

@ -0,0 +1,5 @@
import { BaseHeaderConfig } from './base-config.model';
export interface ActionConfig extends BaseHeaderConfig {
action: ($event) => void;
}

View File

@ -0,0 +1,5 @@
export interface BaseHeaderConfig {
label: string;
icon?: string;
hide?: boolean;
}

View File

@ -0,0 +1,7 @@
import { IconButtonType } from '../../buttons/icon-button/icon-button.component';
import { BaseHeaderConfig } from './base-config.model';
export interface ButtonConfig extends BaseHeaderConfig {
action: ($event) => void;
type?: IconButtonType;
}

View File

@ -0,0 +1,9 @@
import { FilterModel } from '../../filters/popup-filter/model/filter.model';
import { TemplateRef } from '@angular/core';
import { BaseHeaderConfig } from './base-config.model';
export interface FilterConfig extends BaseHeaderConfig {
primaryFilters?: FilterModel[];
primaryFiltersLabel?: string;
filterTemplate?: TemplateRef<any>;
}

View File

@ -0,0 +1,66 @@
<div class="page-header">
<div *ngIf="pageLabel" class="breadcrumb">{{ pageLabel }}</div>
<div class="filters" *ngIf="filters$ | async as filters">
<div translate="filters.filter-by" *ngIf="filters.length"></div>
<ng-container *ngFor="let config of filters; trackBy: trackByLabel">
<redaction-popup-filter
(filtersChanged)="filterService.filterEntities()"
*ngIf="!config.hide"
[filterLabel]="config.label"
[icon]="config.icon"
[primaryFilters]="config.values"
[filterTemplate]="config.filterTemplate"
></redaction-popup-filter>
</ng-container>
<redaction-input-with-action
*ngIf="searchService.isSearchNeeded"
[form]="searchService.searchForm"
[placeholder]="searchPlaceholder"
type="search"
></redaction-input-with-action>
<div
(click)="resetFilters()"
*ngIf="(filterService.showResetFilters$ | async) || searchService.searchValue"
class="reset-filters"
translate="reset-filters"
></div>
</div>
<ng-container *ngFor="let config of buttonConfigs; trackBy: trackByLabel">
<redaction-icon-button
(action)="config.action($event)"
*ngIf="!config.hide"
[icon]="config.icon"
[text]="config.label"
[type]="config.type"
></redaction-icon-button>
</ng-container>
<div class="actions" *ngIf="showCloseButton || actionConfigs">
<ng-container *ngFor="let config of actionConfigs; trackBy: trackByLabel">
<redaction-circle-button
(action)="config.action($event)"
*ngIf="!config.hide"
[icon]="config.icon"
[tooltip]="config.label"
tooltipPosition="below"
></redaction-circle-button>
</ng-container>
<!-- Extra custom actions here -->
<ng-content></ng-content>
<redaction-circle-button
[class.ml-6]="actionConfigs"
*ngIf="showCloseButton && permissionsService.isUser()"
icon="red:close"
redactionNavigateLastDossiersScreen
tooltip="common.close"
tooltipPosition="below"
></redaction-circle-button>
</div>
</div>

View File

@ -0,0 +1,3 @@
.ml-6 {
margin-left: 6px;
}

View File

@ -0,0 +1,39 @@
import { Component, Input } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { ActionConfig } from '@shared/components/page-header/models/action-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 { map } from 'rxjs/operators';
@Component({
selector: 'redaction-page-header',
templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.scss']
})
export class PageHeaderComponent<T> {
@Input() pageLabel: string;
@Input() showCloseButton: boolean;
@Input() actionConfigs: ActionConfig[];
@Input() buttonConfigs: ButtonConfig[];
@Input() searchPlaceholder: string;
constructor(
readonly permissionsService: PermissionsService,
readonly filterService: FilterService<T>,
readonly searchService: SearchService<T>
) {}
get filters$() {
return this.filterService.allFilters$.pipe(map(all => all.filter(f => f.icon)));
}
resetFilters() {
this.filterService.reset();
this.searchService.reset();
}
trackByLabel(index: number, item) {
return item.label;
}
}

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
import { orderBy } from 'lodash';
import { SortingOrders } from '@services/sorting.service';
@Pipe({ name: 'sortBy' })
export class SortByPipe implements PipeTransform {
@ -8,7 +9,7 @@ export class SortByPipe implements PipeTransform {
return value;
} // no array
if (!column || column === '') {
if (order === 'asc') {
if (order === SortingOrders.ASC) {
return value.sort();
} else {
return value.sort().reverse();

View File

@ -0,0 +1,62 @@
import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core';
import * as moment from 'moment';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe as BaseDatePipe } from '@angular/common';
const HOURS_IN_A_DAY = 24;
const MINUTES_IN_AN_HOUR = 60;
@Pipe({
name: 'date'
})
export class DatePipe extends BaseDatePipe implements PipeTransform {
constructor(
@Inject(LOCALE_ID) private readonly _locale: string,
private readonly _translateService: TranslateService
) {
super(_locale);
}
transform(value: null | undefined, format?: string, timezone?: string, locale?: string): null;
transform(
value: Date | string | number | null | undefined,
format?: string,
timezone?: string,
locale?: string
): string | null;
transform(value: any, format?: string, timezone?: string, locale?: string): string {
if (format === 'timeFromNow') return this._getTimeFromNow(value);
return super.transform(value, format, timezone, locale);
}
private _getTimeFromNow(item: string) {
const date = moment(item);
const now = new Date(Date.now());
const daysLeft = date.diff(now, 'days');
const hoursFromNow = date.diff(now, 'hours');
const hoursLeft = hoursFromNow - HOURS_IN_A_DAY * daysLeft;
const minutesFromNow = date.diff(now, 'minutes');
const minutesLeft = minutesFromNow - HOURS_IN_A_DAY * MINUTES_IN_AN_HOUR * daysLeft;
if (daysLeft === 0 && hoursLeft === 0 && minutesLeft > 0)
return this._translate('time.less-than-an-hour');
const hoursSuffix = this._translate(`time.hour${hoursLeft === 1 ? '' : 's'}`);
const hoursDisplay = `${hoursLeft} ${hoursSuffix}`;
if (daysLeft === 0 && hoursLeft > 0) return hoursDisplay;
const daysSuffix = this._translate(`time.day${daysLeft === 1 ? '' : 's'}`);
const daysDisplay = `${daysLeft} ${daysSuffix}`;
if (daysLeft > 0 && hoursLeft > 0) return `${daysDisplay} ${hoursDisplay}`;
if (daysLeft > 0) return daysDisplay;
return this._translate(`time.no-time-left`);
}
private _translate(value: string, params?: { [key: string]: string }) {
return this._translateService.instant(value, params);
}
}

View File

@ -0,0 +1,106 @@
import { ChangeDetectorRef, Injectable } from '@angular/core';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
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> {
_allFilters$ = new BehaviorSubject<FilterWrapper[]>([]);
constructor(
private readonly _screenStateService: ScreenStateService<T>,
private readonly _searchService: SearchService<T>,
private readonly _changeDetector: ChangeDetectorRef
) {}
get filters() {
return Object.values(this._allFilters$.getValue());
}
get showResetFilters$() {
return this.allFilters$.pipe(
map(all => this._toFlatFilters(all)),
filter(f => !!f.find(el => el.checked)),
distinctUntilChanged()
);
}
filterChecked$(slug: string, key: string) {
const filters = this.getFilter$(slug);
return filters.pipe(map(all => all.find(f => f.key === key)?.checked));
}
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 {
const filtered = getFilteredEntities(this._screenStateService.entities, this.filters);
this._screenStateService.setFilteredEntities(filtered);
this._searchService.executeSearchImmediately();
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(
filter(f => f !== null && f !== undefined),
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.filters.forEach(item => {
item.values.forEach(child => {
child.checked = false;
child.indeterminate = false;
child.filters?.forEach(f => {
f.checked = false;
f.indeterminate = false;
});
});
});
this._allFilters$.next(this.filters);
this.filterEntities();
}
private _toFlatFilters(entities: FilterWrapper[]): FilterModel[] {
const flatChildren = (filters: FilterModel[]) =>
(filters ?? []).reduce((acc, f) => [...acc, ...(f?.filters ?? [])], []);
return entities.reduce((acc, f) => [...acc, ...f.values, ...flatChildren(f.values)], []);
}
}

View File

@ -0,0 +1,112 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
@Injectable()
export class ScreenStateService<T> {
entities$ = new BehaviorSubject<T[]>([]);
filteredEntities$ = new BehaviorSubject<T[]>([]);
displayedEntities$ = new BehaviorSubject<T[]>([]);
selectedEntitiesIds$ = new BehaviorSubject<string[]>([]);
private _idKey: string;
get entities(): T[] {
return Object.values(this.entities$.getValue());
}
get filteredEntities(): 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());
}
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[]>): void {
this.filteredEntities$.next(newEntities);
}
setSelectedEntitiesIds(newEntities: Partial<string[]>): void {
this.selectedEntitiesIds$.next(newEntities);
}
setDisplayedEntities(newEntities: Partial<T[]>): void {
this.displayedEntities$.next(newEntities);
}
setIdKey(value: string): void {
this._idKey = value;
}
get areAllEntitiesSelected(): boolean {
return (
this.displayedEntities.length !== 0 &&
this.selectedEntitiesIds.length === this.displayedEntities.length
);
}
get areSomeEntitiesSelected$(): Observable<boolean> {
return this.selectedEntitiesIds$.pipe(map(all => all.length > 0));
}
isSelected(entity: T): boolean {
return this.selectedEntitiesIds.indexOf(entity[this._getIdKey]) !== -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.filter((el, idx) => idx !== currentEntityIdx)
);
}
toggleSelectAll(): void {
if (this.areAllEntitiesSelected) return this.setSelectedEntitiesIds([]);
this.setSelectedEntitiesIds(this._displayedEntitiesIds);
}
updateSelection(): void {
if (!this._idKey) return;
const ids = this._displayedEntitiesIds.filter(id => this.selectedEntitiesIds.includes(id));
this.setSelectedEntitiesIds(ids);
}
logCurrentState(): void {
console.log('Entities', this.entities);
console.log('Displayed', this.displayedEntities);
console.log('Filtered', this.filteredEntities);
console.log('Selected', this.selectedEntitiesIds);
}
private get _displayedEntitiesIds(): string[] {
return this.displayedEntities.map(entity => entity[this._getIdKey]);
}
private get _getIdKey(): string {
if (!this._idKey) throw new Error('Not implemented');
return this._idKey;
}
}

View File

@ -0,0 +1,63 @@
import { Injectable } from '@angular/core';
import { debounce } from '@utils/debounce';
import { ScreenStateService } from '@shared/services/screen-state.service';
import { FormBuilder } from '@angular/forms';
@Injectable()
export class SearchService<T> {
private _searchValue = '';
private _searchKey: string;
readonly searchForm = this._formBuilder.group({
query: ['']
});
constructor(
private readonly _screenStateService: ScreenStateService<T>,
private readonly _formBuilder: FormBuilder
) {
this.searchForm.valueChanges.subscribe(() => this.executeSearch());
}
@debounce(200)
executeSearch(): void {
this._searchValue = this.searchValue.toLowerCase();
this.executeSearchImmediately();
}
executeSearchImmediately(): void {
const displayed =
this._screenStateService.filteredEntities || this._screenStateService.entities;
if (!this._searchKey) {
return this._screenStateService.setDisplayedEntities(displayed);
}
this._screenStateService.setDisplayedEntities(
displayed.filter(entity =>
this._searchField(entity).toLowerCase().includes(this._searchValue)
)
);
this._screenStateService.updateSelection();
}
setSearchKey(value: string): void {
this._searchKey = value;
}
get isSearchNeeded(): boolean {
return !!this._searchKey;
}
get searchValue(): string {
return this.searchForm.get('query').value;
}
reset(): void {
this.searchForm.reset({ query: '' });
}
protected _searchField(entity: T): string {
return entity[this._searchKey];
}
}

View File

@ -37,6 +37,8 @@ import { QuickFiltersComponent } from './components/filters/quick-filters/quick-
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import { AssignUserDropdownComponent } from './components/assign-user-dropdown/assign-user-dropdown.component';
import { InputWithActionComponent } from '@shared/components/input-with-action/input-with-action.component';
import { PageHeaderComponent } from './components/page-header/page-header.component';
import { DatePipe } from '@shared/pipes/date.pipe';
const buttons = [
ChevronButtonComponent,
@ -67,12 +69,14 @@ const components = [
DictionaryManagerComponent,
QuickFiltersComponent,
AssignUserDropdownComponent,
PageHeaderComponent,
...buttons
];
const utils = [
HumanizePipe,
DatePipe,
SyncWidthDirective,
HasScrollbarDirective,
NavigateLastDossiersScreenDirective

View File

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

View File

@ -1,7 +1,14 @@
import { Injectable } from '@angular/core';
export class SortingOption {
order: 'asc' | 'desc';
export type SortingOrder = 'asc' | 'desc';
export enum SortingOrders {
ASC = 'asc',
DESC = 'desc'
}
export interface SortingOption {
order: SortingOrder;
column: string;
}
@ -14,30 +21,50 @@ export type ScreenName =
| 'file-attributes-listing'
| 'dossier-attributes-listing';
@Injectable({
providedIn: 'root'
})
export enum ScreenNames {
DOSSIER_LISTING = 'dossier-listing',
DOSSIER_OVERVIEW = 'dossier-overview',
DICTIONARY_LISTING = 'dictionary-listing',
DOSSIER_TEMPLATES_LISTING = 'dossier-templates-listing',
DEFAULT_COLORS = 'default-colors',
FILE_ATTRIBUTES_LISTING = 'file-attributes-listing',
DOSSIER_ATTRIBUTES_LISTING = 'dossier-attributes-listing'
}
@Injectable()
export class SortingService {
private readonly _options: { [key: string]: SortingOption } = {
'dossier-listing': { column: 'dossier.dossierName', order: 'asc' },
'dossier-overview': { column: 'filename', order: 'asc' },
'dictionary-listing': { column: 'label', order: 'asc' },
'dossier-templates-listing': { column: 'name', order: 'asc' },
'default-colors': { column: 'key', order: 'asc' },
'file-attributes-listing': { column: 'label', order: 'asc' },
'dossier-attributes-listing': { column: 'label', order: 'asc' }
private _currentScreenName: string;
private readonly _options: { [key in ScreenName]: SortingOption } = {
[ScreenNames.DOSSIER_LISTING]: { column: 'dossier.dossierName', order: SortingOrders.ASC },
[ScreenNames.DOSSIER_OVERVIEW]: { column: 'filename', order: SortingOrders.ASC },
[ScreenNames.DICTIONARY_LISTING]: { column: 'label', order: SortingOrders.ASC },
[ScreenNames.DOSSIER_TEMPLATES_LISTING]: { column: 'name', order: SortingOrders.ASC },
[ScreenNames.DEFAULT_COLORS]: { column: 'key', order: SortingOrders.ASC },
[ScreenNames.FILE_ATTRIBUTES_LISTING]: { column: 'label', order: SortingOrders.ASC },
[ScreenNames.DOSSIER_ATTRIBUTES_LISTING]: { column: 'label', order: 'asc' }
};
toggleSort(screen: ScreenName, column: string) {
if (this._options[screen].column === column) {
const currentOrder = this._options[screen].order;
this._options[screen].order = currentOrder === 'asc' ? 'desc' : 'asc';
setScreenName(value: string) {
this._currentScreenName = value;
}
toggleSort(column: string) {
if (this._options[this._currentScreenName].column === column) {
this._currentOrder = this._currentOrder === SortingOrders.ASC ? SortingOrders.DESC : SortingOrders.ASC;
} else {
this._options[screen] = { column, order: 'asc' };
this._options[this._currentScreenName] = { column, order: SortingOrders.ASC };
}
}
getSortingOption(screen: ScreenName) {
return this._options[screen];
getSortingOption() {
return this._options[this._currentScreenName];
}
private get _currentOrder(): string {
return this._options[this._currentScreenName].order;
}
private set _currentOrder(value: string) {
this._options[this._currentScreenName].order = value;
}
}

View File

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

View File

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

View File

@ -1271,6 +1271,34 @@
},
"title": "Configure SMTP Account"
},
"trash": {
"label": "Trash",
"table-header": {
"title": "{{length}} deleted dossiers",
"info": "Deleted items can be restored up to {{hours}} hours from their deletions"
},
"bulk": {
"delete": "Forever Delete Selected Dossiers",
"restore": "Restore Selected Dossiers"
},
"action": {
"delete": "Delete forever",
"restore": "Restore"
},
"search": "Search...",
"table-col-names": {
"name": "Name",
"owner": "Owner",
"deleted-on": "Deleted on",
"time-to-restore": "Time to restore"
},
"no-data": {
"title": "There are no dossiers yet."
},
"no-match": {
"title": "No dossiers match your current filters."
}
},
"sorting": {
"alphabetically": "Alphabetically",
"custom": "Custom",
@ -1289,13 +1317,9 @@
"children": {
"admin": "Settings",
"downloads": "My Downloads",
"language": {
"de": "German",
"en": "English",
"label": "Language"
},
"logout": "Logout",
"my-profile": "My Profile"
"my-profile": "My Profile",
"trash": "Trash",
"logout": "Logout"
}
}
}
@ -1381,5 +1405,13 @@
"text-placeholder": "Enter text"
},
"title": "Watermark"
},
"time": {
"no-time-left": "Time to restore already passed",
"less-than-an-hour": "< 1 hour",
"hour": "hour",
"hours": "hours",
"day": "day",
"days": "days"
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<title>711C9D82-CAA8-47BE-954A-A9DA22CE85E6</title>
<g id="Trash" stroke="none" stroke-width="1" fill="currentColor" fill-rule="evenodd">
<g id="05.-Trash-bulk-actions" transform="translate(-133.000000, -130.000000)">
<g id="Group-36" transform="translate(0.000000, 112.000000)">
<g id="Group-9" transform="translate(123.000000, 0.000000)">
<g id="Group-21" transform="translate(0.000000, 8.000000)" fill="currentColor"
fill-rule="nonzero">
<g id="Put-back" transform="translate(10.000000, 10.000000)">
<path
d="M5,4.42 L3.8,4.42 L6.5,1.72 L5,0.42 L0,5.42 L5.1,10.52 L6.5,9.12 L3.8,6.42 L5,6.42 C8.9,6.42 12,9.52 12,13.42 L14,13.42 C14,8.52 10,4.42 5,4.42 Z"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

@ -19,10 +19,12 @@ export interface Dossier {
dossierTemplateId?: string;
downloadFileTypes?: Array<Dossier.DownloadFileTypesEnum>;
dueDate?: string;
hardDeletedTime?: string;
memberIds?: Array<string>;
ownerId?: string;
reportTemplateIds?: Array<string>;
reportTypes?: Array<Dossier.ReportTypesEnum>;
softDeletedTime?: string;
status?: Dossier.StatusEnum;
watermarkEnabled?: boolean;
}