Merge branch 'master' into RED-1546

This commit is contained in:
Dan Percic 2021-07-20 02:27:40 +03:00
commit df2565cb08
90 changed files with 1285 additions and 1426 deletions

View File

@ -7,7 +7,7 @@
{{ message }}
</div>
<div *ngIf="actions && actions.length" class="actions-wrapper">
<div *ngIf="actions?.length" class="actions-wrapper">
<a (click)="callAction($event, action.action)" *ngFor="let action of actions">
{{ action.title }}
</a>

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import { Toast, ToastPackage, ToastrService } from 'ngx-toastr';
import { ToasterOptions } from '@services/toaster.service';
@Component({
selector: 'redaction-toast',
templateUrl: './toast.component.html',
styleUrls: ['./toast.component.scss']
})
@ -12,12 +12,10 @@ export class ToastComponent extends Toast {
}
get actions() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return this.options.actions;
return (this.options as ToasterOptions)?.actions;
}
callAction($event: MouseEvent, action: Function) {
callAction($event: MouseEvent, action: () => void) {
$event.stopPropagation();
if (action) {
action();

View File

@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http';
import { Observable } from 'rxjs';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '@services/toaster.service';
import { TranslateService } from '@ngx-translate/core';
import { TypeValueWrapper } from '@models/file/type-value.wrapper';
import { humanize } from '../../../../utils/functions';
@ -21,7 +21,7 @@ export class AddEditDictionaryDialogComponent {
constructor(
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _formBuilder: FormBuilder,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _translateService: TranslateService,
private readonly _dialogRef: MatDialogRef<AddEditDictionaryDialogComponent>,
@Inject(MAT_DIALOG_DATA)
@ -89,20 +89,16 @@ export class AddEditDictionaryDialogComponent {
() => this._dialogRef.close(true),
error => {
if (error.status === 409) {
this._notifyError('add-edit-dictionary.error.dictionary-already-exists');
this._toaster.error('add-edit-dictionary.error.dictionary-already-exists');
} else if (error.status === 400) {
this._notifyError('add-edit-dictionary.error.invalid-color-or-rank');
this._toaster.error('add-edit-dictionary.error.invalid-color-or-rank');
} else {
this._notifyError('add-edit-dictionary.error.generic');
this._toaster.error('add-edit-dictionary.error.generic');
}
}
);
}
private _notifyError(message: string) {
this._notificationService.showToastNotification(this._translateService.instant(message), null, NotificationType.ERROR);
}
private _formToObject(): TypeValue {
return {
caseInsensitive: !this.dictionaryForm.get('caseSensitive').value,

View File

@ -1,12 +1,12 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DossierAttributeConfig, FileAttributeConfig } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { LoadingService } from '@services/loading.service';
import { ErrorMessageService } from '@services/error-message.service';
import { NotificationService, NotificationType } from '@services/notification.service';
import { LoadingService } from '../../../../services/loading.service';
import { HttpErrorResponse } from '@angular/common/http';
import { Toaster } from '../../../../services/toaster.service';
import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component';
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
@Component({
@ -14,7 +14,7 @@ import { DossierAttributesService } from '@shared/services/controller-wrappers/d
templateUrl: './add-edit-dossier-attribute-dialog.component.html',
styleUrls: ['./add-edit-dossier-attribute-dialog.component.scss']
})
export class AddEditDossierAttributeDialogComponent {
export class AddEditDossierAttributeDialogComponent extends AutoUnsubscribeComponent implements OnDestroy {
dossierAttributeForm: FormGroup;
dossierAttribute: DossierAttributeConfig;
dossierTemplateId: string;
@ -30,12 +30,12 @@ export class AddEditDossierAttributeDialogComponent {
private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _dossierAttributesService: DossierAttributesService,
private readonly _errorMessageService: ErrorMessageService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
public dialogRef: MatDialogRef<AddEditDossierAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { dossierAttribute: DossierAttributeConfig; dossierTemplateId: string }
) {
super();
this.dossierAttribute = data.dossierAttribute;
this.dossierTemplateId = data.dossierTemplateId;
@ -76,13 +76,9 @@ export class AddEditDossierAttributeDialogComponent {
() => {
this.dialogRef.close(true);
},
(err: HttpErrorResponse) => {
(error: HttpErrorResponse) => {
this._loadingService.stop();
this._notificationService.showToastNotification(
this._errorMessageService.getMessage(err, 'add-edit-dossier-attribute.error.generic'),
null,
NotificationType.ERROR
);
this._toaster.error('add-edit-dossier-attribute.error.generic', { error: error });
}
);
}

View File

@ -1,7 +1,7 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Colors, DictionaryControllerService } from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { DefaultColorType } from '@models/default-color-key.model';
@ -21,7 +21,7 @@ export class EditColorDialogComponent {
constructor(
private readonly _formBuilder: FormBuilder,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _translateService: TranslateService,
private readonly _dialogRef: MatDialogRef<EditColorDialogComponent>,
@Inject(MAT_DIALOG_DATA)
@ -50,17 +50,10 @@ export class EditColorDialogComponent {
try {
await this._dictionaryControllerService.setColors(colors, this._dossierTemplateId).toPromise();
this._dialogRef.close(true);
this._notificationService.showToastNotification(
this._translateService.instant('edit-color-dialog.success', {
color: this._translateService.instant('default-colors-screen.types.' + this.colorKey)
})
);
const color = this._translateService.instant(`default-colors-screen.types.${this.colorKey}`);
this._toaster.info('edit-color-dialog.success', { params: { color: color } });
} catch (e) {
this._notificationService.showToastNotification(
this._translateService.instant('edit-color-dialog.error'),
null,
NotificationType.ERROR
);
this._toaster.error('edit-color-dialog.error');
}
}
}

View File

@ -2,15 +2,15 @@
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
[active]="screenStateService.areAllEntitiesSelected$ | async"
[indeterminate]="screenStateService.notAllEntitiesSelected$ | async"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'file-attributes-csv-import.table-header.title' | translate: { length: allEntities.length } }}
{{ 'file-attributes-csv-import.table-header.title' | translate: { length: (screenStateService.allEntitiesLength$ | async) } }}
</span>
<ng-container *ngIf="areSomeEntitiesSelected$ | async">
<ng-container *ngIf="screenStateService.areSomeEntitiesSelected$ | async">
<redaction-circle-button
[matMenuTriggerFor]="readOnlyMenu"
[tooltip]="'file-attributes-csv-import.table-header.actions.read-only' | translate"
@ -81,7 +81,7 @@
</div>
<redaction-empty-state
*ngIf="noData"
*ngIf="screenStateService.noData$ | async"
[text]="'file-attributes-csv-import.no-data.title' | translate"
icon="red:attribute"
></redaction-empty-state>
@ -91,7 +91,7 @@
<div
(mouseenter)="setHoveredColumn.emit(field.csvColumn)"
(mouseleave)="setHoveredColumn.emit()"
*cdkVirtualFor="let field of displayedEntities$ | async"
*cdkVirtualFor="let field of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
class="table-item"
>
<div (click)="toggleEntitySelected($event, field)" class="selection-column">

View File

@ -20,41 +20,39 @@ export class ActiveFieldsListingComponent extends BaseListingComponent<Field> im
@Output() toggleFieldActive = new EventEmitter<Field>();
readonly typeOptions = [FileAttributeConfig.TypeEnum.TEXT, FileAttributeConfig.TypeEnum.NUMBER, FileAttributeConfig.TypeEnum.DATE];
protected readonly _primaryKey = 'id';
constructor(protected readonly _injector: Injector) {
super(_injector);
this._screenStateService.setIdKey('csvColumn');
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.entities) {
this._screenStateService.setEntities(this.entities);
this._screenStateService.setDisplayedEntities(this.entities);
this._screenStateService.updateSelection();
this.screenStateService.setEntities(this.entities);
this.screenStateService.updateSelection();
}
}
deactivateSelection() {
this.allEntities.filter(field => this.isSelected(field)).forEach(field => (field.primaryAttribute = false));
this._screenStateService.setEntities([...this.allEntities.filter(field => !this.isSelected(field))]);
this.screenStateService.setEntities(this.allEntities.filter(field => !this.isSelected(field)));
this.entitiesChange.emit(this.allEntities);
this._screenStateService.setSelectedEntitiesIds([]);
this.screenStateService.setSelectedEntities([]);
}
setAttributeForSelection(attribute: string, value: any) {
for (const csvColumn of this._screenStateService.selectedEntitiesIds) {
this.allEntities.find(f => f.csvColumn === csvColumn)[attribute] = value;
for (const item of this.screenStateService.selectedEntities) {
this.allEntities.find(f => f.csvColumn === item.csvColumn)[attribute] = value;
}
}
togglePrimary(field: Field) {
if (field.primaryAttribute) {
field.primaryAttribute = false;
} else {
for (const f of this.allEntities) {
f.primaryAttribute = false;
}
field.primaryAttribute = true;
return;
}
for (const f of this.allEntities) f.primaryAttribute = false;
field.primaryAttribute = true;
}
}

View File

@ -90,7 +90,7 @@
</div>
<div *ngIf="isSearchOpen" class="search-input-container">
<redaction-input-with-action
[form]="searchForm"
[form]="searchService.searchForm"
[placeholder]="'file-attributes-csv-import.search.placeholder' | translate"
type="search"
width="full"
@ -101,7 +101,7 @@
(click)="toggleFieldActive(field)"
(mouseenter)="setHoveredColumn(field.csvColumn)"
(mouseleave)="setHoveredColumn()"
*ngFor="let field of displayedEntities$ | async"
*ngFor="let field of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
class="csv-header-pill-wrapper"
>
<div [class.selected]="isActive(field)" class="csv-header-pill">

View File

@ -6,7 +6,7 @@ import * as Papa from 'papaparse';
import { FileAttributeConfig, FileAttributesConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { TranslateService } from '@ngx-translate/core';
import { FilterService } from '@shared/services/filter.service';
import { SearchService } from '@shared/services/search.service';
@ -26,12 +26,13 @@ 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'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class FileAttributesCsvImportDialogComponent extends BaseListingComponent<Field> {
protected readonly _primaryKey = 'id';
csvFile: File;
dossierTemplateId: string;
parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] };
@ -44,18 +45,17 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
keepPreview = false;
columnSample = [];
initialParseConfig: { delimiter?: string; encoding?: string } = {};
protected readonly _searchKey = 'csvColumn';
constructor(
private readonly _appStateService: AppStateService,
private readonly _fileAttributesControllerService: FileAttributesControllerService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>,
readonly dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>,
protected readonly _injector: Injector,
@Inject(MAT_DIALOG_DATA)
public data: {
readonly data: {
csv: File;
dossierTemplateId: string;
existingConfiguration: FileAttributesConfig;
@ -96,8 +96,7 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
this.parseResult.meta.fields = Object.keys(this.parseResult.data[0]);
}
this._screenStateService.setEntities(this.parseResult.meta.fields.map(field => this._buildAttribute(field)));
this._screenStateService.setDisplayedEntities(this.allEntities);
this.screenStateService.setEntities(this.parseResult.meta.fields.map(field => this._buildAttribute(field)));
this.activeFields = [];
for (const entity of this.allEntities) {
@ -204,19 +203,9 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
try {
await this._fileAttributesControllerService.setFileAttributesConfig(fileAttributes, this.dossierTemplateId).toPromise();
this._notificationService.showToastNotification(
this._translateService.instant('file-attributes-csv-import.save.success', {
count: this.activeFields.length
}),
null,
NotificationType.SUCCESS
);
this._toaster.success('file-attributes-csv-import.save.success', { params: { count: this.activeFields.length } });
} catch (e) {
this._notificationService.showToastNotification(
this._translateService.instant('file-attributes-csv-import.save.error'),
null,
NotificationType.ERROR
);
this._toaster.error('file-attributes-csv-import.save.error');
}
this.dialogRef.close(true);

View File

@ -132,5 +132,3 @@
</div>
</div>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -1,10 +1,11 @@
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuditControllerService, AuditResponse, AuditSearchRequest } from '@redaction/red-ui-http';
import { TranslateService } from '@ngx-translate/core';
import { Moment } from 'moment';
import { applyIntervalConstraints } from '@utils/date-inputs-utils';
import { LoadingService } from '../../../../services/loading.service';
import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component';
const PAGE_SIZE = 50;
@ -13,16 +14,14 @@ const PAGE_SIZE = 50;
templateUrl: './audit-screen.component.html',
styleUrls: ['./audit-screen.component.scss']
})
export class AuditScreenComponent {
export class AuditScreenComponent extends AutoUnsubscribeComponent implements OnDestroy {
readonly ALL_CATEGORIES = 'all-categories';
readonly ALL_USERS = 'audit-screen.all-users';
filterForm: FormGroup;
viewReady = false;
categories: string[] = [];
userIds: Set<string>;
logs: AuditResponse;
currentPage = 1;
private _previousFrom: Moment;
private _previousTo: Moment;
@ -31,8 +30,9 @@ export class AuditScreenComponent {
readonly permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
private readonly _auditControllerService: AuditControllerService,
private readonly _translateService: TranslateService
private readonly _loadingService: LoadingService
) {
super();
this.filterForm = this._formBuilder.group({
category: [this.ALL_CATEGORIES],
userId: [this.ALL_USERS],
@ -40,7 +40,7 @@ export class AuditScreenComponent {
to: []
});
this.filterForm.valueChanges.subscribe(value => {
this.addSubscription = this.filterForm.valueChanges.subscribe(value => {
if (!this._updateDateFilters(value)) {
this._fetchData();
}
@ -71,7 +71,7 @@ export class AuditScreenComponent {
}
private _fetchData(page?: number) {
this.viewReady = false;
this._loadingService.start();
const promises = [];
const category = this.filterForm.get('category').value;
const userId = this.filterForm.get('userId').value;
@ -101,7 +101,7 @@ export class AuditScreenComponent {
for (const id of this.logs.data.map(log => log.userId).filter(uid => !!uid)) {
this.userIds.add(id);
}
this.viewReady = true;
this._loadingService.stop();
});
}
}

View File

@ -22,14 +22,12 @@
<div class="content-container">
<div class="header-item">
<span class="all-caps-label">
{{ 'default-colors-screen.table-header.title' | translate: { length: (allEntities$ | async).length } }}
{{ 'default-colors-screen.table-header.title' | translate: { length: screenStateService.allEntitiesLength$ | async } }}
</span>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'default-colors-screen.table-col-names.key' | translate"
[withSort]="true"
column="key"
@ -46,10 +44,7 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines -->
<div
*cdkVirtualFor="let color of allEntities$ | async | sortBy: sortingOption.order:sortingOption.column"
class="table-item"
>
<div *cdkVirtualFor="let color of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">
<div>
<div [translate]="'default-colors-screen.types.' + color.key" class="table-item-title heading"></div>
</div>

View File

@ -1,4 +1,4 @@
import { Component, Injector, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { Colors, DictionaryControllerService } from '@redaction/red-ui-http';
import { ActivatedRoute } from '@angular/router';
@ -8,13 +8,14 @@ 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';
import { DefaultColorType } from '@models/default-color-key.model';
import { SortingService } from '../../../../services/sorting.service';
@Component({
templateUrl: './default-colors-screen.component.html',
styleUrls: ['./default-colors-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DefaultColorsScreenComponent
@ -25,6 +26,7 @@ export class DefaultColorsScreenComponent
implements OnInit
{
private _colorsObj: Colors;
protected readonly _primaryKey = 'key';
constructor(
private readonly _appStateService: AppStateService,
@ -36,7 +38,6 @@ export class DefaultColorsScreenComponent
protected readonly _injector: Injector
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DEFAULT_COLORS);
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
@ -53,9 +54,7 @@ export class DefaultColorsScreenComponent
colorKey: color.key,
dossierTemplateId: this._appStateService.activeDossierTemplateId
},
async () => {
await this._loadColors();
}
async () => await this._loadColors()
);
}
@ -63,12 +62,11 @@ export class DefaultColorsScreenComponent
this._loadingService.start();
const data = await this._dictionaryControllerService.getColors(this._appStateService.activeDossierTemplateId).toPromise();
this._colorsObj = data;
this._screenStateService.setEntities(
Object.keys(data).map(key => ({
key,
value: data[key]
}))
);
const entities = Object.keys(data).map(key => ({
key,
value: data[key]
}));
this.screenStateService.setEntities(entities);
this._loadingService.stop();
}
}

View File

@ -24,18 +24,18 @@
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
[active]="screenStateService.areAllEntitiesSelected$ | async"
[indeterminate]="screenStateService.notAllEntitiesSelected$ | async"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'dictionary-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }}
{{ 'dictionary-listing.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) } }}
</span>
<redaction-circle-button
(action)="openDeleteDictionariesDialog($event)"
*ngIf="(areSomeEntitiesSelected$ | async) && permissionsService.isAdmin()"
*ngIf="canBulkDelete$(permissionsService.isAdmin()) | async"
[tooltip]="'dictionary-listing.bulk.delete' | translate"
icon="red:trash"
type="dark-bg"
@ -43,7 +43,7 @@
<div class="attributes-actions-container">
<redaction-input-with-action
[form]="searchForm"
[form]="searchService.searchForm"
[placeholder]="'dictionary-listing.search' | translate"
type="search"
></redaction-input-with-action>
@ -59,20 +59,16 @@
</div>
</div>
<div [class.no-data]="!allEntities.length" class="table-header" redactionSyncWidth="table-item">
<div [class.no-data]="screenStateService.noData$ | async" class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dictionary-listing.table-col-names.type' | translate"
[withSort]="true"
column="label"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dictionary-listing.table-col-names.order-of-importance' | translate"
[withSort]="true"
class="flex-center"
@ -89,7 +85,7 @@
<redaction-empty-state
(action)="openAddEditDictionaryDialog()"
*ngIf="!allEntities.length"
*ngIf="screenStateService.noData$ | async"
[buttonLabel]="'dictionary-listing.no-data.action' | translate"
[showButton]="permissionsService.isAdmin()"
[text]="'dictionary-listing.no-data.title' | translate"
@ -97,13 +93,13 @@
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && (displayedEntities$ | async)?.length === 0"
*ngIf="noMatch$ | async"
[text]="'dictionary-listing.no-match.title' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div
*cdkVirtualFor="let dict of displayedEntities$ | async | sortBy: sortingOption.order:sortingOption.column"
*cdkVirtualFor="let dict of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[routerLink]="[dict.type]"
class="table-item pointer"
>
@ -141,7 +137,7 @@
<div class="actions-container">
<div *ngIf="permissionsService.isAdmin()" class="action-buttons">
<redaction-circle-button
(action)="openDeleteDictionariesDialog($event, [dict.type])"
(action)="openDeleteDictionariesDialog($event, [dict])"
[tooltip]="'dictionary-listing.action.delete' | translate"
icon="red:trash"
type="dark-bg"
@ -162,7 +158,7 @@
<div class="right-container" redactionHasScrollbar>
<redaction-simple-doughnut-chart
*ngIf="allEntities.length"
*ngIf="(screenStateService.noData$ | async) === false"
[config]="chartData"
[counterText]="'dictionary-listing.stats.charts.entries' | translate"
[radius]="82"

View File

@ -12,12 +12,12 @@ 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 { SortingService } from '../../../../services/sorting.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { AdminDialogService } from '../../services/admin-dialog.service';
const toChartConfig = (dict: TypeValueWrapper): DoughnutChartConfig => ({
value: dict.entries ? dict.entries.length : 0,
value: dict.entries?.length ?? 0,
color: dict.hexColor,
label: dict.label,
key: dict.type
@ -31,6 +31,8 @@ const toChartConfig = (dict: TypeValueWrapper): DoughnutChartConfig => ({
export class DictionaryListingScreenComponent extends BaseListingComponent<TypeValueWrapper> implements OnInit {
chartData: DoughnutChartConfig[] = [];
protected readonly _primaryKey = 'label';
constructor(
private readonly _dialogService: AdminDialogService,
private readonly _dictionaryControllerService: DictionaryControllerService,
@ -43,9 +45,6 @@ export class DictionaryListingScreenComponent extends BaseListingComponent<TypeV
) {
super(_injector);
_loadingService.start();
this._sortingService.setScreenName(ScreenNames.DOSSIER_LISTING);
this._searchService.setSearchKey('label');
this._screenStateService.setIdKey('type');
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
@ -53,11 +52,16 @@ export class DictionaryListingScreenComponent extends BaseListingComponent<TypeV
this._loadDictionaryData();
}
openDeleteDictionariesDialog($event?: MouseEvent, types = this._screenStateService.selectedEntitiesIds) {
openDeleteDictionariesDialog($event?: MouseEvent, types = this.screenStateService.selectedEntities) {
this._dialogService.openDialog('confirm', $event, null, async () => {
this._loadingService.start();
await this._dictionaryControllerService.deleteTypes(types, this._appStateService.activeDossierTemplateId).toPromise();
this._screenStateService.setSelectedEntitiesIds([]);
await this._dictionaryControllerService
.deleteTypes(
types.map(t => t.type),
this._appStateService.activeDossierTemplateId
)
.toPromise();
this.screenStateService.setSelectedEntities([]);
await this._appStateService.loadDictionaryData();
this._loadDictionaryData(false);
this._calculateData();
@ -88,15 +92,13 @@ export class DictionaryListingScreenComponent extends BaseListingComponent<TypeV
const entities = Object.values(appStateDictionaryData).filter(d => !d.virtual);
if (!loadEntries)
this._screenStateService.setEntities(
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._screenStateService.setDisplayedEntities(this.allEntities);
else this.screenStateService.setEntities(entities);
if (!loadEntries) return;

View File

@ -4,25 +4,15 @@
<redaction-admin-side-nav type="settings"></redaction-admin-side-nav>
<div>
<div class="page-header">
<div class="breadcrumb" translate="digital-signature"></div>
<div class="actions">
<redaction-circle-button
*ngIf="permissionsService.isUser()"
[tooltip]="'common.close' | translate"
class="ml-6"
icon="red:close"
redactionNavigateLastDossiersScreen
tooltipPosition="below"
></redaction-circle-button>
</div>
</div>
<redaction-page-header
[pageLabel]="'digital-signature' | translate"
[showCloseButton]="permissionsService.isUser()"
></redaction-page-header>
<div class="red-content-inner">
<div class="content-container">
<div class="content-container-content">
<form (keyup)="formChanged()" *ngIf="digitalSignatureForm" [formGroup]="digitalSignatureForm" autocomplete="off">
<form *ngIf="digitalSignatureForm" [formGroup]="digitalSignatureForm" autocomplete="off">
<input #fileInput (change)="fileChanged($event, fileInput)" class="file-upload-input" hidden type="file" />
<redaction-empty-state
@ -112,5 +102,3 @@
</div>
</div>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -1,30 +1,31 @@
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { DigitalSignature, DigitalSignatureControllerService } from '@redaction/red-ui-http';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService, NotificationType } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Toaster } from '../../../../services/toaster.service';
import { PermissionsService } from '@services/permissions.service';
import { lastIndexOfEnd } from '@utils/functions';
import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component';
import { LoadingService } from '../../../../services/loading.service';
@Component({
selector: 'redaction-digital-signature-screen',
templateUrl: './digital-signature-screen.component.html',
styleUrls: ['./digital-signature-screen.component.scss']
})
export class DigitalSignatureScreenComponent {
export class DigitalSignatureScreenComponent extends AutoUnsubscribeComponent implements OnDestroy {
digitalSignature: DigitalSignature;
digitalSignatureForm: FormGroup;
viewReady = false;
digitalSignatureExists = false;
constructor(
private readonly _digitalSignatureControllerService: DigitalSignatureControllerService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder,
private readonly _translateService: TranslateService,
private readonly _loadingService: LoadingService,
readonly permissionsService: PermissionsService
) {
super();
this.loadDigitalSignatureAndInitializeForm();
}
@ -43,50 +44,28 @@ export class DigitalSignatureScreenComponent {
? this._digitalSignatureControllerService.updateDigitalSignature(digitalSignature)
: this._digitalSignatureControllerService.saveDigitalSignature(digitalSignature);
observable.subscribe(
this.addSubscription = observable.subscribe(
() => {
this.loadDigitalSignatureAndInitializeForm();
this._notificationService.showToastNotification(
this._translateService.instant('digital-signature-screen.action.save-success'),
null,
NotificationType.SUCCESS
);
this._toaster.success('digital-signature-screen.action.save-success');
},
error => {
if (error.status === 400) {
this._notificationService.showToastNotification(
this._translateService.instant('digital-signature-screen.action.certificate-not-valid-error'),
null,
NotificationType.ERROR
);
this._toaster.error('digital-signature-screen.action.certificate-not-valid-error');
} else {
this._notificationService.showToastNotification(
this._translateService.instant('digital-signature-screen.action.save-error'),
null,
NotificationType.ERROR
);
this._toaster.error('digital-signature-screen.action.save-error');
}
}
);
}
removeDigitalSignature() {
this._digitalSignatureControllerService.deleteDigitalSignature().subscribe(
this.addSubscription = this._digitalSignatureControllerService.deleteDigitalSignature().subscribe(
() => {
this.loadDigitalSignatureAndInitializeForm();
this._notificationService.showToastNotification(
this._translateService.instant('digital-signature-screen.action.delete-success'),
null,
NotificationType.SUCCESS
);
this._toaster.success('digital-signature-screen.action.delete-success');
},
() => {
this._notificationService.showToastNotification(
this._translateService.instant('digital-signature-screen.action.delete-error'),
null,
NotificationType.ERROR
);
}
() => this._toaster.error('digital-signature-screen.action.delete-error')
);
}
@ -104,8 +83,8 @@ export class DigitalSignatureScreenComponent {
}
loadDigitalSignatureAndInitializeForm() {
this.viewReady = false;
this._digitalSignatureControllerService
this._loadingService.start();
this.addSubscription = this._digitalSignatureControllerService
.getDigitalSignature()
.subscribe(
digitalSignature => {
@ -119,12 +98,10 @@ export class DigitalSignatureScreenComponent {
)
.add(() => {
this._initForm();
this.viewReady = true;
this._loadingService.stop();
});
}
formChanged() {}
private _initForm() {
this.digitalSignatureForm = this._formBuilder.group({
certificateName: [this.digitalSignature.certificateName, Validators.required],

View File

@ -20,22 +20,25 @@
<redaction-admin-side-nav type="dossier-templates"></redaction-admin-side-nav>
<div class="content-container">
<div *ngIf="(allEntities$ | async)?.length" class="header-item">
<div *ngIf="(screenStateService.noData$ | async) === false" class="header-item">
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
[active]="screenStateService.areAllEntitiesSelected$ | async"
[indeterminate]="screenStateService.notAllEntitiesSelected$ | async"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'dossier-attributes-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }}
{{
'dossier-attributes-listing.table-header.title'
| translate: { length: (screenStateService.displayedLength$ | async) }
}}
</span>
<redaction-circle-button
(action)="openConfirmDeleteAttributeDialog($event)"
*ngIf="permissionsService.isAdmin() && areSomeEntitiesSelected$ | async"
*ngIf="permissionsService.isAdmin() && screenStateService.areSomeEntitiesSelected$ | async"
[tooltip]="'dossier-attributes-listing.bulk.delete' | translate"
icon="red:trash"
type="dark-bg"
@ -43,7 +46,7 @@
<div class="attributes-actions-container">
<redaction-input-with-action
[form]="searchForm"
[form]="searchService.searchForm"
[placeholder]="'dossier-attributes-listing.search' | translate"
type="search"
></redaction-input-with-action>
@ -58,42 +61,46 @@
</div>
</div>
<div *ngIf="(allEntities$ | async)?.length" class="table-header" redactionSyncWidth="table-item">
<div *ngIf="(screenStateService.noData$ | async) === false" class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-attributes-listing.table-col-names.label' | translate"
[withSort]="true"
column="label"
></redaction-table-col-name>
<redaction-table-col-name label="dossier-attributes-listing.table-col-names.placeholder"></redaction-table-col-name>
<redaction-table-col-name
[label]="'dossier-attributes-listing.table-col-names.placeholder' | translate"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-attributes-listing.table-col-names.type' | translate"
[withSort]="true"
column="type"
></redaction-table-col-name>
<div></div>
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state
(action)="openAddEditAttributeDialog($event)"
*ngIf="noData"
*ngIf="screenStateService.noData$ | async"
[buttonLabel]="'dossier-attributes-listing.no-data.action' | translate"
[showButton]="permissionsService.isAdmin()"
[text]="'dossier-attributes-listing.no-data.title' | translate"
icon="red:attribute"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch" [text]="'dossier-attributes-listing.no-match.title' | translate"></redaction-empty-state>
<redaction-empty-state
*ngIf="noMatch$ | async"
[text]="'dossier-attributes-listing.no-match.title' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar>
<div
*cdkVirtualFor="let attribute of displayedEntities$ | async | sortBy: sortingOption.order:sortingOption.column"
*cdkVirtualFor="let attribute of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
class="table-item pointer"
>
<div (click)="toggleEntitySelected($event, attribute)" class="selection-column">
@ -118,15 +125,14 @@
[tooltip]="'dossier-attributes-listing.action.edit' | translate"
icon="red:edit"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-circle-button
(action)="openConfirmDeleteAttributeDialog($event, attribute)"
[tooltip]="'file-attributes-listing.action.delete' | translate"
icon="red:trash"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>

View File

@ -5,7 +5,7 @@ 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 { 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';
@ -18,6 +18,8 @@ import { DossierAttributesService } from '@shared/services/controller-wrappers/d
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierAttributesListingScreenComponent extends BaseListingComponent<DossierAttributeConfig> implements OnInit {
protected readonly _primaryKey = 'label';
constructor(
protected readonly _injector: Injector,
private readonly _appStateService: AppStateService,
@ -28,9 +30,6 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen
readonly permissionsService: PermissionsService
) {
super(_injector);
this._searchService.setSearchKey('label');
this._screenStateService.setIdKey('id');
this._sortingService.setScreenName(ScreenNames.DOSSIER_ATTRIBUTES_LISTING);
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
@ -41,7 +40,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._screenStateService.selectedEntitiesIds;
const ids = dossierAttribute ? [dossierAttribute.id] : this.screenStateService.selectedEntities.map(item => item.id);
await this._dossierAttributesService.deleteConfigs(ids);
await this._loadData();
});
@ -61,8 +60,7 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen
private async _loadData() {
this._loadingService.start();
const attributes = await this._dossierAttributesService.getConfig();
this._screenStateService.setEntities(attributes);
this.filterService.filterEntities();
this.screenStateService.setEntities(attributes);
this._loadingService.stop();
}
}

View File

@ -4,17 +4,10 @@
<redaction-admin-side-nav type="settings"></redaction-admin-side-nav>
<div>
<div class="page-header">
<div class="breadcrumb" translate="dossier-templates"></div>
<redaction-circle-button
*ngIf="permissionsService.isUser()"
[tooltip]="'common.close' | translate"
icon="red:close"
redactionNavigateLastDossiersScreen
tooltipPosition="below"
></redaction-circle-button>
</div>
<redaction-page-header
[pageLabel]="'dossier-templates' | translate"
[showCloseButton]="permissionsService.isUser()"
></redaction-page-header>
<div class="red-content-inner">
<div class="content-container">
@ -22,28 +15,29 @@
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
[active]="screenStateService.areAllEntitiesSelected$ | async"
[indeterminate]="screenStateService.notAllEntitiesSelected$ | async"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'dossier-templates-listing.table-header.title' | translate: { length: (displayedEntities$ | async).length } }}
{{
'dossier-templates-listing.table-header.title'
| translate: { length: (screenStateService.displayedLength$ | async) }
}}
</span>
<ng-container *ngIf="areSomeEntitiesSelected$ | async">
<redaction-circle-button
(action)="openDeleteTemplatesDialog($event)"
*ngIf="permissionsService.isAdmin()"
[tooltip]="'dossier-templates-listing.bulk.delete' | translate"
icon="red:trash"
type="dark-bg"
></redaction-circle-button>
</ng-container>
<redaction-circle-button
(action)="openDeleteTemplatesDialog($event)"
*ngIf="canBulkDelete$(permissionsService.isAdmin()) | async"
icon="red:trash"
[tooltip]="'dossier-templates-listing.bulk.delete' | translate"
type="dark-bg"
></redaction-circle-button>
<div class="actions flex-1">
<redaction-input-with-action
[form]="searchForm"
[form]="searchService.searchForm"
[placeholder]="'dossier-templates-listing.search' | translate"
type="search"
></redaction-input-with-action>
@ -58,12 +52,10 @@
</div>
</div>
<div [class.no-data]="noData" class="table-header" redactionSyncWidth="table-item">
<div [class.no-data]="screenStateService.noData$ | async" class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-templates-listing.table-col-names.name' | translate"
[withSort]="true"
column="name"
@ -73,38 +65,33 @@
class="user-column"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-templates-listing.table-col-names.created-on' | translate"
[withSort]="true"
column="dateAdded"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-templates-listing.table-col-names.modified-on' | translate"
[withSort]="true"
column="dateModified"
></redaction-table-col-name>
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state
*ngIf="noData"
*ngIf="screenStateService.noData$ | async"
[text]="'dossier-templates-listing.no-data.title' | translate"
icon="red:template"
></redaction-empty-state>
<redaction-empty-state
*ngIf="noMatch"
*ngIf="noMatch$ | async"
[text]="'dossier-templates-listing.no-match.title' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div
*cdkVirtualFor="
let dossierTemplate of displayedEntities$ | async | sortBy: sortingOption.order:sortingOption.column
"
*cdkVirtualFor="let dossierTemplate of sortedDisplayedEntities$ | async"
[routerLink]="[dossierTemplate.dossierTemplateId, 'dictionaries']"
class="table-item pointer"
>

View File

@ -9,8 +9,8 @@ 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';
import { SortingService } from '../../../../services/sorting.service';
@Component({
templateUrl: './dossier-templates-listing-screen.component.html',
@ -19,6 +19,8 @@ import { BaseListingComponent } from '@shared/base/base-listing.component';
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierTemplatesListingScreenComponent extends BaseListingComponent<DossierTemplateModelWrapper> implements OnInit {
protected _primaryKey = 'name';
constructor(
private readonly _dialogService: AdminDialogService,
private readonly _appStateService: AppStateService,
@ -29,9 +31,6 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent
readonly userPreferenceService: UserPreferenceService
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DOSSIER_TEMPLATES_LISTING);
this._searchService.setSearchKey('name');
this._screenStateService.setIdKey('dossierTemplateId');
}
ngOnInit(): void {
@ -41,8 +40,10 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent
openDeleteTemplatesDialog($event?: MouseEvent) {
return this._dialogService.openDialog('confirm', $event, null, async () => {
this._loadingService.start();
await this._dossierTemplateControllerService.deleteDossierTemplates(this._screenStateService.selectedEntitiesIds).toPromise();
this._screenStateService.setSelectedEntitiesIds([]);
await this._dossierTemplateControllerService
.deleteDossierTemplates(this.screenStateService.selectedEntities.map(d => d.dossierTemplateId))
.toPromise();
this.screenStateService.setSelectedEntities([]);
await this._appStateService.loadAllDossierTemplates();
await this._appStateService.loadDictionaryData();
this.loadDossierTemplatesData();
@ -52,8 +53,7 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent
loadDossierTemplatesData() {
this._loadingService.start();
this._appStateService.reset();
this._screenStateService.setEntities(this._appStateService.dossierTemplates);
this.filterService.filterEntities();
this.screenStateService.setEntities(this._appStateService.dossierTemplates);
this._loadDossierTemplateStats();
this._loadingService.stop();
}
@ -67,7 +67,7 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent
}
private _loadDossierTemplateStats() {
this._screenStateService.entities.forEach(rs => {
this.screenStateService.allEntities.forEach(rs => {
const dictionaries = this._appStateService.dictionaryData[rs.dossierTemplateId];
if (dictionaries) {
rs.dictionariesCount = Object.keys(dictionaries)

View File

@ -24,27 +24,28 @@
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
[active]="screenStateService.areAllEntitiesSelected$ | async"
[indeterminate]="screenStateService.notAllEntitiesSelected$ | async"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'file-attributes-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }}
{{
'file-attributes-listing.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) }
}}
</span>
<redaction-circle-button
(click)="openConfirmDeleteAttributeDialog($event)"
*ngIf="permissionsService.isAdmin() && areSomeEntitiesSelected$ | async"
*ngIf="canBulkDelete$(permissionsService.isAdmin()) | async"
[tooltip]="'file-attributes-listing.bulk-actions.delete' | translate"
icon="red:trash"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<div class="attributes-actions-container">
<redaction-input-with-action
[form]="searchForm"
[form]="searchService.searchForm"
[placeholder]="'file-attributes-listing.search' | translate"
type="search"
></redaction-input-with-action>
@ -69,28 +70,22 @@
</div>
</div>
<div [class.no-data]="noData" class="table-header" redactionSyncWidth="table-item">
<div [class.no-data]="screenStateService.noData$ | async" class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'file-attributes-listing.table-col-names.name' | translate"
[withSort]="true"
column="label"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'file-attributes-listing.table-col-names.type' | translate"
[withSort]="true"
column="type"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'file-attributes-listing.table-col-names.read-only' | translate"
[withSort]="true"
class="flex-center"
@ -114,19 +109,19 @@
</div>
<redaction-empty-state
*ngIf="noData"
*ngIf="screenStateService.noData$ | async"
[text]="'file-attributes-listing.no-data.title' | translate"
icon="red:attribute"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch" [text]="'file-attributes-listing.no-match.title' | translate"></redaction-empty-state>
<redaction-empty-state
*ngIf="noMatch$ | async"
[text]="'file-attributes-listing.no-match.title' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines -->
<div
*cdkVirtualFor="let attribute of displayedEntities$ | async | sortBy: sortingOption.order:sortingOption.column"
class="table-item"
>
<div *cdkVirtualFor="let attribute of sortedDisplayedEntities$ | async" class="table-item">
<div (click)="toggleEntitySelected($event, attribute)" class="selection-column">
<redaction-round-checkbox [active]="isSelected(attribute)"></redaction-round-checkbox>
</div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, ElementRef, Injector, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, Injector, OnDestroy, 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';
@ -8,7 +8,7 @@ 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 { SortingService } from '../../../../services/sorting.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
@Component({
@ -17,7 +17,9 @@ import { BaseListingComponent } from '@shared/base/base-listing.component';
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class FileAttributesListingScreenComponent extends BaseListingComponent<FileAttributeConfig> implements OnInit {
export class FileAttributesListingScreenComponent extends BaseListingComponent<FileAttributeConfig> implements OnInit, OnDestroy {
protected readonly _primaryKey = 'label';
private _existingConfiguration: FileAttributesConfig;
@ViewChild('fileInput') private _fileInput: ElementRef;
@ -31,9 +33,6 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
protected readonly _injector: Injector
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.FILE_ATTRIBUTES_LISTING);
this._searchService.setSearchKey('label');
this._screenStateService.setIdKey('id');
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
@ -65,7 +64,10 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
.toPromise();
} else {
await this._fileAttributesService
.deleteFileAttributes(this._screenStateService.selectedEntitiesIds, this._appStateService.activeDossierTemplateId)
.deleteFileAttributes(
this.screenStateService.selectedEntities.map(f => f.id),
this._appStateService.activeDossierTemplateId
)
.toPromise();
}
await this._loadData();
@ -89,17 +91,16 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
}
private async _loadData() {
this._loadingService.start();
try {
this._loadingService.start();
const response = await this._fileAttributesService
.getFileAttributesConfiguration(this._appStateService.activeDossierTemplateId)
.toPromise();
this._existingConfiguration = response;
this._screenStateService.setEntities(response?.fileAttributeConfigs || []);
} catch (e) {
} finally {
this.filterService.filterEntities();
this._loadingService.stop();
}
this.screenStateService.setEntities(response?.fileAttributeConfigs || []);
} catch (e) {}
this._loadingService.stop();
}
}

View File

@ -3,22 +3,12 @@
<redaction-admin-side-nav type="settings"></redaction-admin-side-nav>
<div *ngIf="viewReady">
<div class="page-header">
<div class="breadcrumb" translate="license-information"></div>
<div class="actions">
<button (click)="sendMail()" color="primary" mat-flat-button translate="license-info-screen.email-report"></button>
<redaction-circle-button
*ngIf="permissionsService.isUser()"
[tooltip]="'common.close' | translate"
class="ml-6"
icon="red:close"
redactionNavigateLastDossiersScreen
tooltipPosition="below"
></redaction-circle-button>
</div>
</div>
<div>
<redaction-page-header
[pageLabel]="'license-information' | translate"
[showCloseButton]="permissionsService.isUser()"
[buttonConfigs]="buttonConfigs"
></redaction-page-header>
<div class="red-content-inner">
<div class="content-container">
@ -129,5 +119,3 @@
</div>
</div>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -4,6 +4,9 @@ import { LicenseReport, LicenseReportControllerService } from '@redaction/red-ui
import { AppConfigService } from '@app-config/app-config.service';
import * as moment from 'moment';
import { TranslateService } from '@ngx-translate/core';
import { LoadingService } from '../../../../services/loading.service';
import { ButtonConfig } from '../../../shared/components/page-header/models/button-config.model';
import { IconButtonTypes } from '../../../shared/components/buttons/icon-button/icon-button.component';
@Component({
selector: 'redaction-license-information-screen',
@ -16,7 +19,6 @@ export class LicenseInformationScreenComponent implements OnInit {
unlicensedInfo: LicenseReport = {};
totalLicensedNumberOfPages = 0;
analysisPercentageOfLicense = 100;
viewReady = false;
barChart: any[] = [];
lineChartSeries: any[] = [];
yAxisLabel = this._translateService.instant('license-info-screen.chart.pages-per-month');
@ -31,13 +33,23 @@ export class LicenseInformationScreenComponent implements OnInit {
group: 'Ordinal',
domain: ['#0389ec']
};
buttonConfigs: ButtonConfig[] = [
{
label: this._translateService.instant('license-info-screen.email-report'),
action: () => this.sendMail(),
type: IconButtonTypes.PRIMARY
}
];
constructor(
readonly permissionsService: PermissionsService,
readonly appConfigService: AppConfigService,
private readonly _licenseReportController: LicenseReportControllerService,
private readonly _translateService: TranslateService
) {}
private readonly _translateService: TranslateService,
private readonly _loadingService: LoadingService
) {
_loadingService.start();
}
get currentYear(): number {
return new Date().getFullYear();
@ -68,7 +80,7 @@ export class LicenseInformationScreenComponent implements OnInit {
Promise.all(promises).then(reports => {
[this.currentInfo, this.totalInfo, this.unlicensedInfo] = reports;
this.viewReady = true;
this._loadingService.stop();
this.analysisPercentageOfLicense =
this.totalLicensedNumberOfPages > 0
? (this.currentInfo.numberOfAnalyzedPages / this.totalLicensedNumberOfPages) * 100

View File

@ -1,7 +1,7 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { RulesControllerService } from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver';
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
@ -17,7 +17,7 @@ import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorCon
templateUrl: './rules-screen.component.html',
styleUrls: ['./rules-screen.component.scss']
})
export class RulesScreenComponent extends ComponentHasChanges {
export class RulesScreenComponent extends ComponentHasChanges implements OnInit {
editorOptions: IStandaloneEditorConstructionOptions = {
theme: 'vs',
language: 'java',
@ -39,13 +39,16 @@ export class RulesScreenComponent extends ComponentHasChanges {
readonly permissionsService: PermissionsService,
private readonly _rulesControllerService: RulesControllerService,
private readonly _appStateService: AppStateService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
protected readonly _translateService: TranslateService,
private readonly _activatedRoute: ActivatedRoute
) {
super(_translateService);
this._appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
this._initialize();
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
async ngOnInit() {
await this._initialize();
}
get hasChanges(): boolean {
@ -83,27 +86,20 @@ export class RulesScreenComponent extends ComponentHasChanges {
async save(): Promise<void> {
this.processing = true;
this._rulesControllerService
await this._rulesControllerService
.uploadRules({
rules: this._codeEditor.getModel().getValue(),
dossierTemplateId: this._appStateService.activeDossierTemplateId
})
.subscribe(
() => {
this._initialize();
this._notificationService.showToastNotification(
this._translateService.instant('rules-screen.success.generic'),
null,
NotificationType.SUCCESS
);
.toPromise()
.then(
async () => {
await this._initialize();
this._toaster.success('rules-screen.success.generic');
},
() => {
this.processing = false;
this._notificationService.showToastNotification(
this._translateService.instant('rules-screen.error.generic'),
null,
NotificationType.ERROR
);
this._toaster.error('rules-screen.error.generic');
}
);
}
@ -148,13 +144,16 @@ export class RulesScreenComponent extends ComponentHasChanges {
} as IModelDeltaDecoration;
}
private _initialize() {
this._rulesControllerService.downloadRules(this._appStateService.activeDossierTemplateId).subscribe(
rules => {
this.currentLines = this.initialLines = rules.rules.split('\n');
this.revert();
},
() => (this.processing = false)
);
private async _initialize() {
await this._rulesControllerService
.downloadRules(this._appStateService.activeDossierTemplateId)
.toPromise()
.then(
rules => {
this.currentLines = this.initialLines = rules.rules.split('\n');
this.revert();
},
() => (this.processing = false)
);
}
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AdminDialogService } from '../../services/admin-dialog.service';
@ -8,15 +8,15 @@ import {
SmtpConfigurationControllerService,
SMTPConfigurationModel
} from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Toaster } from '../../../../services/toaster.service';
import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component';
@Component({
selector: 'redaction-smtp-config-screen',
templateUrl: './smtp-config-screen.component.html',
styleUrls: ['./smtp-config-screen.component.scss']
})
export class SmtpConfigScreenComponent implements OnInit {
export class SmtpConfigScreenComponent extends AutoUnsubscribeComponent implements OnInit, OnDestroy {
viewReady = false;
configForm: FormGroup;
generalSettings: GeneralConfigurationModel = {
@ -31,11 +31,11 @@ export class SmtpConfigScreenComponent implements OnInit {
private readonly _smtpConfigService: SmtpConfigurationControllerService,
private readonly _formBuilder: FormBuilder,
private readonly _dialogService: AdminDialogService,
private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService,
private readonly _toaster: Toaster,
private readonly _generalSettingsControllerService: GeneralSettingsControllerService
) {
this.configForm = this._formBuilder.group({
super();
this.configForm = _formBuilder.group({
host: [undefined, Validators.required],
port: [25],
from: [undefined, [Validators.required, Validators.email]],
@ -50,7 +50,7 @@ export class SmtpConfigScreenComponent implements OnInit {
password: [undefined]
});
this.configForm.controls.auth.valueChanges.subscribe(auth => {
this.addSubscription = this.configForm.controls.auth.valueChanges.subscribe(auth => {
if (auth) {
this.openAuthConfigDialog();
}
@ -110,17 +110,9 @@ export class SmtpConfigScreenComponent implements OnInit {
this.viewReady = false;
try {
await this._smtpConfigService.testSMTPConfiguration(this.configForm.getRawValue()).toPromise();
this._notificationService.showToastNotification(
this._translateService.instant('smtp-config-screen.test.success'),
undefined,
NotificationType.SUCCESS
);
this._toaster.success('smtp-config-screen.test.success');
} catch (e) {
this._notificationService.showToastNotification(
this._translateService.instant('smtp-config-screen.test.error'),
undefined,
NotificationType.ERROR
);
this._toaster.error('smtp-config-screen.test.error');
} finally {
this.viewReady = true;
}

View File

@ -9,18 +9,18 @@
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
[active]="screenStateService.areAllEntitiesSelected$ | async"
[indeterminate]="screenStateService.notAllEntitiesSelected$ | async"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'trash.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }}
{{ 'trash.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) } }}
</span>
<redaction-circle-button
(action)="bulkRestore()"
*ngIf="areSomeEntitiesSelected$ | async"
*ngIf="screenStateService.areSomeEntitiesSelected$ | async"
[tooltip]="'trash.bulk.restore' | translate"
icon="red:put-back"
type="dark-bg"
@ -28,19 +28,17 @@
<redaction-circle-button
(action)="bulkDelete()"
*ngIf="areSomeEntitiesSelected$ | async"
*ngIf="screenStateService.areSomeEntitiesSelected$ | async"
[tooltip]="'trash.bulk.delete' | translate"
icon="red:trash"
type="dark-bg"
></redaction-circle-button>
</div>
<div [class.no-data]="noData" class="table-header" redactionSyncWidth="table-item">
<div [class.no-data]="screenStateService.noData$ | async" class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'trash.table-col-names.name' | translate"
[withSort]="true"
column="name"
@ -50,15 +48,11 @@
class="user-column"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'trash.table-col-names.deleted-on' | translate"
[withSort]="true"
column="dateDeleted"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'trash.table-col-names.time-to-restore' | translate"
[withSort]="true"
column="timeToRestore"
@ -66,18 +60,16 @@
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state *ngIf="noData" [text]="'trash.no-data.title' | translate" icon="red:template"></redaction-empty-state>
<redaction-empty-state
*ngIf="screenStateService.noData$ | async"
[text]="'trash.no-data.title' | translate"
icon="red:template"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch" [text]="'trash.no-match.title' | translate"></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'trash.no-match.title' | translate"></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 *cdkVirtualFor="let entity of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">
<div (click)="toggleEntitySelected($event, entity)" class="selection-column">
<redaction-round-checkbox [active]="isSelected(entity)"></redaction-round-checkbox>
</div>

View File

@ -8,7 +8,7 @@ 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 { SortingService } from '../../../../services/sorting.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { DossiersService } from '../../../dossier/services/dossiers.service';
@ -21,6 +21,7 @@ import { DossiersService } from '../../../dossier/services/dossiers.service';
export class TrashScreenComponent extends BaseListingComponent<Dossier> implements OnInit {
readonly itemSize = 85;
private readonly _deleteRetentionHours = this._appConfigService.getConfig(AppConfigKey.DELETE_RETENTION_HOURS);
protected readonly _primaryKey = 'dossierName';
constructor(
private readonly _appStateService: AppStateService,
@ -32,36 +33,29 @@ export class TrashScreenComponent extends BaseListingComponent<Dossier> implemen
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());
this.screenStateService.setEntities(await this._dossiersService.getDeleted());
}
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) {
bulkDelete(dossierIds = this.screenStateService.selectedEntities.map(d => d.dossierId)) {
this._loadingService.loadWhile(this._hardDelete(dossierIds));
}
bulkRestore(dossierIds = this._screenStateService.selectedEntitiesIds) {
bulkRestore(dossierIds = this.screenStateService.selectedEntities.map(d => d.dossierId)) {
this._loadingService.loadWhile(this._restore(dossierIds));
}
@ -76,9 +70,8 @@ export class TrashScreenComponent extends BaseListingComponent<Dossier> implemen
}
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();
const entities = this.screenStateService.allEntities.filter(e => !ids.includes(e.dossierId));
this.screenStateService.setEntities(entities);
this.screenStateService.setSelectedEntities([]);
}
}

View File

@ -9,7 +9,7 @@
<div class="actions">
<redaction-input-with-action
[form]="searchForm"
[form]="searchService.searchForm"
[placeholder]="'user-listing.search' | translate"
type="search"
></redaction-input-with-action>
@ -37,18 +37,18 @@
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
[active]="screenStateService.areAllEntitiesSelected$ | async"
[indeterminate]="screenStateService.notAllEntitiesSelected$ | async"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'user-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length } }}
{{ 'user-listing.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) } }}
</span>
<redaction-circle-button
(action)="bulkDelete()"
*ngIf="areSomeEntitiesSelected$ | async"
*ngIf="screenStateService.areSomeEntitiesSelected$ | async"
[disabled]="(canDeleteSelected$ | async) === false"
[tooltip]="
(canDeleteSelected$ | async)
@ -79,14 +79,11 @@
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state
*ngIf="(displayedEntities$ | async)?.length === 0"
[text]="'user-listing.no-match.title' | translate"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'user-listing.no-match.title' | translate"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines -->
<div *cdkVirtualFor="let user of displayedEntities$ | async; trackBy: trackById" class="table-item">
<div *cdkVirtualFor="let user of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">
<div (click)="toggleEntitySelected($event, user)" class="selection-column">
<redaction-round-checkbox [active]="isSelected(user)"></redaction-round-checkbox>
</div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Injector, OnInit, QueryList, ViewChildren } from '@angular/core';
import { 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';
@ -19,10 +19,12 @@ import { map } from 'rxjs/operators';
@Component({
templateUrl: './user-listing-screen.component.html',
styleUrls: ['./user-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class UserListingScreenComponent extends BaseListingComponent<User> implements OnInit {
protected readonly _primaryKey = 'userId';
readonly canDeleteSelected$ = this._canDeleteSelected$;
collapsedDetails = false;
chartData: DoughnutChartConfig[] = [];
@ViewChildren(InitialsAvatarComponent)
@ -39,12 +41,11 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
protected readonly _injector: Injector
) {
super(_injector);
this._screenStateService.setIdKey('userId');
}
get canDeleteSelected$(): Observable<boolean> {
const entities$ = this._screenStateService.selectedEntitiesIds$;
return entities$.pipe(map(all => all.indexOf(this.userService.userId) === -1));
get _canDeleteSelected$(): Observable<boolean> {
const entities$ = this.screenStateService.selectedEntities$;
return entities$.pipe(map(all => all.indexOf(this.userService.user) === -1));
}
async ngOnInit() {
@ -79,17 +80,12 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
}
bulkDelete() {
this.openDeleteUsersDialog(this._screenStateService.entities.filter(u => this.isSelected(u)));
}
trackById(index: number, user: User) {
return user.userId;
this.openDeleteUsersDialog(this.screenStateService.allEntities.filter(u => this.isSelected(u)));
}
private async _loadData() {
this._screenStateService.setEntities(await this._userControllerService.getAllUsers().toPromise());
this.screenStateService.setEntities(await this._userControllerService.getAllUsers().toPromise());
await this.userService.loadAllUsers();
this.filterService.filterEntities();
this._computeStats();
this._loadingService.stop();
}

View File

@ -7,8 +7,7 @@ import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { debounce } from '@utils/debounce';
import { WatermarkControllerService, WatermarkModelRes } from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Toaster } from '../../../../services/toaster.service';
import { ActivatedRoute } from '@angular/router';
import { BASE_HREF } from '../../../../tokens';
import { stampPDFPage } from '../../../../utils/page-stamper';
@ -39,15 +38,14 @@ export class WatermarkScreenComponent implements OnInit {
readonly permissionsService: PermissionsService,
readonly appStateService: AppStateService,
@Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _translateService: TranslateService,
private readonly _watermarkControllerService: WatermarkControllerService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _http: HttpClient,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _formBuilder: FormBuilder,
private readonly _activatedRoute: ActivatedRoute
) {
this.appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
this._initForm();
}
@ -81,24 +79,12 @@ export class WatermarkScreenComponent implements OnInit {
? this._watermarkControllerService.saveWatermark(watermark, this.appStateService.activeDossierTemplateId)
: this._watermarkControllerService.deleteWatermark(this.appStateService.activeDossierTemplateId);
observable.subscribe(
observable.toPromise().then(
() => {
this._loadWatermark();
this._notificationService.showToastNotification(
this._translateService.instant(
watermark.text ? 'watermark-screen.action.change-success' : 'watermark-screen.action.delete-success'
),
null,
NotificationType.SUCCESS
);
this._toaster.success(watermark.text ? 'watermark-screen.action.change-success' : 'watermark-screen.action.delete-success');
},
() => {
this._notificationService.showToastNotification(
this._translateService.instant('watermark-screen.action.error'),
null,
NotificationType.ERROR
);
}
() => this._toaster.error('watermark-screen.action.error')
);
}
@ -188,11 +174,41 @@ export class WatermarkScreenComponent implements OnInit {
private _initForm() {
this.configForm = this._formBuilder.group({
text: [{ value: null, disabled: !this.permissionsService.isAdmin() }],
hexColor: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required],
opacity: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required],
fontSize: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required],
fontType: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required],
orientation: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required]
hexColor: [
{
value: null,
disabled: !this.permissionsService.isAdmin()
},
Validators.required
],
opacity: [
{
value: null,
disabled: !this.permissionsService.isAdmin()
},
Validators.required
],
fontSize: [
{
value: null,
disabled: !this.permissionsService.isAdmin()
},
Validators.required
],
fontType: [
{
value: null,
disabled: !this.permissionsService.isAdmin()
},
Validators.required
],
orientation: [
{
value: null,
disabled: !this.permissionsService.isAdmin()
},
Validators.required
]
});
}
}

View File

@ -21,8 +21,7 @@
[tooltip]="'dossier-overview.assign-me' | translate"
icon="red:assign-me"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-circle-button
(action)="setToUnderApproval()"
@ -30,8 +29,7 @@
[tooltip]="'dossier-overview.under-approval' | translate"
icon="red:ready-for-approval"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-circle-button
(action)="setToUnderReview()"
@ -39,8 +37,7 @@
[tooltip]="'dossier-overview.under-review' | translate"
icon="red:undo"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-file-download-btn [dossier]="dossier" [file]="selectedFiles"></redaction-file-download-btn>
@ -52,8 +49,7 @@
[tooltip]="canApprove ? ('dossier-overview.approve' | translate) : ('dossier-overview.approve-disabled' | translate)"
icon="red:approved"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<!-- Back to approval -->
<redaction-circle-button
@ -62,8 +58,7 @@
[tooltip]="'dossier-overview.under-approval' | translate"
icon="red:undo"
type="dark-bg"
>
</redaction-circle-button>
></redaction-circle-button>
<redaction-circle-button
(action)="ocr()"

View File

@ -1,15 +1,14 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Output } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
import { FileManagementControllerService, ReanalysisControllerService } from '@redaction/red-ui-http';
import { PermissionsService } from '@services/permissions.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { FileActionService } from '../../services/file-action.service';
import { from, Observable } from 'rxjs';
import { StatusOverlayService } from '@upload-download/services/status-overlay.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { LoadingService } from '@services/loading.service';
import { ConfirmationDialogInput } from '@shared/dialogs/confirmation-dialog/confirmation-dialog.component';
import { LoadingService } from '../../../../services/loading.service';
import { ConfirmationDialogInput } from '../../../shared/dialogs/confirmation-dialog/confirmation-dialog.component';
import { ScreenStateService } from '../../../shared/services/screen-state.service';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -19,23 +18,19 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
styleUrls: ['./dossier-overview-bulk-actions.component.scss']
})
export class DossierOverviewBulkActionsComponent {
@Input()
selectedFileIds: string[];
@Output()
reload = new EventEmitter();
constructor(
private readonly _appStateService: AppStateService,
private readonly _userService: UserService,
private readonly _dialogService: DossiersDialogService,
private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _permissionsService: PermissionsService,
private readonly _fileActionService: FileActionService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _loadingService: LoadingService,
private readonly _translateService: TranslateService
private readonly _translateService: TranslateService,
private readonly _screenStateService: ScreenStateService<FileStatusWrapper>
) {}
get dossier() {
@ -43,30 +38,27 @@ export class DossierOverviewBulkActionsComponent {
}
get selectedFiles(): FileStatusWrapper[] {
return this.selectedFileIds.map(fileId =>
this._appStateService.getFileById(this._appStateService.activeDossier.dossier.dossierId, fileId)
);
return this._screenStateService.selectedEntities;
}
get areAllFilesSelected() {
return (
this._appStateService.activeDossier.files.length !== 0 &&
this.selectedFileIds.length === this._appStateService.activeDossier.files.length
this.selectedFiles.length === this._appStateService.activeDossier.files.length
);
}
get areSomeFilesSelected() {
return this.selectedFileIds.length > 0;
return this.selectedFiles.length > 0;
}
get allSelectedFilesCanBeAssignedIntoSameState() {
if (this.areSomeFilesSelected) {
const selectedFiles = this.selectedFiles;
const allFilesAreUnderReviewOrUnassigned = selectedFiles.reduce(
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce(
(acc, file) => acc && (file.isUnderReview || file.isUnassigned),
true
);
const allFilesAreUnderApproval = selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval;
}
return false;
@ -144,11 +136,14 @@ export class DossierOverviewBulkActionsComponent {
async () => {
this._loadingService.start();
await this._fileManagementControllerService
.deleteFiles(this.selectedFileIds, this._appStateService.activeDossierId)
.deleteFiles(
this.selectedFiles.map(item => item.fileId),
this._appStateService.activeDossierId
)
.toPromise();
await this._appStateService.reloadActiveDossierFiles();
this.reload.emit();
this.selectedFileIds.splice(0, this.selectedFileIds.length);
this._screenStateService.setSelectedEntities([]);
this._loadingService.stop();
}
);
@ -158,12 +153,9 @@ export class DossierOverviewBulkActionsComponent {
// If more than 1 approver - show dialog and ask who to assign
if (this._appStateService.activeDossier.approverIds.length > 1) {
this._loadingService.start();
const files = this.selectedFileIds.map(fileId =>
this._appStateService.getFileById(this._appStateService.activeDossierId, fileId)
);
this._dialogService.openAssignFileToUserDialog(
files,
this.selectedFiles,
'approver',
() => {
this.reload.emit();
@ -203,11 +195,10 @@ export class DossierOverviewBulkActionsComponent {
assign() {
this._loadingService.start();
const files = this.selectedFileIds.map(fileId => this._appStateService.getFileById(this._appStateService.activeDossierId, fileId));
const mode = files[0].isUnderApproval ? 'approver' : 'reviewer';
const mode = this.selectedFiles[0].isUnderApproval ? 'approver' : 'reviewer';
this._dialogService.openAssignFileToUserDialog(files, mode, () => {
this._dialogService.openAssignFileToUserDialog(this.selectedFiles, mode, () => {
this.reload.emit();
this._loadingService.stop();
});

View File

@ -42,9 +42,7 @@
<div *ngIf="hasFiles" class="mt-24">
<redaction-simple-doughnut-chart
(toggleFilter)="filterService.filterEntities()"
[config]="documentsChartData"
[filter]="filterService.getFilter$('statusFilters') | async"
[radius]="63"
[strokeWidth]="15"
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier'"
@ -55,8 +53,8 @@
<div *ngIf="hasFiles" class="mt-24 legend pb-32">
<div
(click)="filterService.toggleFilter('needsWorkFilters', filter.key)"
*ngFor="let filter of filterService.getFilter$('needsWorkFilters') | async"
[class.active]="filterService.filterChecked$('needsWorkFilters', filter.key) | async"
*ngFor="let filter of needsWorkFilters$ | async"
[class.active]="filter.checked"
>
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</div>
@ -69,9 +67,9 @@
></redaction-dossier-details-stats>
</div>
<div *ngIf="!!appStateService.activeDossier.dossier.description" class="pb-32">
<div *ngIf="appStateService.activeDossier.dossier.description as description" class="pb-32">
<div class="heading" translate="dossier-overview.dossier-details.description"></div>
<div class="mt-8">{{ appStateService.activeDossier.dossier.description }}</div>
<div class="mt-8">{{ description }}</div>
</div>
</ng-container>

View File

@ -7,9 +7,8 @@ import { TranslateChartService } from '@services/translate-chart.service';
import { StatusSorter } from '@utils/sorters/status-sorter';
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';
import { Toaster } from '../../../../services/toaster.service';
import { FilterService } from '../../../shared/services/filter.service';
import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
@Component({
@ -26,14 +25,16 @@ export class DossierDetailsComponent implements OnInit {
@Output() openDossierDictionaryDialog = new EventEmitter();
@Output() toggleCollapse = new EventEmitter();
readonly needsWorkFilters$ = this.filterService.getFilterModels$('needsWorkFilters');
constructor(
readonly appStateService: AppStateService,
readonly translateChartService: TranslateChartService,
readonly permissionsService: PermissionsService,
readonly filterService: FilterService<FileStatusWrapper>,
readonly filterService: FilterService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _userService: UserService,
private readonly _notificationService: NotificationService
private readonly _toaster: Toaster
) {}
get memberIds(): string[] {
@ -71,7 +72,7 @@ export class DossierDetailsComponent implements OnInit {
key: key
});
}
this.documentsChartData.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]);
this.documentsChartData.sort(StatusSorter.byStatus);
this.documentsChartData = this.translateChartService.translateStatus(this.documentsChartData);
this._changeDetectorRef.detectChanges();
}
@ -80,11 +81,11 @@ export class DossierDetailsComponent implements OnInit {
this.owner = typeof user === 'string' ? this._userService.getRedUserById(user) : user;
const dw = Object.assign({}, this.appStateService.activeDossier);
dw.dossier.ownerId = this.owner.userId;
await this.appStateService.addOrUpdateDossier(dw.dossier);
await this.appStateService.createOrUpdateDossier(dw.dossier);
const ownerName = this._userService.getNameForId(this.owner.userId);
const dossierName = this.appStateService.activeDossier.name;
const msg = 'Successfully assigned ' + ownerName + ' to dossier: ' + dossierName;
this._notificationService.showToastNotification(msg);
this._toaster.info(msg);
}
}

View File

@ -2,7 +2,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { FileManagementControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
@ -19,16 +18,13 @@ export class DossierListingActionsComponent {
constructor(
readonly permissionsService: PermissionsService,
readonly appStateService: AppStateService,
private readonly _dialogService: DossiersDialogService,
private readonly _fileManagementControllerService: FileManagementControllerService
private readonly _dialogService: DossiersDialogService
) {}
openEditDossierDialog($event: MouseEvent, dossierWrapper: DossierWrapper) {
this._dialogService.openDialog('editDossier', $event, {
dossierWrapper,
afterSave: () => {
this.actionPerformed.emit();
}
afterSave: () => this.actionPerformed.emit()
});
}
@ -51,7 +47,7 @@ export class DossierListingActionsComponent {
}, {});
return Object.keys(obj)
.sort((a, b) => StatusSorter[a] - StatusSorter[b])
.sort(StatusSorter.byStatus)
.map(status => ({ length: obj[status], color: status }));
}
}

View File

@ -27,7 +27,6 @@
<div>
<redaction-simple-doughnut-chart
[config]="documentsChartData"
[filter]="filterService.getFilter$('statusFilters') | async"
[radius]="80"
[strokeWidth]="15"
[subtitle]="'dossier-listing.stats.charts.total-documents'"

View File

@ -9,9 +9,9 @@ import { FilterService } from '@shared/services/filter.service';
styleUrls: ['./dossier-listing-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DossierListingDetailsComponent<T> {
export class DossierListingDetailsComponent {
@Input() dossiersChartData: DoughnutChartConfig[];
@Input() documentsChartData: DoughnutChartConfig[];
constructor(readonly appStateService: AppStateService, readonly filterService: FilterService<T>) {}
constructor(readonly appStateService: AppStateService, readonly filterService: FilterService) {}
}

View File

@ -2,9 +2,9 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from
import { PermissionsService } from '@services/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { PageRange, ReanalysisControllerService } from '@redaction/red-ui-http';
import { FileDataModel } from '@models/file/file-data.model';
import { NotificationService, NotificationType } from '@services/notification.service';
import { LoadingService } from '@services/loading.service';
import { FileDataModel } from '../../../../models/file/file-data.model';
import { Toaster } from '../../../../services/toaster.service';
import { LoadingService } from '../../../../services/loading.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
@ -23,9 +23,8 @@ export class PageExclusionComponent implements OnChanges {
readonly permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _notificationService: NotificationService,
private readonly _loadingService: LoadingService,
private readonly _translateService: TranslateService
private readonly _toaster: Toaster,
private readonly _loadingService: LoadingService
) {
this.excludePagesForm = this._formBuilder.group({
value: ['']
@ -80,11 +79,7 @@ export class PageExclusionComponent implements OnChanges {
this.excludePagesForm.reset();
this.actionPerformed.emit('exclude-pages');
} catch (e) {
this._notificationService.showToastNotification(
this._translateService.instant('file-preview.tabs.exclude-pages.error'),
null,
NotificationType.ERROR
);
this._toaster.error('file-preview.tabs.exclude-pages.error');
this._loadingService.stop();
}
}

View File

@ -1,8 +1,8 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Dossier, DossierControllerService, StatusControllerService } from '@redaction/red-ui-http';
import { Dossier } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DossierWrapper } from '@state/model/dossier.wrapper';
@ -21,10 +21,8 @@ export class TeamMembersManagerComponent implements OnInit {
constructor(
readonly userService: UserService,
private readonly _dossierControllerService: DossierControllerService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder,
private readonly _statusControllerService: StatusControllerService,
private readonly _appStateService: AppStateService
) {}
@ -88,14 +86,10 @@ export class TeamMembersManagerComponent implements OnInit {
dw.dossier.memberIds = memberIds;
dw.dossier.approverIds = approverIds;
dw.dossier.ownerId = ownerId;
result = await this._appStateService.addOrUpdateDossier(dw.dossier);
result = await this._appStateService.createOrUpdateDossier(dw.dossier);
this.save.emit(result);
} catch (error) {
this._notificationService.showToastNotification(
'Failed: ' + error.error ? error.error.message : error,
null,
NotificationType.ERROR
);
this._toaster.error('Failed: ' + error.error ? error.error.message : error);
}
}

View File

@ -64,7 +64,7 @@ export class AddDossierDialogComponent {
dossier.memberIds = foundDossier.memberIds;
}
const savedDossier = await this._appStateService.addOrUpdateDossier(dossier);
const savedDossier = await this._appStateService.createOrUpdateDossier(dossier);
if (savedDossier) {
this.dialogRef.close({ dossier: savedDossier });
}
@ -72,7 +72,7 @@ export class AddDossierDialogComponent {
async saveDossierAndAddMembers() {
const dossier: Dossier = this._formToObject();
const savedDossier = await this._appStateService.addOrUpdateDossier(dossier);
const savedDossier = await this._appStateService.createOrUpdateDossier(dossier);
if (savedDossier) {
this.dialogRef.close({ addMembers: true, dossier: savedDossier });
}

View File

@ -1,9 +1,9 @@
import { Component, Inject } from '@angular/core';
import { DossierControllerService, StatusControllerService } from '@redaction/red-ui-http';
import { StatusControllerService } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { DossierWrapper } from '@state/model/dossier.wrapper';
@ -16,7 +16,6 @@ class DialogData {
}
@Component({
selector: 'redaction-dossier-details-dialog',
templateUrl: './assign-reviewer-approver-dialog.component.html',
styleUrls: ['./assign-reviewer-approver-dialog.component.scss']
})
@ -26,13 +25,12 @@ export class AssignReviewerApproverDialogComponent {
constructor(
readonly userService: UserService,
private readonly _dossierControllerService: DossierControllerService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder,
private readonly _statusControllerService: StatusControllerService,
private readonly _appStateService: AppStateService,
public dialogRef: MatDialogRef<AssignReviewerApproverDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData
private readonly _dialogRef: MatDialogRef<AssignReviewerApproverDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData
) {
this._loadData();
}
@ -52,10 +50,8 @@ export class AssignReviewerApproverDialogComponent {
return true;
}
const reviewerId = this.selectedSingleUser;
for (const file of this.data.files) {
if (file.currentReviewer !== reviewerId) {
if (file.currentReviewer !== this.selectedSingleUser) {
return true;
}
}
@ -94,14 +90,10 @@ export class AssignReviewerApproverDialogComponent {
file.reviewerName = this.userService.getNameForId(selectedUser);
}
} catch (error) {
this._notificationService.showToastNotification(
'Failed: ' + error.error ? error.error.message : error,
null,
NotificationType.ERROR
);
this._toaster.error('Failed: ' + error.error ? error.error.message : error);
}
this.dialogRef.close();
this._dialogRef.close();
}
private _loadData() {

View File

@ -71,7 +71,7 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS
downloadFileTypes: this.dossierForm.get('downloadFileTypes').value,
reportTypes: this.dossierForm.get('reportTypes').value
};
const updatedDossier = await this._appStateService.addOrUpdateDossier(dossier);
const updatedDossier = await this._appStateService.createOrUpdateDossier(dossier);
this.updateDossier.emit(updatedDossier);
}

View File

@ -6,8 +6,7 @@ import { DossierWrapper } from '@state/model/dossier.wrapper';
import { EditDossierGeneralInfoComponent } from './general-info/edit-dossier-general-info.component';
import { EditDossierDownloadPackageComponent } from './download-package/edit-dossier-download-package.component';
import { EditDossierSectionInterface } from './edit-dossier-section.interface';
import { NotificationService, NotificationType } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Toaster } from '../../../../services/toaster.service';
import { EditDossierDictionaryComponent } from './dictionary/edit-dossier-dictionary.component';
import { EditDossierTeamMembersComponent } from './team-members/edit-dossier-team-members.component';
import { EditDossierAttributesComponent } from './attributes/edit-dossier-attributes.component';
@ -32,12 +31,8 @@ export class EditDossierDialogComponent {
@ViewChild(EditDossierAttributesComponent) attributesComponent: EditDossierAttributesComponent;
constructor(
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService,
private readonly _toaster: Toaster,
private readonly _changeRef: ChangeDetectorRef,
private readonly _dialogRef: MatDialogRef<EditDossierDialogComponent>,
@Inject(MAT_DIALOG_DATA)
private readonly _data: {
dossierWrapper: DossierWrapper;
@ -102,11 +97,7 @@ export class EditDossierDialogComponent {
}
updatedDossier(updatedDossier: DossierWrapper) {
this._notificationService.showToastNotification(
this._translateService.instant('edit-dossier-dialog.change-successful'),
null,
NotificationType.SUCCESS
);
this._toaster.success('edit-dossier-dialog.change-successful');
if (updatedDossier) {
this.dossierWrapper = updatedDossier;
@ -131,11 +122,7 @@ export class EditDossierDialogComponent {
changeTab(key: Section) {
if (this.activeComponent.changed) {
this._notificationService.showToastNotification(
this._translateService.instant('edit-dossier-dialog.unsaved-changes'),
null,
NotificationType.ERROR
);
this._toaster.error('edit-dossier-dialog.unsaved-changes');
return;
}
this.activeNav = key;

View File

@ -10,8 +10,7 @@ import { PermissionsService } from '@services/permissions.service';
import { Router } from '@angular/router';
import { MatDialogRef } from '@angular/material/dialog';
import { EditDossierDialogComponent } from '../edit-dossier-dialog.component';
import { NotificationService, NotificationType } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Toaster } from '../../../../../services/toaster.service';
@Component({
selector: 'redaction-edit-dossier-general-info',
@ -34,8 +33,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
private readonly _dialogService: DossiersDialogService,
private readonly _router: Router,
private readonly _editDossierDialogRef: MatDialogRef<EditDossierDialogComponent>,
private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService
private readonly _toaster: Toaster
) {}
get changed() {
@ -100,7 +98,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
dueDate: this.hasDueDate ? this.dossierForm.get('dueDate').value : undefined,
dossierTemplateId: this.dossierForm.get('dossierTemplateId').value
};
const updatedDossier = await this._appStateService.addOrUpdateDossier(dossier);
const updatedDossier = await this._appStateService.createOrUpdateDossier(dossier);
if (updatedDossier) this.updateDossier.emit(updatedDossier);
}
@ -113,11 +111,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
}
private _notifyDossierDeleted() {
this._notificationService.showToastNotification(
this._translateService.instant('edit-dossier-dialog.delete-successful'),
null,
NotificationType.SUCCESS
);
this._toaster.success('edit-dossier-dialog.delete-successful');
}
private _filterInvalidDossierTemplates() {

View File

@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AppStateService } from '@state/app-state.service';
import { MatDialogRef } from '@angular/material/dialog';
import { ForceRedactionRequest, LegalBasisMappingControllerService } from '@redaction/red-ui-http';
import { NotificationService } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '@services/user.service';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
@ -29,7 +29,7 @@ export class ForceRedactionDialogComponent implements OnInit {
private readonly _appStateService: AppStateService,
private readonly _userService: UserService,
private readonly _formBuilder: FormBuilder,
private readonly _notificationService: NotificationService,
private readonly _notificationService: Toaster,
private readonly _translateService: TranslateService,
private readonly _legalBasisMappingControllerService: LegalBasisMappingControllerService,
private readonly _manualAnnotationService: ManualAnnotationService,

View File

@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AppStateService } from '@state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AddRedactionRequest, LegalBasisMappingControllerService } from '@redaction/red-ui-http';
import { NotificationService } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '@services/user.service';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
@ -37,7 +37,7 @@ export class ManualAnnotationDialogComponent implements OnInit {
private readonly _appStateService: AppStateService,
private readonly _userService: UserService,
private readonly _formBuilder: FormBuilder,
private readonly _notificationService: NotificationService,
private readonly _notificationService: Toaster,
private readonly _translateService: TranslateService,
private readonly _legalBasisMappingControllerService: LegalBasisMappingControllerService,
private readonly _manualAnnotationService: ManualAnnotationService,

View File

@ -8,52 +8,25 @@
<div class="red-content-inner">
<div class="content-container">
<div class="header-item">
<span class="all-caps-label">
{{ 'dossier-listing.table-header.title' | translate: { length: (displayedEntities$ | async)?.length || 0 } }}
</span>
<redaction-quick-filters></redaction-quick-filters>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-listing.table-col-names.name' | translate"
[withSort]="true"
column="dossier.dossierName"
></redaction-table-col-name>
<redaction-table-col-name [label]="'dossier-listing.table-col-names.needs-work' | translate"></redaction-table-col-name>
<redaction-table-col-name
[label]="'dossier-listing.table-col-names.owner' | translate"
class="user-column"
></redaction-table-col-name>
<redaction-table-col-name
[label]="'dossier-listing.table-col-names.status' | translate"
class="flex-end"
></redaction-table-col-name>
<div class="scrollbar-placeholder"></div>
</div>
<redaction-table-header
[tableHeaderLabel]="'dossier-listing.table-header.title'"
[tableColConfigs]="tableColConfigs"
></redaction-table-header>
<redaction-empty-state
(action)="openAddDossierDialog()"
*ngIf="noData"
*ngIf="screenStateService.noData$ | async"
[buttonLabel]="'dossier-listing.no-data.action' | translate"
[showButton]="permissionsService.isManager()"
[text]="'dossier-listing.no-data.title' | translate"
icon="red:folder"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch" [text]="'dossier-listing.no-match.title' | translate"></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'dossier-listing.no-match.title' | translate"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="let dw of displayedEntities$ | async | sortBy: sortingOption.order:sortingOption.column"
*cdkVirtualFor="let dw of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[class.pointer]="!!dw"
[routerLink]="[!!dw ? '/main/dossiers/' + dw.dossier.dossierId : []]"
class="table-item"
@ -112,7 +85,7 @@
<div class="right-container" redactionHasScrollbar>
<redaction-dossier-listing-details
*ngIf="(allEntities$ | async)?.length !== 0"
*ngIf="(screenStateService.noData$ | async) === false"
[documentsChartData]="documentsChartData"
[dossiersChartData]="dossiersChartData"
></redaction-dossier-listing-details>

View File

@ -1,14 +1,13 @@
import { Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { AfterViewInit, ChangeDetectorRef, 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';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { groupBy } from '@utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { PermissionsService } from '@services/permissions.service';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { Subscription, timer } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { timer } from 'rxjs';
import { filter } 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';
@ -28,16 +27,22 @@ 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';
import { SortingService } from '@services/sorting.service';
import { TableColConfig } from '../../../shared/components/table-col-name/table-col-name.component';
const isLeavingScreen = event => event instanceof NavigationStart && event.url !== '/main/dossiers';
@Component({
templateUrl: './dossier-listing-screen.component.html',
styleUrls: ['./dossier-listing-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierListingScreenComponent extends BaseListingComponent<DossierWrapper> implements OnInit, OnDestroy, OnAttach, OnDetach {
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
export class DossierListingScreenComponent
extends BaseListingComponent<DossierWrapper>
implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach
{
readonly itemSize = 95;
protected readonly _primaryKey = 'dossierName';
buttonConfigs: ButtonConfig[] = [
{
label: this._translateService.instant('dossier-listing.add-new'),
@ -47,19 +52,34 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
type: 'primary'
}
];
tableColConfigs: TableColConfig[] = [
{
label: this._translateService.instant('dossier-listing.table-col-names.name'),
withSort: true,
column: 'dossierName'
},
{
label: this._translateService.instant('dossier-listing.table-col-names.needs-work')
},
{
label: this._translateService.instant('dossier-listing.table-col-names.owner'),
class: 'user-column'
},
{
label: this._translateService.instant('dossier-listing.table-col-names.status'),
class: 'flex-end'
}
];
readonly itemSize = 85;
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
private _dossierAutoUpdateTimer: Subscription;
private _lastScrollPosition: number;
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,
private readonly _userService: UserService,
private readonly _dialogService: DossiersDialogService,
@ -67,70 +87,47 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
private readonly _router: Router,
private readonly _appStateService: AppStateService,
private readonly _userPreferenceService: UserPreferenceService,
readonly changeDetectorRef: ChangeDetectorRef,
protected readonly _injector: Injector
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DOSSIER_LISTING);
this._searchService.setSearchKey('name');
this._appStateService.reset();
this._loadEntitiesFromState();
}
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;
}
ngOnInit(): void {
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.addSubscription = timer(0, 10000).subscribe(async () => {
await this._appStateService.loadAllDossiers();
this._loadEntitiesFromState();
this.calculateData();
});
this._routerEventsScrollPositionSub = this._router.events
.pipe(filter(event => event instanceof NavigationStart))
.subscribe((event: NavigationStart) => {
if (event.url !== '/main/dossiers') {
this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top');
}
});
this.addSubscription = this._appStateService.fileChanged.subscribe(() => {
this.calculateData();
});
this.addSubscription = this._router.events.pipe(filter(isLeavingScreen)).subscribe(() => {
this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top');
});
}
ngAfterViewInit() {
this.changeDetectorRef.detectChanges();
}
ngOnAttach() {
this.scrollViewport.scrollTo({ top: this._lastScrollPosition });
this._appStateService.reset();
this._loadEntitiesFromState();
this.ngOnInit();
this.scrollViewport.scrollTo({ top: this._lastScrollPosition });
}
ngOnDetach() {
ngOnDetach(): void {
this.ngOnDestroy();
}
ngOnDestroy(): void {
this._dossierAutoUpdateTimer.unsubscribe();
this._routerEventsScrollPositionSub.unsubscribe();
this._fileChangedSub.unsubscribe();
}
getDossierTemplate(dw: DossierWrapper): DossierTemplateModel {
return this._appStateService.getDossierTemplateById(dw.dossier.dossierTemplateId);
}
@ -147,6 +144,18 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
});
}
private get _userId() {
return this._userService.userId;
}
private get _activeDossiersCount(): number {
return this.screenStateService.allEntities.filter(p => p.dossier.status === Dossier.StatusEnum.ACTIVE).length;
}
private get _inactiveDossiersCount(): number {
return this.screenStateService.allEntities.length - this._activeDossiersCount;
}
calculateData() {
this._computeAllFilters();
@ -165,12 +174,12 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
key: key
});
}
this.documentsChartData.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]);
this.documentsChartData.sort(StatusSorter.byStatus);
this.documentsChartData = this._translateChartService.translateStatus(this.documentsChartData);
}
private _loadEntitiesFromState() {
this._screenStateService.setEntities(this._appStateService.allDossiers);
this.screenStateService.setEntities(this._appStateService.allDossiers);
}
private _computeAllFilters() {
@ -179,7 +188,7 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
const allDistinctNeedsWork = new Set<string>();
const allDistinctDossierTemplates = new Set<string>();
this._screenStateService?.entities?.forEach(entry => {
this.screenStateService?.allEntities?.forEach(entry => {
// all people
entry.dossier.memberIds.forEach(f => allDistinctPeople.add(f));
// Needs work
@ -200,11 +209,11 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
label: this._translateService.instant(status)
}));
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
values: statusFilters.sort(StatusSorter.byKey),
values: statusFilters.sort(StatusSorter.byStatus),
checker: dossierStatusChecker
});
@ -213,7 +222,7 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
label: this._userService.getNameForId(userId)
}));
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'peopleFilters',
label: this._translateService.instant('filters.people'),
icon: 'red:user',
@ -226,7 +235,7 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
label: `filter.${type}`
}));
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
@ -242,23 +251,21 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
label: this._appStateService.getDossierTemplateById(id).name
}));
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'dossierTemplateFilters',
label: this._translateService.instant('filters.dossier-templates'),
icon: 'red:template',
hide: this.filterService.getFilter('dossierTemplateFilters')?.values?.length <= 1,
hide: this.filterService.getFilterGroup('dossierTemplateFilters')?.values?.length <= 1,
values: dossierTemplateFilters,
checker: dossierTemplateChecker
});
const quickFilters = this._createQuickFilters();
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'quickFilters',
values: quickFilters,
checker: (dw: DossierWrapper) => quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
});
this.filterService.filterEntities();
}
private _createQuickFilters() {
@ -267,22 +274,22 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
{
key: 'my-dossiers',
label: myDossiersLabel,
checker: (dw: DossierWrapper) => dw.ownerId === this._user.id
checker: (dw: DossierWrapper) => dw.ownerId === this._userId
},
{
key: 'to-approve',
label: this._translateService.instant('dossier-listing.quick-filters.to-approve'),
checker: (dw: DossierWrapper) => dw.approverIds.includes(this._user.id)
checker: (dw: DossierWrapper) => dw.approverIds.includes(this._userId)
},
{
key: 'to-review',
label: this._translateService.instant('dossier-listing.quick-filters.to-review'),
checker: (dw: DossierWrapper) => dw.memberIds.includes(this._user.id)
checker: (dw: DossierWrapper) => dw.memberIds.includes(this._userId)
},
{
key: 'other',
label: this._translateService.instant('dossier-listing.quick-filters.other'),
checker: (dw: DossierWrapper) => !dw.memberIds.includes(this._user.id)
checker: (dw: DossierWrapper) => !dw.memberIds.includes(this._userId)
}
];

View File

@ -5,17 +5,17 @@
[showCloseButton]="true"
>
<redaction-file-download-btn
[disabled]="areSomeEntitiesSelected$ | async"
[disabled]="screenStateService.areSomeEntitiesSelected$ | async"
[dossier]="activeDossier"
[file]="allEntities$ | async"
[file]="screenStateService.allEntities$ | async"
tooltipPosition="below"
></redaction-file-download-btn>
<redaction-circle-button
(action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn()"
[disabled]="areSomeEntitiesSelected$ | async"
[tooltipClass]="'small ' + ((areSomeEntitiesSelected$ | async) ? '' : 'warn')"
[disabled]="screenStateService.areSomeEntitiesSelected$ | async"
[tooltipClass]="'small ' + ((screenStateService.areSomeEntitiesSelected$ | async) ? '' : 'warn')"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
icon="red:refresh"
tooltipPosition="below"
@ -40,38 +40,31 @@
<div class="select-all-container">
<redaction-round-checkbox
(click)="toggleSelectAll()"
[active]="areAllEntitiesSelected"
[indeterminate]="(areSomeEntitiesSelected$ | async) && !areAllEntitiesSelected"
[active]="screenStateService.areAllEntitiesSelected$ | async"
[indeterminate]="screenStateService.notAllEntitiesSelected$ | async"
></redaction-round-checkbox>
</div>
<span class="all-caps-label">
{{ 'dossier-overview.table-header.title' | translate: { length: (displayedEntities$ | async)?.length || 0 } }}
{{ 'dossier-overview.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) || 0 } }}
</span>
<redaction-dossier-overview-bulk-actions
(reload)="bulkActionPerformed()"
[selectedFileIds]="selectedEntitiesIds$ | async"
></redaction-dossier-overview-bulk-actions>
<redaction-dossier-overview-bulk-actions (reload)="bulkActionPerformed()"></redaction-dossier-overview-bulk-actions>
<redaction-quick-filters></redaction-quick-filters>
</div>
<div [class.no-data]="noData" class="table-header" redactionSyncWidth="table-item">
<div [class.no-data]="screenStateService.noData$ | async" class="table-header" redactionSyncWidth="table-item">
<!-- Table column names-->
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-overview.table-col-names.name' | translate"
[withSort]="true"
column="filename"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-overview.table-col-names.added-on' | translate"
[withSort]="true"
column="added"
@ -80,8 +73,6 @@
<redaction-table-col-name [label]="'dossier-overview.table-col-names.needs-work' | translate"></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-overview.table-col-names.assigned-to' | translate"
[withSort]="true"
class="user-column"
@ -89,16 +80,12 @@
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-overview.table-col-names.pages' | translate"
[withSort]="true"
column="pages"
></redaction-table-col-name>
<redaction-table-col-name
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[label]="'dossier-overview.table-col-names.status' | translate"
[withSort]="true"
class="flex-end"
@ -109,21 +96,18 @@
<redaction-empty-state
(action)="fileInput.click()"
*ngIf="noData"
*ngIf="screenStateService.noData$ | async"
[buttonLabel]="'dossier-overview.no-data.action' | translate"
[text]="'dossier-overview.no-data.title' | translate"
buttonIcon="red:upload"
icon="red:document"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch" [text]="'dossier-overview.no-match.title' | translate"></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'dossier-overview.no-match.title' | translate"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="
let fileStatus of displayedEntities$ | async | sortBy: sortingOption.order:sortingOption.column;
trackBy: trackByFileId
"
*cdkVirtualFor="let fileStatus of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[class.disabled]="fileStatus.isExcluded"
[class.last-opened]="isLastOpenedFile(fileStatus)"
[class.pointer]="permissionsService.canOpenFile(fileStatus)"

View File

@ -1,6 +1,6 @@
import { ChangeDetectorRef, Component, ElementRef, HostListener, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { AppStateService } from '@state/app-state.service';
import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service';
import { FileUploadModel } from '@upload-download/model/file-upload.model';
@ -10,11 +10,9 @@ import { TranslateService } from '@ngx-translate/core';
import * as moment from 'moment';
import { DossierDetailsComponent } from '../../components/dossier-details/dossier-details.component';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { PermissionsService } from '@services/permissions.service';
import { UserService } from '@services/user.service';
import { FileStatus, UserPreferenceControllerService } from '@redaction/red-ui-http';
import { Subscription, timer } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { timer } from 'rxjs';
import { filter } from 'rxjs/operators';
import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { convertFiles, handleFileDrop } from '@utils/file-drop-utils';
@ -28,11 +26,12 @@ import { ActionConfig } from '@shared/components/page-header/models/action-confi
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 { SortingService } from '../../../../services/sorting.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { LoadingService } from '@services/loading.service';
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
import { UserPreferenceService } from '../../../../services/user-preference.service';
@Component({
templateUrl: './dossier-overview-screen.component.html',
@ -43,25 +42,23 @@ export class DossierOverviewScreenComponent
extends BaseListingComponent<FileStatusWrapper>
implements OnInit, OnDestroy, OnDetach, OnAttach
{
collapsedDetails = false;
readonly itemSize = 80;
protected readonly _primaryKey = 'filename';
collapsedDetails = false;
actionConfigs: ActionConfig[];
dossierAttributes: DossierAttributeWithValue[] = [];
@ViewChild(DossierDetailsComponent, { static: false })
private readonly _dossierDetailsComponent: DossierDetailsComponent;
private _filesAutoUpdateTimer: Subscription;
private _routerEventsScrollPositionSub: Subscription;
private _fileChangedSub: Subscription;
private readonly _lastOpenedFileKey = 'Dossier-Recent-' + this.activeDossier.dossierId;
private _lastScrollPosition: number;
private _lastOpenedFileId = '';
@ViewChild('needsWorkTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkTemplate: TemplateRef<any>;
@ViewChild('fileInput') private _fileInput: ElementRef;
constructor(
readonly permissionsService: PermissionsService,
private readonly _userService: UserService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _dialogService: DossiersDialogService,
private readonly _fileUploadService: FileUploadService,
private readonly _statusOverlayService: StatusOverlayService,
@ -69,7 +66,7 @@ export class DossierOverviewScreenComponent
private readonly _translateService: TranslateService,
private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _appStateService: AppStateService,
private readonly _userPreferenceControllerService: UserPreferenceControllerService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _appConfigService: AppConfigService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _loadingService: LoadingService,
@ -77,9 +74,6 @@ export class DossierOverviewScreenComponent
protected readonly _injector: Injector
) {
super(_injector);
this._sortingService.setScreenName(ScreenNames.DOSSIER_OVERVIEW);
this._searchService.setSearchKey('searchField');
this._screenStateService.setIdKey('fileId');
this._loadEntitiesFromState();
}
@ -87,42 +81,38 @@ export class DossierOverviewScreenComponent
return this._appStateService.activeDossier;
}
get user() {
return this._userService.user;
get userId() {
return this._userService.userId;
}
get checkedRequiredFilters() {
return this.filterService.getFilter('quickFilters')?.values.filter(f => f.required && f.checked);
return this.filterService.getFilterGroup('quickFilters')?.values.filter(f => f.required && f.checked);
}
get checkedNotRequiredFilters() {
return this.filterService.getFilter('quickFilters')?.values.filter(f => !f.required && f.checked);
return this.filterService.getFilterGroup('quickFilters')?.values.filter(f => !f.required && f.checked);
}
isLastOpenedFile(fileStatus: FileStatusWrapper): boolean {
return this._lastOpenedFileId === fileStatus.fileId;
isLastOpenedFile({ fileId }: FileStatusWrapper): boolean {
return this._userPreferenceService.getLastOpenedFileId(this._lastOpenedFileKey) === fileId;
}
async ngOnInit() {
async ngOnInit(): Promise<void> {
this._fileDropOverlayService.initFileDropHandling();
this.calculateData();
this._filesAutoUpdateTimer = timer(0, 7500)
.pipe(
tap(async () => {
await this._appStateService.reloadActiveDossierFilesIfNecessary();
this._loadEntitiesFromState();
})
)
.subscribe();
this.addSubscription = timer(0, 7500).subscribe(async () => {
await this._appStateService.reloadActiveDossierFilesIfNecessary();
this._loadEntitiesFromState();
});
this._fileChangedSub = this._appStateService.fileChanged.subscribe(() => {
this.addSubscription = this._appStateService.fileChanged.subscribe(() => {
this.calculateData();
});
this._routerEventsScrollPositionSub = this._router.events
.pipe(filter(events => events instanceof NavigationStart))
this.addSubscription = this._router.events
.pipe(filter(event => event instanceof NavigationStart))
.subscribe((event: NavigationStart) => {
if (!event.url.endsWith(this._appStateService.activeDossierId)) {
this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top');
@ -131,13 +121,6 @@ export class DossierOverviewScreenComponent
this._loadingService.start();
const userAttributes = await this._userPreferenceControllerService.getAllUserAttributes();
if (userAttributes === null || userAttributes === undefined) return;
const key = 'Dossier-Recent-' + this.activeDossier.dossierId;
if (userAttributes[key]?.length > 0) {
this._lastOpenedFileId = userAttributes[key][0];
}
this.dossierAttributes = await this._dossierAttributesService.getValues(this.activeDossier);
this._loadingService.stop();
@ -145,9 +128,7 @@ export class DossierOverviewScreenComponent
ngOnDestroy(): void {
this._fileDropOverlayService.cleanupFileDropHandling();
this._filesAutoUpdateTimer.unsubscribe();
this._fileChangedSub.unsubscribe();
this._routerEventsScrollPositionSub.unsubscribe();
super.ngOnDestroy();
}
async ngOnAttach() {
@ -165,33 +146,13 @@ export class DossierOverviewScreenComponent
.reanalyzeDossier()
.then(() => {
this.reloadDossiers();
this._notificationService.showToastNotification(
this._translateService.instant('dossier-overview.reanalyse-dossier.success'),
null,
NotificationType.SUCCESS
);
this._toaster.success('dossier-overview.reanalyse-dossier.success');
})
.catch(() => {
this._notificationService.showToastNotification(
this._translateService.instant('dossier-overview.reanalyse-dossier.error'),
null,
NotificationType.ERROR
);
});
}
isError(fileStatusWrapper: FileStatusWrapper) {
return fileStatusWrapper.status === FileStatus.StatusEnum.ERROR;
}
isProcessing(fileStatusWrapper: FileStatusWrapper) {
return [FileStatus.StatusEnum.REPROCESS, FileStatus.StatusEnum.FULLREPROCESS, FileStatus.StatusEnum.PROCESSING].includes(
fileStatusWrapper.status
);
.catch(() => this._toaster.error('dossier-overview.reanalyse-dossier.error'));
}
reloadDossiers() {
this._appStateService.getFiles(this._appStateService.activeDossier, false).then(() => {
this._appStateService.getFiles(this.activeDossier, false).then(() => {
this.calculateData();
});
}
@ -202,16 +163,10 @@ export class DossierOverviewScreenComponent
this._loadEntitiesFromState();
this._computeAllFilters();
this.filterService.filterEntities();
this._dossierDetailsComponent?.calculateChartConfig();
this._changeDetectorRef.detectChanges();
}
trackByFileId(index: number, item: FileStatusWrapper) {
return item.fileId;
}
@HostListener('drop', ['$event'])
onDrop(event: DragEvent) {
handleFileDrop(event, this.activeDossier, this._uploadFiles.bind(this));
@ -235,7 +190,7 @@ export class DossierOverviewScreenComponent
}
bulkActionPerformed() {
this._screenStateService.selectedEntitiesIds$.next([]);
this.screenStateService.setSelectedEntities([]);
this.reloadDossiers();
}
@ -271,15 +226,12 @@ export class DossierOverviewScreenComponent
moment(file.lastUpdated).add(this._appConfigService.getConfig(AppConfigKey.RECENT_PERIOD_IN_HOURS), 'hours').isAfter(moment());
private _loadEntitiesFromState() {
if (this.activeDossier) this._screenStateService.setEntities(this.activeDossier.files);
if (this.activeDossier) this.screenStateService.setEntities(this.activeDossier.files);
}
private async _uploadFiles(files: FileUploadModel[]) {
const fileCount = await this._fileUploadService.uploadFiles(files);
if (fileCount) {
this._statusOverlayService.openUploadStatusOverlay();
}
// this._changeDetectorRef.detectChanges();
if (fileCount) this._statusOverlayService.openUploadStatusOverlay();
}
private _computeAllFilters() {
@ -290,7 +242,7 @@ export class DossierOverviewScreenComponent
const allDistinctAddedDates = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
this._screenStateService.entities.forEach(file => {
this.screenStateService.allEntities.forEach(file => {
allDistinctPeople.add(file.currentReviewer);
allDistinctFileStatusWrapper.add(file.status);
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'));
@ -309,11 +261,11 @@ export class DossierOverviewScreenComponent
label: this._translateService.instant(item)
}));
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'statusFilters',
label: this._translateService.instant('filters.status'),
icon: 'red:status',
values: statusFilters.sort(StatusSorter.byKey),
values: statusFilters.sort(StatusSorter.byStatus),
checker: keyChecker('status')
});
@ -332,7 +284,7 @@ export class DossierOverviewScreenComponent
label: this._userService.getNameForId(userId)
});
});
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'peopleFilters',
label: this._translateService.instant('filters.assigned-people'),
icon: 'red:user',
@ -345,7 +297,7 @@ export class DossierOverviewScreenComponent
label: this._translateService.instant('filter.' + item)
}));
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
@ -356,7 +308,7 @@ export class DossierOverviewScreenComponent
checkerArgs: this.permissionsService
});
this.filterService.addFilter({
this.filterService.addFilterGroup({
slug: 'quickFilters',
values: this._createQuickFilters(),
checker: (file: FileStatusWrapper) =>
@ -370,7 +322,7 @@ export class DossierOverviewScreenComponent
private _createQuickFilters() {
let quickFilters = [];
if (this._screenStateService.entities.filter(this.recentlyModifiedChecker).length > 0) {
if (this.screenStateService.allEntities.filter(this.recentlyModifiedChecker).length > 0) {
const recentPeriod = this._appConfigService.getConfig(AppConfigKey.RECENT_PERIOD_IN_HOURS);
quickFilters = [
{
@ -389,7 +341,7 @@ export class DossierOverviewScreenComponent
{
key: 'assigned-to-me',
label: this._translateService.instant('dossier-overview.quick-filters.assigned-to-me'),
checker: (file: FileStatusWrapper) => file.currentReviewer === this.user.id
checker: (file: FileStatusWrapper) => file.currentReviewer === this.userId
},
{
key: 'unassigned',
@ -399,7 +351,7 @@ export class DossierOverviewScreenComponent
{
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
checker: (file: FileStatusWrapper) => !!file.currentReviewer && file.currentReviewer !== this.userId
}
];
}

View File

@ -12,8 +12,7 @@ import { AnnotationData, FileDataModel } from '@models/file/file-data.model';
import { FileActionService } from '../../services/file-action.service';
import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { tap } from 'rxjs/operators';
import { NotificationService } from '@services/notification.service';
import { Toaster } from '../../../../services/toaster.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { PermissionsService } from '@services/permissions.service';
import { Subscription, timer } from 'rxjs';
@ -34,9 +33,10 @@ import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { handleFilterDelta, processFilters } from '@shared/components/filters/popup-filter/utils/filter-utils';
import { LoadingService } from '@services/loading.service';
import { LoadingService } from '../../../../services/loading.service';
import { stampPDFPage } from '../../../../utils/page-stamper';
import { TranslateService } from '@ngx-translate/core';
import { AutoUnsubscribeComponent } from '../../../shared/base/auto-unsubscribe.component';
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
@ -45,7 +45,7 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss']
})
export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, OnDetach {
export class FilePreviewScreenComponent extends AutoUnsubscribeComponent implements OnInit, OnDestroy, OnAttach, OnDetach {
dialogRef: MatDialogRef<any>;
viewMode: ViewMode = 'STANDARD';
fullScreen = false;
@ -59,7 +59,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
secondaryFilters: FilterModel[];
canPerformAnnotationActions: boolean;
filesAutoUpdateTimer: Subscription;
fileReanalysedSubscription: Subscription;
hideSkipped = false;
displayPDFViewer = false;
viewDocumentInfo = false;
@ -81,7 +80,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
private readonly _activatedRoute: ActivatedRoute,
private readonly _dialogService: DossiersDialogService,
private readonly _router: Router,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _annotationProcessingService: AnnotationProcessingService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _fileActionService: FileActionService,
@ -92,6 +91,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
private readonly _loadingService: LoadingService,
private readonly _translateService: TranslateService
) {
super();
document.documentElement.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.fullScreen = false;
@ -225,7 +225,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
ngOnDetach() {
this.displayPDFViewer = false;
this.viewReady = false;
this._unsubscribeFromFileUpdates();
super.ngOnDestroy();
}
async ngOnAttach(previousRoute: ActivatedRouteSnapshot) {
@ -253,10 +253,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
this.viewReady = true;
}
ngOnDestroy(): void {
this._unsubscribeFromFileUpdates();
}
rebuildFilters(deletePreviousAnnotations: boolean = false) {
const startTime = new Date().getTime();
if (deletePreviousAnnotations) {
@ -473,7 +469,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
await this._statusControllerService.setFileReviewer(dossierId, fileId, reviewerId).toPromise();
const msg = `Successfully assigned ${reviewerName} to file: ${filename}`;
this._notificationService.showToastNotification(msg);
this._toaster.info(msg);
await this.appStateService.reloadActiveFile();
this._updateCanPerformActions();
this.editingReviewer = false;
@ -492,7 +488,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
}
downloadOriginalFile() {
this._fileManagementControllerService
this.addSubscription = this._fileManagementControllerService
.downloadOriginalFile(this.dossierId, this.fileId, true, this.fileData.fileStatus.cacheIdentifier, 'response')
.subscribe(data => {
download(data, this.fileData.fileStatus.filename);
@ -543,10 +539,8 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
}
private _subscribeToFileUpdates(): void {
this.filesAutoUpdateTimer = timer(0, 5000)
.pipe(tap(async () => await this.appStateService.reloadActiveFile()))
.subscribe();
this.fileReanalysedSubscription = this.appStateService.fileReanalysed.subscribe(async (fileStatus: FileStatusWrapper) => {
this.addSubscription = timer(0, 5000).subscribe(async () => await this.appStateService.reloadActiveFile());
this.addSubscription = this.appStateService.fileReanalysed.subscribe(async (fileStatus: FileStatusWrapper) => {
if (fileStatus.fileId === this.fileId) {
await this._loadFileData(!this._reloadFileOnReanalysis);
this._reloadFileOnReanalysis = false;
@ -557,11 +551,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
});
}
private _unsubscribeFromFileUpdates(): void {
this.filesAutoUpdateTimer.unsubscribe();
this.fileReanalysedSubscription.unsubscribe();
}
private _updateCanPerformActions() {
this.canPerformAnnotationActions =
this.permissionsService.canPerformAnnotationActions() &&

View File

@ -1,11 +1,25 @@
import { Injectable } from '@angular/core';
import { DossierControllerService } from '@redaction/red-ui-http';
import { Dossier, DossierControllerService } from '@redaction/red-ui-http';
@Injectable()
@Injectable({
providedIn: 'root'
})
export class DossiersService {
constructor(private readonly _dossierControllerService: DossierControllerService) {}
getDeletedDossiers() {
createOrUpdate(dossier: Dossier): Promise<Dossier> {
return this._dossierControllerService.createOrUpdateDossier(dossier).toPromise();
}
delete(dossierId: string): Promise<unknown> {
return this._dossierControllerService.deleteDossier(dossierId).toPromise();
}
getAll(): Promise<Dossier[]> {
return this._dossierControllerService.getDossiers().toPromise();
}
getDeleted(): Promise<Dossier[]> {
return this._dossierControllerService.getDeletedDossiers().toPromise();
}

View File

@ -7,7 +7,7 @@ import {
ManualRedactionControllerService
} from '@redaction/red-ui-http';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '../../../services/toaster.service';
import { TranslateService } from '@ngx-translate/core';
import { tap } from 'rxjs/operators';
import { UserService } from '@services/user.service';
@ -38,7 +38,7 @@ export class ManualAnnotationService {
private readonly _appStateService: AppStateService,
private readonly _userService: UserService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _toaster: Toaster,
private readonly _manualRedactionControllerService: ManualRedactionControllerService,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _permissionsService: PermissionsService
@ -76,8 +76,12 @@ export class ManualAnnotationService {
return obs.pipe(
tap(
() => this._notify(this._getMessage(mode)),
error => this._notify(this._getMessage(mode, modifyDictionary, true), NotificationType.ERROR, error)
() => this._toaster.success(this._getMessage(mode), { positionClass: 'toast-file-preview' }),
error =>
this._toaster.error(this._getMessage(mode, modifyDictionary, true), {
params: error,
positionClass: 'toast-file-preview'
})
)
);
}
@ -223,13 +227,6 @@ export class ManualAnnotationService {
}
}
private _notify(key: string, type: NotificationType = NotificationType.SUCCESS, data?: any) {
this._notificationService.showToastNotification(this._translateService.instant(key, data), null, type, {
positionClass: 'toast-file-preview',
actions: []
});
}
private _getMessage(mode: Mode, modifyDictionary?: boolean, error: boolean = false) {
return (
'annotation-actions.message.' +

View File

@ -0,0 +1,28 @@
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
/**
* Inherit this class when you need to subscribe to observables in your components
*/
@Component({ template: '' })
export abstract class AutoUnsubscribeComponent implements OnDestroy {
private _subscriptions = new Subscription();
/**
* Call this method when you want to subscribe to an observable
* @param subscription - the new subscription to add to subscriptions array
*/
set addSubscription(subscription: Subscription) {
this._subscriptions.closed = false;
this._subscriptions.add(subscription);
}
/**
* This method unsubscribes active subscriptions
* If you implement OnDestroy in a component that inherits AutoUnsubscribeComponent,
* then you must explicitly call super.ngOnDestroy()
*/
ngOnDestroy(): void {
this._subscriptions.unsubscribe();
}
}

View File

@ -1,95 +1,97 @@
import { Component, Injector, ViewChild } from '@angular/core';
import { SortingOption, SortingService } from '@services/sorting.service';
import { Component, Injector, OnDestroy, ViewChild } from '@angular/core';
import { SortingOrders, SortingService } from '@services/sorting.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { FilterService } from '../services/filter.service';
import { SearchService } from '../services/search.service';
import { ScreenStateService } from '../services/screen-state.service';
import { Observable } from 'rxjs';
import { FilterModel } from '../components/filters/popup-filter/model/filter.model';
import { combineLatest, Observable } from 'rxjs';
import { AutoUnsubscribeComponent } from './auto-unsubscribe.component';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { PermissionsService } from '../../../services/permissions.service';
@Component({ template: '' })
export abstract class BaseListingComponent<T> {
export abstract class BaseListingComponent<T> extends AutoUnsubscribeComponent implements OnDestroy {
@ViewChild(CdkVirtualScrollViewport)
readonly scrollViewport: CdkVirtualScrollViewport;
readonly filterService: FilterService<T>;
protected readonly _sortingService: SortingService;
protected readonly _searchService: SearchService<T>;
protected readonly _screenStateService: ScreenStateService<T>;
readonly permissionsService: PermissionsService;
readonly filterService: FilterService;
readonly sortingService: SortingService;
readonly searchService: SearchService<T>;
readonly screenStateService: ScreenStateService<T>;
readonly sortedDisplayedEntities$: Observable<T[]>;
readonly noMatch$: Observable<boolean>;
/**
* Key used in the *trackBy* function with **ngFor* or **cdkVirtualFor*
* and in the default sorting and as the search field
* @protected
*/
protected abstract _primaryKey: string;
protected constructor(protected readonly _injector: Injector) {
this.filterService = this._injector.get<FilterService<T>>(FilterService);
this._sortingService = this._injector.get<SortingService>(SortingService);
this._searchService = this._injector.get<SearchService<T>>(SearchService);
this._screenStateService = this._injector.get<ScreenStateService<T>>(ScreenStateService);
super();
this.trackByPrimaryKey = this.trackByPrimaryKey.bind(this);
this.permissionsService = this._injector.get(PermissionsService);
this.filterService = this._injector.get(FilterService);
this.sortingService = this._injector.get(SortingService);
this.searchService = this._injector.get(SearchService);
this.screenStateService = this._injector.get<ScreenStateService<T>>(ScreenStateService);
setTimeout(() => this.setInitialConfig());
this.sortedDisplayedEntities$ = this._sortedDisplayedEntities$;
this.noMatch$ = this._noMatch$;
}
get selectedEntitiesIds$(): Observable<string[]> {
return this._screenStateService.selectedEntitiesIds$;
setInitialConfig() {
this.sortingService.setSortingOption({
column: this._primaryKey,
order: SortingOrders.ASC
});
this.searchService.setSearchKey(this._primaryKey);
}
get displayedEntities$(): Observable<T[]> {
return this._screenStateService.displayedEntities$;
ngOnDestroy(): void {
super.ngOnDestroy();
}
get allEntities$(): Observable<T[]> {
return this._screenStateService.entities$;
}
get displayedEntities(): T[] {
return this._screenStateService.displayedEntities;
private get _sortedDisplayedEntities$(): Observable<T[]> {
return this.screenStateService.displayedEntities$.pipe(map(entities => this.sortingService.defaultSort(entities)));
}
get allEntities(): T[] {
return this._screenStateService.entities;
return this.screenStateService.allEntities;
}
get areAllEntitiesSelected() {
return this._screenStateService.areAllEntitiesSelected;
private get _noMatch$(): Observable<boolean> {
return combineLatest([this.screenStateService.allEntitiesLength$, this.screenStateService.displayedLength$]).pipe(
map(([hasEntities, hasDisplayedEntities]) => hasEntities && !hasDisplayedEntities),
distinctUntilChanged()
);
}
get areSomeEntitiesSelected$() {
return this._screenStateService.areSomeEntitiesSelected$;
}
get sortingOption(): SortingOption {
return this._sortingService.getSortingOption();
}
get searchForm() {
return this._searchService.searchForm;
}
get noMatch(): boolean {
return this.allEntities.length && this.displayedEntities?.length === 0;
}
get noData(): boolean {
return this.allEntities.length === 0;
}
getFilter$(slug: string): Observable<FilterModel[]> {
return this.filterService.getFilter$(slug);
}
resetFilters() {
this.filterService.reset();
}
toggleSort($event) {
this._sortingService.toggleSort($event);
canBulkDelete$(hasPermission = true): Observable<boolean> {
return this.screenStateService.areSomeEntitiesSelected$.pipe(
map(areSomeEntitiesSelected => areSomeEntitiesSelected && hasPermission),
distinctUntilChanged()
);
}
toggleSelectAll() {
return this._screenStateService.toggleSelectAll();
return this.screenStateService.selectEntities();
}
toggleEntitySelected(event: MouseEvent, entity: T) {
event.stopPropagation();
return this._screenStateService.toggleEntitySelected(entity);
return this.screenStateService.selectEntities([entity]);
}
isSelected(entity: T) {
return this._screenStateService.isSelected(entity);
isSelected(entity: T): boolean {
return this.screenStateService.isSelected(entity);
}
trackByPrimaryKey(index: number, item: T) {
return item[this._primaryKey];
}
}

View File

@ -1,11 +1,12 @@
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { FileDownloadService } from '@upload-download/services/file-download.service';
import { NotificationService } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Toaster } from '@services/toaster.service';
import { BASE_HREF } from '../../../../../tokens';
import { AutoUnsubscribeComponent } from '@shared/base/auto-unsubscribe.component';
import { TranslateService } from '@ngx-translate/core';
export type MenuState = 'OPEN' | 'CLOSED';
@ -15,7 +16,7 @@ export type MenuState = 'OPEN' | 'CLOSED';
styleUrls: ['./file-download-btn.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileDownloadBtnComponent {
export class FileDownloadBtnComponent extends AutoUnsubscribeComponent implements OnDestroy {
@Input() dossier: DossierWrapper;
@Input() file: FileStatusWrapper | FileStatusWrapper[];
@Input() tooltipPosition: 'above' | 'below' | 'before' | 'after' = 'above';
@ -27,9 +28,11 @@ export class FileDownloadBtnComponent {
@Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _permissionsService: PermissionsService,
private readonly _fileDownloadService: FileDownloadService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService
) {}
private readonly _toaster: Toaster,
private readonly _translateService: TranslateService
) {
super();
}
get canDownloadFiles() {
if (!Array.isArray(this.file)) {
@ -47,12 +50,10 @@ export class FileDownloadBtnComponent {
downloadFiles($event: MouseEvent) {
$event.stopPropagation();
this._fileDownloadService.downloadFiles(Array.isArray(this.file) ? this.file : [this.file], this.dossier).subscribe(() => {
this._notificationService.showToastNotification(
this._translateService.instant('download-status.queued', {
baseUrl: this._baseHref
})
);
});
this.addSubscription = this._fileDownloadService
.downloadFiles(Array.isArray(this.file) ? this.file : [this.file], this.dossier)
.subscribe(() => {
this._toaster.info('download-status.queued', { params: { baseUrl: this._baseHref } });
});
}
}

View File

@ -1,9 +1,10 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
@Component({
selector: 'redaction-empty-state',
templateUrl: './empty-state.component.html',
styleUrls: ['./empty-state.component.scss']
styleUrls: ['./empty-state.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateComponent implements OnInit {
@Input() text: string;
@ -15,8 +16,6 @@ export class EmptyStateComponent implements OnInit {
@Input() verticalPadding = 120;
@Output() action = new EventEmitter();
constructor() {}
ngOnInit(): void {
this.showButton = this.showButton && this.action.observers.length > 0;
}

View File

@ -1,7 +1,7 @@
import { FilterModel } from './filter.model';
import { TemplateRef } from '@angular/core';
export interface FilterWrapper {
export interface FilterGroup {
slug: string;
label?: string;
icon?: string;

View File

@ -2,7 +2,7 @@ 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 { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
import { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) {
copySettings(oldFilters, newFilters);
@ -159,7 +159,7 @@ export const addedDateChecker = (dw: DossierWrapper, filter: FilterModel) => dw.
export const dossierApproverChecker = (dw: DossierWrapper, filter: FilterModel) => dw.approverIds.includes(filter.key);
export function getFilteredEntities<T>(entities: T[], filters: FilterWrapper[]) {
export function getFilteredEntities<T>(entities: T[], filters: FilterGroup[]) {
const filteredEntities: T[] = [];
for (const entity of entities) {
let add = true;

View File

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

View File

@ -1,14 +1,14 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FilterModel } from '../popup-filter/model/filter.model';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FilterService } from '@shared/services/filter.service';
@Component({
selector: 'redaction-quick-filters',
templateUrl: './quick-filters.component.html',
styleUrls: ['./quick-filters.component.scss']
styleUrls: ['./quick-filters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class QuickFiltersComponent<T> {
@Output() filtersChanged = new EventEmitter<FilterModel[]>();
export class QuickFiltersComponent {
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters');
constructor(readonly filterService: FilterService<T>) {}
constructor(readonly filterService: FilterService) {}
}

View File

@ -6,7 +6,7 @@
<ng-container *ngFor="let config of filters; trackBy: trackByLabel">
<redaction-popup-filter
(filtersChanged)="filterService.filterEntities()"
(filtersChanged)="filterService.refresh()"
*ngIf="!config.hide"
[filterLabel]="config.label"
[filterTemplate]="config.filterTemplate"
@ -16,31 +16,26 @@
</ng-container>
<redaction-input-with-action
*ngIf="searchService.isSearchNeeded"
*ngIf="searchPlaceholder"
[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 (click)="resetFilters()" *ngIf="showResetFilters$ | async" 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"
[label]="config.label"
[type]="config.type"
></redaction-icon-button>
</ng-container>
<div class="actions" *ngIf="showCloseButton || actionConfigs || buttonConfigs">
<ng-container *ngFor="let config of buttonConfigs; trackBy: trackByLabel">
<redaction-icon-button
(action)="config.action($event)"
*ngIf="!config.hide"
[icon]="config.icon"
[label]="config.label"
[type]="config.type"
></redaction-icon-button>
</ng-container>
<div *ngIf="showCloseButton || actionConfigs" class="actions">
<ng-container *ngFor="let config of actionConfigs; trackBy: trackByLabel">
<redaction-circle-button
(action)="config.action($event)"
@ -55,9 +50,9 @@
<ng-content></ng-content>
<redaction-circle-button
*ngIf="showCloseButton && permissionsService.isUser()"
[class.ml-6]="actionConfigs"
*ngIf="showCloseButton"
[tooltip]="'common.close' | translate"
[class.ml-6]="actionConfigs"
icon="red:close"
redactionNavigateLastDossiersScreen
tooltipPosition="below"

View File

@ -1,10 +1,10 @@
import { Component, Input } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { Component, Input, Optional } from '@angular/core';
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';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { combineLatest, Observable, of } from 'rxjs';
@Component({
selector: 'redaction-page-header',
@ -18,22 +18,30 @@ export class PageHeaderComponent<T> {
@Input() buttonConfigs: ButtonConfig[];
@Input() searchPlaceholder: string;
constructor(
readonly permissionsService: PermissionsService,
readonly filterService: FilterService<T>,
readonly searchService: SearchService<T>
) {}
readonly filters$ = this.filterService?.filterGroups$.pipe(map(all => all.filter(f => f.icon)));
readonly showResetFilters$ = this._showResetFilters$;
get filters$() {
return this.filterService.allFilters$.pipe(map(all => all.filter(f => f.icon)));
constructor(@Optional() readonly filterService: FilterService, @Optional() readonly searchService: SearchService<T>) {}
get _showResetFilters$(): Observable<boolean> {
if (!this.filterService) return of(false);
const filtersLength$ = this.filters$.pipe(
map(f => f.length),
distinctUntilChanged()
);
return combineLatest([filtersLength$, this.filterService.showResetFilters$, this.searchService.valueChanges$]).pipe(
map(([hasFilters, showResetFilters, searchValue]) => hasFilters && (showResetFilters || !!searchValue)),
distinctUntilChanged()
);
}
resetFilters() {
resetFilters(): void {
this.filterService.reset();
this.searchService.reset();
}
trackByLabel(index: number, item) {
trackByLabel<K extends { label?: string }>(index: number, item: K): string {
return item.label;
}
}

View File

@ -2,7 +2,7 @@
<svg [attr.height]="size" [attr.width]="size" [style.min-width]="size" attr.viewBox="0 0 {{ size }} {{ size }}" class="donut-chart">
<g *ngFor="let value of config; let i = index">
<circle
*ngIf="exists(i)"
*ngIf="!!chartData[i]"
[attr.cx]="cx"
[attr.cy]="cy"
[attr.r]="radius"
@ -27,8 +27,8 @@
<div
(click)="selectValue(val.key)"
*ngFor="let val of config"
[class.active]="filterService.filterChecked$('statusFilters', val.key) | async"
[class.filter-disabled]="!filter"
[class.active]="filterChecked$(val.key) | async"
[class.filter-disabled]="(statusFilters$ | async)?.length === 0"
>
<redaction-status-bar
[config]="[

View File

@ -1,7 +1,8 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { Component, Input, OnChanges } 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';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
export interface DoughnutChartConfig {
value: number;
@ -16,17 +17,14 @@ export interface DoughnutChartConfig {
templateUrl: './simple-doughnut-chart.component.html',
styleUrls: ['./simple-doughnut-chart.component.scss']
})
export class SimpleDoughnutChartComponent<T> implements OnChanges {
export class SimpleDoughnutChartComponent implements OnChanges {
@Input() subtitle: string;
@Input() config: DoughnutChartConfig[] = [];
@Input() radius = 85;
@Input() strokeWidth = 20;
@Input() direction: 'row' | 'column' = 'column';
@Input() filter: FilterModel[];
@Input() totalType: 'sum' | 'count' = 'sum';
@Input() counterText: string;
@Output()
toggleFilter = new EventEmitter<string>();
chartData: any[] = [];
perimeter: number;
@ -34,13 +32,15 @@ export class SimpleDoughnutChartComponent<T> implements OnChanges {
cy = 0;
size = 0;
constructor(readonly filterService: FilterService<T>) {}
readonly statusFilters$ = this.filterService.getFilterModels$('statusFilters') ?? of([]);
get circumference() {
constructor(readonly filterService: FilterService) {}
get circumference(): number {
return 2 * Math.PI * this.radius;
}
get dataTotal() {
get dataTotal(): number {
return this.config.map(v => v.value).reduce((acc, val) => acc + val, 0);
}
@ -55,47 +55,39 @@ export class SimpleDoughnutChartComponent<T> implements OnChanges {
this.size = this.strokeWidth + this.radius * 2;
}
calculateChartData() {
const newData = [];
let angleOffset = -90;
this.config.forEach(dataVal => {
if (dataVal.value > 0) {
const data = {
degrees: angleOffset
};
newData.push(data);
angleOffset = this.dataPercentage(dataVal.value) * 360 + angleOffset;
} else {
newData.push(null);
}
});
this.chartData = newData;
filterChecked$(key: string): Observable<boolean> {
return this.statusFilters$.pipe(map(all => all?.find(e => e.key === key)?.checked));
}
calculateStrokeDashOffset(dataVal) {
calculateChartData() {
let angleOffset = -90;
this.chartData = this.config.map(dataVal => {
if (dataVal.value === 0) return null;
const res = { degrees: angleOffset };
angleOffset = this.dataPercentage(dataVal.value) * 360 + angleOffset;
return res;
});
}
calculateStrokeDashOffset(dataVal: number): number {
const strokeDiff = this.dataPercentage(dataVal) * this.circumference;
return this.circumference - strokeDiff;
}
dataPercentage(dataVal) {
dataPercentage(dataVal: number): number {
return dataVal / this.dataTotal;
}
returnCircleTransformValue(index) {
returnCircleTransformValue(index: number) {
return `rotate(${this.chartData[index].degrees}, ${this.cx}, ${this.cy})`;
}
getLabel(config: DoughnutChartConfig): string {
return this.totalType === 'sum' ? `${config.value} ${config.label}` : `${config.label} (${config.value} ${this.counterText})`;
getLabel({ label, value }: DoughnutChartConfig): string {
return this.totalType === 'sum' ? `${value} ${label}` : `${label} (${value} ${this.counterText})`;
}
selectValue(key: string) {
selectValue(key: string): void {
this.filterService.toggleFilter('statusFilters', key);
this.filterService.filterEntities();
this.toggleFilter.emit(key);
}
exists(index: number) {
return !!this.chartData[index];
}
}

View File

@ -1,23 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { orderBy } from 'lodash';
import { SortingOrders } from '@services/sorting.service';
@Pipe({ name: 'sortBy' })
export class SortByPipe implements PipeTransform {
transform<T>(value: T[], order = '', column: string = ''): T[] {
if (!value || order === '' || !order) {
return value;
} // no array
if (!column || column === '') {
if (order === SortingOrders.ASC) {
return value.sort();
} else {
return value.sort().reverse();
}
} // sort 1d array
if (value.length <= 1) {
return value;
} // array with only one item
return orderBy(value, [column], [order]);
}
}

View File

@ -1,9 +1,13 @@
<div (click)="withSort && toggleSort.emit(column)" [class.pointer]="withSort" [ngClass]="class">
<div (click)="withSort && sortingService?.toggleSort(column)" [class.pointer]="withSort" [ngClass]="class">
<mat-icon *ngIf="!!leftIcon" [svgIcon]="leftIcon"></mat-icon>
<span class="all-caps-label">{{ label }}</span>
<mat-icon *ngIf="!!rightIcon" [matTooltip]="rightIconTooltip" [svgIcon]="rightIcon" matTooltipPosition="above"></mat-icon>
<div *ngIf="withSort" [class.force-display]="activeSortingOption.column === column" class="sort-arrows-container">
<mat-icon *ngIf="activeSortingOption?.order === 'asc'" svgIcon="red:sort-asc"></mat-icon>
<mat-icon *ngIf="activeSortingOption?.order === 'desc'" svgIcon="red:sort-desc"></mat-icon>
<div
*ngIf="withSort && sortingService"
[class.force-display]="sortingService.sortingOption?.column === column"
class="sort-arrows-container"
>
<mat-icon *ngIf="sortingService.sortingOption?.order === 'asc'" svgIcon="red:sort-asc"></mat-icon>
<mat-icon *ngIf="sortingService.sortingOption?.order === 'desc'" svgIcon="red:sort-desc"></mat-icon>
</div>
</div>

View File

@ -1,5 +1,15 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { SortingOption } from '@services/sorting.service';
import { Component, Input, Optional } from '@angular/core';
import { SortingService } from '@services/sorting.service';
export interface TableColConfig {
readonly column?: string;
readonly label: string;
readonly withSort?: boolean;
readonly class?: string;
readonly leftIcon?: string;
readonly rightIcon?: string;
readonly rightIconTooltip?: string;
}
@Component({
selector: 'redaction-table-col-name',
@ -7,7 +17,6 @@ import { SortingOption } from '@services/sorting.service';
styleUrls: ['./table-col-name.component.scss']
})
export class TableColNameComponent {
@Input() activeSortingOption: SortingOption;
@Input() column: string;
@Input() label: string;
@Input() withSort = false;
@ -16,5 +25,5 @@ export class TableColNameComponent {
@Input() rightIcon: string;
@Input() rightIconTooltip: string;
@Output() toggleSort = new EventEmitter<string>();
constructor(@Optional() readonly sortingService: SortingService) {}
}

View File

@ -0,0 +1,22 @@
<div class="header-item">
<span class="all-caps-label">
{{ tableHeaderLabel | translate: { length: (screenStateService.displayedLength$ | async) } }}
</span>
<redaction-quick-filters></redaction-quick-filters>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name
*ngFor="let config of tableColConfigs"
[withSort]="config.withSort"
[column]="config.column"
[label]="config.label"
[class]="config.class"
[leftIcon]="config.leftIcon"
[rightIcon]="config.rightIcon"
[rightIconTooltip]="config.rightIconTooltip"
></redaction-table-col-name>
<div class="scrollbar-placeholder"></div>
</div>

View File

@ -0,0 +1,16 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { TableColConfig } from '@shared/components/table-col-name/table-col-name.component';
import { ScreenStateService } from '@shared/services/screen-state.service';
@Component({
selector: 'redaction-table-header',
templateUrl: './table-header.component.html',
styleUrls: ['./table-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableHeaderComponent<T> {
@Input() tableHeaderLabel: string;
@Input() tableColConfigs: TableColConfig[];
constructor(readonly screenStateService: ScreenStateService<T>) {}
}

View File

@ -25,6 +25,7 @@ export class SyncWidthDirective implements AfterViewInit, OnDestroy {
@debounce(10)
matchWidth() {
const headerItems = this._elementRef.nativeElement.children;
// const tableRows = document.getElementsByClassName(this.redactionSyncWidth);
const tableRows = this._elementRef.nativeElement.parentElement.getElementsByClassName(this.redactionSyncWidth);
if (!tableRows || !tableRows.length) {

View File

@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
import { SortingService } from '../../../services/sorting.service';
@Pipe({ name: 'sortBy' })
export class SortByPipe implements PipeTransform {
constructor(private readonly _sortingService: SortingService) {}
transform<T>(value: T[], order = '', column: string = ''): T[] {
return this._sortingService.sort(value, order, column);
}
}

View File

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { NotificationService, NotificationType } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Toaster } from '../../../services/toaster.service';
import { DictionaryControllerService } from '@redaction/red-ui-http';
import { tap } from 'rxjs/operators';
@ -11,11 +10,7 @@ const MIN_WORD_LENGTH = 2;
providedIn: 'root'
})
export class DictionarySaveService {
constructor(
private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService,
private readonly _dictionaryControllerService: DictionaryControllerService
) {}
constructor(private readonly _toaster: Toaster, private readonly _dictionaryControllerService: DictionaryControllerService) {}
saveEntries(
entries: string[],
@ -44,29 +39,13 @@ export class DictionarySaveService {
return obs.pipe(
tap(
() => {
if (showToast) {
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.success.generic'),
null,
NotificationType.SUCCESS
);
}
if (showToast) this._toaster.success('dictionary-overview.success.generic');
},
() => {
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.error.generic'),
null,
NotificationType.ERROR
);
}
() => this._toaster.error('dictionary-overview.error.generic')
)
);
} else {
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.error.entries-too-short'),
null,
NotificationType.ERROR
);
this._toaster.error('dictionary-overview.error.entries-too-short');
return throwError('Entries too short');
}

View File

@ -1,86 +1,59 @@
import { ChangeDetectorRef, Injectable } from '@angular/core';
import { 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 { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
@Injectable()
export class FilterService<T> {
_allFilters$ = new BehaviorSubject<FilterWrapper[]>([]);
export class FilterService {
private readonly _filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
private readonly _refresh$ = new BehaviorSubject(null);
constructor(
private readonly _screenStateService: ScreenStateService<T>,
private readonly _searchService: SearchService<T>,
private readonly _changeDetector: ChangeDetectorRef
) {}
readonly filterGroups$ = this._refresh$.pipe(switchMap(() => this._filterGroups$.asObservable()));
readonly showResetFilters$ = this._showResetFilters$;
get filters() {
return Object.values(this._allFilters$.getValue());
get filterGroups(): FilterGroup[] {
return Object.values(this._filterGroups$.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));
refresh(): void {
this._refresh$.next(null);
}
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];
const filters = this.filterGroups.find(f => f.slug === slug).values;
let found = filters.find(f => f.key === key);
if (!found) found = filters.map(f => f.filters?.find(ff => ff.key === key))[0];
found.checked = !found.checked;
this._allFilters$.next(this.filters);
this.filterEntities();
this.refresh();
}
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]);
addFilterGroup(value: FilterGroup): void {
const oldFilters = this.getFilterGroup(value.slug)?.values;
if (!oldFilters) return this._filterGroups$.next([...this.filterGroups, value]);
value.values = processFilters(oldFilters, value.values);
this._allFilters$.next([...this.filters.filter(f => f.slug !== value.slug), value]);
this._filterGroups$.next([...this.filterGroups.filter(f => f.slug !== value.slug), value]);
}
getFilter(slug: string): FilterWrapper {
return this.filters.find(f => f?.slug === slug);
getFilterGroup(slug: string): FilterGroup {
return this.filterGroups.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)
);
getFilterModels$(filterGroupSlug: string): Observable<FilterModel[]> {
return this.getFilterGroup$(filterGroupSlug).pipe(map(f => f?.values));
}
getFilterWrapper$(slug: string): Observable<FilterWrapper> {
return this.allFilters$.pipe(map(all => all.find(f => f?.slug === slug)));
}
get allFilters$(): Observable<FilterWrapper[]> {
return this._allFilters$.asObservable();
getFilterGroup$(slug: string): Observable<FilterGroup> {
return this.filterGroups$.pipe(map(all => all.find(f => f.slug === slug)));
}
reset(): void {
this.filters.forEach(item => {
this.filterGroups.forEach(item => {
item.values.forEach(child => {
child.checked = false;
child.indeterminate = false;
@ -90,11 +63,19 @@ export class FilterService<T> {
});
});
});
this._allFilters$.next(this.filters);
this.filterEntities();
this.refresh();
}
private _toFlatFilters(entities: FilterWrapper[]): FilterModel[] {
private get _showResetFilters$(): Observable<boolean> {
return this.filterGroups$.pipe(
map(all => this._toFlatFilters(all)),
map(f => !!f.find(el => el.checked)),
distinctUntilChanged()
);
}
private _toFlatFilters(entities: FilterGroup[]): FilterModel[] {
const flatChildren = (filters: FilterModel[]) => (filters ?? []).reduce((acc, f) => [...acc, ...(f?.filters ?? [])], []);
return entities.reduce((acc, f) => [...acc, ...f.values, ...flatChildren(f.values)], []);

View File

@ -1,107 +1,152 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { FilterService } from '@shared/services/filter.service';
import { SearchService } from '@shared/services/search.service';
import { getFilteredEntities } from '@shared/components/filters/popup-filter/utils/filter-utils';
const toLengthValue = entities => entities?.length ?? 0;
@Injectable()
export class ScreenStateService<T> {
entities$ = new BehaviorSubject<T[]>([]);
filteredEntities$ = new BehaviorSubject<T[]>([]);
displayedEntities$ = new BehaviorSubject<T[]>([]);
selectedEntitiesIds$ = new BehaviorSubject<string[]>([]);
private readonly _allEntities$ = new BehaviorSubject<T[]>([]);
readonly allEntities$ = this._allEntities$.asObservable();
readonly allEntitiesLength$ = this._allEntitiesLength$;
private _idKey: string;
private readonly _displayedEntities$ = new BehaviorSubject<T[]>([]);
readonly displayedEntities$ = this._getDisplayedEntities$;
readonly displayedLength$ = this._displayedLength$;
get entities(): T[] {
return Object.values(this.entities$.getValue());
private readonly _selectedEntities$ = new BehaviorSubject<T[]>([]);
readonly selectedEntities$ = this._selectedEntities$.asObservable();
readonly selectedLength$ = this._selectedLength$;
readonly noData$ = this._noData$;
readonly areAllEntitiesSelected$ = this._areAllEntitiesSelected$;
readonly areSomeEntitiesSelected$ = this._areSomeEntitiesSelected$;
readonly notAllEntitiesSelected$ = this._notAllEntitiesSelected$;
constructor(private readonly _filterService: FilterService, private readonly _searchService: SearchService<T>) {
// setInterval(() => {
// console.log('All entities subs: ', this._allEntities$.observers);
// console.log('Displayed entities subs: ', this._displayedEntities$.observers);
// console.log('Selected entities subs: ', this._selectedEntities$.observers);
// }, 10000);
}
get filteredEntities(): T[] {
return Object.values(this.filteredEntities$.getValue());
get allEntities(): T[] {
return Object.values(this._allEntities$.getValue());
}
get selectedEntitiesIds(): string[] {
return Object.values(this.selectedEntitiesIds$.getValue());
get selectedEntities(): T[] {
return Object.values(this._selectedEntities$.getValue());
}
get displayedEntities(): T[] {
return Object.values(this.displayedEntities$.getValue());
return Object.values(this._displayedEntities$.getValue());
}
map<K>(func: (state: T[]) => K): Observable<K> {
return this.entities$.asObservable().pipe(
map((state: T[]) => func(state)),
setEntities(newEntities: Partial<T[]>): void {
this._allEntities$.next(newEntities);
}
setSelectedEntities(newEntities: Partial<T[]>): void {
this._selectedEntities$.next(newEntities);
}
isSelected(entity: T): boolean {
return this.selectedEntities.indexOf(entity) !== -1;
}
selectEntities(entities?: T[]): void {
if (entities !== undefined && entities !== null && entities.length > 0) {
return entities.forEach(entity => this._selectOne(entity));
}
return this._selectAll();
}
updateSelection(): void {
const items = this.displayedEntities.filter(item => this.selectedEntities.includes(item));
this.setSelectedEntities(items);
}
logCurrentState(): void {
console.log('Entities', this.allEntities);
console.log('Displayed', this.displayedEntities);
console.log('Selected', this.selectedEntities);
}
get _getDisplayedEntities$(): Observable<T[]> {
const filterGroups$ = this._filterService.filterGroups$;
const searchValue$ = this._searchService.valueChanges$;
return combineLatest([this.allEntities$, filterGroups$, searchValue$]).pipe(
map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)),
map(entities => this._searchService.searchIn(entities)),
tap(entities => this._displayedEntities$.next(entities))
);
}
private get _allEntitiesLength$(): Observable<number> {
return this.allEntities$.pipe(map(toLengthValue), distinctUntilChanged());
}
private get _displayedLength$(): Observable<number> {
return this.displayedEntities$.pipe(map(toLengthValue), distinctUntilChanged());
}
private get _selectedLength$(): Observable<number> {
return this.selectedEntities$.pipe(map(toLengthValue), distinctUntilChanged());
}
private get _areAllEntitiesSelected$(): Observable<boolean> {
return combineLatest([this.displayedLength$, this.selectedLength$]).pipe(
map(([displayedLength, selectedLength]) => displayedLength && displayedLength === selectedLength),
distinctUntilChanged()
);
}
setEntities(newEntities: Partial<T[]>): void {
this.entities$.next(newEntities);
/**
* Indicates that some entities are selected. If all are selected this returns true
*/
private get _areSomeEntitiesSelected$(): Observable<boolean> {
return this.selectedLength$.pipe(
map(value => !!value),
distinctUntilChanged()
);
}
setFilteredEntities(newEntities: Partial<T[]>): void {
this.filteredEntities$.next(newEntities);
/**
* Indicates that some entities are selected, but not all
*/
private get _notAllEntitiesSelected$(): Observable<boolean> {
return combineLatest([this.areAllEntitiesSelected$, this.areSomeEntitiesSelected$]).pipe(
map(([allEntitiesAreSelected, someEntitiesAreSelected]) => !allEntitiesAreSelected && someEntitiesAreSelected),
distinctUntilChanged()
);
}
setSelectedEntitiesIds(newEntities: Partial<string[]>): void {
this.selectedEntitiesIds$.next(newEntities);
private get _noData$(): Observable<boolean> {
return this.allEntitiesLength$.pipe(
map(length => length === 0),
distinctUntilChanged()
);
}
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]);
private _selectOne(entity: T): void {
const currentEntityIdx = this.selectedEntities.indexOf(entity);
if (currentEntityIdx === -1) {
const currentEntityId = entity[this._getIdKey];
return this.setSelectedEntitiesIds([...this.selectedEntitiesIds, currentEntityId]);
return this.setSelectedEntities([...this.selectedEntities, entity]);
}
this.setSelectedEntitiesIds(this.selectedEntitiesIds.filter((el, idx) => idx !== currentEntityIdx));
this.setSelectedEntities(this.selectedEntities.filter((el, idx) => idx !== currentEntityIdx));
}
toggleSelectAll(): void {
if (this.areAllEntitiesSelected) return this.setSelectedEntitiesIds([]);
this.setSelectedEntitiesIds(this._displayedEntitiesIds);
private _selectAll(): void {
if (this._allEntitiesSelected) return this.setSelectedEntities([]);
this.setSelectedEntities(this.displayedEntities);
}
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;
private get _allEntitiesSelected() {
return this.displayedEntities.length !== 0 && this.displayedEntities.length === this.selectedEntities.length;
}
}

View File

@ -1,48 +1,30 @@
import { Injectable } from '@angular/core';
import { debounce } from '@utils/debounce';
import { ScreenStateService } from '@shared/services/screen-state.service';
import { FormBuilder } from '@angular/forms';
import { startWith } from 'rxjs/operators';
@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());
}
readonly valueChanges$ = this.searchForm.get('query').valueChanges.pipe(startWith(''));
@debounce(200)
executeSearch(): void {
this._searchValue = this.searchValue.toLowerCase();
this.executeSearchImmediately();
}
constructor(private readonly _formBuilder: FormBuilder) {}
executeSearchImmediately(): void {
const displayed = this._screenStateService.filteredEntities || this._screenStateService.entities;
searchIn(entities: T[]) {
if (!this._searchKey) return 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();
const searchValue = this.searchValue.toLowerCase();
return entities.filter(entity => this._searchField(entity).includes(searchValue));
}
setSearchKey(value: string): void {
this._searchKey = value;
}
get isSearchNeeded(): boolean {
return !!this._searchKey;
}
get searchValue(): string {
return this.searchForm.get('query').value;
}
@ -51,7 +33,7 @@ export class SearchService<T> {
this.searchForm.reset({ query: '' });
}
protected _searchField(entity: T): string {
return entity[this._searchKey];
private _searchField(entity: T): string {
return entity[this._searchKey].toLowerCase();
}
}

View File

@ -24,7 +24,7 @@ import { DictionaryAnnotationIconComponent } from './components/dictionary-annot
import { HiddenActionComponent } from './components/hidden-action/hidden-action.component';
import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component';
import { EmptyStateComponent } from './components/empty-state/empty-state.component';
import { SortByPipe } from './components/sort-pipe/sort-by.pipe';
import { SortByPipe } from './pipes/sort-by.pipe';
import { RoundCheckboxComponent } from './components/checkbox/round-checkbox.component';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
@ -39,6 +39,7 @@ import { AssignUserDropdownComponent } from './components/assign-user-dropdown/a
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';
import { TableHeaderComponent } from './components/table-header/table-header.component';
const buttons = [ChevronButtonComponent, CircleButtonComponent, FileDownloadBtnComponent, IconButtonComponent, UserButtonComponent];
@ -73,9 +74,9 @@ const utils = [HumanizePipe, DatePipe, SyncWidthDirective, HasScrollbarDirective
const modules = [MatConfigModule, TranslateModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule];
@NgModule({
declarations: [...components, ...utils],
declarations: [...components, ...utils, TableHeaderComponent],
imports: [CommonModule, ...modules, MonacoEditorModule],
exports: [...modules, ...components, ...utils],
exports: [...modules, ...components, ...utils, TableHeaderComponent],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{

View File

@ -8,11 +8,11 @@ import { HttpErrorResponse } from '@angular/common/http';
export class ErrorMessageService {
constructor(private readonly _translateService: TranslateService) {}
_parseErrorResponse(err: HttpErrorResponse) {
_parseErrorResponse(err: HttpErrorResponse): string {
return err?.error?.message?.includes('message') ? ` ${err.error.message.match('"message":"(.*?)\\"')[1]}` : '';
}
getMessage(err: HttpErrorResponse, defaultMessage: string) {
return this._translateService.instant(defaultMessage) + this._parseErrorResponse(err);
getMessage(error: HttpErrorResponse, defaultMessage: string): string {
return this._translateService.instant(defaultMessage) + this._parseErrorResponse(error);
}
}

View File

@ -1,5 +1,5 @@
import { EventEmitter, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
const MIN_LOADING_TIME = 300;
@ -7,16 +7,15 @@ const MIN_LOADING_TIME = 300;
providedIn: 'root'
})
export class LoadingService {
private readonly _loadingEvent = new EventEmitter();
private readonly _loadingEvent = new BehaviorSubject(false);
private _loadingStarted: number;
get isLoading(): Observable<boolean> {
return this._loadingEvent;
return this._loadingEvent.asObservable();
}
start(): void {
// setTimeout is used so that value doesn't change after it was checked for changes
setTimeout(() => this._loadingEvent.next(true));
this._loadingEvent.next(true);
this._loadingStarted = new Date().getTime();
}
@ -37,7 +36,7 @@ export class LoadingService {
}
private _stop() {
this._loadingEvent.next(false);
setTimeout(() => this._loadingEvent.next(false));
}
private _stopAfter(timeout: number) {

View File

@ -1,47 +0,0 @@
import { Injectable } from '@angular/core';
import { ActiveToast, ToastrService } from 'ngx-toastr';
import { IndividualConfig } from 'ngx-toastr/toastr/toastr-config';
import { NavigationStart, Router } from '@angular/router';
export enum NotificationType {
SUCCESS = 'SUCCESS',
WARNING = 'WARNING',
ERROR = 'ERROR',
INFO = 'INFO'
}
export class ToastAction {
title: string;
action?: () => any;
}
@Injectable({
providedIn: 'root'
})
export class NotificationService {
constructor(private readonly _toastr: ToastrService, private readonly _router: Router) {
_router.events.subscribe(event => {
if (event instanceof NavigationStart) {
this._toastr.clear();
}
});
}
showToastNotification(
message: string,
title?: string,
notificationType = NotificationType.INFO,
options?: Partial<IndividualConfig> & { actions?: ToastAction[] }
): ActiveToast<any> {
switch (notificationType) {
case NotificationType.ERROR:
return this._toastr.error(message, title, options);
case NotificationType.SUCCESS:
return this._toastr.success(message, title, options);
case NotificationType.WARNING:
return this._toastr.warning(message, title, options);
case NotificationType.INFO:
return this._toastr.info(message, title, options);
}
}
}

View File

@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { orderBy } from 'lodash';
export type SortingOrder = 'asc' | 'desc';
export enum SortingOrders {
export const enum SortingOrders {
ASC = 'asc',
DESC = 'desc'
}
@ -12,59 +13,52 @@ export interface SortingOption {
column: string;
}
export type ScreenName =
| 'dossier-listing'
| 'dossier-overview'
| 'dictionary-listing'
| 'dossier-templates-listing'
| 'default-colors'
| 'file-attributes-listing'
| 'dossier-attributes-listing';
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 _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' }
};
private _sortingOption: SortingOption;
setScreenName(value: string) {
this._currentScreenName = value;
setSortingOption(value: SortingOption): void {
this._sortingOption = value;
}
sort<T>(values: T[], order = '', column: string = ''): T[] {
if (!values || order === '' || !order) {
return values;
} // no array
if (!column || column === '') {
if (order === SortingOrders.ASC) {
return values.sort();
} else {
return values.sort().reverse();
}
} // sort 1d array
if (values.length <= 1) {
return values;
} // array with only one item
return orderBy(values, [column], [order]);
}
defaultSort<T>(values: T[]) {
return this.sort(values, this.sortingOption?.order, this.sortingOption?.column);
}
toggleSort(column: string) {
if (this._options[this._currentScreenName].column === column) {
if (this._sortingOption.column === column) {
this._currentOrder = this._currentOrder === SortingOrders.ASC ? SortingOrders.DESC : SortingOrders.ASC;
} else {
this._options[this._currentScreenName] = { column, order: SortingOrders.ASC };
this._sortingOption = { column, order: SortingOrders.ASC };
}
}
getSortingOption() {
return this._options[this._currentScreenName];
get sortingOption(): SortingOption {
return this._sortingOption;
}
private get _currentOrder(): string {
return this._options[this._currentScreenName].order;
private get _currentOrder(): SortingOrder {
return this._sortingOption.order;
}
private set _currentOrder(value: string) {
this._options[this._currentScreenName].order = value;
private set _currentOrder(value: SortingOrder) {
this._sortingOption.order = value;
}
}

View File

@ -0,0 +1,82 @@
import { Injectable } from '@angular/core';
import { ActiveToast, ToastrService } from 'ngx-toastr';
import { IndividualConfig } from 'ngx-toastr/toastr/toastr-config';
import { NavigationStart, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorMessageService } from '@services/error-message.service';
import { filter } from 'rxjs/operators';
const enum NotificationType {
SUCCESS = 'SUCCESS',
WARNING = 'WARNING',
INFO = 'INFO'
}
export interface ToasterOptions extends IndividualConfig {
title?: string;
/**
* These params are used as interpolateParams for translate service
*/
params?: object;
actions?: { title?: string; action: () => void }[];
}
export interface ErrorToasterOptions extends ToasterOptions {
/**
* Pass an http error that will be processed by error message service and shown in toast
*/
error?: HttpErrorResponse;
}
@Injectable({
providedIn: 'root'
})
export class Toaster {
constructor(
private readonly _toastr: ToastrService,
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _errorMessageService: ErrorMessageService
) {
_router.events.pipe(filter(event => event instanceof NavigationStart)).subscribe(() => {
_toastr.clear();
});
}
error(message: string, options?: Partial<ErrorToasterOptions>) {
if (options?.error) message = this._errorMessageService.getMessage(options.error, message);
else message = this._translateService.instant(message, options?.params);
return this._toastr.error(message, options?.title, options);
}
info(message: string, options?: Partial<ToasterOptions>) {
return this._showToastNotification(message, NotificationType.INFO, options);
}
success(message: string, options?: Partial<ToasterOptions>) {
return this._showToastNotification(message, NotificationType.SUCCESS, options);
}
warning(message: string, options?: Partial<ToasterOptions>) {
return this._showToastNotification(message, NotificationType.WARNING, options);
}
private _showToastNotification(
message: string,
notificationType = NotificationType.INFO,
options?: Partial<ToasterOptions>
): ActiveToast<unknown> {
message = this._translateService.instant(message, options?.params);
switch (notificationType) {
case NotificationType.SUCCESS:
return this._toastr.success(message, options?.title, options);
case NotificationType.WARNING:
return this._toastr.warning(message, options?.title, options);
case NotificationType.INFO:
return this._toastr.info(message, options?.title, options);
}
}
}

View File

@ -1,15 +1,36 @@
import { Injectable } from '@angular/core';
import { UserPreferenceControllerService } from '@redaction/red-ui-http';
interface UserAttributes {
[p: string]: string[];
}
@Injectable({
providedIn: 'root'
})
export class UserPreferenceService {
private _userAttributes: UserAttributes = {};
constructor(private readonly _userPreferenceControllerService: UserPreferenceControllerService) {
_userPreferenceControllerService.getAllUserAttributes().subscribe(attributes => {
this._userAttributes = attributes ?? {};
});
}
get userAttributes(): UserAttributes {
return this._userAttributes;
}
get areDevFeaturesEnabled() {
const value = sessionStorage.getItem('redaction.enable-dev-features');
if (value) {
return value === 'true';
return value ? value === 'true' : false;
}
getLastOpenedFileId(key: string): string {
if (this.userAttributes[key]?.length > 0) {
return this.userAttributes[key][0];
}
return false;
return '';
}
toggleDevFeatures() {

View File

@ -16,7 +16,7 @@ export interface ProfileModel {
export class UserWrapper {
name: string;
constructor(private _currentUser: KeycloakProfile, public roles: string[], public id: string) {
constructor(private readonly _currentUser: KeycloakProfile, public roles: string[], public id: string) {
this.name = this.firstName && this.lastName ? `${this.firstName} ${this.lastName}` : this.username;
}
@ -150,8 +150,8 @@ export class UserService {
return this.getName(this.getUserById(userId));
}
getName(user?: User) {
return user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : user?.username;
getName({ firstName, lastName, username }: User = {}) {
return firstName && lastName ? `${firstName} ${lastName}` : username;
}
isManager(user: User | UserWrapper = this.user): boolean {

View File

@ -2,7 +2,6 @@ import { EventEmitter, Injectable } from '@angular/core';
import {
DictionaryControllerService,
Dossier,
DossierControllerService,
DossierTemplateControllerService,
FileAttributesConfig,
FileAttributesControllerService,
@ -10,7 +9,7 @@ import {
ReanalysisControllerService,
StatusControllerService
} from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '@services/notification.service';
import { Toaster } from '../services/toaster.service';
import { TranslateService } from '@ngx-translate/core';
import { Event, NavigationEnd, ResolveStart, Router } from '@angular/router';
import { UserService } from '@services/user.service';
@ -21,6 +20,7 @@ import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { DossierWrapper } from './model/dossier.wrapper';
import { TypeValueWrapper } from '@models/file/type-value.wrapper';
import { DossierTemplateModelWrapper } from '@models/file/dossier-template-model.wrapper';
import { DossiersService } from '../modules/dossier/services/dossiers.service';
export interface AppState {
dossiers: DossierWrapper[];
@ -46,8 +46,8 @@ export class AppStateService {
constructor(
private readonly _router: Router,
private readonly _userService: UserService,
private readonly _dossierControllerService: DossierControllerService,
private readonly _notificationService: NotificationService,
private readonly _dossiersService: DossiersService,
private readonly _toaster: Toaster,
private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _translateService: TranslateService,
private readonly _dictionaryControllerService: DictionaryControllerService,
@ -65,15 +65,15 @@ export class AppStateService {
activeDictionaryType: null
};
this._router.events.subscribe((event: Event) => {
_router.events.subscribe((event: Event) => {
if (AppStateService._isFileOverviewRoute(event)) {
const url = (event as ResolveStart).url.replace('/main/dossiers/', '');
const [dossierId, , fileId] = url.split(/[/?]/);
this.activateFile(dossierId, fileId);
return this.activateFile(dossierId, fileId);
}
if (AppStateService._isDossierOverviewRoute(event)) {
const dossierId = (event as ResolveStart).url.replace('/main/dossiers/', '');
this.activateDossier(dossierId);
return this.activateDossier(dossierId);
}
if (AppStateService._isRandomRoute(event)) {
this._appState.activeDossierId = null;
@ -88,11 +88,7 @@ export class AppStateService {
}
get aggregatedFiles(): FileStatusWrapper[] {
const result: FileStatusWrapper[] = [];
this._appState.dossiers.forEach(p => {
result.push(...p.files);
});
return result;
return this.allDossiers.reduce((acc, { files }) => [...acc, ...files], []);
}
get activeDossierTemplateId(): string {
@ -128,7 +124,7 @@ export class AppStateService {
}
get activeDossier(): DossierWrapper | undefined {
return this._appState.dossiers.find(p => p.dossierId === this.activeDossierId);
return this.allDossiers.find(p => p.dossierId === this.activeDossierId);
}
get allDossiers(): DossierWrapper[] {
@ -147,18 +143,14 @@ export class AppStateService {
return this._appState.activeFileId;
}
get totalAnalysedPages() {
get totalAnalysedPages(): number {
return this._appState.totalAnalysedPages;
}
get totalPeople() {
get totalPeople(): number {
return this._appState.totalPeople;
}
get totalDocuments() {
return this._appState.totalDocuments;
}
private static _isFileOverviewRoute(event: Event) {
return event instanceof ResolveStart && event.url.includes('/main/dossiers/') && event.url.includes('/file/');
}
@ -177,18 +169,15 @@ export class AppStateService {
}
}
getDictionaryColor(type?: string, dossierTemplateId?: string) {
if (!dossierTemplateId && this.activeDossier) {
dossierTemplateId = this.activeDossier.dossierTemplateId;
}
getDictionaryColor(type?: string, dossierTemplateId = this.activeDossier?.dossierTemplateId) {
if (!dossierTemplateId) {
dossierTemplateId = this.dossierTemplates.length > 0 ? this.dossierTemplates[0].dossierTemplateId : undefined;
dossierTemplateId = this.dossierTemplates[0]?.dossierTemplateId;
}
if (!dossierTemplateId) {
return undefined;
}
const color = this._dictionaryData[dossierTemplateId][type]?.hexColor;
return color ? color : this._dictionaryData[dossierTemplateId]['default'].hexColor;
return color ?? this._dictionaryData[dossierTemplateId]['default'].hexColor;
}
getDossierTemplateById(id: string): DossierTemplateModelWrapper {
@ -220,20 +209,21 @@ export class AppStateService {
}
async loadAllDossiers(emitEvents: boolean = true) {
const dossiers = await this._dossierControllerService.getDossiers().toPromise();
if (dossiers) {
const mappedDossiers = dossiers.map(p => new DossierWrapper(p, this._getExistingFiles(p.dossierId)));
const fileData = await this._statusControllerService.getFileStatusForDossiers(mappedDossiers.map(p => p.dossierId)).toPromise();
for (const dossierId of Object.keys(fileData)) {
const dossier = mappedDossiers.find(p => p.dossierId === dossierId);
this._processFiles(dossier, fileData[dossierId], emitEvents);
}
this._appState.dossiers = mappedDossiers;
this._computeStats();
const dossiers = await this._dossiersService.getAll();
if (!dossiers) {
return;
}
const mappedDossiers = dossiers.map(p => new DossierWrapper(p, this._getExistingFiles(p.dossierId)));
const fileData = await this._statusControllerService.getFileStatusForDossiers(mappedDossiers.map(p => p.dossierId)).toPromise();
for (const dossierId of Object.keys(fileData)) {
const dossier = mappedDossiers.find(p => p.dossierId === dossierId);
this._processFiles(dossier, fileData[dossierId], emitEvents);
}
this._appState.dossiers = mappedDossiers;
this._computeStats();
}
async reloadActiveFile() {
@ -246,10 +236,10 @@ export class AppStateService {
const activeFileWrapper = new FileStatusWrapper(
activeFile,
this._userService.getNameForId(activeFile.currentReviewer),
this.activeDossier.dossierTemplateId,
this._appState.fileAttributesConfig[this.activeDossier.dossierTemplateId]
this.activeDossierTemplateId,
this._appState.fileAttributesConfig[this.activeDossierTemplateId]
);
this.activeDossier.files = this.activeDossier.files.map(file =>
this.activeDossier.files = this.activeDossier?.files.map(file =>
file.fileId === activeFileWrapper.fileId ? activeFileWrapper : file
);
@ -261,32 +251,26 @@ export class AppStateService {
return activeFileWrapper;
}
async getFiles(dossier?: DossierWrapper, emitEvents: boolean = true) {
if (!dossier) {
dossier = this.activeDossier;
}
async getFiles(dossier: DossierWrapper = this.activeDossier, emitEvents = true) {
const files = await this._statusControllerService.getDossierStatus(dossier.dossierId).toPromise();
return this._processFiles(dossier, files, emitEvents);
}
async reanalyzeDossier(dossier?: DossierWrapper) {
if (!dossier) {
dossier = this.activeDossier;
}
await this._reanalysisControllerService.reanalyzeDossier(dossier.dossierId).toPromise();
async reanalyzeDossier({ dossierId }: DossierWrapper = this.activeDossier) {
await this._reanalysisControllerService.reanalyzeDossier(dossierId).toPromise();
}
activateDossier(dossierId: string) {
activateDossier(dossierId: string): void {
this._appState.activeFileId = null;
this._appState.activeDossierId = dossierId;
if (!this.activeDossier) {
this._appState.activeDossierId = null;
this._router.navigate(['/main/dossiers']);
this._router.navigate(['/main/dossiers']).then();
return;
} else {
this.updateDossierDictionary(this.activeDossier.dossierTemplateId, dossierId);
}
this.updateDossierDictionary(this.activeDossier.dossierTemplateId, dossierId);
}
updateDossierDictionary(dossierTemplateId: string, dossierId: string) {
@ -302,7 +286,7 @@ export class AppStateService {
}
activateFile(dossierId: string, fileId: string) {
if (this._appState.activeDossierId === dossierId && this._appState.activeFileId === fileId) return;
if (this.activeDossierId === dossierId && this.activeFileId === fileId) return;
this.activateDossier(dossierId);
if (this.activeDossier) {
this._appState.activeFileId = fileId;
@ -341,29 +325,19 @@ export class AppStateService {
}
deleteDossier(dossier: DossierWrapper) {
return this._dossierControllerService
.deleteDossier(dossier.dossierId)
.toPromise()
.then(
() => {
const index = this._appState.dossiers.findIndex(p => p.dossier.dossierId === dossier.dossierId);
this._appState.dossiers.splice(index, 1);
this._appState.dossiers = [...this._appState.dossiers];
},
() => {
this._notificationService.showToastNotification(
this._translateService.instant('dossiers.delete.delete-failed', dossier),
null,
NotificationType.ERROR
);
}
);
return this._dossiersService.delete(dossier.dossierId).then(
() => {
const index = this.allDossiers.findIndex(p => p.dossierId === dossier.dossierId);
this._appState.dossiers.splice(index, 1);
},
() => this._toaster.error('dossiers.delete.delete-failed', { params: dossier })
);
}
async addOrUpdateDossier(dossier: Dossier) {
async createOrUpdateDossier(dossier: Dossier) {
try {
const updatedDossier = await this._dossierControllerService.createOrUpdateDossier(dossier).toPromise();
let foundDossier = this._appState.dossiers.find(p => p.dossier.dossierId === updatedDossier.dossierId);
const updatedDossier = await this._dossiersService.createOrUpdate(dossier);
let foundDossier = this.allDossiers.find(p => p.dossierId === updatedDossier.dossierId);
if (foundDossier) {
Object.assign((foundDossier.dossier = updatedDossier));
} else {
@ -373,19 +347,15 @@ export class AppStateService {
this._appState.dossiers = [...this._appState.dossiers];
return foundDossier;
} catch (error) {
this._notificationService.showToastNotification(
this._translateService.instant(
error.status === 409 ? 'add-dossier-dialog.errors.dossier-already-exists' : 'add-dossier-dialog.errors.generic'
),
null,
NotificationType.ERROR
this._toaster.error(
error.status === 409 ? 'add-dossier-dialog.errors.dossier-already-exists' : 'add-dossier-dialog.errors.generic'
);
}
}
async reloadActiveDossierFiles() {
if (this.activeDossierId) {
await this.getFiles(this.activeDossier);
await this.getFiles();
}
}
@ -408,7 +378,7 @@ export class AppStateService {
}
async loadAllDossiersIfNecessary() {
if (!this._appState.dossiers.length) {
if (!this.allDossiers.length) {
await this.loadAllDossiers();
}
}
@ -642,7 +612,7 @@ export class AppStateService {
return [typeObs, colorsObs];
}
async loadDictionaryData() {
async loadDictionaryData(): Promise<void> {
const obj = {};
const observables = [];
@ -660,9 +630,9 @@ export class AppStateService {
this._dictionaryData = obj;
}
private _getExistingFiles(dossierId: string) {
const found = this._appState.dossiers.find(p => p.dossier.dossierId === dossierId);
return found ? found.files : [];
private _getExistingFiles(dossierId: string): FileStatusWrapper[] {
const dossier = this.allDossiers.find(p => p.dossierId === dossierId);
return dossier?.files ?? [];
}
private _processFiles(dossier: DossierWrapper, files: FileStatus[], emitEvents: boolean = true) {
@ -727,25 +697,18 @@ export class AppStateService {
let totalAnalysedPages = 0;
let totalDocuments = 0;
const totalPeople = new Set<string>();
this._appState.dossiers.forEach(p => {
totalDocuments += p.files.length;
if (p.dossier.memberIds) {
p.dossier.memberIds.forEach(m => totalPeople.add(m));
this.allDossiers.forEach(d => {
totalDocuments += d.files.length;
if (d.dossier.memberIds) {
d.dossier.memberIds.forEach(m => totalPeople.add(m));
}
let numberOfPages = 0;
p.files.forEach(f => {
numberOfPages += f.numberOfPages;
});
p.totalNumberOfPages = numberOfPages;
totalAnalysedPages += numberOfPages;
d.totalNumberOfPages = d.files.reduce((acc, file) => acc + file.numberOfPages, 0);
totalAnalysedPages += d.totalNumberOfPages;
});
this._appState.totalPeople = totalPeople.size;
this._appState.totalAnalysedPages = totalAnalysedPages;
this._appState.totalDocuments = totalDocuments;
if (this.activeDossierId && this.activeFileId) {
this.activateFile(this.activeDossierId, this.activeFileId);
}
}
}

View File

@ -1,3 +1,13 @@
type StatusSorterItem = { key?: string } | string;
const byStatus = (a: StatusSorterItem, b: StatusSorterItem) => {
if (typeof a !== typeof b) return;
const x = typeof a === 'string' ? a : a.key;
const y = typeof b === 'string' ? b : b.key;
return (StatusSorter[x] = StatusSorter[y]);
};
export const StatusSorter = {
ERROR: 0,
UNPROCESSED: 1,
@ -9,5 +19,5 @@ export const StatusSorter = {
UNDER_REVIEW: 15,
UNDER_APPROVAL: 20,
APPROVED: 25,
byKey: (a: { key: string }, b: { key: string }) => StatusSorter[a.key] - StatusSorter[b.key]
byStatus: byStatus
};

View File

@ -1337,6 +1337,36 @@
"title": "No dossiers match your current filters."
}
},
"sorting": {
"alphabetically": "Alphabetically",
"custom": "Custom",
"number-of-analyses": "Number of analyses",
"number-of-pages": "Number of pages",
"oldest": "Oldest",
"recent": "Recent"
},
"submitted": "Submitted",
"suggestion": "Suggestion for redaction",
"top-bar": {
"navigation-items": {
"back": "Back",
"dossiers": "Active Dossier",
"my-account": {
"children": {
"admin": "Settings",
"downloads": "My Downloads",
"language": {
"de": "German",
"en": "English",
"label": "Language"
},
"my-profile": "My Profile",
"trash": "Trash",
"logout": "Logout"
}
}
}
},
"type": "Type",
"upload-status": {
"dialog": {