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:
commit
b70d711f87
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]"
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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']"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -45,9 +45,9 @@
|
||||
|
||||
<div *ngIf="hasFiles" class="mt-24">
|
||||
<redaction-simple-doughnut-chart
|
||||
(toggleFilter)="toggleFilter('statusFilters', $event)"
|
||||
(toggleFilter)="filterService.filterEntities()"
|
||||
[config]="documentsChartData"
|
||||
[filter]="filters.statusFilters"
|
||||
[filter]="filterService.getFilter$('statusFilters') | async"
|
||||
[radius]="63"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier'"
|
||||
@ -57,9 +57,9 @@
|
||||
|
||||
<div *ngIf="hasFiles" class="mt-24 legend pb-32">
|
||||
<div
|
||||
(click)="toggleFilter('needsWorkFilters', filter.key)"
|
||||
*ngFor="let filter of filters.needsWorkFilters"
|
||||
[class.active]="filter.checked"
|
||||
(click)="filterService.toggleFilter('needsWorkFilters', filter.key)"
|
||||
*ngFor="let filter of filterService.getFilter$('needsWorkFilters') | async"
|
||||
[class.active]="filterService.filterChecked$('needsWorkFilters', filter.key) | async"
|
||||
>
|
||||
<redaction-type-filter [filter]="filter"></redaction-type-filter>
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { groupBy } from '@utils/functions';
|
||||
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { TranslateChartService } from '@services/translate-chart.service';
|
||||
import { StatusSorter } from '@utils/sorters/status-sorter';
|
||||
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { User } from '@redaction/red-ui-http';
|
||||
import { NotificationService } from '@services/notification.service';
|
||||
import { FilterService } from '../../../shared/services/filter.service';
|
||||
import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-dossier-details',
|
||||
@ -19,8 +20,6 @@ export class DossierDetailsComponent implements OnInit {
|
||||
documentsChartData: DoughnutChartConfig[] = [];
|
||||
owner: User;
|
||||
editingOwner = false;
|
||||
@Input() filters: { needsWorkFilters: FilterModel[]; statusFilters: FilterModel[] };
|
||||
@Output() filtersChanged = new EventEmitter();
|
||||
@Output() openAssignDossierMembersDialog = new EventEmitter();
|
||||
@Output() openDossierDictionaryDialog = new EventEmitter();
|
||||
@Output() toggleCollapse = new EventEmitter();
|
||||
@ -29,6 +28,7 @@ export class DossierDetailsComponent implements OnInit {
|
||||
readonly appStateService: AppStateService,
|
||||
readonly translateChartService: TranslateChartService,
|
||||
readonly permissionsService: PermissionsService,
|
||||
readonly filterService: FilterService<FileStatusWrapper>,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _notificationService: NotificationService
|
||||
@ -76,12 +76,6 @@ export class DossierDetailsComponent implements OnInit {
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
toggleFilter(filterType: 'needsWorkFilters' | 'statusFilters', key: string): void {
|
||||
const filter = this.filters[filterType].find(f => f.key === key);
|
||||
filter.checked = !filter.checked;
|
||||
this.filtersChanged.emit(this.filters);
|
||||
}
|
||||
|
||||
async assignOwner(user: User | string) {
|
||||
this.owner = typeof user === 'string' ? this._userService.getRedUserById(user) : user;
|
||||
const dw = Object.assign({}, this.appStateService.activeDossier);
|
||||
|
||||
@ -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'"
|
||||
|
||||
@ -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>
|
||||
) {}
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<button
|
||||
(click)="scroll(ButtonType.TOP)"
|
||||
[hidden]="!showScroll(ButtonType.TOP)"
|
||||
(click)="scroll(buttonType.TOP)"
|
||||
[hidden]="!showScroll(buttonType.TOP)"
|
||||
class="scroll-button top pointer"
|
||||
>
|
||||
<mat-icon svgIcon="red:arrow-down-o"></mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="scroll(ButtonType.BOTTOM)"
|
||||
[hidden]="!showScroll(ButtonType.BOTTOM)"
|
||||
(click)="scroll(buttonType.BOTTOM)"
|
||||
[hidden]="!showScroll(buttonType.BOTTOM)"
|
||||
class="scroll-button bottom pointer"
|
||||
>
|
||||
<mat-icon svgIcon="red:arrow-down-o"></mat-icon>
|
||||
|
||||
@ -12,7 +12,7 @@ enum ButtonType {
|
||||
styleUrls: ['./scroll-button.component.scss']
|
||||
})
|
||||
export class ScrollButtonComponent {
|
||||
ButtonType = ButtonType;
|
||||
buttonType = ButtonType;
|
||||
|
||||
@Input()
|
||||
scrollViewport: CdkVirtualScrollViewport;
|
||||
|
||||
@ -47,6 +47,7 @@ import { ScrollButtonComponent } from './components/scroll-button/scroll-button.
|
||||
import { ChangeLegalBasisDialogComponent } from './dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
|
||||
import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component';
|
||||
import { RecategorizeImageDialogComponent } from './dialogs/recategorize-image-dialog/recategorize-image-dialog.component';
|
||||
import { DossiersService } from './services/dossiers.service';
|
||||
|
||||
const screens = [
|
||||
DossierListingScreenComponent,
|
||||
@ -97,6 +98,7 @@ const components = [
|
||||
];
|
||||
|
||||
const services = [
|
||||
DossiersService,
|
||||
DossiersDialogService,
|
||||
FileActionService,
|
||||
AnnotationActionsService,
|
||||
|
||||
@ -1,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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -96,10 +96,6 @@ cdk-virtual-scroll-viewport {
|
||||
}
|
||||
}
|
||||
|
||||
.ml-6 {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -62,6 +62,7 @@ export class IconsModule {
|
||||
'pages',
|
||||
'plus',
|
||||
'preview',
|
||||
'put-back',
|
||||
'radio-indeterminate',
|
||||
'radio-selected',
|
||||
'read-only',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
</div>
|
||||
|
||||
<redaction-circle-button
|
||||
class="pl-1"
|
||||
(action)="save.emit(value)"
|
||||
icon="red:check"
|
||||
tooltip="assign-user.save"
|
||||
|
||||
@ -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>();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div
|
||||
(click)="toggle(filter)"
|
||||
*ngFor="let filter of filters"
|
||||
(click)="filterService.toggleFilter('quickFilters', filter.key)"
|
||||
*ngFor="let filter of filterService.getFilter$('quickFilters') | async"
|
||||
[class.active]="filter.checked"
|
||||
class="quick-filter"
|
||||
>
|
||||
|
||||
@ -1,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>) {}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { BaseHeaderConfig } from './base-config.model';
|
||||
|
||||
export interface ActionConfig extends BaseHeaderConfig {
|
||||
action: ($event) => void;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export interface BaseHeaderConfig {
|
||||
label: string;
|
||||
icon?: string;
|
||||
hide?: boolean;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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>;
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1,3 @@
|
||||
.ml-6 {
|
||||
margin-left: 6px;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
attr.width="{{ size }}"
|
||||
class="donut-chart"
|
||||
>
|
||||
<g *ngFor="let value of parsedConfig; let i = index">
|
||||
<g *ngFor="let value of config; let i = index">
|
||||
<circle
|
||||
*ngIf="exists(i)"
|
||||
[attr.stroke]="value.color.includes('#') ? value.color : ''"
|
||||
@ -34,9 +34,9 @@
|
||||
<div class="breakdown-container">
|
||||
<div>
|
||||
<div
|
||||
(click)="selectValue(val)"
|
||||
*ngFor="let val of parsedConfig"
|
||||
[class.active]="val.checked"
|
||||
(click)="selectValue(val.key)"
|
||||
*ngFor="let val of config"
|
||||
[class.active]="filterService.filterChecked$('statusFilters', val.key) | async"
|
||||
[class.filter-disabled]="!filter"
|
||||
>
|
||||
<redaction-status-bar
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
|
||||
import { Color } from '@utils/types';
|
||||
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
|
||||
import { FilterService } from '@shared/services/filter.service';
|
||||
|
||||
export class DoughnutChartConfig {
|
||||
export interface DoughnutChartConfig {
|
||||
value: number;
|
||||
color: Color;
|
||||
label: string;
|
||||
@ -15,7 +16,7 @@ export class DoughnutChartConfig {
|
||||
templateUrl: './simple-doughnut-chart.component.html',
|
||||
styleUrls: ['./simple-doughnut-chart.component.scss']
|
||||
})
|
||||
export class SimpleDoughnutChartComponent implements OnChanges {
|
||||
export class SimpleDoughnutChartComponent<T> implements OnChanges {
|
||||
@Input() subtitle: string;
|
||||
@Input() config: DoughnutChartConfig[] = [];
|
||||
@Input() radius = 85;
|
||||
@ -32,16 +33,8 @@ export class SimpleDoughnutChartComponent implements OnChanges {
|
||||
cx = 0;
|
||||
cy = 0;
|
||||
size = 0;
|
||||
parsedConfig: {
|
||||
color: Color;
|
||||
active?: boolean;
|
||||
checked: boolean;
|
||||
label: string;
|
||||
value: number;
|
||||
key?: string;
|
||||
}[];
|
||||
|
||||
constructor() {}
|
||||
constructor(readonly filterService: FilterService<T>) {}
|
||||
|
||||
get circumference() {
|
||||
return 2 * Math.PI * this.radius;
|
||||
@ -60,10 +53,6 @@ export class SimpleDoughnutChartComponent implements OnChanges {
|
||||
this.cx = this.radius + this.strokeWidth / 2;
|
||||
this.cy = this.radius + this.strokeWidth / 2;
|
||||
this.size = this.strokeWidth + this.radius * 2;
|
||||
this.parsedConfig = this.config.map(el => ({
|
||||
...el,
|
||||
checked: this.filter?.find(f => f.key === el.key)?.checked
|
||||
}));
|
||||
}
|
||||
|
||||
calculateChartData() {
|
||||
@ -102,8 +91,10 @@ export class SimpleDoughnutChartComponent implements OnChanges {
|
||||
: `${config.label} (${config.value} ${this.counterText})`;
|
||||
}
|
||||
|
||||
selectValue(val: any) {
|
||||
this.toggleFilter.emit(val.key);
|
||||
selectValue(key: string) {
|
||||
this.filterService.toggleFilter('statusFilters', key);
|
||||
this.filterService.filterEntities();
|
||||
this.toggleFilter.emit(key);
|
||||
}
|
||||
|
||||
exists(index: number) {
|
||||
|
||||
@ -1,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();
|
||||
|
||||
62
apps/red-ui/src/app/modules/shared/pipes/date.pipe.ts
Normal file
62
apps/red-ui/src/app/modules/shared/pipes/date.pipe.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
106
apps/red-ui/src/app/modules/shared/services/filter.service.ts
Normal file
106
apps/red-ui/src/app/modules/shared/services/filter.service.ts
Normal 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)], []);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,5 +5,7 @@ export const RedactionFilterSorter = {
|
||||
image: 3,
|
||||
hint: 4,
|
||||
suggestion: 5,
|
||||
none: 6
|
||||
none: 6,
|
||||
byKey: (a: { key: string }, b: { key: string }) =>
|
||||
RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
|
||||
};
|
||||
|
||||
@ -8,5 +8,6 @@ export const StatusSorter = {
|
||||
UNASSIGNED: 10,
|
||||
UNDER_REVIEW: 15,
|
||||
UNDER_APPROVAL: 20,
|
||||
APPROVED: 25
|
||||
APPROVED: 25,
|
||||
byKey: (a: { key: string }, b: { key: string }) => StatusSorter[a.key] - StatusSorter[b.key]
|
||||
};
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
20
apps/red-ui/src/assets/icons/general/put-back.svg
Normal file
20
apps/red-ui/src/assets/icons/general/put-back.svg
Normal 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 |
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user