Merge branch 'master' into RED-2769

This commit is contained in:
Adina Țeudan 2022-01-25 09:48:11 +02:00
commit a1726be3f9
128 changed files with 15694 additions and 3719 deletions

View File

@ -1,6 +1,6 @@
{
"cli": {
"analytics": "4b8eed12-a1e6-4b7a-9ea2-925b27941271"
"analytics": false
},
"version": 1,
"projects": {

View File

@ -1,3 +1,4 @@
<router-outlet></router-outlet>
<iqser-full-page-loading-indicator></iqser-full-page-loading-indicator>
<iqser-connection-status></iqser-connection-status>
<iqser-full-page-error></iqser-full-page-error>

View File

@ -1,30 +1,29 @@
<form (submit)="executeCurrentAction()">
<iqser-input-with-action
(click)="openMenuIfValue()"
(valueChange)="valueChanges$.next($event)"
[placeholder]="placeholder"
></iqser-input-with-action>
<iqser-input-with-action
(action)="executeCurrentAction()"
(click)="openMenuIfValue()"
(valueChange)="valueChanges$.next($event)"
[placeholder]="placeholder"
></iqser-input-with-action>
<mat-menu #menu="matMenu" class="search-menu" xPosition="after">
<ng-template matMenuContent>
<div class="wrapper">
<button
(click)="item.action(valueChanges$.getValue())"
*ngFor="let item of shownActions; let index = index"
[class.highlight]="(currentActionIdx$ | async) === index"
class="spotlight-row pointer"
>
<mat-icon [svgIcon]="item.icon"></mat-icon>
<span>{{ item.text }}</span>
</button>
</div>
</ng-template>
</mat-menu>
<mat-menu #menu="matMenu" class="search-menu" xPosition="after">
<ng-template matMenuContent>
<div class="wrapper">
<button
(click)="item.action(valueChanges$.getValue())"
*ngFor="let item of shownActions; let index = index"
[class.highlight]="(currentActionIdx$ | async) === index"
class="spotlight-row pointer"
>
<mat-icon [svgIcon]="item.icon"></mat-icon>
<span>{{ item.text }}</span>
</button>
</div>
</ng-template>
</mat-menu>
<!-- https://material.angular.io/components/menu/overview#toggling-the-menu-programmatically -->
<!-- To toggle menu programmatically a matMenuTriggerFor directive is needed -->
<div [matMenuTriggerFor]="menu"></div>
<!-- https://material.angular.io/components/menu/overview#toggling-the-menu-programmatically -->
<!-- To toggle menu programmatically a matMenuTriggerFor directive is needed -->
<div [matMenuTriggerFor]="menu"></div>
<!-- A hack to avoid subscribing in component -->
<ng-container *ngIf="showActions$ | async"></ng-container>
</form>
<!-- A hack to avoid subscribing in component -->
<ng-container *ngIf="showActions$ | async"></ng-container>

View File

@ -1,6 +1,6 @@
import { AnnotationWrapper } from './annotation.wrapper';
import { isArray } from 'rxjs/internal-compatibility';
import { User } from '@red/domain';
import { isArray } from 'lodash';
export class AnnotationPermissions {
canUndo = true;

View File

@ -283,11 +283,7 @@ export class AnnotationWrapper {
private static _setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntryWrapper) {
if (redactionLogEntryWrapper.recommendation) {
if (redactionLogEntryWrapper.redacted) {
annotationWrapper.superType = 'recommendation';
} else {
annotationWrapper.superType = 'skipped';
}
annotationWrapper.superType = 'recommendation';
return;
}

View File

@ -1,34 +1,46 @@
import { Dictionary, File, IRedactionLog, IRedactionLogEntry, IViewedPage, User, ViewMode } from '@red/domain';
import { Dictionary, File, IRedactionLog, IRedactionLogEntry, IViewedPage, ViewMode } from '@red/domain';
import { AnnotationWrapper } from './annotation.wrapper';
import { RedactionLogEntryWrapper } from './redaction-log-entry.wrapper';
import * as moment from 'moment';
import { BehaviorSubject } from 'rxjs';
export class FileDataModel {
static readonly DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
hasChangeLog: boolean;
allAnnotations: AnnotationWrapper[];
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
readonly blob$ = new BehaviorSubject<Blob>(undefined);
readonly file$ = new BehaviorSubject<File>(undefined);
constructor(
public file: File,
public fileData: Blob,
private readonly _file: File,
private readonly _blob: Blob,
private _redactionLog: IRedactionLog,
public viewedPages?: IViewedPage[],
private _dictionaryData?: { [p: string]: Dictionary },
private _areDevFeaturesEnabled?: boolean,
) {
this.file$.next(_file);
this.blob$.next(_blob);
this._buildAllAnnotations();
}
get file(): File {
return this.file$.value;
}
set file(file: File) {
this.file$.next(file);
}
get redactionLog(): IRedactionLog {
return this._redactionLog;
}
set redactionLog(redactionLog: IRedactionLog) {
this._redactionLog = redactionLog;
this._buildAllAnnotations();
}
get redactionLog() {
return this._redactionLog;
}
getVisibleAnnotations(viewMode: ViewMode) {
return this.allAnnotations.filter(annotation => {
if (viewMode === 'STANDARD') {
@ -47,7 +59,7 @@ export class FileDataModel {
const previousAnnotations = this.allAnnotations || [];
this.allAnnotations = entries
.map(entry => AnnotationWrapper.fromData(entry))
.filter(ann => ann.manual || !this.file.excludedPages.includes(ann.pageNumber));
.filter(ann => ann.manual || !this._file.excludedPages.includes(ann.pageNumber));
if (!this._areDevFeaturesEnabled) {
this.allAnnotations = this.allAnnotations.filter(annotation => !annotation.isFalsePositive);
@ -121,11 +133,11 @@ export class FileDataModel {
}
private _isChangeLogEntry(redactionLogEntry: IRedactionLogEntry, wrapper: RedactionLogEntryWrapper) {
if (this.file.numberOfAnalyses > 1) {
redactionLogEntry.changes.sort((a, b) => moment(a.dateTime).valueOf() - moment(b.dateTime).valueOf());
if (this._file.numberOfAnalyses > 1) {
const viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1);
viableChanges.sort((a, b) => moment(a.dateTime).valueOf() - moment(b.dateTime).valueOf());
const lastChange =
redactionLogEntry.changes.length >= 1 ? redactionLogEntry.changes[redactionLogEntry.changes.length - 1] : undefined;
const lastChange = viableChanges.length >= 1 ? viableChanges[viableChanges.length - 1] : undefined;
const page = redactionLogEntry.positions?.[0].page;
const viewedPage = this.viewedPages.filter(p => p.page === page).pop();
@ -134,14 +146,14 @@ export class FileDataModel {
if (viewedPage) {
const viewTime = moment(viewedPage.viewedTime).valueOf() - FileDataModel.DELTA_VIEW_TIME;
// these are all unseen changes
const relevantChanges = redactionLogEntry.changes.filter(change => moment(change.dateTime).valueOf() > viewTime);
const relevantChanges = viableChanges.filter(change => moment(change.dateTime).valueOf() > viewTime);
// at least one unseen change
if (relevantChanges.length > 0) {
// at least 1 relevant change
wrapper.changeLogType = relevantChanges[relevantChanges.length - 1].type;
wrapper.isChangeLogEntry = true;
viewedPage.showAsUnseen = moment(viewedPage.viewedTime).valueOf() < moment(lastChange.dateTime).valueOf();
this.hasChangeLog = true;
this.hasChangeLog$.next(true);
} else {
// no relevant changes - hide removed anyway
wrapper.isChangeLogEntry = false;

View File

@ -1,43 +0,0 @@
export const NotificationCategories = {
inAppNotifications: 'inAppNotifications',
emailNotifications: 'emailNotifications',
} as const;
export const NotificationCategoriesValues = Object.values(NotificationCategories);
export const DossierNotificationsTypes = {
dossierOwnerSet: 'DOSSIER_OWNER_SET',
dossierOwnerRemoved: 'DOSSIER_OWNER_REMOVED',
userBecomseDossierMember: 'USER_BECOMES_DOSSIER_MEMBER',
userRemovedAsDossierMember: 'USER_REMOVED_AS_DOSSIER_MEMBER',
userPromotedToApprover: 'USER_PROMOTED_TO_APPROVER',
userDegradedToReviewer: 'USER_DEGRADED_TO_REVIEWER',
dossierOwnerDeleted: 'DOSSIER_OWNER_DELETED',
dossierDeleted: 'DOSSIER_DELETED',
} as const;
export const DossierNotificationsTypesValues = Object.values(DossierNotificationsTypes);
export const DocumentNotificationsTypes = {
assignReviewer: 'ASSIGN_REVIEWER',
assignApprover: 'ASSIGN_APPROVER',
unassignedFromFile: 'UNASSIGNED_FROM_FILE',
// documentUnderReview: 'DOCUMENT_UNDER_REVIEW',
// documentUnderApproval: 'DOCUMENT_UNDER_APPROVAL',
documentApproved: 'DOCUMENT_APPROVED',
} as const;
export const DocumentNotificationsTypesValues = Object.values(DocumentNotificationsTypes);
export const OtherNotificationsTypes = {
downloadReady: 'DOWNLOAD_READY',
} as const;
export const OtherNotificationsTypesValues = Object.values(OtherNotificationsTypes);
export const NotificationGroupsKeys = ['dossier', 'document', 'other'] as const;
export const NotificationGroupsValues = [
DossierNotificationsTypesValues,
DocumentNotificationsTypesValues,
OtherNotificationsTypesValues,
] as const;

View File

@ -7,25 +7,25 @@
}}</mat-slide-toggle>
</div>
<div class="options-content" *ngIf="isCategoryActive(category)">
<div class="radio-container" *ngIf="category === 'emailNotifications'">
<div class="radio-button" *ngFor="let type of emailNotificationScheduleTypes">
<iqser-round-checkbox [active]="getEmailNotificationType() === type" (click)="setEmailNotificationType(type)">
</iqser-round-checkbox>
<span> {{ translations[type.toLocaleLowerCase()] | translate }} </span>
</div>
</div>
<div *ngIf="isCategoryActive(category)" class="options-content">
<!-- <div class="radio-container" *ngIf="category === 'emailNotifications'">-->
<!-- <div class="radio-button" *ngFor="let type of emailNotificationScheduleTypes">-->
<!-- <iqser-round-checkbox [active]="getEmailNotificationType() === type" (click)="setEmailNotificationType(type)">-->
<!-- </iqser-round-checkbox>-->
<!-- <span> {{ translations[type.toLocaleLowerCase()] | translate }} </span>-->
<!-- </div>-->
<!-- </div>-->
<div class="statement" translate="notifications-screen.options-title"></div>
<div class="group" *ngFor="let key of notificationGroupsKeys; let i = index">
<div class="group-title" [translate]="translations[key]"></div>
<div *ngFor="let key of notificationGroupsKeys; let i = index" class="group">
<div [translate]="translations[key]" class="group-title"></div>
<div class="iqser-input-group">
<mat-checkbox
*ngFor="let preference of notificationGroupsValues[i]"
color="primary"
[checked]="isPreferenceChecked(category, preference)"
(change)="addRemovePreference($event.checked, category, preference)"
*ngFor="let preference of notificationGroupsValues[i]"
[checked]="isPreferenceChecked(category, preference)"
color="primary"
>
{{ translations[preference] | translate }}
</mat-checkbox>

View File

@ -4,8 +4,12 @@ import { notificationsTranslations } from '../../../translations/notifications-t
import { NotificationPreferencesService } from '../../../services/notification-preferences.service';
import { LoadingService, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { NotificationCategoriesValues, NotificationGroupsKeys, NotificationGroupsValues } from '../constants';
import { EmailNotificationScheduleTypesValues } from '@red/domain';
import {
EmailNotificationScheduleTypesValues,
NotificationCategoriesValues,
NotificationGroupsKeys,
NotificationGroupsValues,
} from '@red/domain';
@Component({
selector: 'redaction-notifications-screen',
@ -33,16 +37,6 @@ export class NotificationsScreenComponent implements OnInit {
await this._initializeForm();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
inAppNotificationsEnabled: [undefined],
emailNotificationsEnabled: [undefined],
emailNotificationType: [undefined],
emailNotifications: [undefined],
inAppNotifications: [undefined],
});
}
isCategoryActive(category: string) {
return this.formGroup.get(`${category}Enabled`).value;
}
@ -80,6 +74,16 @@ export class NotificationsScreenComponent implements OnInit {
this._loadingService.stop();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
inAppNotificationsEnabled: [undefined],
emailNotificationsEnabled: [undefined],
emailNotificationType: [undefined],
emailNotifications: [undefined],
inAppNotifications: [undefined],
});
}
private async _initializeForm() {
this._loadingService.start();

View File

@ -73,7 +73,7 @@ export class UserProfileScreenComponent implements OnInit {
}
if (this.profileChanged) {
const value = this.form.value as IProfile;
const value = this.form.getRawValue() as IProfile;
delete value.language;
await this._userService

View File

@ -12,7 +12,18 @@ import {
import { curveLinear } from 'd3-shape';
import { scaleBand, scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import { BaseChartComponent, calculateViewDimensions, ColorHelper, LineSeriesComponent, ViewDimensions } from '@swimlane/ngx-charts';
import {
BaseChartComponent,
calculateViewDimensions,
Color,
ColorHelper,
LegendPosition,
LineSeriesComponent,
Orientation,
ScaleType,
ViewDimensions,
} from '@swimlane/ngx-charts';
import { ILineChartSeries } from './models';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
@ -25,7 +36,7 @@ export class ComboChartComponent extends BaseChartComponent {
@Input() curve: any = curveLinear;
@Input() legend = false;
@Input() legendTitle = 'Legend';
@Input() legendPosition = 'right';
@Input() legendPosition: LegendPosition = LegendPosition.Right;
@Input() xAxis;
@Input() yAxis;
@Input() showXAxisLabel;
@ -38,33 +49,33 @@ export class ComboChartComponent extends BaseChartComponent {
@Input() gradient: boolean;
@Input() showGridLines = true;
@Input() activeEntries: any[] = [];
@Input() schemeType: string;
@Input() schemeType: ScaleType;
@Input() xAxisTickFormatting: any;
@Input() yAxisTickFormatting: any;
@Input() yRightAxisTickFormatting: any;
@Input() roundDomains = false;
@Input() colorSchemeLine: any;
@Input() colorSchemeLine: Color;
@Input() autoScale;
@Input() lineChart: any;
@Input() lineChart: ILineChartSeries[];
@Input() yLeftAxisScaleFactor: any;
@Input() yRightAxisScaleFactor: any;
@Input() rangeFillOpacity: number;
@Input() animations = true;
@Input() noBarWhenZero = true;
@Output() activate: EventEmitter<any> = new EventEmitter();
@Output() deactivate: EventEmitter<any> = new EventEmitter();
@Output() activate = new EventEmitter<{ value; entries: unknown[] }>();
@Output() deactivate = new EventEmitter<{ value; entries: unknown[] }>();
@ContentChild('tooltipTemplate') tooltipTemplate: TemplateRef<any>;
@ContentChild('seriesTooltipTemplate') seriesTooltipTemplate: TemplateRef<any>;
@ContentChild('tooltipTemplate') tooltipTemplate: TemplateRef<unknown>;
@ContentChild('seriesTooltipTemplate') seriesTooltipTemplate: TemplateRef<unknown>;
@ViewChild(LineSeriesComponent) lineSeriesComponent: LineSeriesComponent;
dims: ViewDimensions;
xScale: any;
yScale: any;
xDomain: any;
yDomain: any;
xDomain: string[] | number[];
yDomain: string[] | number[];
transform: string;
colors: ColorHelper;
colorsLine: ColorHelper;
@ -72,19 +83,19 @@ export class ComboChartComponent extends BaseChartComponent {
xAxisHeight = 0;
yAxisWidth = 0;
legendOptions: any;
scaleType = 'linear';
scaleType: ScaleType = ScaleType.Linear;
xScaleLine;
yScaleLine;
xDomainLine;
yDomainLine;
seriesDomain;
scaledAxis;
combinedSeries;
combinedSeries: ILineChartSeries[];
xSet;
filteredDomain;
hoveredVertical;
yOrientLeft = 'left';
yOrientRight = 'right';
yOrientLeft: Orientation = Orientation.Left;
yOrientRight: Orientation = Orientation.Right;
legendSpacing = 0;
bandwidth: number;
barPadding = 8;
@ -176,15 +187,11 @@ export class ComboChartComponent extends BaseChartComponent {
return this.combinedSeries.map(d => d.name);
}
isDate(value): boolean {
if (value instanceof Date) {
return true;
}
return false;
isDate(value): value is Date {
return value instanceof Date;
}
getScaleType(values): string {
getScaleType(values): ScaleType {
let date = true;
let num = true;
@ -199,16 +206,16 @@ export class ComboChartComponent extends BaseChartComponent {
}
if (date) {
return 'time';
return ScaleType.Time;
}
if (num) {
return 'linear';
return ScaleType.Linear;
}
return 'ordinal';
return ScaleType.Ordinal;
}
getXDomainLine(): any[] {
let values = [];
let values: number[] = [];
for (const results of this.lineChart) {
for (const d of results.series) {
@ -239,7 +246,7 @@ export class ComboChartComponent extends BaseChartComponent {
}
getYDomainLine(): any[] {
const domain = [];
const domain: number[] = [];
for (const results of this.lineChart) {
for (const d of results.series) {
@ -263,7 +270,7 @@ export class ComboChartComponent extends BaseChartComponent {
const max = Math.max(...domain);
if (this.yRightAxisScaleFactor) {
const minMax = this.yRightAxisScaleFactor(min, max);
return [Math.min(0, minMax.min), minMax.max];
return [Math.min(0, minMax.min as number), minMax.max];
} else {
min = Math.min(0, min);
return [min, max];
@ -317,12 +324,12 @@ export class ComboChartComponent extends BaseChartComponent {
}
getYDomain() {
const values = this.results.map(d => d.value);
const values: number[] = this.results.map(d => d.value);
const min = Math.min(0, ...values);
const max = Math.max(...values);
if (this.yLeftAxisScaleFactor) {
const minMax = this.yLeftAxisScaleFactor(min, max);
return [Math.min(0, minMax.min), minMax.max];
return [Math.min(0, minMax.min as number), minMax.max];
} else {
return [min, max];
}
@ -333,7 +340,7 @@ export class ComboChartComponent extends BaseChartComponent {
}
setColors(): void {
let domain;
let domain: number[] | string[];
if (this.schemeType === 'ordinal') {
domain = this.xDomain;
} else {

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { animate, style, transition, trigger } from '@angular/animations';
import { formatLabel } from '@swimlane/ngx-charts';
import { Bar, BarOrientation, formatLabel, PlacementTypes, StyleTypes } from '@swimlane/ngx-charts';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
@ -17,7 +17,7 @@ import { formatLabel } from '@swimlane/ngx-charts';
[fill]="bar.color"
[stops]="bar.gradientStops"
[data]="bar.data"
[orientation]="'vertical'"
[orientation]="orientations.Vertical"
[roundEdges]="bar.roundEdges"
[gradient]="gradient"
[isActive]="isActive(bar.data)"
@ -27,8 +27,8 @@ import { formatLabel } from '@swimlane/ngx-charts';
(deactivate)="deactivate.emit($event)"
ngx-tooltip
[tooltipDisabled]="tooltipDisabled"
[tooltipPlacement]="0"
[tooltipType]="1"
[tooltipPlacement]="tooltipPlacements.Top"
[tooltipType]="tooltipTypes.tooltip"
[tooltipTitle]="bar.tooltipText"
></svg:g>
`,
@ -67,6 +67,9 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bars: any;
x: any;
y: any;
readonly tooltipTypes = StyleTypes;
readonly tooltipPlacements = PlacementTypes;
readonly orientations = BarOrientation;
ngOnChanges(): void {
this.update();
@ -91,7 +94,7 @@ export class ComboSeriesVerticalComponent implements OnChanges {
const formattedLabel = formatLabel(label);
const roundEdges = this.type === 'standard';
const bar: any = {
const bar: Bar = {
value,
label,
roundEdges,
@ -101,8 +104,15 @@ export class ComboSeriesVerticalComponent implements OnChanges {
height: 0,
x: 0,
y: 0,
ariaLabel: label,
tooltipText: label,
color: undefined,
gradientStops: undefined,
};
let offset0 = d0;
let offset1 = offset0 + value;
if (this.type === 'standard') {
bar.height = Math.abs(this.yScale(value) - this.yScale(0));
bar.x = this.xScale(label);
@ -113,18 +123,14 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bar.y = this.yScale(value);
}
} else if (this.type === 'stacked') {
const offset0 = d0;
const offset1 = offset0 + value;
d0 += value;
bar.height = this.yScale(offset0) - this.yScale(offset1);
bar.x = 0;
bar.y = this.yScale(offset1);
bar.offset0 = offset0;
bar.offset1 = offset1;
// bar.offset0 = offset0;
// bar.offset1 = offset1;
} else if (this.type === 'normalized') {
let offset0 = d0;
let offset1 = offset0 + value;
d0 += value;
if (total > 0) {
@ -138,8 +144,8 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bar.height = this.yScale(offset0) - this.yScale(offset1);
bar.x = 0;
bar.y = this.yScale(offset1);
bar.offset0 = offset0;
bar.offset1 = offset1;
// bar.offset0 = offset0;
// bar.offset1 = offset1;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
value = (offset1 - offset0).toFixed(2) + '%';
@ -152,8 +158,8 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bar.color = this.colors.getColor(value);
bar.gradientStops = this.colors.getLinearGradientStops(value);
} else {
bar.color = this.colors.getColor(bar.offset1);
bar.gradientStops = this.colors.getLinearGradientStops(bar.offset1, bar.offset0);
bar.color = this.colors.getColor(offset1);
bar.gradientStops = this.colors.getLinearGradientStops(offset1, offset0);
}
}

View File

@ -0,0 +1,11 @@
export interface ISeries {
name: number;
value: number;
min: number;
max: number;
}
export interface ILineChartSeries {
name: string;
series: ISeries[];
}

View File

@ -95,11 +95,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="!valid || !changed" color="primary" mat-flat-button type="button">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-dictionary.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Observable } from 'rxjs';
@ -6,7 +6,7 @@ import { BaseDialogComponent, shareDistinctLast, Toaster } from '@iqser/common-u
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { AppStateService } from '@state/app-state.service';
import { toKebabCase } from '@utils/functions';
import { toSnakeCase } from '@utils/functions';
import { DictionaryService } from '@shared/services/dictionary.service';
import { Dictionary, IDictionary } from '@red/domain';
import { UserService } from '@services/user.service';
@ -21,7 +21,6 @@ import { HttpStatusCode } from '@angular/common/http';
})
export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
readonly dictionary = this._data.dictionary;
readonly form: FormGroup = this._getForm(this.dictionary);
readonly canEditLabel$ = this._canEditLabel$;
readonly technicalName$: Observable<string>;
readonly dialogHeader = this._translateService.instant('add-edit-dictionary.title', {
@ -29,7 +28,6 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
name: this._data.dictionary?.label,
});
readonly hasColor$: Observable<boolean>;
readonly disabled = false;
private readonly _dossierTemplateId = this._data.dossierTemplateId;
constructor(
@ -39,37 +37,18 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
private readonly _appStateService: AppStateService,
private readonly _translateService: TranslateService,
private readonly _dictionaryService: DictionaryService,
private readonly _dialogRef: MatDialogRef<AddEditDictionaryDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDictionaryDialogComponent>,
@Inject(MAT_DIALOG_DATA)
private readonly _data: { readonly dictionary: Dictionary; readonly dossierTemplateId: string },
) {
super();
super(_injector, _dialogRef);
this.form = this._getForm(this.dictionary);
this.initialFormValue = this.form.getRawValue();
this.hasColor$ = this._colorEmpty$;
this.technicalName$ = this.form.get('label').valueChanges.pipe(map(value => this._toTechnicalName(value)));
}
get valid(): boolean {
return this.form.valid;
}
get changed(): boolean {
if (!this.dictionary) {
return true;
}
for (const key of Object.keys(this.form.getRawValue())) {
if (key === 'caseSensitive') {
if (this.getDictCaseSensitive(this.dictionary) !== this.form.get(key).value) {
return true;
}
} else if (this.dictionary[key] !== this.form.get(key).value) {
return true;
}
}
return false;
}
private get _canEditLabel$() {
return this.userService.currentUser$.pipe(
map(user => user.isAdmin || !this._data.dictionary),
@ -126,11 +105,11 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
private _toTechnicalName(value: string) {
const existingTechnicalNames = Object.keys(this._appStateService.dictionaryData[this._dossierTemplateId]);
const baseTechnicalName = toKebabCase(value.trim());
const baseTechnicalName = toSnakeCase(value.trim());
let technicalName = baseTechnicalName;
let suffix = 1;
while (existingTechnicalNames.includes(technicalName)) {
technicalName = [baseTechnicalName, suffix++].join('-');
technicalName = [baseTechnicalName, suffix++].join('_');
}
return technicalName;
}

View File

@ -35,11 +35,11 @@
</div>
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="form.invalid || !changed" color="primary" mat-flat-button type="button">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-dossier-attribute.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,8 +1,8 @@
import { Component, HostListener, Inject, OnDestroy } from '@angular/core';
import { Component, HostListener, Inject, Injector, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DossierAttributeConfigTypes, FileAttributeConfigTypes, IDossierAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AutoUnsubscribe, IqserEventTarget, LoadingService, Toaster } from '@iqser/common-ui';
import { BaseDialogComponent, IqserEventTarget, LoadingService, Toaster } from '@iqser/common-ui';
import { HttpErrorResponse } from '@angular/common/http';
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
import { dossierAttributeTypesTranslations } from '../../translations/dossier-attribute-types-translations';
@ -12,9 +12,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
templateUrl: './add-edit-dossier-attribute-dialog.component.html',
styleUrls: ['./add-edit-dossier-attribute-dialog.component.scss'],
})
export class AddEditDossierAttributeDialogComponent extends AutoUnsubscribe implements OnDestroy {
export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent implements OnDestroy {
dossierAttribute: IDossierAttributeConfig = this.data.dossierAttribute;
readonly form: FormGroup = this._getForm(this.dossierAttribute);
readonly translations = dossierAttributeTypesTranslations;
readonly typeOptions = Object.keys(DossierAttributeConfigTypes);
@ -23,11 +22,14 @@ export class AddEditDossierAttributeDialogComponent extends AutoUnsubscribe impl
private readonly _loadingService: LoadingService,
private readonly _dossierAttributesService: DossierAttributesService,
private readonly _toaster: Toaster,
readonly dialogRef: MatDialogRef<AddEditDossierAttributeDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA)
readonly data: { readonly dossierAttribute: IDossierAttributeConfig },
) {
super();
super(_injector, _dialogRef);
this.form = this._getForm(this.dossierAttribute);
this.initialFormValue = this.form.getRawValue();
}
get changed(): boolean {
@ -55,7 +57,7 @@ export class AddEditDossierAttributeDialogComponent extends AutoUnsubscribe impl
this._dossierAttributesService.createOrUpdate(attribute).subscribe(
() => {
this.dialogRef.close(true);
this._dialogRef.close(true);
},
(error: HttpErrorResponse) => {
this._loadingService.stop();

View File

@ -33,16 +33,11 @@
<div class="validity">
<div>
<mat-checkbox
(change)="hasValidFrom = !hasValidFrom"
[checked]="hasValidFrom"
class="filter-menu-checkbox"
color="primary"
>
<mat-checkbox (change)="toggleHasValid('from')" [checked]="hasValidFrom" class="filter-menu-checkbox" color="primary">
{{ 'add-edit-dossier-template.form.valid-from' | translate }}
</mat-checkbox>
<mat-checkbox (change)="hasValidTo = !hasValidTo" [checked]="hasValidTo" class="filter-menu-checkbox" color="primary">
<mat-checkbox (change)="toggleHasValid('to')" [checked]="hasValidTo" class="filter-menu-checkbox" color="primary">
{{ 'add-edit-dossier-template.form.valid-to' | translate }}
</mat-checkbox>
</div>
@ -87,11 +82,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="!valid || !changed" color="primary" mat-flat-button type="button">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-dossier-template.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,6 +1,6 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, Injector } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as moment from 'moment';
import { Moment } from 'moment';
@ -17,7 +17,6 @@ import { HttpStatusCode } from '@angular/common/http';
styleUrls: ['./add-edit-dossier-template-dialog.component.scss'],
})
export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
readonly form: FormGroup = this._getForm();
hasValidFrom: boolean;
hasValidTo: boolean;
downloadTypesEnum: DownloadFileType[] = ['ORIGINAL', 'PREVIEW', 'REDACTED'];
@ -25,66 +24,50 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
key: type,
label: downloadTypesTranslations[type],
}));
readonly disabled = false;
private _previousValidFrom: Moment;
private _previousValidTo: Moment;
private _lastValidFrom: Moment;
private _lastValidTo: Moment;
constructor(
private readonly _appStateService: AppStateService,
private readonly _toaster: Toaster,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _formBuilder: FormBuilder,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierTemplateDialogComponent>,
private readonly _loadingService: LoadingService,
public dialogRef: MatDialogRef<AddEditDossierTemplateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly dossierTemplate: IDossierTemplate,
) {
super();
super(_injector, _dialogRef);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
this.hasValidFrom = !!this.dossierTemplate?.validFrom;
this.hasValidTo = !!this.dossierTemplate?.validTo;
this._previousValidFrom = this.form.get('validFrom').value;
this._previousValidTo = this.form.get('validTo').value;
this._previousValidFrom = this._lastValidFrom = this.form.get('validFrom').value;
this._previousValidTo = this._lastValidTo = this.form.get('validTo').value;
this.form.valueChanges.subscribe(value => {
this.addSubscription = this.form.valueChanges.subscribe(value => {
this._applyValidityIntervalConstraints(value);
});
this.addSubscription = this.form.controls['validFrom'].valueChanges.subscribe(value => {
this._lastValidFrom = value ? value : this._lastValidFrom;
});
this.addSubscription = this.form.controls['validTo'].valueChanges.subscribe(value => {
this._lastValidFrom = value ? value : this._lastValidFrom;
});
}
get valid(): boolean {
return this.form.valid;
}
get changed(): boolean {
if (!this.dossierTemplate) {
return true;
toggleHasValid(extremity: string) {
if (extremity === 'from') {
this.hasValidFrom = !this.hasValidFrom;
this.form.controls['validFrom'].setValue(this.hasValidFrom ? this._lastValidFrom : null);
} else {
this.hasValidTo = !this.hasValidTo;
this.form.controls['validTo'].setValue(this.hasValidTo ? this._lastValidTo : null);
}
for (const key of Object.keys(this.form.getRawValue())) {
const formValue = this.form.get(key).value;
const objectValue = this.dossierTemplate[key];
if (key === 'validFrom') {
if (this.hasValidFrom !== !!objectValue || (this.hasValidFrom && !moment(objectValue).isSame(moment(formValue)))) {
return true;
}
} else if (key === 'validTo') {
if (this.hasValidTo !== !!objectValue || (this.hasValidTo && !moment(objectValue).isSame(moment(formValue)))) {
return true;
}
} else if (formValue instanceof Array) {
if (objectValue.length !== formValue.length) {
return true;
}
for (const item of objectValue) {
if (!formValue.includes(item)) {
return true;
}
}
} else if (objectValue !== formValue) {
return true;
}
}
return false;
}
async save() {
@ -95,10 +78,10 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
...this.form.getRawValue(),
validFrom: this.hasValidFrom ? this.form.get('validFrom').value : null,
validTo: this.hasValidTo ? this.form.get('validTo').value : null,
};
} as IDossierTemplate;
await this._dossierTemplatesService.createOrUpdate(dossierTemplate).toPromise();
await this._appStateService.loadDictionaryData();
this.dialogRef.close(true);
this._dialogRef.close(true);
} catch (error: any) {
const message =
error.status === HttpStatusCode.Conflict
@ -136,7 +119,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
}
private _requiredIfValidator(predicate) {
return formControl => {
return (formControl: AbstractControl) => {
if (!formControl.parent) {
return null;
}

View File

@ -84,11 +84,11 @@
</div>
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="!valid || !changed" color="primary" mat-flat-button type="button">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-file-attribute.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { fileAttributeTypesTranslations } from '../../translations/file-attribute-types-translations';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { BaseDialogComponent } from '@iqser/common-ui';
import { BaseDialogComponent } from '../../../../../../../../libs/common-ui/src';
@Component({
selector: 'redaction-add-edit-file-attribute-dialog',
@ -13,12 +13,10 @@ import { BaseDialogComponent } from '@iqser/common-ui';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
readonly disabled = false;
DISPLAYED_FILTERABLE_LIMIT = 3;
translations = fileAttributeTypesTranslations;
fileAttribute: IFileAttributeConfig = this.data.fileAttribute;
dossierTemplateId: string = this.data.dossierTemplateId;
readonly form!: FormGroup;
readonly typeOptions = Object.keys(FileAttributeConfigTypes);
readonly canSetDisplayed!: boolean;
readonly canSetFilterable!: boolean;
@ -26,7 +24,8 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
constructor(
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesService: FileAttributesService,
public dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: {
fileAttribute: IFileAttributeConfig;
@ -35,32 +34,11 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
numberOfFilterableAttrs: number;
},
) {
super();
super(_injector, _dialogRef);
this.canSetDisplayed = data.numberOfDisplayedAttrs < this.DISPLAYED_FILTERABLE_LIMIT || data.fileAttribute?.displayedInFileList;
this.canSetFilterable = data.numberOfFilterableAttrs < this.DISPLAYED_FILTERABLE_LIMIT || data.fileAttribute?.filterable;
this.form = this._getForm(this.fileAttribute);
}
get valid(): boolean {
return this.form.valid;
}
get changed(): boolean {
if (!this.fileAttribute) {
return true;
}
for (const key of Object.keys(this.form.getRawValue())) {
if (key === 'readonly') {
if (this.fileAttribute.editable === this.form.get(key).value) {
return true;
}
} else if (this.fileAttribute[key] !== this.form.get(key).value) {
return true;
}
}
return false;
this.initialFormValue = this.form.getRawValue();
}
save() {
@ -69,7 +47,7 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
editable: !this.form.get('readonly').value,
...this.form.getRawValue(),
};
this.dialogRef.close(fileAttribute);
this._dialogRef.close(fileAttribute);
}
private _getForm(fileAttribute: IFileAttributeConfig): FormGroup {

View File

@ -134,13 +134,14 @@ export class UserDetailsComponent extends AutoUnsubscribe implements OnChanges,
}
private _getForm(): FormGroup {
console.log(this.user);
return this._formBuilder.group({
firstName: [this.user?.firstName, Validators.required],
lastName: [this.user?.lastName, Validators.required],
email: [
{
value: this.user?.email,
disabled: !!this.user,
disabled: !!this.user?.email,
},
[Validators.required, Validators.email],
],

View File

@ -28,11 +28,11 @@
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="!valid || !changed" color="primary" mat-flat-button type="button">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'edit-color-dialog.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,4 +1,4 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, Injector } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DefaultColorType, IColors } from '@red/domain';
import { BaseDialogComponent, Toaster } from '@iqser/common-ui';
@ -19,10 +19,7 @@ interface IEditColorData {
styleUrls: ['./edit-color-dialog.component.scss'],
})
export class EditColorDialogComponent extends BaseDialogComponent {
readonly form: FormGroup;
translations = defaultColorsTranslations;
readonly disabled = false;
private readonly _initialColor: string;
private readonly _dossierTemplateId: string;
constructor(
@ -30,23 +27,16 @@ export class EditColorDialogComponent extends BaseDialogComponent {
private readonly _dictionaryService: DictionaryService,
private readonly _toaster: Toaster,
private readonly _translateService: TranslateService,
private readonly _dialogRef: MatDialogRef<EditColorDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<EditColorDialogComponent>,
@Inject(MAT_DIALOG_DATA)
readonly data: IEditColorData,
) {
super();
super(_injector, _dialogRef);
this._dossierTemplateId = data.dossierTemplateId;
this._initialColor = data.colors[data.colorKey];
this.form = this._getForm();
}
get changed(): boolean {
return this.form.get('color').value !== this._initialColor;
}
get valid(): boolean {
return this.form.valid;
this.initialFormValue = this.form.getRawValue();
}
async save() {

View File

@ -6,7 +6,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { RouterHistoryService } from '@services/router-history.service';
import { DigitalSignatureService } from '../../services/digital-signature.service';
import { IDigitalSignature } from '@red/domain';
import { IDigitalSignature, IDigitalSignatureRequest } from '@red/domain';
import { HttpStatusCode } from '@angular/common/http';
@Component({
@ -40,29 +40,30 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements
}
saveDigitalSignature() {
const digitalSignature = {
...this.form.getRawValue(),
const formValue = this.form.getRawValue();
const digitalSignature: IDigitalSignature = {
...formValue,
};
//adjusted for chrome auto-complete / password manager
digitalSignature.password = digitalSignature.keySecret;
digitalSignature.password = formValue.keySecret;
const observable = this.digitalSignatureExists
? this._digitalSignatureService.update(digitalSignature)
: this._digitalSignatureService.save(digitalSignature);
this.addSubscription = observable.subscribe(
() => {
this.addSubscription = observable.subscribe({
next: () => {
this.loadDigitalSignatureAndInitializeForm();
this._toaster.success(_('digital-signature-screen.action.save-success'));
},
error => {
error: error => {
if (error.status === HttpStatusCode.BadRequest) {
this._toaster.error(_('digital-signature-screen.action.certificate-not-valid-error'));
} else {
this._toaster.error(_('digital-signature-screen.action.save-error'));
}
},
);
});
}
removeDigitalSignature() {
@ -85,23 +86,23 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements
this.form.get('certificateName').setValue(file.name);
input.value = null;
};
fileReader.readAsDataURL(file);
fileReader.readAsDataURL(file as Blob);
}
loadDigitalSignatureAndInitializeForm() {
this._loadingService.start();
this.addSubscription = this._digitalSignatureService
this._digitalSignatureService
.getSignature()
.subscribe(
digitalSignature => {
.subscribe({
next: digitalSignature => {
this.digitalSignatureExists = true;
this.digitalSignature = digitalSignature;
},
() => {
error: () => {
this.digitalSignatureExists = false;
this.digitalSignature = {};
},
)
})
.add(() => {
this.form = this._getForm();
this._loadingService.stop();

View File

@ -11,7 +11,7 @@
<ng-template #bulkActions>
<iqser-circle-button
(action)="openConfirmDeleteDialog()"
*ngIf="userService.currentUser.isAdmin && listingService.areSomeSelected$ | async"
*ngIf="userPreferenceService.areDevFeaturesEnabled && userService.currentUser.isAdmin && listingService.areSomeSelected$ | async"
[tooltip]="'justifications-listing.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
@ -26,7 +26,7 @@
<div class="table-header-actions">
<iqser-icon-button
(action)="openAddJustificationDialog()"
*ngIf="userService.currentUser.isAdmin"
*ngIf="userPreferenceService.areDevFeaturesEnabled && userService.currentUser.isAdmin"
[label]="'justifications-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:plus"

View File

@ -14,6 +14,7 @@ import { JustificationsService } from '@services/entity-services/justifications.
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { JustificationsDialogService } from '../justifications-dialog.service';
import { UserService } from '@services/user.service';
import { UserPreferenceService } from '@services/user-preference.service';
@Component({
selector: 'redaction-justifications-screen',
@ -43,6 +44,7 @@ export class JustificationsScreenComponent extends ListingComponent<Justificatio
private readonly _loadingService: LoadingService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dialogService: JustificationsDialogService,
readonly userPreferenceService: UserPreferenceService,
readonly userService: UserService,
) {
super(_injector);

View File

@ -24,7 +24,7 @@
<iqser-circle-button
(action)="openConfirmDeleteDialog()"
*ngIf="userService.currentUser.isAdmin"
*ngIf="userPreferenceService.areDevFeaturesEnabled && userService.currentUser.isAdmin"
[tooltip]="'justifications-listing.actions.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"

View File

@ -3,6 +3,7 @@ import { Justification } from '@red/domain';
import { CircleButtonTypes, ListingService, LoadingService } from '@iqser/common-ui';
import { JustificationsDialogService } from '../justifications-dialog.service';
import { UserService } from '@services/user.service';
import { UserPreferenceService } from '@services/user-preference.service';
@Component({
selector: 'redaction-table-item',
@ -18,6 +19,7 @@ export class TableItemComponent {
private readonly _dialogService: JustificationsDialogService,
private readonly _loadingService: LoadingService,
private readonly _listingService: ListingService<Justification>,
readonly userPreferenceService: UserPreferenceService,
readonly userService: UserService,
) {}

View File

@ -8,6 +8,7 @@ import { UserService } from '@services/user.service';
import { RouterHistoryService } from '@services/router-history.service';
import { LicenseReportService } from '../../services/licence-report.service';
import { ILicenseReport } from '@red/domain';
import { Color, ScaleType } from '@swimlane/ngx-charts';
@Component({
selector: 'redaction-license-information-screen',
@ -31,14 +32,16 @@ export class LicenseInformationScreenComponent implements OnInit {
analysisPercentageOfLicense = 100;
barChart: any[];
lineChartSeries: any[] = [];
lineChartScheme = {
lineChartScheme: Color = {
name: 'Line chart scheme',
selectable: true,
group: 'Ordinal',
group: ScaleType.Ordinal,
domain: ['#dd4d50', '#5ce594', '#0389ec'],
};
comboBarScheme = {
comboBarScheme: Color = {
name: 'Combo bar scheme',
selectable: true,
group: 'Ordinal',
group: ScaleType.Ordinal,
domain: ['#0389ec'],
};

View File

@ -92,6 +92,31 @@ export class ReportsScreenComponent implements OnInit {
}
const dossierTemplateId = this._dossierTemplatesService.activeDossierTemplateId;
if (this.availableTemplates.some(template => template.fileName === file.name)) {
const data = new ConfirmationDialogInput({
title: _('confirmation-dialog.report-template-same-name.title'),
question: _('confirmation-dialog.report-template-same-name.question'),
confirmationText: _('confirmation-dialog.report-template-same-name.confirmation-text'),
denyText: _('confirmation-dialog.report-template-same-name.deny-text'),
translateParams: {
fileName: file.name,
},
});
this._dialogService.openDialog('confirm', null, data, null, async result => {
if (result) {
await this._openConfirmationDialog(file, dossierTemplateId);
}
});
} else {
await this._openConfirmationDialog(file, dossierTemplateId);
}
this._fileInput.nativeElement.value = null;
}
private async _openConfirmationDialog(file: File, dossierTemplateId: string) {
if (this._isExcelFile(file)) {
const data = new ConfirmationDialogInput({
title: _('confirmation-dialog.upload-report-template.title'),
@ -113,7 +138,6 @@ export class ReportsScreenComponent implements OnInit {
await this._reportTemplateService.uploadTemplateForm(dossierTemplateId, false, file).toPromise();
await this._loadReportTemplates();
}
this._fileInput.nativeElement.value = null;
}
private async _deleteTemplate(template: IReportTemplate) {

View File

@ -34,6 +34,7 @@ export class AdminDialogService extends DialogService<DialogType> {
protected readonly _config: DialogConfig<DialogType> = {
confirm: {
component: ConfirmationDialogComponent,
dialogConfig: { disableClose: false },
},
addEditDictionary: {
component: AddEditDictionaryDialogComponent,
@ -49,18 +50,19 @@ export class AdminDialogService extends DialogService<DialogType> {
},
deleteFileAttribute: {
component: ConfirmDeleteFileAttributeDialogComponent,
dialogConfig: { disableClose: false },
},
importFileAttributes: {
component: FileAttributesCsvImportDialogComponent,
dialogConfig: largeDialogConfig,
dialogConfig: { ...largeDialogConfig, ...{ disableClose: false } },
},
deleteUsers: {
component: ConfirmDeleteUsersDialogComponent,
dialogConfig: { autoFocus: true },
dialogConfig: { autoFocus: true, disableClose: false },
},
addEditUser: {
component: AddEditUserDialogComponent,
dialogConfig: { autoFocus: true },
dialogConfig: { autoFocus: true, disableClose: false },
},
smtpAuthConfig: {
component: SmtpAuthDialogComponent,

View File

@ -28,6 +28,7 @@ export class RedRoleGuard implements CanActivate {
if (
this._userService.currentUser.isUserAdmin &&
!this._userService.currentUser.isAdmin &&
!this._userService.currentUser.isUser &&
!(state.url.startsWith('/main/admin/users') || state.url.startsWith('/main/my-profile'))
) {
this._router.navigate(['/main/admin/users']);

View File

@ -1,5 +1,5 @@
<section class="dialog">
<form (submit)="saveDossier()" [formGroup]="form">
<form (submit)="save()" [formGroup]="form">
<div class="dialog-header heading-l" translate="add-dossier-dialog.header-new"></div>
<div class="dialog-content">
@ -91,7 +91,7 @@
</button>
<iqser-icon-button
(action)="saveDossier(true)"
(action)="save({ addMembers: true })"
[disabled]="disabled"
[label]="'add-dossier-dialog.actions.save-and-add-members' | translate"
[type]="iconButtonTypes.dark"

View File

@ -1,10 +1,10 @@
import { Component } from '@angular/core';
import { Component, Injector } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { DownloadFileType, IDossierRequest, IDossierTemplate, IReportTemplate } from '@red/domain';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import * as moment from 'moment';
import { downloadTypesTranslations } from '../../../../translations/download-types-translations';
import { IconButtonTypes } from '@iqser/common-ui';
import { BaseDialogComponent, IconButtonTypes, SaveOptions } from '@iqser/common-ui';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { ReportTemplateService } from '@services/report-template.service';
@ -13,10 +13,9 @@ import { ReportTemplateService } from '@services/report-template.service';
templateUrl: './add-dossier-dialog.component.html',
styleUrls: ['./add-dossier-dialog.component.scss'],
})
export class AddDossierDialogComponent {
export class AddDossierDialogComponent extends BaseDialogComponent {
readonly iconButtonTypes = IconButtonTypes;
readonly form: FormGroup;
hasDueDate = false;
downloadTypes: { key: DownloadFileType; label: string }[] = ['ORIGINAL', 'PREVIEW', 'REDACTED'].map((type: DownloadFileType) => ({
key: type,
@ -30,10 +29,13 @@ export class AddDossierDialogComponent {
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _formBuilder: FormBuilder,
private readonly _reportTemplateController: ReportTemplateService,
readonly dialogRef: MatDialogRef<AddDossierDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddDossierDialogComponent>,
) {
super(_injector, _dialogRef);
this._getDossierTemplates();
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
}
private _getForm(): FormGroup {
@ -75,10 +77,10 @@ export class AddDossierDialogComponent {
reportTemplateValueMapper = (reportTemplate: IReportTemplate) => reportTemplate.templateId;
async saveDossier(addMembers = false) {
async save(options?: SaveOptions) {
const savedDossier = await this._dossiersService.createOrUpdate(this._formToObject()).toPromise();
if (savedDossier) {
this.dialogRef.close({ dossier: savedDossier, addMembers });
this._dialogRef.close({ dossier: savedDossier, addMembers: options?.addMembers });
}
}

View File

@ -38,12 +38,12 @@
</div>
<div class="dialog-actions">
<button [disabled]="!form.valid || !changed" color="primary" mat-flat-button type="submit">
<button [disabled]="disabled" color="primary" mat-flat-button type="submit">
{{ 'change-legal-basis-dialog.actions.save' | translate }}
</button>
<div class="all-caps-label cancel" mat-dialog-close translate="change-legal-basis-dialog.actions.cancel"></div>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,4 +1,4 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject, Injector, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -6,6 +6,7 @@ import { PermissionsService } from '@services/permissions.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { JustificationsService } from '@services/entity-services/justifications.service';
import { Dossier } from '@red/domain';
import { BaseDialogComponent } from '@iqser/common-ui';
export interface LegalBasisOption {
label?: string;
@ -16,8 +17,7 @@ export interface LegalBasisOption {
@Component({
templateUrl: './change-legal-basis-dialog.component.html',
})
export class ChangeLegalBasisDialogComponent implements OnInit {
form: FormGroup = this._getForm();
export class ChangeLegalBasisDialogComponent extends BaseDialogComponent implements OnInit {
isDocumentAdmin: boolean;
legalOptions: LegalBasisOption[] = [];
@ -26,16 +26,12 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
private readonly _dossiersService: DossiersService,
private readonly _permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
readonly dialogRef: MatDialogRef<ChangeLegalBasisDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<ChangeLegalBasisDialogComponent>,
@Inject(MAT_DIALOG_DATA) private readonly _data: { annotations: AnnotationWrapper[]; dossier: Dossier },
) {}
get changed(): boolean {
return (
this.form.get('reason').value.legalBasis !== this._data.annotations[0].legalBasis ||
this.form.get('section').value !== this._data.annotations[0].section ||
this.form.get('classification').value !== this._data.annotations[0].value
);
) {
super(_injector, _dialogRef);
this.form = this._getForm();
}
get allRectangles(): boolean {
@ -43,6 +39,7 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
}
async ngOnInit() {
super.ngOnInit();
const data = await this._justificationsService.getForDossierTemplate(this._data.dossier.dossierTemplateId).toPromise();
this.legalOptions = data
@ -56,6 +53,7 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
this.form.patchValue({
reason: this.legalOptions.find(option => option.legalBasis === this._data.annotations[0].legalBasis),
});
this.initialFormValue = this.form.getRawValue();
}
private _getForm(): FormGroup {
@ -69,7 +67,7 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
}
save() {
this.dialogRef.close({
this._dialogRef.close({
legalBasis: this.form.get('reason').value.legalBasis,
section: this.form.get('section').value,
comment: this.form.get('comment').value,

View File

@ -1,7 +1,7 @@
<section *ngIf="!!form" class="dialog">
<div class="dialog-header heading-l" translate="document-info.title"></div>
<form (submit)="saveDocumentInfo()" [formGroup]="form">
<form (submit)="save()" [formGroup]="form">
<div class="dialog-content">
<div *ngFor="let attr of attributes" class="iqser-input-group w-300">
<label>{{ attr.label }}</label>
@ -9,11 +9,11 @@
</div>
</div>
<div class="dialog-actions">
<button [disabled]="form.invalid" color="primary" mat-flat-button type="submit">
<button [disabled]="disabled" color="primary" mat-flat-button type="submit">
{{ 'document-info.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,16 +1,17 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject, Injector, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Dossier, File, IFileAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FilesService } from '@services/entity-services/files.service';
import { BaseDialogComponent } from '@iqser/common-ui';
@Component({
templateUrl: './document-info-dialog.component.html',
styleUrls: ['./document-info-dialog.component.scss'],
})
export class DocumentInfoDialogComponent implements OnInit {
export class DocumentInfoDialogComponent extends BaseDialogComponent implements OnInit {
form: FormGroup;
attributes: IFileAttributeConfig[];
@ -21,27 +22,31 @@ export class DocumentInfoDialogComponent implements OnInit {
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesService: FileAttributesService,
private readonly _filesService: FilesService,
public dialogRef: MatDialogRef<DocumentInfoDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<DocumentInfoDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: File,
) {
super(_injector, _dialogRef);
this._dossier = this._dossiersService.find(this.data.dossierId);
}
async ngOnInit() {
super.ngOnInit();
this.attributes = (
await this._fileAttributesService.getFileAttributesConfig(this._dossier.dossierTemplateId).toPromise()
).fileAttributeConfigs.filter(attr => attr.editable);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
}
async saveDocumentInfo() {
async save() {
const attributeIdToValue = {
...this.data.fileAttributes?.attributeIdToValue,
...this.form.getRawValue(),
};
await this._fileAttributesService.setFileAttributes({ attributeIdToValue }, this.data.dossierId, this.data.fileId).toPromise();
this._filesService.reload(this.data.dossierId, this.data.fileId);
this.dialogRef.close(true);
this._dialogRef.close(true);
}
private _getForm(): FormGroup {

View File

@ -66,9 +66,10 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa
async save(): EditDossierSaveResult {
const dossierAttributeList = this.attributes.map(attr => ({
dossierAttributeConfigId: attr.id,
value: this.isSpecificType(attr, DossierAttributeConfigTypes.DATE)
? moment(this.currentAttrValue(attr)).format('YYYY-MM-DD')
: this.currentAttrValue(attr),
value:
this.isSpecificType(attr, DossierAttributeConfigTypes.DATE) && !!this.currentAttrValue(attr)
? moment(this.currentAttrValue(attr)).format('YYYY-MM-DD')
: this.currentAttrValue(attr),
}));
try {
await this._dossierAttributesService.setAttributes(this.dossier, dossierAttributeList).toPromise();

View File

@ -1,12 +1,22 @@
<div class="header-wrapper">
<div class="heading">
<div>{{ dossierDictionary?.label }}</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ 'edit-dossier-dialog.dictionary.entries' | translate: { length: (dossierDictionary?.entries || []).length } }}
<div class="header-left">
<div class="heading">
<div>{{ dossierDictionary?.label }}</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ 'edit-dossier-dialog.dictionary.entries' | translate: { length: (dossierDictionary?.entries || []).length } }}
</div>
</div>
</div>
<form [formGroup]="form" *ngIf="form">
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="addToDictionaryAction" name="addToDictionaryAction">
{{ 'add-edit-dictionary.form.add-to-dictionary-action' | translate }}
</mat-checkbox>
</div>
</form>
</div>
<div *ngIf="canEdit" class="display-name">

View File

@ -2,6 +2,14 @@
display: flex;
justify-content: space-between;
.header-left {
display: flex;
.iqser-input-group {
margin-left: 24px;
}
}
.display-name {
display: flex;
align-items: center;

View File

@ -7,6 +7,7 @@ import { DictionaryService } from '@shared/services/dictionary.service';
import { CircleButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'redaction-edit-dossier-dictionary',
@ -15,10 +16,13 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
})
export class EditDossierDictionaryComponent implements EditDossierSectionInterface, OnInit {
@Input() dossier: Dossier;
form: FormGroup;
canEdit = false;
readonly circleButtonTypes = CircleButtonTypes;
dossierDictionary: IDictionary;
readonly circleButtonTypes = CircleButtonTypes;
@ViewChild(DictionaryManagerComponent, { static: false }) private readonly _dictionaryManager: DictionaryManagerComponent;
constructor(
@ -26,11 +30,20 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa
private readonly _dictionaryService: DictionaryService,
private readonly _permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _formBuilder: FormBuilder,
private readonly _toaster: Toaster,
) {}
get formChanged() {
if (this.form) {
return this.form.get('addToDictionaryAction').value !== this.dossierDictionary.addToDictionaryAction;
}
return false;
}
get changed(): boolean {
return this._dictionaryManager.editor.hasChanges;
return this._dictionaryManager.editor.hasChanges || this.formChanged;
}
get disabled(): boolean {
@ -38,13 +51,14 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa
}
get valid(): boolean {
return this._dictionaryManager.editor.hasChanges;
return this._dictionaryManager.editor.hasChanges || this.formChanged;
}
async ngOnInit() {
this._loadingService.start();
this.canEdit = this._permissionsService.isDossierMember(this.dossier);
await this._updateDossierDictionary();
this.form = this._getForm();
this._loadingService.stop();
}
@ -66,6 +80,15 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa
async save(): EditDossierSaveResult {
try {
const dictionary: IDictionary = {
...this.dossierDictionary,
type: 'dossier_redaction',
addToDictionaryAction: this.form.get('addToDictionaryAction').value,
};
await this._dictionaryService
.updateDictionary(dictionary, this.dossier.dossierTemplateId, 'dossier_redaction', this.dossier.id)
.toPromise();
await this._dictionaryService
.saveEntries(
this._dictionaryManager.editor.currentEntries,
@ -76,6 +99,7 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa
false,
)
.toPromise();
await this._updateDossierDictionary();
return { success: true };
} catch (error) {
@ -85,6 +109,20 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa
revert() {
this._dictionaryManager.revert();
this.form.reset({
addToDictionaryAction: this.dossierDictionary.addToDictionaryAction,
});
}
private _getForm(): FormGroup {
return this._formBuilder.group({
addToDictionaryAction: [
{
value: this.dossierDictionary.addToDictionaryAction,
disabled: !this._permissionsService.isOwner(this.dossier),
},
],
});
}
private async _updateDossierDictionary() {

View File

@ -53,7 +53,7 @@
{{ 'edit-dossier-dialog.actions.save' | translate }}
</button>
<iqser-icon-button
(action)="save(true)"
(action)="save({ closeAfterSave: true })"
[disabled]="disabled || !valid || !changed"
[label]="'edit-dossier-dialog.actions.save-and-close' | translate"
[type]="iconButtonTypes.dark"
@ -65,5 +65,5 @@
</div>
</div>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,10 +1,10 @@
import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core';
import { ChangeDetectorRef, Component, Inject, Injector, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Dossier } from '@red/domain';
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 { BaseDialogComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { BaseDialogComponent, ConfirmOptions, IconButtonTypes, LoadingService, SaveOptions, Toaster } from '@iqser/common-ui';
import { EditDossierDictionaryComponent } from './dictionary/edit-dossier-dictionary.component';
import { EditDossierAttributesComponent } from './attributes/edit-dossier-attributes.component';
@ -40,16 +40,17 @@ export class EditDossierDialogComponent extends BaseDialogComponent {
private readonly _toaster: Toaster,
private readonly _dossiersService: DossiersService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _dialogRef: MatDialogRef<EditDossierDialogComponent>,
private readonly _loadingService: LoadingService,
private readonly _permissionsService: PermissionsService,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<EditDossierDialogComponent>,
@Inject(MAT_DIALOG_DATA)
private readonly _data: {
dossierId: string;
section?: Section;
},
) {
super();
super(_injector, _dialogRef);
this.navItems = [
{
key: 'dossierInfo',
@ -126,7 +127,7 @@ export class EditDossierDialogComponent extends BaseDialogComponent {
return this.activeComponent?.disabled;
}
async save(closeAfterSave: boolean = false) {
async save(options?: SaveOptions) {
this._loadingService.start();
const result = await this.activeComponent.save();
this._loadingService.stop();
@ -135,7 +136,7 @@ export class EditDossierDialogComponent extends BaseDialogComponent {
this._toaster.success(_('edit-dossier-dialog.change-successful'), { params: { dossierName: this._dossierName } });
}
if (result.success && closeAfterSave) {
if (result.success && options?.closeAfterSave) {
this._dialogRef.close();
}
}
@ -146,9 +147,19 @@ export class EditDossierDialogComponent extends BaseDialogComponent {
changeTab(key: Section) {
if (this.changed) {
this._toaster.error(_('edit-dossier-dialog.unsaved-changes'));
return;
this._openConfirmDialog().then(async result => {
if (result in ConfirmOptions) {
if (result === ConfirmOptions.CONFIRM) {
await this.save();
} else {
this.revert();
}
this.activeNav = key;
}
this._waitingForConfirmation = false;
});
} else {
this.activeNav = key;
}
this.activeNav = key;
}
}

View File

@ -1,5 +1,5 @@
<section class="dialog">
<form (submit)="handleForceAnnotation()" [formGroup]="redactionForm">
<form (submit)="save()" [formGroup]="form">
<div class="dialog-header heading-l" translate="manual-annotation.dialog.header.force-redaction" *ngIf="!isHintDialog"></div>
<div class="dialog-header heading-l" translate="manual-annotation.dialog.header.force-hint" *ngIf="isHintDialog"></div>
@ -19,7 +19,7 @@
<div class="iqser-input-group w-400" *ngIf="!isHintDialog">
<label translate="manual-annotation.dialog.content.legalBasis"></label>
<input [value]="redactionForm.get('reason').value?.legalBasis" disabled type="text" />
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
</div>
<div [class.required]="!isDocumentAdmin" class="iqser-input-group w-300">
@ -29,11 +29,11 @@
</div>
<div class="dialog-actions">
<button [disabled]="!redactionForm.valid" color="primary" mat-flat-button type="submit">
<button [disabled]="disabled" color="primary" mat-flat-button type="submit">
{{ 'manual-annotation.dialog.actions.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,7 +1,7 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject, Injector, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Toaster } from '@iqser/common-ui';
import { BaseDialogComponent, Toaster } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '@services/user.service';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
@ -21,8 +21,7 @@ export interface LegalBasisOption {
templateUrl: './force-annotation-dialog.component.html',
styleUrls: ['./force-annotation-dialog.component.scss'],
})
export class ForceAnnotationDialogComponent implements OnInit {
redactionForm: FormGroup;
export class ForceAnnotationDialogComponent extends BaseDialogComponent implements OnInit {
isDocumentAdmin: boolean;
legalOptions: LegalBasisOption[] = [];
@ -35,10 +34,13 @@ export class ForceAnnotationDialogComponent implements OnInit {
private readonly _justificationsService: JustificationsService,
private readonly _manualAnnotationService: ManualAnnotationService,
private readonly _permissionsService: PermissionsService,
public dialogRef: MatDialogRef<ForceAnnotationDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<ForceAnnotationDialogComponent>,
@Inject(MAT_DIALOG_DATA) private readonly _data: { readonly dossier: Dossier; readonly hint: boolean },
) {
this.redactionForm = this._getForm();
super(_injector, _dialogRef);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
}
get isHintDialog() {
@ -55,6 +57,7 @@ export class ForceAnnotationDialogComponent implements OnInit {
}
async ngOnInit() {
super.ngOnInit();
const data = await this._justificationsService.getForDossierTemplate(this._data.dossier.dossierTemplateId).toPromise();
this.legalOptions = data.map(lbm => ({
@ -66,17 +69,17 @@ export class ForceAnnotationDialogComponent implements OnInit {
this.legalOptions.sort((a, b) => a.label.localeCompare(b.label));
}
handleForceAnnotation() {
this.dialogRef.close(this._createForceRedactionRequest());
save() {
this._dialogRef.close(this._createForceRedactionRequest());
}
private _createForceRedactionRequest(): ILegalBasisChangeRequest {
const request: ILegalBasisChangeRequest = {};
const legalOption: LegalBasisOption = this.redactionForm.get('reason').value;
const legalOption: LegalBasisOption = this.form.get('reason').value;
request.legalBasis = legalOption.legalBasis;
request.comment = this.redactionForm.get('comment').value;
request.comment = this.form.get('comment').value;
return request;
}

View File

@ -1,5 +1,5 @@
<section class="dialog">
<form (submit)="handleAddRedaction()" [formGroup]="redactionForm">
<form (submit)="save()" [formGroup]="form">
<div [translate]="title" class="dialog-header heading-l"></div>
<div class="dialog-content">
@ -54,7 +54,7 @@
<div *ngIf="!isDictionaryRequest" class="iqser-input-group w-400">
<label translate="manual-annotation.dialog.content.legalBasis"></label>
<input [value]="redactionForm.get('reason').value?.legalBasis" disabled type="text" />
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
</div>
<div *ngIf="data.manualRedactionEntryWrapper.manualRedactionEntry.rectangle" class="iqser-input-group w-400">
@ -74,11 +74,11 @@
</div>
<div class="dialog-actions">
<button [disabled]="!redactionForm.valid" color="primary" mat-flat-button type="submit">
<button [disabled]="disabled" color="primary" mat-flat-button type="submit">
{{ 'manual-annotation.dialog.actions.save' | translate }}
</button>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,4 +1,4 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject, Injector, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AppStateService } from '@state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@ -9,6 +9,8 @@ import { PermissionsService } from '@services/permissions.service';
import { JustificationsService } from '@services/entity-services/justifications.service';
import { Dictionary, Dossier, File, IAddRedactionRequest } from '@red/domain';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { BaseDialogComponent } from '@iqser/common-ui';
import { DictionaryService } from '@shared/services/dictionary.service';
export interface LegalBasisOption {
label?: string;
@ -21,9 +23,7 @@ export interface LegalBasisOption {
templateUrl: './manual-annotation-dialog.component.html',
styleUrls: ['./manual-annotation-dialog.component.scss'],
})
export class ManualAnnotationDialogComponent implements OnInit {
redactionForm: FormGroup;
export class ManualAnnotationDialogComponent extends BaseDialogComponent implements OnInit {
isDocumentAdmin: boolean;
isDictionaryRequest: boolean;
isFalsePositiveRequest: boolean;
@ -40,18 +40,21 @@ export class ManualAnnotationDialogComponent implements OnInit {
private readonly _manualAnnotationService: ManualAnnotationService,
private readonly _permissionsService: PermissionsService,
private readonly _dossiersService: DossiersService,
private readonly _dictionaryService: DictionaryService,
public dialogRef: MatDialogRef<ManualAnnotationDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<ManualAnnotationDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { manualRedactionEntryWrapper: ManualRedactionEntryWrapper; file: File },
) {
super(_injector, _dialogRef);
this._dossier = this._dossiersService.find(this.data.file.dossierId);
this.isDocumentAdmin = this._permissionsService.isApprover(this._dossier);
this.isFalsePositiveRequest = this.data.manualRedactionEntryWrapper.type === 'FALSE_POSITIVE';
this.isDictionaryRequest = this.data.manualRedactionEntryWrapper.type === 'DICTIONARY' || this.isFalsePositiveRequest;
this.redactionForm = this._getForm();
this.possibleDictionaries = this._possibleDictionaries;
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
}
get title() {
@ -59,17 +62,21 @@ export class ManualAnnotationDialogComponent implements OnInit {
}
get displayedDictionaryLabel() {
const dictType = this.redactionForm.get('dictionary').value;
const dictType = this.form.get('dictionary').value;
if (dictType) {
return this.possibleDictionaries.find(d => d.type === dictType).label;
}
return null;
}
private get _possibleDictionaries(): Dictionary[] {
private async _getPossibleDictionaries(): Promise<Dictionary[]> {
const possibleDictionaries: Dictionary[] = [];
const dossier = this._dossier;
const dossierDictionary = await this._dictionaryService
.getForType(dossier.dossierTemplateId, 'dossier_redaction', dossier.dossierId)
.toPromise();
for (const key of Object.keys(this._appStateService.dictionaryData[dossier.dossierTemplateId])) {
const dictionaryData = this._appStateService.getDictionary(key, dossier.dossierTemplateId);
if (!dictionaryData.virtual && dictionaryData.addToDictionaryAction) {
@ -77,14 +84,21 @@ export class ManualAnnotationDialogComponent implements OnInit {
}
}
if (dossierDictionary.addToDictionaryAction) {
// TODO fix this in the backend
possibleDictionaries.push(new Dictionary({ ...dossierDictionary, type: 'dossier_redaction' }));
}
possibleDictionaries.sort((a, b) => a.label.localeCompare(b.label));
return possibleDictionaries;
}
async ngOnInit() {
const data = await this._justificationsService.getForDossierTemplate(this._dossier.dossierTemplateId).toPromise();
super.ngOnInit();
this.possibleDictionaries = await this._getPossibleDictionaries();
const data = await this._justificationsService.getForDossierTemplate(this._dossier.dossierTemplateId).toPromise();
this.legalOptions = data.map(lbm => ({
legalBasis: lbm.reason,
description: lbm.description,
@ -94,11 +108,11 @@ export class ManualAnnotationDialogComponent implements OnInit {
this.legalOptions.sort((a, b) => a.label.localeCompare(b.label));
}
handleAddRedaction() {
save() {
this._enhanceManualRedaction(this.data.manualRedactionEntryWrapper.manualRedactionEntry);
this._manualAnnotationService.addAnnotation(this.data.manualRedactionEntryWrapper.manualRedactionEntry, this.data.file).subscribe(
response => this.dialogRef.close(new ManualAnnotationResponse(this.data.manualRedactionEntryWrapper, response)),
() => this.dialogRef.close(),
response => this._dialogRef.close(new ManualAnnotationResponse(this.data.manualRedactionEntryWrapper, response)),
() => this._dialogRef.close(),
);
}
@ -123,22 +137,26 @@ export class ManualAnnotationDialogComponent implements OnInit {
}
private _enhanceManualRedaction(addRedactionRequest: IAddRedactionRequest) {
const legalOption: LegalBasisOption = this.redactionForm.get('reason').value;
addRedactionRequest.type = this.redactionForm.get('dictionary').value;
const legalOption: LegalBasisOption = this.form.get('reason').value;
addRedactionRequest.type = this.form.get('dictionary').value;
if (legalOption) {
addRedactionRequest.reason = legalOption.description;
addRedactionRequest.legalBasis = legalOption.legalBasis;
}
addRedactionRequest.addToDictionary = this.isDictionaryRequest;
// todo fix this in backend
addRedactionRequest.addToDictionary = this.isDictionaryRequest && addRedactionRequest.type !== 'dossier_redaction';
addRedactionRequest.addToDossierDictionary = this.isDictionaryRequest && addRedactionRequest.type === 'dossier_redaction';
if (!addRedactionRequest.reason) {
addRedactionRequest.reason = 'Dictionary Request';
}
const commentValue = this.redactionForm.get('comment').value;
const commentValue = this.form.get('comment').value;
addRedactionRequest.comment = commentValue ? { text: commentValue } : null;
addRedactionRequest.section = this.redactionForm.get('section').value;
addRedactionRequest.value = addRedactionRequest.rectangle
? this.redactionForm.get('classification').value
: addRedactionRequest.value;
addRedactionRequest.section = this.form.get('section').value;
addRedactionRequest.value = addRedactionRequest.rectangle ? this.form.get('classification').value : addRedactionRequest.value;
}
get disabled() {
return this.form.invalid;
}
}

View File

@ -1,5 +1,5 @@
<section class="dialog">
<form (submit)="save()" [formGroup]="recategorizeImageForm">
<form (submit)="save()" [formGroup]="form">
<div class="dialog-header heading-l" translate="recategorize-image-dialog.header"></div>
<div class="dialog-content">
@ -23,12 +23,12 @@
</div>
<div class="dialog-actions">
<button [disabled]="!recategorizeImageForm.valid || !changed" color="primary" mat-flat-button type="submit">
<button [disabled]="disabled" color="primary" mat-flat-button type="submit">
{{ 'recategorize-image-dialog.actions.save' | translate }}
</button>
<div class="all-caps-label cancel" mat-dialog-close translate="recategorize-image-dialog.actions.cancel"></div>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,17 +1,17 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Component, Inject, Injector, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { PermissionsService } from '@services/permissions.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { imageCategoriesTranslations } from '../../translations/image-categories-translations';
import { ImageCategory } from '../../models/image-category.model';
import { Dossier } from '@red/domain';
import { BaseDialogComponent } from '@iqser/common-ui';
@Component({
templateUrl: './recategorize-image-dialog.component.html',
})
export class RecategorizeImageDialogComponent implements OnInit {
recategorizeImageForm: FormGroup;
export class RecategorizeImageDialogComponent extends BaseDialogComponent implements OnInit {
isDocumentAdmin: boolean;
typeOptions: ImageCategory[] = ['signature', 'logo', 'formula', 'image'];
translations = imageCategoriesTranslations;
@ -19,27 +19,32 @@ export class RecategorizeImageDialogComponent implements OnInit {
constructor(
private readonly _permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<RecategorizeImageDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<RecategorizeImageDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { annotations: AnnotationWrapper[]; dossier: Dossier },
) {}
) {
super(_injector, _dialogRef);
}
get changed(): boolean {
return this.recategorizeImageForm.get('type').value !== this.data.annotations[0].type;
return this.form.get('type').value !== this.data.annotations[0].type;
}
ngOnInit() {
super.ngOnInit();
this.isDocumentAdmin = this._permissionsService.isApprover(this.data.dossier);
this.recategorizeImageForm = this._formBuilder.group({
this.form = this._formBuilder.group({
type: [this.data.annotations[0].type, Validators.required],
comment: this.isDocumentAdmin ? [null] : [null, Validators.required],
});
this.initialFormValue = this.form.getRawValue();
}
save() {
this.dialogRef.close({
type: this.recategorizeImageForm.get('type').value,
comment: this.recategorizeImageForm.get('comment').value,
this._dialogRef.close({
type: this.form.get('type').value,
comment: this.form.get('comment').value,
});
}
}

View File

@ -7,7 +7,7 @@
) | translate: { hint: data.hint }
}}
</div>
<form (submit)="confirm()" [formGroup]="redactionForm">
<form (submit)="save()" [formGroup]="form">
<div class="dialog-content">
{{
(data.removeFromDictionary
@ -46,12 +46,12 @@
</div>
<div class="dialog-actions">
<button [disabled]="!redactionForm.valid" color="primary" mat-flat-button type="submit">
<button [disabled]="disabled" color="primary" mat-flat-button type="submit">
{{ 'remove-annotations-dialog.confirm' | translate }}
</button>
<div class="all-caps-label cancel" mat-dialog-close translate="remove-annotations-dialog.cancel"></div>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,10 +1,10 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, Injector } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { TranslateService } from '@ngx-translate/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { PermissionsService } from '@services/permissions.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { humanize } from '@iqser/common-ui';
import { FormBuilder, Validators } from '@angular/forms';
import { BaseDialogComponent, humanize } from '@iqser/common-ui';
import { Dossier } from '@red/domain';
export interface RemoveAnnotationsDialogInput {
@ -18,23 +18,24 @@ export interface RemoveAnnotationsDialogInput {
templateUrl: './remove-annotations-dialog.component.html',
styleUrls: ['./remove-annotations-dialog.component.scss'],
})
export class RemoveAnnotationsDialogComponent {
redactionForm: FormGroup;
export class RemoveAnnotationsDialogComponent extends BaseDialogComponent {
constructor(
private readonly _translateService: TranslateService,
private readonly _formBuilder: FormBuilder,
readonly permissionsService: PermissionsService,
public dialogRef: MatDialogRef<RemoveAnnotationsDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<RemoveAnnotationsDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: RemoveAnnotationsDialogInput,
) {
this.redactionForm = this._formBuilder.group({
super(_injector, _dialogRef);
this.form = this._formBuilder.group({
comment: this.permissionsService.isApprover(this.data.dossier) ? [null] : [null, Validators.required],
});
this.initialFormValue = this.form.getRawValue();
}
confirm() {
this.dialogRef.close({ comment: this.redactionForm.getRawValue().comment });
save() {
this._dialogRef.close({ comment: this.form.getRawValue().comment });
}
printable(annotation: AnnotationWrapper) {

View File

@ -1,5 +1,5 @@
<section class="dialog">
<form (submit)="save()" [formGroup]="resizeForm">
<form (submit)="save()" [formGroup]="form">
<div class="dialog-header heading-l" translate="resize-annotation-dialog.header"></div>
<div class="dialog-content">
@ -10,12 +10,12 @@
</div>
<div class="dialog-actions">
<button [disabled]="!resizeForm.valid" color="primary" mat-flat-button type="submit">
<button [disabled]="disabled" color="primary" mat-flat-button type="submit">
{{ 'resize-annotation-dialog.actions.save' | translate }}
</button>
<div class="all-caps-label cancel" mat-dialog-close translate="resize-annotation-dialog.actions.cancel"></div>
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,34 +1,40 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject, Injector, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FormBuilder, Validators } from '@angular/forms';
import { PermissionsService } from '@services/permissions.service';
import { Dossier } from '@red/domain';
import { BaseDialogComponent } from '@iqser/common-ui';
@Component({
templateUrl: './resize-annotation-dialog.component.html',
})
export class ResizeAnnotationDialogComponent implements OnInit {
resizeForm: FormGroup;
export class ResizeAnnotationDialogComponent extends BaseDialogComponent implements OnInit {
isDocumentAdmin: boolean;
constructor(
private readonly _permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<ResizeAnnotationDialogComponent>,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<ResizeAnnotationDialogComponent>,
@Inject(MAT_DIALOG_DATA) private readonly _data: { dossier: Dossier },
) {}
) {
super(_injector, _dialogRef);
}
ngOnInit() {
super.ngOnInit();
this.isDocumentAdmin = this._permissionsService.isApprover(this._data.dossier);
this.resizeForm = this._formBuilder.group({
this.form = this._formBuilder.group({
comment: this.isDocumentAdmin ? [null] : [null, Validators.required],
});
this.initialFormValue = this.form.getRawValue();
}
save() {
this.dialogRef.close({
comment: this.resizeForm.get('comment').value,
this._dialogRef.close({
comment: this.form.get('comment').value,
});
}
}

View File

@ -10,10 +10,7 @@ import { SharedModule } from '@shared/shared.module';
import { DossiersRoutingModule } from './dossiers-routing.module';
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
import { DossiersDialogService } from './services/dossiers-dialog.service';
import { AnnotationActionsService } from './services/annotation-actions.service';
import { PdfViewerDataService } from './services/pdf-viewer-data.service';
import { ManualAnnotationService } from './services/manual-annotation.service';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from './services/annotation-processing.service';
import { EditDossierDialogComponent } from './dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
import { EditDossierGeneralInfoComponent } from './dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component';
@ -56,14 +53,7 @@ const components = [
...dialogs,
];
const services = [
DossiersDialogService,
AnnotationActionsService,
ManualAnnotationService,
PdfViewerDataService,
AnnotationDrawService,
AnnotationProcessingService,
];
const services = [DossiersDialogService, ManualAnnotationService, AnnotationProcessingService];
@NgModule({
declarations: [...components],

View File

@ -5,10 +5,15 @@
[showCloseButton]="true"
[viewModeSelection]="viewModeSelection"
>
<redaction-file-download-btn [files]="entitiesService.all$ | async" tooltipPosition="below"></redaction-file-download-btn>
<redaction-file-download-btn
[disabled]="listingService.areSomeSelected$ | async"
[files]="entitiesService.all$ | async"
tooltipPosition="below"
></redaction-file-download-btn>
<iqser-circle-button
(action)="exportFilesAsCSV()"
[disabled]="listingService.areSomeSelected$ | async"
[tooltip]="'dossier-overview.header-actions.download-csv' | translate"
icon="iqser:csv"
tooltipPosition="below"
@ -17,7 +22,8 @@
<iqser-circle-button
(action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn(dossier) && analysisForced"
[tooltipClass]="'small ' + ((listingService.areSomeSelected$ | async) ? '' : 'warn')"
[disabled]="listingService.areSomeSelected$ | async"
[tooltipClass]="'small warn'"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
[type]="circleButtonTypes.warn"
icon="iqser:refresh"

View File

@ -47,7 +47,7 @@ export class ScreenHeaderComponent implements OnInit {
) {}
ngOnInit() {
this.actionConfigs = this.configService.actionConfig(this.dossier.dossierId);
this.actionConfigs = this.configService.actionConfig(this.dossier.dossierId, this.listingService.areSomeSelected$);
}
async reanalyseDossier() {

View File

@ -1,6 +1,11 @@
<div class="needs-work">
<redaction-annotation-icon *ngIf="file.analysisRequired" [color]="analysisColor" label="A" type="square"></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="file.hasUpdates" [color]="updatedColor" label="U" type="square"></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="file.hasUpdates && file.assignee === userService.currentUser.id"
[color]="updatedColor"
label="U"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="file.hasRedactions" [color]="redactionColor" label="R" type="square"></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="file.hasImages" [color]="imageColor" label="I" type="square"></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="file.hintsOnly" [color]="hintColor" label="H" type="circle"></redaction-annotation-icon>

View File

@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { File } from '@red/domain';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { UserService } from '../../../../../../../services/user.service';
@Component({
selector: 'redaction-file-workload',
@ -12,7 +13,11 @@ import { DossiersService } from '@services/entity-services/dossiers.service';
export class FileWorkloadComponent {
@Input() file: File;
constructor(private readonly _appStateService: AppStateService, private readonly _dossiersService: DossiersService) {}
constructor(
public readonly userService: UserService,
private readonly _appStateService: AppStateService,
private readonly _dossiersService: DossiersService,
) {}
get suggestionColor() {
return this._getDictionaryColor('suggestion');

View File

@ -102,7 +102,7 @@ export class ConfigService {
};
}
actionConfig(dossierId: string): List<ActionConfig> {
actionConfig(dossierId: string, disabled$: Observable<boolean>): List<ActionConfig> {
return [
{
label: this._translateService.instant('dossier-overview.header-actions.edit'),
@ -110,6 +110,7 @@ export class ConfigService {
icon: 'iqser:edit',
hide: !this._userService.currentUser.isManager,
helpModeKey: 'edit-dossier-attributes',
disabled$,
},
];
}

View File

@ -15,19 +15,19 @@ import { FileDropOverlayService } from '@upload-download/services/file-drop-over
import { FileUploadModel } from '@upload-download/model/file-upload.model';
import { FileUploadService } from '@upload-download/services/file-upload.service';
import { StatusOverlayService } from '@upload-download/services/status-overlay.service';
import * as moment from 'moment';
import { Observable, timer } from 'rxjs';
import { Observable } from 'rxjs';
import { filter, skip, switchMap, tap } from 'rxjs/operators';
import { convertFiles, Files, handleFileDrop } from '@utils/index';
import {
CircleButtonTypes,
CustomError,
DefaultListingServices,
ErrorService,
ListingComponent,
ListingModes,
LoadingService,
NestedFilter,
OnAttach,
OnDetach,
TableColumnConfig,
TableComponent,
WorkflowConfig,
@ -37,7 +37,6 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { PermissionsService } from '@services/permissions.service';
import { ActivatedRoute, Router } from '@angular/router';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { ConfigService as AppConfigService } from '@services/config.service';
import { ConfigService } from '../config.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
@ -45,7 +44,6 @@ import { LongPressEvent } from '@shared/directives/long-press.directive';
import { UserPreferenceService } from '@services/user-preference.service';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FilesService } from '@services/entity-services/files.service';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
@Component({
templateUrl: './dossier-overview-screen.component.html',
@ -53,7 +51,7 @@ import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossierOverviewScreenComponent) }],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossierOverviewScreenComponent extends ListingComponent<File> implements OnInit, OnDestroy, OnDetach, OnAttach {
export class DossierOverviewScreenComponent extends ListingComponent<File> implements OnInit, OnDestroy, OnAttach {
readonly listingModes = ListingModes;
readonly circleButtonTypes = CircleButtonTypes;
readonly tableHeaderLabel = _('dossier-overview.table-header.title');
@ -78,21 +76,21 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
constructor(
protected readonly _injector: Injector,
private readonly _router: Router,
readonly permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _dossiersService: DossiersService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _appConfigService: AppConfigService,
private readonly _fileUploadService: FileUploadService,
private readonly _filesService: FilesService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _dossierAttributesService: DossierAttributesService,
private readonly _fileAttributesService: FileAttributesService,
readonly configService: ConfigService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _fileMapService: FilesMapService,
activatedRoute: ActivatedRoute,
private readonly _errorService: ErrorService,
readonly permissionsService: PermissionsService,
readonly configService: ConfigService,
readonly activatedRoute: ActivatedRoute,
) {
super(_injector);
this.dossierId = activatedRoute.snapshot.paramMap.get('dossierId');
@ -118,6 +116,8 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
async ngOnInit(): Promise<void> {
this._loadEntitiesFromState();
this._setRemovableSubscriptions();
this.addSubscription = this._fileMapService
.get$(this.dossierId)
.pipe(
@ -128,18 +128,17 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
this._fileDropOverlayService.initFileDropHandling(this.dossierId);
this.addSubscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL)
.pipe(
switchMap(() => this._filesService.hasChanges$(this.dossierId)),
filter(changed => changed),
switchMap(() => this._reloadFiles()),
)
.subscribe();
this.addSubscription = this.configService.listingMode$.subscribe(() => {
this._computeAllFilters();
});
this.addSubscription = this._dossiersService.dossierFileChanges$
.pipe(
filter(dossierId => dossierId === this.dossierId),
switchMap(dossierId => this._filesService.loadAll(dossierId)),
)
.subscribe();
this.addSubscription = this._dossierTemplatesService
.getEntityChanged$(this.currentDossier.dossierTemplateId)
.pipe(
@ -166,11 +165,10 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
ngOnAttach() {
this._fileDropOverlayService.initFileDropHandling(this.dossierId);
this._setRemovableSubscriptions();
this._tableComponent?.scrollToLastIndex();
}
ngOnDetach() {}
forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.areDevFeaturesEnabled;
}
@ -192,8 +190,18 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
(this._fileInput as any).nativeElement.value = null;
}
recentlyModifiedChecker = (file: File) =>
moment(file.lastUpdated).add(this._appConfigService.values.RECENT_PERIOD_IN_HOURS, 'hours').isAfter(moment());
private _setRemovableSubscriptions(): void {
this.addActiveScreenSubscription = this._dossiersService
.getEntityDeleted$(this.dossierId)
.pipe(tap(() => this._handleDeletedDossier()))
.subscribe();
}
private _handleDeletedDossier(): void {
this._errorService.set(
new CustomError(_('error.deleted-entity.dossier.label'), _('error.deleted-entity.dossier.action'), 'iqser:expand'),
);
}
private _updateFileAttributes(): void {
this._fileAttributeConfigs =
@ -204,11 +212,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<File> imple
this._computeAllFilters();
}
private async _reloadFiles() {
await this._filesService.loadAll(this.dossierId).toPromise();
this._computeAllFilters();
}
private _loadEntitiesFromState() {
this.currentDossier = this._dossiersService.find(this.dossierId);
this._computeAllFilters();

View File

@ -38,10 +38,10 @@ export class DossiersListingDetailsComponent {
private async _toDossierChartData(dossiers: Dossier[]): Promise<DoughnutChartConfig[]> {
// TODO: deleted dossiers count should come with stats
const deletedDossiers = await this.dossiersService.getDeleted();
// const deletedDossiers = await this.dossiersService.getDeleted();
return [
{ value: dossiers.length, color: 'ACTIVE', label: _('active') },
{ value: deletedDossiers.length, color: 'DELETED', label: _('archived') },
// { value: deletedDossiers.length, color: 'DELETED', label: _('archived') },
];
}

View File

@ -1,19 +1,17 @@
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier } from '@red/domain';
import { UserService } from '@services/user.service';
import { PermissionsService } from '@services/permissions.service';
import { TranslateChartService } from '@services/translate-chart.service';
import { timer } from 'rxjs';
import { Router } from '@angular/router';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { DefaultListingServicesTmp, EntitiesService, ListingComponent, OnAttach, OnDetach, TableComponent } from '@iqser/common-ui';
import { DefaultListingServicesTmp, EntitiesService, ListingComponent, OnAttach, TableComponent } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ConfigService } from '../config.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FilesService } from '@services/entity-services/files.service';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { switchMap, tap } from 'rxjs/operators';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { tap } from 'rxjs/operators';
@Component({
templateUrl: './dossiers-listing-screen.component.html',
@ -25,7 +23,7 @@ import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossiersListingScreenComponent extends ListingComponent<Dossier> implements OnInit, OnDestroy, OnAttach, OnDetach {
export class DossiersListingScreenComponent extends ListingComponent<Dossier> implements OnInit, OnAttach {
readonly currentUser = this._userService.currentUser;
readonly tableColumnConfigs = this._configService.tableConfig;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
@ -57,22 +55,13 @@ export class DossiersListingScreenComponent extends ListingComponent<Dossier> im
}
ngOnInit(): void {
this.addSubscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL)
.pipe(switchMap(() => this._dossiersService.loadAllIfChanged()))
.subscribe();
this.addSubscription = this._dossiersService.all$.pipe(tap(() => this._computeAllFilters())).subscribe();
}
ngOnAttach(): void {
this.ngOnInit();
this._tableComponent?.scrollToLastIndex();
}
ngOnDetach(): void {
this.ngOnDestroy();
}
openAddDossierDialog(): void {
this._dialogService.openDialog('addDossier', null, null, async (addResponse: { dossier: Dossier; addMembers: boolean }) => {
await this._router.navigate([addResponse.dossier.routerLink]);

View File

@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { PermissionsService } from '@services/permissions.service';
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { AnnotationActionsService } from '../../../../services/annotation-actions.service';
import { AnnotationActionsService } from '../../services/annotation-actions.service';
import { WebViewerInstance } from '@pdftron/webviewer';
import { UserService } from '@services/user.service';
import { Dossier, File } from '@red/domain';

View File

@ -8,7 +8,7 @@
<div class="comment-actions">
<iqser-circle-button
(action)="deleteComment(comment)"
(action)="deleteComment($event, comment)"
*ngIf="permissionsService.canDeleteComment(comment, file)"
[iconSize]="10"
[size]="20"

View File

@ -1,13 +1,15 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnChanges, ViewChild } from '@angular/core';
import { File, IComment } from '@red/domain';
import { ManualAnnotationService } from '../../../../services/manual-annotation.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { UserService } from '@services/user.service';
import { PermissionsService } from '@services/permissions.service';
import { InputWithActionComponent, trackBy } from '@iqser/common-ui';
import { AutoUnsubscribe, InputWithActionComponent, LoadingService, trackBy } from '@iqser/common-ui';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { CommentingService } from '../../services/commenting.service';
import { tap } from 'rxjs/operators';
@Component({
selector: 'redaction-comments',
@ -15,11 +17,11 @@ import { Observable } from 'rxjs';
styleUrls: ['./comments.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CommentsComponent {
export class CommentsComponent extends AutoUnsubscribe implements OnChanges {
@Input() annotation: AnnotationWrapper;
readonly trackBy = trackBy();
readonly file$: Observable<File>;
@HostBinding('class.hidden') private _hidden = true;
@HostBinding('class.hidden') _hidden = true;
@ViewChild(InputWithActionComponent) private readonly _input: InputWithActionComponent;
private readonly _fileId: string;
private readonly _dossierId: string;
@ -28,49 +30,59 @@ export class CommentsComponent {
readonly permissionsService: PermissionsService,
private readonly _userService: UserService,
private readonly _manualAnnotationService: ManualAnnotationService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _commentingService: CommentingService,
private readonly _loadingService: LoadingService,
private readonly _changeRef: ChangeDetectorRef,
readonly filesMapService: FilesMapService,
activatedRoute: ActivatedRoute,
) {
super();
this._fileId = activatedRoute.snapshot.paramMap.get('fileId');
this._dossierId = activatedRoute.snapshot.paramMap.get('dossierId');
this.file$ = filesMapService.watch$(this._dossierId, this._fileId);
}
addComment(value: string): void {
ngOnChanges() {
this.addSubscription = this._commentingService
.isActive$(this.annotation.id)
.pipe(
tap(active => {
this._hidden = !active;
}),
)
.subscribe();
}
async addComment(value: string): Promise<void> {
if (!value) {
return;
}
this._manualAnnotationService
this._loadingService.start();
const commentId = await this._manualAnnotationService
.addComment(value, this.annotation.id, this._dossierId, this._fileId)
.toPromise()
.then(commentId => {
this.annotation.comments.push({
text: value,
id: commentId,
annotationId: this.annotation.id,
user: this._userService.currentUser.id,
});
this._input.reset();
this._changeDetectorRef.markForCheck();
});
.toPromise();
this.annotation.comments.push({
text: value,
id: commentId,
annotationId: this.annotation.id,
user: this._userService.currentUser.id,
});
this._input.reset();
this._changeRef.markForCheck();
this._loadingService.stop();
}
toggleExpandComments($event?: MouseEvent): void {
$event?.stopPropagation();
this._hidden = !this._hidden;
this._commentingService.toggle(this.annotation.id);
}
deleteComment(comment: IComment): void {
this._manualAnnotationService
.deleteComment(comment.id, this.annotation.id, this._dossierId, this._fileId)
.toPromise()
.then(() => {
this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1);
if (!this.annotation.comments.length) {
this._hidden = true;
}
this._changeDetectorRef.markForCheck();
});
async deleteComment($event: MouseEvent, comment: IComment): Promise<void> {
$event.stopPropagation();
this._loadingService.start();
await this._manualAnnotationService.deleteComment(comment.id, this.annotation.id, this._dossierId, this._fileId).toPromise();
this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1);
this._changeRef.markForCheck();
this._loadingService.stop();
}
}

View File

@ -38,13 +38,6 @@
</div>
</div>
<div *ngIf="!isReadOnly && file.isProcessing" class="justify-center read-only d-flex">
<div class="flex-align-items-center">
<span [translate]="'file-status.processing'" class="read-only-text"></span>
<mat-progress-bar [mode]="'indeterminate'" class="w-100"></mat-progress-bar>
</div>
</div>
<div *ngIf="multiSelectActive$ | async" class="multi-select">
<div class="selected-wrapper">
<iqser-round-checkbox
@ -103,7 +96,6 @@
[file]="file"
[number]="pageNumber"
[showDottedIcon]="hasOnlyManualRedactionsAndIsExcluded(pageNumber)"
[viewedPages]="viewedPages"
></redaction-page-indicator>
</div>
@ -224,9 +216,9 @@
<ng-template #annotationFilterActionTemplate let-filter="filter">
<iqser-circle-button
(action)="toggleSkipped.emit($event)"
(action)="skippedService.toggleSkipped($event)"
*ngIf="filter.id === 'skipped'"
[icon]="hideSkipped ? 'red:visibility-off' : 'red:visibility'"
[icon]="(skippedService.hideSkipped$ | async) ? 'red:visibility-off' : 'red:visibility'"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
</ng-template>

View File

@ -28,10 +28,11 @@ import { PermissionsService } from '@services/permissions.service';
import { WebViewerInstance } from '@pdftron/webviewer';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { File, IViewedPage } from '@red/domain';
import { File } from '@red/domain';
import { ExcludedPagesService } from '../../services/excluded-pages.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { DocumentInfoService } from '../../services/document-info.service';
import { SkippedService } from '../../services/skipped.service';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -51,16 +52,13 @@ export class FileWorkloadComponent {
@Input() activeViewerPage: number;
@Input() shouldDeselectAnnotationsOnPageChange: boolean;
@Input() dialogRef: MatDialogRef<unknown>;
@Input() viewedPages: IViewedPage[];
@Input() file!: File;
@Input() hideSkipped: boolean;
@Input() annotationActionsTemplate: TemplateRef<unknown>;
@Input() viewer: WebViewerInstance;
@Output() readonly shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter<boolean>();
@Output() readonly selectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly selectPage = new EventEmitter<number>();
@Output() readonly toggleSkipped = new EventEmitter<MouseEvent>();
@Output() readonly annotationsChanged = new EventEmitter<AnnotationWrapper>();
displayedPages: number[] = [];
pagesPanelActive = true;
@ -76,6 +74,7 @@ export class FileWorkloadComponent {
readonly excludedPagesService: ExcludedPagesService,
readonly multiSelectService: MultiSelectService,
readonly documentInfoService: DocumentInfoService,
readonly skippedService: SkippedService,
private readonly _permissionsService: PermissionsService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _filterService: FilterService,
@ -225,7 +224,7 @@ export class FileWorkloadComponent {
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed'): void {
if (this._annotationsElement) {
const elements = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
const elements: HTMLElement[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
FileWorkloadComponent._scrollToFirstElement(elements, mode);
}
}
@ -235,7 +234,7 @@ export class FileWorkloadComponent {
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0 || !this._annotationsElement) {
return;
}
const elements = this._annotationsElement.nativeElement.querySelectorAll(
const elements: HTMLElement[] = this._annotationsElement.nativeElement.querySelectorAll(
`div[annotation-id="${this._firstSelectedAnnotation?.id}"].active`,
);
FileWorkloadComponent._scrollToFirstElement(elements);
@ -412,7 +411,7 @@ export class FileWorkloadComponent {
private _scrollQuickNavigationToPage(page: number) {
if (this._quickNavigationElement) {
const elements = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
const elements: HTMLElement[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
FileWorkloadComponent._scrollToFirstElement(elements);
}
}

View File

@ -6,6 +6,7 @@ import { ViewedPagesService } from '@services/entity-services/viewed-pages.servi
import { File, IViewedPage } from '@red/domain';
import { AutoUnsubscribe } from '@iqser/common-ui';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
@Component({
selector: 'redaction-page-indicator',
@ -18,7 +19,6 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
@Input() active = false;
@Input() showDottedIcon = false;
@Input() number: number;
@Input() viewedPages: IViewedPage[];
@Input() activeSelection = false;
@Output() readonly pageSelected = new EventEmitter<number>();
@ -33,25 +33,17 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
private readonly _configService: ConfigService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _permissionService: PermissionsService,
private readonly _stateService: FilePreviewStateService,
) {
super();
}
get activePage() {
return this.viewedPages?.find(p => p.page === this.number);
return this._viewedPages.find(p => p.page === this.number);
}
private _setReadState() {
const readBefore = this.read;
const activePage = this.activePage;
if (!activePage) {
this.read = false;
} else {
// console.log('setting read to',activePage.showAsUnseen, !activePage.showAsUnseen);
this.read = !activePage.showAsUnseen;
}
// console.log(this.number, readBefore, activePage, this.read);
this._changeDetectorRef.detectChanges();
private get _viewedPages(): IViewedPage[] {
return this._stateService.fileData?.viewedPages || [];
}
ngOnChanges() {
@ -87,20 +79,33 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
}
}
private _setReadState() {
const readBefore = this.read;
const activePage = this.activePage;
if (!activePage) {
this.read = false;
} else {
// console.log('setting read to',activePage.showAsUnseen, !activePage.showAsUnseen);
this.read = !activePage.showAsUnseen;
}
// console.log(this.number, readBefore, activePage, this.read);
this._changeDetectorRef.detectChanges();
}
private async _markPageRead() {
await this._viewedPagesService.addPage({ page: this.number }, this.file.dossierId, this.file.fileId).toPromise();
if (this.activePage) {
this.activePage.showAsUnseen = false;
} else {
this.viewedPages?.push({ page: this.number, fileId: this.file.fileId });
this._viewedPages.push({ page: this.number, fileId: this.file.fileId });
}
this._setReadState();
}
private async _markPageUnread() {
await this._viewedPagesService.removePage(this.file.dossierId, this.file.fileId, this.number).toPromise();
this.viewedPages?.splice(
this.viewedPages?.findIndex(p => p.page === this.number),
this._viewedPages.splice(
this._viewedPages.findIndex(p => p.page === this.number),
1,
);
this._setReadState();

View File

@ -22,12 +22,12 @@ import {
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualAnnotationService } from '../../../../services/manual-annotation.service';
import { environment } from '@environments/environment';
import { AnnotationDrawService } from '../../../../services/annotation-draw.service';
import { AnnotationActionsService } from '../../../../services/annotation-actions.service';
import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationActionsService } from '../../services/annotation-actions.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { BASE_HREF } from '../../../../../../tokens';
import { ConfigService } from '@services/config.service';
import { ConfirmationDialogInput, LoadingService } from '@iqser/common-ui';
import { AutoUnsubscribe, ConfirmationDialogInput, LoadingService, shareDistinctLast } from '@iqser/common-ui';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { loadCompareDocumentWrapper } from '../../../../utils/compare-mode.utils';
import { PdfViewerUtils } from '../../../../utils/pdf-viewer.utils';
@ -36,11 +36,13 @@ import { ActivatedRoute } from '@angular/router';
import { toPosition } from '../../../../utils/pdf-calculation.utils';
import { ViewModeService } from '../../services/view-mode.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { filter, switchMap, tap } from 'rxjs/operators';
import Tools = Core.Tools;
import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
const ALLOWED_KEYBOARD_SHORTCUTS = ['+', '-', 'p', 'r', 'Escape'] as const;
const ALLOWED_KEYBOARD_SHORTCUTS: readonly string[] = ['+', '-', 'p', 'r', 'Escape'] as const;
const dataElements = {
ADD_REDACTION: 'add-redaction',
ADD_DICTIONARY: 'add-dictionary',
@ -61,8 +63,7 @@ const dataElements = {
templateUrl: './pdf-viewer.component.html',
styleUrls: ['./pdf-viewer.component.scss'],
})
export class PdfViewerComponent implements OnInit, OnChanges {
@Input() fileData: Blob;
export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges {
@Input() file: File;
@Input() dossier: Dossier;
@Input() canPerformActions = false;
@ -95,9 +96,12 @@ export class PdfViewerComponent implements OnInit, OnChanges {
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _configService: ConfigService,
private readonly _loadingService: LoadingService,
private readonly _stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
) {}
) {
super();
}
private get _toggleTooltipsBtnTitle(): string {
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
@ -105,9 +109,27 @@ export class PdfViewerComponent implements OnInit, OnChanges {
});
}
private get _toggleTooltipsIcon(): string {
return this._convertPath(
this._userPreferenceService.getFilePreviewTooltipsPreference()
? '/assets/icons/general/pdftron-action-enable-tooltips.svg'
: '/assets/icons/general/pdftron-action-disable-tooltips.svg',
);
}
async ngOnInit() {
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this);
await this.loadViewer();
await this._loadViewer();
this.addActiveScreenSubscription = this._stateService.fileData$
.pipe(
filter(fileData => !!fileData),
switchMap(fileData => fileData.blob$),
// Skip document reload if file content hasn't changed
shareDistinctLast(),
tap(() => this._loadDocument()),
)
.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
@ -115,16 +137,12 @@ export class PdfViewerComponent implements OnInit, OnChanges {
return;
}
if (changes.fileData) {
this._loadDocument();
}
if (changes.canPerformActions) {
this._handleCustomActions();
}
}
uploadFile(files: any) {
uploadFile(files: FileList) {
const fileToCompare = files[0];
this.compareFileInput.nativeElement.value = null;
@ -140,7 +158,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer);
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer());
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this._stateService.fileData.blob$.value.arrayBuffer());
const loadCompareDocument = async () => {
this._loadingService.start();
@ -153,7 +171,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.instance,
this.file,
() => {
this.viewModeService.set('COMPARE');
this.viewModeService.compareMode = true;
},
() => {
this.utils.navigateToPage(1);
@ -190,10 +208,10 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
async closeCompareMode() {
this.viewModeService.set('STANDARD');
this.viewModeService.compareMode = false;
const pdfNet = this.instance.Core.PDFNet;
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer());
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this._stateService.fileData.blob$.value.arrayBuffer());
this.instance.UI.loadDocument(currentDocument, {
filename: this.file ? this.file.filename : 'document.pdf',
});
@ -202,7 +220,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.utils.navigateToPage(1);
}
async loadViewer() {
private async _loadViewer() {
this.instance = await WebViewer(
{
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
@ -211,7 +229,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
css: this._convertPath('/assets/pdftron/stylesheet.css'),
backendType: 'ems',
},
this.viewer.nativeElement,
this.viewer.nativeElement as HTMLElement,
);
this.documentViewer = this.instance.Core.documentViewer;
@ -223,7 +241,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.utils.disableHotkeys();
this._configureTextPopup();
this.annotationManager.addEventListener('annotationSelected', (annotations, action) => {
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => {
this.annotationSelected.emit(this.annotationManager.getSelectedAnnotations().map(ann => ann.Id));
if (action === 'deselected') {
this._toggleRectangleAnnotationAction(true);
@ -233,7 +251,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
});
this.annotationManager.addEventListener('annotationChanged', annotations => {
this.annotationManager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
// when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing
@ -243,7 +261,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
});
this.documentViewer.addEventListener('pageNumberUpdated', pageNumber => {
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
if (this.shouldDeselectAnnotationsOnPageChange) {
this.utils.deselectAllAnnotations();
}
@ -253,9 +271,9 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.documentViewer.addEventListener('documentLoaded', this._setReadyAndInitialState);
this.documentViewer.addEventListener('keyUp', $event => {
this.documentViewer.addEventListener('keyUp', ($event: KeyboardEvent) => {
// arrows and full-screen
if ($event.target?.tagName?.toLowerCase() !== 'input') {
if (($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input') {
if ($event.key.startsWith('Arrow') || $event.key === 'f') {
this._ngZone.run(() => {
this.keyUp.emit($event);
@ -265,7 +283,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
}
if (ALLOWED_KEYBOARD_SHORTCUTS.indexOf($event.key) < 0) {
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
$event.preventDefault();
$event.stopPropagation();
}
@ -293,8 +311,6 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
}
});
this._loadDocument();
}
private _setInitialDisplayMode() {
@ -354,13 +370,14 @@ export class PdfViewerComponent implements OnInit, OnChanges {
type: 'actionButton',
element: 'tooltips',
dataElement: dataElements.TOGGLE_TOOLTIPS,
img: this._convertPath('/assets/icons/general/pdftron-action-toggle-tooltips.svg'),
img: this._toggleTooltipsIcon,
title: this._toggleTooltipsBtnTitle,
onClick: async () => {
await this._userPreferenceService.toggleFilePreviewTooltipsPreference();
this._updateTooltipsVisibility();
this.instance.UI.updateElement(dataElements.TOGGLE_TOOLTIPS, {
title: this._toggleTooltipsBtnTitle,
img: this._toggleTooltipsIcon,
});
},
},
@ -381,36 +398,34 @@ export class PdfViewerComponent implements OnInit, OnChanges {
const originalHeaderItems = header.getItems();
originalHeaderItems.splice(8, 0, ...headerItems);
if (this._userPreferenceService.areDevFeaturesEnabled) {
const devHeaderItems = [
{
type: 'actionButton',
element: 'compare',
dataElement: dataElements.COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
title: 'Compare',
onClick: () => {
this.compareFileInput.nativeElement.click();
},
const compareHeaderItems = [
{
type: 'actionButton',
element: 'compare',
dataElement: dataElements.COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
title: 'Compare',
onClick: () => {
this.compareFileInput.nativeElement.click();
},
{
type: 'actionButton',
element: 'closeCompare',
dataElement: dataElements.CLOSE_COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
title: 'Leave Compare Mode',
onClick: async () => {
await this.closeCompareMode();
},
},
{
type: 'actionButton',
element: 'closeCompare',
dataElement: dataElements.CLOSE_COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
title: 'Leave Compare Mode',
onClick: async () => {
await this.closeCompareMode();
},
{
type: 'divider',
dataElement: dataElements.COMPARE_TOOL_DIVIDER,
},
];
},
{
type: 'divider',
dataElement: dataElements.COMPARE_TOOL_DIVIDER,
},
];
originalHeaderItems.splice(9, 0, ...devHeaderItems);
}
originalHeaderItems.splice(9, 0, ...compareHeaderItems);
});
this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]);
@ -561,7 +576,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads = this.documentViewer.getSelectedTextQuads();
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.documentViewer.getSelectedTextQuads();
const text = this.documentViewer.getSelectedText();
const manualRedaction = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type));
@ -617,7 +632,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
for (const quad of quads[key]) {
const page = parseInt(key, 10);
const pageHeight = this.documentViewer.getPageHeight(page);
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuads(page, quad) : quad));
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuad(page, quad) : quad));
}
}
@ -627,11 +642,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
}
private _loadDocument() {
if (!this.fileData) {
return;
}
this.instance.UI.loadDocument(this.fileData, {
this.instance.UI.loadDocument(this._stateService.fileData.blob$.value, {
filename: this.file ? this.file.filename : 'document.pdf',
});
}
@ -640,7 +651,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this._ngZone.run(() => {
this.utils.ready = true;
this.viewerReady.emit(this.instance);
const routePageNumber = this._activatedRoute.snapshot.queryParams.page;
const routePageNumber: number = this._activatedRoute.snapshot.queryParams.page;
this.pageChanged.emit(routePageNumber || 1);
this._setInitialDisplayMode();
this._updateTooltipsVisibility();

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="viewModeService.viewMode$ | async as viewMode">
<div
<button
(click)="switchView.emit('STANDARD')"
[class.active]="viewModeService.isStandard"
[matTooltip]="'file-preview.standard-tooltip' | translate"
@ -7,27 +7,27 @@
iqserHelpMode="standard-view"
>
{{ 'file-preview.standard' | translate }}
</div>
</button>
<div
(click)="canSwitchToDeltaView && switchView.emit('DELTA')"
<button
(click)="switchView.emit('DELTA')"
[class.active]="viewModeService.isDelta"
[class.disabled]="!canSwitchToDeltaView"
[disabled]="(canSwitchToDeltaView$ | async) === false"
[matTooltip]="'file-preview.delta-tooltip' | translate"
class="red-tab"
iqserHelpMode="delta-view"
>
{{ 'file-preview.delta' | translate }}
</div>
</button>
<div
<button
(click)="canSwitchToRedactedView && switchView.emit('REDACTED')"
[class.active]="viewModeService.isRedacted"
[class.disabled]="!canSwitchToRedactedView"
[disabled]="!canSwitchToRedactedView"
[matTooltip]="'file-preview.redacted-tooltip' | translate"
class="red-tab"
iqserHelpMode="preview-view"
>
{{ 'file-preview.redacted' | translate }}
</div>
</button>
</ng-container>

View File

@ -1,10 +1,12 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { File, ViewMode } from '@red/domain';
import { ViewModeService } from '../../services/view-mode.service';
import { FileDataModel } from '@models/file/file-data.model';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { Observable } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
@Component({
selector: 'redaction-view-switch [file] [fileData]',
selector: 'redaction-view-switch [file]',
templateUrl: './view-switch.component.html',
styleUrls: ['./view-switch.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@ -12,19 +14,18 @@ import { FileDataModel } from '@models/file/file-data.model';
export class ViewSwitchComponent implements OnChanges {
@Output() readonly switchView = new EventEmitter<ViewMode>();
@Input() file: File;
@Input() fileData: FileDataModel;
canSwitchToDeltaView = false;
readonly canSwitchToDeltaView$: Observable<boolean>;
canSwitchToRedactedView = false;
constructor(readonly viewModeService: ViewModeService) {}
constructor(readonly viewModeService: ViewModeService, private readonly _stateService: FilePreviewStateService) {
this.canSwitchToDeltaView$ = this._stateService.fileData$.pipe(
filter(fileData => !!fileData),
switchMap(fileData => fileData?.hasChangeLog$),
);
}
ngOnChanges(changes: SimpleChanges) {
if (changes.fileData) {
const fileData = changes.fileData.currentValue as FileDataModel;
this.canSwitchToDeltaView = fileData?.hasChangeLog;
}
if (changes.file) {
const file = changes?.file.currentValue as File;
this.canSwitchToRedactedView = !file.analysisRequired && !file.excluded;

View File

@ -1,9 +1,9 @@
<ng-container *ngIf="dossier$ | async as dossier">
<ng-container *ngIf="file$ | async as file">
<ng-container *ngIf="fileData?.file$ | async as file">
<section [class.fullscreen]="fullScreen">
<div class="page-header">
<div class="flex flex-1">
<redaction-view-switch (switchView)="switchView($event)" [fileData]="fileData" [file]="file"></redaction-view-switch>
<redaction-view-switch (switchView)="switchView($event)" [file]="file"></redaction-view-switch>
</div>
<div class="flex-1 actions-container">
@ -76,7 +76,6 @@
[canPerformActions]="canPerformAnnotationActions$ | async"
[class.hidden]="!ready"
[dossier]="dossier"
[fileData]="fileData?.fileData"
[file]="file"
[shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange"
></redaction-pdf-viewer>
@ -102,7 +101,6 @@
(deselectAnnotations)="deselectAnnotations($event)"
(selectAnnotations)="selectAnnotations($event)"
(selectPage)="selectPage($event)"
(toggleSkipped)="toggleSkipped($event)"
*ngIf="!file.excluded"
[(shouldDeselectAnnotationsOnPageChange)]="shouldDeselectAnnotationsOnPageChange"
[activeViewerPage]="activeViewerPage"
@ -110,9 +108,7 @@
[annotations]="visibleAnnotations"
[dialogRef]="dialogRef"
[file]="file"
[hideSkipped]="hideSkipped"
[selectedAnnotations]="selectedAnnotations"
[viewedPages]="fileData?.viewedPages"
[viewer]="activeViewer"
></redaction-file-workload>
</div>

View File

@ -5,21 +5,21 @@ import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component
import {
AutoUnsubscribe,
CircleButtonTypes,
CustomError,
Debounce,
ErrorService,
FilterService,
LoadingService,
OnAttach,
OnDetach,
processFilters,
shareDistinctLast,
shareLast,
} from '@iqser/common-ui';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
import { FileDataModel } from '@models/file/file-data.model';
import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { Dossier, File, ViewMode } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
@ -35,7 +35,7 @@ import { handleFilterDelta } from '@utils/filter-utils';
import { FilesService } from '@services/entity-services/files.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { filter, map, switchMap, tap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { WatermarkService } from '@shared/services/watermark.service';
import { ExcludedPagesService } from './services/excluded-pages.service';
@ -43,23 +43,33 @@ import { ViewModeService } from './services/view-mode.service';
import { MultiSelectService } from './services/multi-select.service';
import { DocumentInfoService } from './services/document-info.service';
import { ReanalysisService } from '../../../../services/reanalysis.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { CommentingService } from './services/commenting.service';
import { SkippedService } from './services/skipped.service';
import { AnnotationActionsService } from './services/annotation-actions.service';
import { FilePreviewStateService } from './services/file-preview-state.service';
import { FileDataModel } from '../../../../models/file/file-data.model';
import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet;
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'];
function diff<T>(first: readonly T[], second: readonly T[]): T[] {
// symmetrical difference between two arrays
const a = new Set(first);
const b = new Set(second);
return [...first.filter(x => !b.has(x)), ...second.filter(x => !a.has(x))];
}
@Component({
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss'],
providers: [FilterService, ExcludedPagesService, ViewModeService, MultiSelectService, DocumentInfoService],
providers: [
FilterService,
ExcludedPagesService,
ViewModeService,
MultiSelectService,
DocumentInfoService,
CommentingService,
SkippedService,
AnnotationDrawService,
AnnotationActionsService,
FilePreviewStateService,
PdfViewerDataService,
],
})
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach {
readonly circleButtonTypes = CircleButtonTypes;
@ -67,16 +77,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
dialogRef: MatDialogRef<unknown>;
fullScreen = false;
shouldDeselectAnnotationsOnPageChange = true;
fileData: FileDataModel;
selectedAnnotations: AnnotationWrapper[] = [];
hideSkipped = false;
displayPdfViewer = false;
activeViewerPage: number = null;
@ViewChild(PdfViewerComponent) readonly viewerComponent: PdfViewerComponent;
readonly dossierId: string;
readonly canPerformAnnotationActions$: Observable<boolean>;
readonly dossier$: Observable<Dossier>;
readonly file$: Observable<File>;
readonly fileId: string;
ready = false;
private _instance: WebViewerInstance;
@ -91,6 +98,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
constructor(
readonly permissionsService: PermissionsService,
readonly userPreferenceService: UserPreferenceService,
private readonly _stateService: FilePreviewStateService,
private readonly _watermarkService: WatermarkService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _activatedRoute: ActivatedRoute,
@ -98,7 +106,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _router: Router,
private readonly _annotationProcessingService: AnnotationProcessingService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _fileDownloadService: PdfViewerDataService,
private readonly _pdfViewerDataService: PdfViewerDataService,
private readonly _filesService: FilesService,
private readonly _ngZone: NgZone,
private readonly _fileManagementService: FileManagementService,
@ -108,6 +116,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _filesMapService: FilesMapService,
private readonly _dossiersService: DossiersService,
private readonly _reanalysisService: ReanalysisService,
private readonly _errorService: ErrorService,
private readonly _skippedService: SkippedService,
readonly excludedPagesService: ExcludedPagesService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
@ -117,12 +127,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId);
this.fileId = _activatedRoute.snapshot.paramMap.get('fileId');
this.file$ = _filesMapService.watch$(this.dossierId, this.fileId).pipe(
tap(async file => {
await this._reloadFile(file);
}),
shareLast(),
);
this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
document.documentElement.addEventListener('fullscreenchange', () => {
@ -144,8 +148,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._instance;
}
get fileData(): FileDataModel {
return this._stateService.fileData;
}
private get _canPerformAnnotationActions$() {
return combineLatest([this.file$, this.viewModeService.viewMode$]).pipe(
return combineLatest([
this._stateService.fileData$.pipe(switchMap(fileData => fileData.file$)),
this.viewModeService.viewMode$,
this.viewModeService.compareMode$,
]).pipe(
map(([file, viewMode]) => this.permissionsService.canPerformAnnotationActions(file) && viewMode === 'STANDARD'),
shareDistinctLast(),
);
@ -190,7 +202,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
ngOnDetach(): void {
this.displayPdfViewer = false;
super.ngOnDestroy();
super.ngOnDetach();
this._changeDetectorRef.markForCheck();
}
@ -199,6 +211,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
if (!file.canBeOpened) {
return this._router.navigate([this._dossiersService.find(this.dossierId)?.routerLink]);
}
this.viewModeService.viewMode = 'STANDARD';
await this.ngOnInit();
this._lastPage = previousRoute.queryParams.page;
@ -250,11 +263,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
});
console.log(`[REDACTION] Process time: ${new Date().getTime() - processStartTime} ms`);
console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`);
console.log();
}
handleAnnotationSelected(annotationIds: string[]) {
// TODO: use includes() here
this.selectedAnnotations = annotationIds
.map(id => this.visibleAnnotations.find(annotationWrapper => annotationWrapper.id === id))
.filter(ann => ann !== undefined);
@ -268,7 +279,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
@Debounce(10)
selectAnnotations(annotations?: AnnotationWrapper[]) {
if (annotations) {
this.viewerComponent?.utils?.selectAnnotations(annotations, this.multiSelectService.isActive);
const annotationsToSelect = this.multiSelectService.isActive ? [...this.selectedAnnotations, ...annotations] : annotations;
this.viewerComponent?.utils?.selectAnnotations(annotationsToSelect, this.multiSelectService.isActive);
} else {
this.viewerComponent?.utils?.deselectAllAnnotations();
}
@ -387,7 +399,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
});
// Go to initial page from query params
const pageNumber = this._lastPage || this._activatedRoute.snapshot.queryParams.page;
const pageNumber: string = this._lastPage || this._activatedRoute.snapshot.queryParams.page;
if (pageNumber) {
setTimeout(() => {
this.selectPage(parseInt(pageNumber, 10));
@ -403,6 +415,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async annotationsChangedByReviewAction(annotation: AnnotationWrapper) {
this.multiSelectService.deactivate();
await this._reloadAnnotationsForPage(annotation?.pageNumber || this.activeViewerPage);
}
@ -413,7 +426,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async switchView(viewMode: ViewMode) {
this.viewModeService.set(viewMode);
this.viewModeService.viewMode = viewMode;
await this.updateViewMode();
this._scrollViews();
}
@ -425,16 +438,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
download(data, file.filename);
}
toggleSkipped($event) {
$event.stopPropagation();
$event.preventDefault();
this.hideSkipped = !this.hideSkipped;
this._handleIgnoreAnnotationsDrawing();
return false;
}
private _setActiveViewerPage() {
const currentPage = this._instance?.Core.documentViewer?.getCurrentPage();
if (!currentPage) {
@ -464,17 +467,20 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
private async _reloadFile(file: File): Promise<void> {
const previousFile = this.fileData?.file;
await this._loadFileData(file);
await this._stampPDF();
// file already loaded at least once
if (previousFile) {
// If it has been OCRd, we need to wait for it to load into the viewer
if (previousFile.lastOCRTime !== this.fileData?.file?.lastOCRTime) {
return;
}
}
}
private async _stampPDF() {
if (!this._instance) {
return;
}
const document = this._instance.Core.documentViewer.getDocument();
if (!document) {
if (!this._instance?.Core.documentViewer.getDocument()) {
return;
}
@ -517,7 +523,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
await stampPDFPage(
document,
this._instance.Core.PDFNet,
this._translateService.instant('file-preview.excluded-from-redaction'),
this._translateService.instant('file-preview.excluded-from-redaction') as string,
17,
'courier',
'TOP_LEFT',
@ -528,19 +534,54 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
}
private async _fileUpdated(file: File): Promise<File> {
if (!this.fileData || file.lastProcessed === this.fileData.file.lastProcessed) {
await this._reloadFile(file);
} else {
// File reanalysed
const previousAnnotations = this.visibleAnnotations;
await this._loadFileData(file);
await this._reloadAnnotations(previousAnnotations);
}
return file;
}
private _subscribeToFileUpdates(): void {
this.addSubscription = timer(0, 5000)
this.addActiveScreenSubscription = this._filesMapService
.watch$(this.dossierId, this.fileId)
.pipe(switchMap(file => this._fileUpdated(file)))
.subscribe();
this.addActiveScreenSubscription = timer(0, 5000)
.pipe(switchMap(() => this._filesService.reload(this.dossierId, this.fileId)))
.subscribe();
this.addSubscription = this._filesMapService.fileReanalysed$
.pipe(filter(file => file.fileId === this.fileId))
.subscribe(async file => {
if (file.lastProcessed !== this.fileData?.file.lastProcessed) {
await this._loadFileData(file);
await this._reloadAnnotations();
}
this._loadingService.stop();
});
this.addActiveScreenSubscription = this._dossiersService
.getEntityDeleted$(this.dossierId)
.pipe(tap(() => this._handleDeletedDossier()))
.subscribe();
this.addActiveScreenSubscription = this._filesMapService
.watchDeleted$(this.fileId)
.pipe(tap(() => this._handleDeletedFile()))
.subscribe();
this.addActiveScreenSubscription = this._skippedService.hideSkipped$
.pipe(tap(hideSkipped => this._handleIgnoreAnnotationsDrawing(hideSkipped)))
.subscribe();
}
private _handleDeletedDossier(): void {
this._errorService.set(
new CustomError(_('error.deleted-entity.file-dossier.label'), _('error.deleted-entity.file-dossier.action'), 'iqser:expand'),
);
}
private _handleDeletedFile(): void {
this._errorService.set(
new CustomError(_('error.deleted-entity.file.label'), _('error.deleted-entity.file.action'), 'iqser:expand'),
);
}
private async _loadFileData(file: File): Promise<void | boolean> {
@ -548,13 +589,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._router.navigate([this._dossiersService.find(this.dossierId).routerLink]);
}
const fileData = await this._fileDownloadService.loadDataFor(file, this.fileData).toPromise();
const fileData = await this._pdfViewerDataService.loadDataFor(file).toPromise();
if (file.isPending) {
return;
}
this.fileData = fileData;
this._stateService.fileData = fileData;
}
@Debounce(0)
@ -563,13 +604,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._workloadComponent?.scrollAnnotations();
}
private async _reloadAnnotations() {
this.fileData.redactionLog = await this._fileDownloadService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
this._instance.Core.annotationManager.deleteAnnotations(this._instance.Core.annotationManager.getAnnotationsList(), {
imported: true,
force: true,
});
await this._cleanupAndRedrawAnnotations();
private async _reloadAnnotations(previousAnnotations?: AnnotationWrapper[]) {
this._deleteAnnotations();
await this._cleanupAndRedrawAnnotations(previousAnnotations);
}
private async _reloadAnnotationsForPage(page: number) {
@ -582,24 +619,42 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
const currentPageAnnotations = this.visibleAnnotations.filter(a => a.pageNumber === page);
this.fileData.redactionLog = await this._fileDownloadService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
this.fileData.redactionLog = await this._pdfViewerDataService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
this._deleteAnnotations(currentPageAnnotations);
await this._cleanupAndRedrawAnnotations(currentPageAnnotations, annotation => annotation.pageNumber === page);
}
private _deleteAnnotations(annotationsToDelete?: AnnotationWrapper[]) {
if (!this._instance?.Core.documentViewer.getDocument()) {
return;
}
if (!annotationsToDelete) {
this._instance.Core.annotationManager.deleteAnnotations(this._instance.Core.annotationManager.getAnnotationsList(), {
imported: true,
force: true,
});
}
annotationsToDelete?.forEach(annotation => {
this._findAndDeleteAnnotation(annotation.id);
});
}
private async _cleanupAndRedrawAnnotations(
annotationsToDelete?: AnnotationWrapper[],
currentAnnotations?: AnnotationWrapper[],
newAnnotationsFilter?: (annotation: AnnotationWrapper) => boolean,
) {
if (!this._instance?.Core.documentViewer.getDocument()) {
return;
}
this.rebuildFilters();
if (this.viewModeService.viewMode === 'STANDARD') {
const startTime = new Date().getTime();
annotationsToDelete?.forEach(annotation => {
this._findAndDeleteAnnotation(annotation.id);
});
const newAnnotations = newAnnotationsFilter ? this.allAnnotations.filter(newAnnotationsFilter) : this.allAnnotations;
this._handleDeltaAnnotationFilters(annotationsToDelete ?? [], newAnnotations);
const newAnnotations = newAnnotationsFilter ? this.visibleAnnotations.filter(newAnnotationsFilter) : this.visibleAnnotations;
this._handleDeltaAnnotationFilters(currentAnnotations ?? [], newAnnotations);
await this._redrawAnnotations(newAnnotations);
console.log(
`[REDACTION] Annotations redraw time: ${new Date().getTime() - startTime} ms for ${newAnnotations.length} annotations`,
@ -613,7 +668,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
annotations,
this.fileId,
this.dossierId,
this.hideSkipped,
!!this.viewModeService.isCompare,
);
}
@ -630,6 +684,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const oldPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(currentPageAnnotations);
const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newPageAnnotations);
handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, primaryFilters);
this._filterService.addFilterGroup({
...primaryFilterGroup,
@ -655,9 +710,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
}
private _handleIgnoreAnnotationsDrawing() {
private _handleIgnoreAnnotationsDrawing(hideSkipped: boolean): void {
const ignored = this._getAnnotations(a => a.getCustomData('skipped'));
return this.hideSkipped ? this._hide(ignored) : this._show(ignored);
if (hideSkipped) {
this._hide(ignored);
} else {
this._show(ignored);
}
}
private _getAnnotations(predicate: (value) => unknown) {

View File

@ -1,19 +1,19 @@
import { EventEmitter, Inject, Injectable, NgZone } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { ManualAnnotationService } from './manual-annotation.service';
import { ManualAnnotationService } from '../../../services/manual-annotation.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { getFirstRelevantTextPart } from '@utils/functions';
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { DossiersDialogService } from './dossiers-dialog.service';
import { BASE_HREF } from '../../../tokens';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { BASE_HREF } from '../../../../../tokens';
import { UserService } from '@services/user.service';
import { Core, WebViewerInstance } from '@pdftron/webviewer';
import { Dossier, File, IAddRedactionRequest, ILegalBasisChangeRequest, IRectangle, IResizeRequest } from '@red/domain';
import { toPosition } from '../utils/pdf-calculation.utils';
import { toPosition } from '../../../utils/pdf-calculation.utils';
import { AnnotationDrawService } from './annotation-draw.service';
import { translateQuads } from '../../../utils';
import { translateQuads } from '@utils/pdf-coordinates';
import { DossiersService } from '@services/entity-services/dossiers.service';
import Annotation = Core.Annotations.Annotation;

View File

@ -5,10 +5,11 @@ import { AppStateService } from '@state/app-state.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { UserPreferenceService } from '@services/user-preference.service';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { RedactionLogService } from './redaction-log.service';
import { RedactionLogService } from '../../../services/redaction-log.service';
import { environment } from '@environments/environment';
import { IRectangle, ISectionGrid, ISectionRectangle } from '@red/domain';
import { SkippedService } from './skipped.service';
import Annotation = Core.Annotations.Annotation;
@Injectable()
@ -18,6 +19,7 @@ export class AnnotationDrawService {
private readonly _dossiersService: DossiersService,
private readonly _redactionLogService: RedactionLogService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _skippedService: SkippedService,
) {}
async drawAnnotations(
@ -25,7 +27,6 @@ export class AnnotationDrawService {
annotationWrappers: AnnotationWrapper[],
fileId: string,
dossierId: string,
hideSkipped = false,
compareMode = false,
) {
if (!activeViewer) {
@ -36,7 +37,7 @@ export class AnnotationDrawService {
await pdfNet.runWithCleanup(
async () => {
await this._drawAnnotations(activeViewer, annotationWrappers, fileId, dossierId, hideSkipped, compareMode);
await this._drawAnnotations(activeViewer, annotationWrappers, fileId, dossierId, compareMode);
},
environment.licenseKey ? atob(environment.licenseKey) : null,
);
@ -82,12 +83,9 @@ export class AnnotationDrawService {
annotationWrappers: AnnotationWrapper[],
fileId: string,
dossierId: string,
hideSkipped: boolean,
compareMode: boolean,
) {
const annotations = annotationWrappers.map(annotation =>
this._computeAnnotation(activeViewer, annotation, dossierId, hideSkipped, compareMode),
);
const annotations = annotationWrappers.map(annotation => this._computeAnnotation(activeViewer, annotation, dossierId, compareMode));
const annotationManager = activeViewer.Core.annotationManager;
annotationManager.addAnnotations(annotations, { imported: true });
await annotationManager.drawAnnotationsFromList(annotations);
@ -143,7 +141,6 @@ export class AnnotationDrawService {
activeViewer: WebViewerInstance,
annotationWrapper: AnnotationWrapper,
dossierId: string,
hideSkipped: boolean,
compareMode: boolean,
) {
const pageNumber = compareMode ? annotationWrapper.pageNumber * 2 - 1 : annotationWrapper.pageNumber;
@ -177,7 +174,7 @@ export class AnnotationDrawService {
annotation.Hidden =
annotationWrapper.isChangeLogRemoved ||
(hideSkipped && annotationWrapper.isSkipped) ||
(this._skippedService.hideSkipped && annotationWrapper.isSkipped) ||
annotationWrapper.isOCR ||
annotationWrapper.hidden;
annotation.setCustomData('redact-manager', 'true');

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { shareDistinctLast } from '@iqser/common-ui';
@Injectable()
export class CommentingService {
private _activeAnnotations = new BehaviorSubject<Set<string>>(new Set<string>());
isActive$(annotationId: string): Observable<boolean> {
return this._activeAnnotations.pipe(
map(annotations => annotations.has(annotationId)),
startWith(false),
shareDistinctLast(),
);
}
toggle(annotationId: string): void {
if (this._activeAnnotations.value.has(annotationId)) {
this._deactivate(annotationId);
} else {
this._activate(annotationId);
}
}
private _activate(annotationId: string): void {
const currentValue = this._activeAnnotations.value;
const newSet = new Set<string>(currentValue).add(annotationId);
this._activeAnnotations.next(newSet);
}
private _deactivate(annotationId: string): void {
const currentValue = this._activeAnnotations.value;
const newSet = new Set<string>(currentValue);
newSet.delete(annotationId);
this._activeAnnotations.next(newSet);
}
}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { FileDataModel } from '@models/file/file-data.model';
@Injectable()
export class FilePreviewStateService {
readonly fileData$: Observable<FileDataModel>;
private readonly _fileData$ = new BehaviorSubject<FileDataModel>(undefined);
constructor() {
this.fileData$ = this._fileData$.asObservable();
}
get fileData(): FileDataModel {
return this._fileData$.value;
}
set fileData(fileDataModel: FileDataModel) {
this._fileData$.next(fileDataModel);
}
}

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { skip } from 'rxjs/operators';
import { shareDistinctLast } from '@iqser/common-ui';
@Injectable()
export class SkippedService {
readonly hideSkipped$: Observable<boolean>;
private readonly _hideSkipped$ = new BehaviorSubject(false);
constructor() {
this.hideSkipped$ = this._hideSkipped$.pipe(shareDistinctLast(), skip(1));
}
get hideSkipped(): boolean {
return this._hideSkipped$.value;
}
toggleSkipped($event): void {
$event.stopPropagation();
$event.preventDefault();
this._hideSkipped$.next(!this.hideSkipped);
}
}

View File

@ -5,10 +5,14 @@ import { ViewMode } from '@red/domain';
@Injectable()
export class ViewModeService {
readonly viewMode$: Observable<ViewMode>;
readonly compareMode$: Observable<Boolean>;
private readonly _viewMode$ = new BehaviorSubject<ViewMode>('STANDARD');
private readonly _compareMode$ = new BehaviorSubject<Boolean>(false);
constructor() {
this.viewMode$ = this._viewMode$.asObservable();
this.compareMode$ = this._compareMode$.asObservable();
}
get viewMode() {
@ -23,15 +27,19 @@ export class ViewModeService {
return this._viewMode$.value === 'DELTA';
}
get isCompare() {
return this._viewMode$.value === 'COMPARE';
}
get isRedacted() {
return this._viewMode$.value === 'REDACTED';
}
set(mode: ViewMode) {
set viewMode(mode: ViewMode) {
this._viewMode$.next(mode);
}
get isCompare() {
return this._compareMode$.value;
}
set compareMode(compareMode: boolean) {
this._compareMode$.next(compareMode);
}
}

View File

@ -30,6 +30,7 @@ export class DossiersDialogService extends DialogService<DialogType> {
protected readonly _config: DialogConfig<DialogType> = {
confirm: {
component: ConfirmationDialogComponent,
dialogConfig: { disableClose: false },
},
documentInfo: {
component: DocumentInfoDialogComponent,
@ -45,6 +46,7 @@ export class DossiersDialogService extends DialogService<DialogType> {
},
assignFile: {
component: AssignReviewerApproverDialogComponent,
dialogConfig: { disableClose: false },
},
recategorizeImage: {
component: RecategorizeImageDialogComponent,

View File

@ -3,13 +3,14 @@ import { forkJoin, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { FileDataModel } from '@models/file/file-data.model';
import { PermissionsService } from '@services/permissions.service';
import { File } from '@red/domain';
import { File, IRedactionLog, IViewedPage } from '@red/domain';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { RedactionLogService } from './redaction-log.service';
import { ViewedPagesService } from '@services/entity-services/viewed-pages.service';
import { AppStateService } from '../../../state/app-state.service';
import { DossiersService } from '../../../services/entity-services/dossiers.service';
import { UserPreferenceService } from '../../../services/user-preference.service';
import { FilePreviewStateService } from '../screens/file-preview-screen/services/file-preview-state.service';
@Injectable()
export class PdfViewerDataService {
@ -21,6 +22,7 @@ export class PdfViewerDataService {
private readonly _viewedPagesService: ViewedPagesService,
private readonly _appStateService: AppStateService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _stateService: FilePreviewStateService,
) {}
loadRedactionLogFor(dossierId: string, fileId: string) {
@ -30,16 +32,17 @@ export class PdfViewerDataService {
);
}
loadDataFor(file: File, fileData?: FileDataModel): Observable<FileDataModel> {
const file$ = fileData?.file.cacheIdentifier === file.cacheIdentifier ? of(fileData.fileData) : this.downloadOriginalFile(file);
const reactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId);
loadDataFor(file: File): Observable<FileDataModel> {
const fileData = this._stateService.fileData;
const blob$ = fileData?.file.cacheIdentifier === file.cacheIdentifier ? of(fileData.blob$.value) : this.downloadOriginalFile(file);
const redactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId);
const viewedPages$ = this.getViewedPagesFor(file);
const dossier = this._dossiersService.find(file.dossierId);
return forkJoin([file$, reactionLog$, viewedPages$]).pipe(
return forkJoin([blob$, redactionLog$, viewedPages$]).pipe(
map(
data =>
(data: [blob: Blob, redactionLog: IRedactionLog, viewedPages: IViewedPage[]]) =>
new FileDataModel(
file,
...data,

View File

@ -1,5 +1,6 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
HostBinding,
@ -96,6 +97,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _reanalysisService: ReanalysisService,
private readonly _router: Router,
private readonly _changeRef: ChangeDetectorRef,
) {
super();
}
@ -157,7 +159,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
ariaExpanded: this._excludedPagesService?.shown$,
showDot: !!this.file.excludedPages?.length,
icon: 'red:exclude-pages',
show: !!this._excludedPagesService,
show: !!this._excludedPagesService && !this.file.excluded,
},
{
type: ActionTypes.circleBtn,
@ -224,7 +226,10 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
}
ngOnInit() {
this._dossiersService.getEntityChanged$(this.file.dossierId).pipe(tap(() => this._setup()));
this.addSubscription = this._dossiersService
.getEntityChanged$(this.file.dossierId)
.pipe(tap(() => this._setup()))
.subscribe();
}
ngOnChanges() {
@ -353,6 +358,8 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
this.showReanalyseDossierOverview = this.canReanalyse && this.isDossierOverview && this.analysisForced;
this.buttons = this._buttons;
this._changeRef.markForCheck();
}
private async _setFileApproved() {

View File

@ -19,5 +19,6 @@ export const processingFileStatusTranslations: { [key in ProcessingFileStatus]:
OCR_PROCESSING: _('file-status.ocr-processing'),
PROCESSING: _('file-status.processing'),
REPROCESS: _('file-status.reprocess'),
SURROUNDING_TEXT_PROCESSING: _('file-status.processing'),
UNPROCESSED: _('file-status.unprocessed'),
};

View File

@ -111,9 +111,9 @@ export class PdfViewerUtils {
}
}
translateQuads(page: number, quads: any) {
translateQuad(page: number, quad: Core.Math.Quad) {
const rotation = this._documentViewer.getCompleteRotation(page);
return translateQuads(page, rotation, quads);
return translateQuads(page, rotation, quad);
}
deselectAllAnnotations() {

View File

@ -7,9 +7,9 @@ import {
IPrepareDownloadRequest,
IRemoveDownloadRequest,
} from '@red/domain';
import { interval, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { ConfigService } from '@services/config.service';
import { filter, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import { KeycloakService } from 'keycloak-angular';
import { UserService } from '@services/user.service';
import { EntitiesService, List, mapEach, RequiredParam, Validate } from '@iqser/common-ui';
@ -44,11 +44,12 @@ export class FileDownloadService extends EntitiesService<DownloadStatus, IDownlo
}
async performDownload(status: DownloadStatus) {
const token = await this._keycloakService.getToken();
const anchor = document.createElement('a');
anchor.href = `${this._configService.values.API_URL}/async/download?access_token=${encodeURIComponent(
token,
)}&storageId=${encodeURIComponent(status.storageId)}`;
anchor.href = `${this._configService.values.API_URL}/async/download?storageId=${encodeURIComponent(status.storageId)}`;
if (!this._configService.values.USE_SESSION_FOR_DOWNLOAD) {
const token = await this._keycloakService.getToken();
anchor.href = anchor.href + `&access_token=${encodeURIComponent(token)}`;
}
anchor.download = status.filename;
anchor.target = '_blank';

View File

@ -41,7 +41,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
) {
super(_injector, 'upload');
const fileFetch$ = this._fetchFiles$.pipe(
throttleTime(1500),
throttleTime(250),
switchMap(dossierId => this._filesService.loadAll(dossierId)),
);
this._subscriptions.add(fileFetch$.subscribe());

View File

@ -2,16 +2,27 @@ import { Injectable, Injector } from '@angular/core';
import { EntitiesService, List, mapEach, QueryParam, RequiredParam, shareLast, Toaster, Validate } from '@iqser/common-ui';
import { Dossier, IDossier, IDossierRequest } from '@red/domain';
import { catchError, filter, map, mapTo, switchMap, tap } from 'rxjs/operators';
import { combineLatest, iif, Observable, of, throwError } from 'rxjs';
import { combineLatest, Observable, of, Subject, throwError, timer } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
export interface IDossiersStats {
totalPeople: number;
totalAnalyzedPages: number;
}
interface ChangesDetails {
dossierChanges: [
{
dossierChanges: boolean;
dossierId: string;
fileChanges: boolean;
},
];
}
const DOSSIER_EXISTS_MSG = _('add-dossier-dialog.errors.dossier-already-exists');
const GENERIC_MGS = _('add-dossier-dialog.errors.generic');
@ -20,6 +31,7 @@ const GENERIC_MGS = _('add-dossier-dialog.errors.generic');
})
export class DossiersService extends EntitiesService<Dossier, IDossier> {
readonly generalStats$ = this.all$.pipe(switchMap(entities => this._generalStats$(entities)));
readonly dossierFileChanges$ = new Subject<string>();
constructor(
private readonly _toaster: Toaster,
@ -27,6 +39,13 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
private readonly _dossierStatsService: DossierStatsService,
) {
super(_injector, Dossier, 'dossier');
timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL)
.pipe(
switchMap(() => this.loadAllIfChanged()),
tap(changes => this._emitFileChanges(changes)),
)
.subscribe();
}
loadAll(): Observable<Dossier[]> {
@ -39,8 +58,15 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
);
}
loadAllIfChanged(): Observable<boolean> {
return this.hasChanges$().pipe(switchMap(changed => iif(() => changed, this.loadAll()).pipe(mapTo(changed))));
loadAllIfChanged(): Observable<ChangesDetails> {
return this.hasChangesDetails$().pipe(switchMap(changes => this.loadAll().pipe(mapTo(changes))));
}
hasChangesDetails$(): Observable<ChangesDetails> {
const body = { value: this._lastCheckedForChanges.get('root') ?? '0' };
return this._post<ChangesDetails>(body, `${this._defaultModelPath}/changes/details`).pipe(
filter(changes => changes.dossierChanges.length > 0),
);
}
@Validate()
@ -82,6 +108,10 @@ export class DossiersService extends EntitiesService<Dossier, IDossier> {
return super.delete(body, 'deleted-dossiers/hard-delete', body).toPromise();
}
private _emitFileChanges(changes: ChangesDetails): void {
changes.dossierChanges.filter(change => change.fileChanges).forEach(change => this.dossierFileChanges$.next(change.dossierId));
}
private _computeStats(entities: List<Dossier>): IDossiersStats {
let totalAnalyzedPages = 0;
const totalPeople = new Set<string>();

View File

@ -5,8 +5,8 @@ import { filter, startWith } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class FilesMapService {
readonly fileReanalysed$ = new Subject<File>();
private readonly _entityChanged$ = new Subject<File>();
private readonly _entityDeleted$ = new Subject<File>();
private readonly _map = new Map<string, BehaviorSubject<File[]>>();
get$(dossierId: string) {
@ -37,17 +37,13 @@ export class FilesMapService {
return entities.forEach(entity => this._entityChanged$.next(entity));
}
const reanalysedEntities = [];
const changedEntities = [];
const deletedEntities = this.get(key).filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
const newEntities = entities.map(newEntity => {
const oldEntity = this.get(key, newEntity.id);
if (oldEntity?.lastProcessed !== newEntity.lastProcessed) {
reanalysedEntities.push(newEntity);
}
if (newEntity.isEqual(oldEntity)) {
return oldEntity;
}
@ -60,13 +56,13 @@ export class FilesMapService {
// Emit observables only after entities have been updated
for (const file of reanalysedEntities) {
this.fileReanalysed$.next(file);
}
for (const file of changedEntities) {
this._entityChanged$.next(file);
}
for (const file of deletedEntities) {
this._entityDeleted$.next(file);
}
}
replace(entity: File) {
@ -83,4 +79,8 @@ export class FilesMapService {
startWith(this.get(key, entityId)),
);
}
watchDeleted$(entityId: string): Observable<File> {
return this._entityDeleted$.pipe(filter(entity => entity.id === entityId));
}
}

View File

@ -2,7 +2,7 @@ import { Injectable, Injector } from '@angular/core';
import { GenericService, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import * as moment from 'moment';
import { TranslateService } from '@ngx-translate/core';
import { iif, Observable } from 'rxjs';
import { EMPTY, iif, Observable } from 'rxjs';
import { INotification, Notification, NotificationTypes } from '@red/domain';
import { map, switchMap } from 'rxjs/operators';
import { notificationsTranslations } from '../translations/notifications-translations';
@ -40,7 +40,7 @@ export class NotificationsService extends GenericService<Notification> {
@Validate()
getNotificationsIfChanged(@RequiredParam() includeSeen: boolean): Observable<Notification[]> {
return this.hasChanges$().pipe(switchMap(changed => iif(() => changed, this.getNotifications(includeSeen))));
return this.hasChanges$().pipe(switchMap(changed => iif(() => changed, this.getNotifications(includeSeen), EMPTY)));
}
@Validate()
@ -54,7 +54,7 @@ export class NotificationsService extends GenericService<Notification> {
}
private _new(notification: INotification) {
const message = this._translate(notification, notificationsTranslations[notification.notificationType]);
const message = this._translate(notification, notificationsTranslations[notification.notificationType] as string);
const time = this._getTime(notification.creationDate);
return new Notification(notification, message, time);
}
@ -64,7 +64,7 @@ export class NotificationsService extends GenericService<Notification> {
return moment(date).format('hh:mm A');
}
private _translate(notification: INotification, translation: string) {
private _translate(notification: INotification, translation: string): string {
const fileId = notification.target.fileId;
const dossierId = notification.target.dossierId;
const dossier = this._dossiersService.find(dossierId);

View File

@ -97,7 +97,7 @@ export class PermissionsService {
// TODO: Remove '?', after we make sure file is loaded before page
canPerformAnnotationActions(file: File): boolean {
return (file?.isUnderReview || file?.isUnderApproval) && this.isFileAssignee(file);
return !file.excluded && (file?.isUnderReview || file?.isUnderApproval) && this.isFileAssignee(file);
}
canUndoApproval(file: File | File[]): boolean {

View File

@ -160,6 +160,16 @@ export class AppStateService {
}
}
dictionaryData['dossier_redaction'] = new Dictionary(
{
hexColor: colors.manualRedactionColor || FALLBACK_COLOR,
type: 'dossier_redaction',
hint: false,
recommendation: false,
},
true,
);
dictionaryData['declined-suggestion'] = new Dictionary(
{
hexColor: colors.notRedacted || FALLBACK_COLOR,

View File

@ -1 +1 @@
export const CHANGED_CHECK_INTERVAL = 3000;
export const CHANGED_CHECK_INTERVAL = 5000;

View File

@ -98,9 +98,9 @@ export function removeBraces(str: any): string {
return str.replace(/[{}]/g, '');
}
export function toKebabCase(str: string): string {
export function toSnakeCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.replace(/[\s_]+/g, '_')
.toLowerCase();
}

View File

@ -1,9 +1,9 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dev-08.iqser.cloud/redaction-gateway-v1",
"API_URL": "https://aks-staging.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager",
"AUTO_READ_TIME": 1.5,
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
"DELETE_RETENTION_HOURS": 96,
"EULA_URL": "EULA_URL",
@ -17,7 +17,8 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dev-08.iqser.cloud/auth/realms/redaction",
"OAUTH_URL": "https://aks-staging.iqser.cloud/auth/realms/redaction",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural"
"SELECTION_MODE": "structural",
"USE_SESSION_FOR_DOWNLOAD": false
}

File diff suppressed because it is too large Load Diff

View File

@ -448,12 +448,25 @@
"question": "Are you sure you want to delete {filesCount, plural, one{this document} other{these documents}}?",
"title": "Delete {filesCount, plural, one{{fileName}} other{Selected Documents}}"
},
"unsaved-changes": {
"confirmation-text": "Save and Leave",
"details": "If you leave the tab without saving, all the unsaved changes will be lost.",
"discard-changes-text": "DISCARD CHANGES",
"question": "Are you sure you want to leave the tab? You have unsaved changes.",
"title": "You have unsaved changes"
},
"upload-report-template": {
"alternate-confirmation-text": "Upload as multi-file report",
"confirmation-text": "Upload as single-file report",
"deny-text": "Cancel Upload",
"question": "Please choose if <b>{fileName}</b> is a single or multi-file report template",
"title": "Report Template Upload"
},
"report-template-same-name": {
"confirmation-text": "Yes. Continue upload",
"deny-text": "No. Cancel Upload",
"question": "There is already a Report Template with the name: <b>{fileName}</b>. Do you wish to continue?",
"title": "Report Template Upload"
}
},
"content": "Reason",
@ -943,10 +956,23 @@
"members": "Members",
"team-members": "Team Members"
},
"side-nav-title": "Configurations",
"unsaved-changes": "You have unsaved changes. Save or revert before changing the tab."
"side-nav-title": "Configurations"
},
"error": {
"deleted-entity": {
"dossier": {
"action": "Back to overview",
"label": "This dossier has been deleted!"
},
"file-dossier": {
"action": "Back to overview",
"label": "The dossier of this file has been deleted!"
},
"file": {
"action": "Back to dossier",
"label": "This file has been deleted!"
}
},
"http": {
"generic": "Action failed with code {status}"
},

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="90px" version="1.1" viewBox="-10 -10 110 110" width="90px"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" id="Symbols" stroke="none" stroke-width="1">
<g id="disable-tooltip">
<rect height="100" id="Rectangle" width="100" x="0" y="0"></rect>
<path
d="M50,0 C75,0 95,20 95,45 C95,45.3345638 94.9964181,45.6682321 94.9892904,46.0009689 L84.9863576,46.0005557 C84.9954332,45.6682544 85,45.3347165 85,45 C85,25.5 69.5,10 50,10 C30.5,10 15,25.5 15,45 C15,53.3152174 17.870983,61.1519376 23.1448539,67.5739706 L23.5,68 L25,69.5 L25,86 L43,79.5 L43.2,79.5 L43.2,89.79 L15,100 L15,73 C8.5,65 5,55.5 5,45 C5,20 25,0 50,0 Z M87.5553391,55 L94.6264069,62.0710678 L80.484,76.213 L94.6264069,90.3553391 L87.5553391,97.4264069 L73.413,83.284 L59.2710678,97.4264069 L52.2,90.3553391 L66.342,76.213 L52.2,62.0710678 L59.2710678,55 L73.413,69.142 L87.5553391,55 Z"
fill="#868E96" fill-rule="nonzero" id="Combined-Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More