Merge branch 'master' into VM/RED-6801

This commit is contained in:
Valentin Mihai 2023-07-18 18:38:04 +03:00
commit 7a729ae2a3
76 changed files with 565 additions and 407 deletions

View File

@ -22,6 +22,7 @@ import { CustomRouteReuseStrategy } from '@iqser/common-ui/lib/utils';
import { ifLoggedIn } from '@guards/if-logged-in.guard';
import { ifNotLoggedIn } from '@guards/if-not-logged-in.guard';
import { TenantSelectComponent } from '@iqser/common-ui/lib/tenants';
import { editAttributeGuard } from '@guards/edit-attribute.guard';
const dossierTemplateIdRoutes: IqserRoutes = [
{
@ -37,6 +38,7 @@ const dossierTemplateIdRoutes: IqserRoutes = [
{
path: `:${DOSSIER_ID}`,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
canDeactivate: [editAttributeGuard],
data: {
routeGuards: [DossierFilesGuard],
breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier],
@ -115,7 +117,6 @@ const mainRoutes: IqserRoutes = [
Roles.any,
Roles.templates.read,
Roles.fileAttributes.readConfig,
Roles.watermarks.read,
Roles.dictionaryTypes.read,
Roles.colors.read,
Roles.states.read,
@ -179,7 +180,6 @@ const mainRoutes: IqserRoutes = [
Roles.any,
Roles.templates.read,
Roles.fileAttributes.readConfig,
Roles.watermarks.read,
Roles.dictionaryTypes.read,
Roles.colors.read,
Roles.states.read,

View File

@ -1,13 +1,10 @@
import { Component, inject, Renderer2, ViewContainerRef } from '@angular/core';
import { Component, Renderer2, ViewContainerRef } from '@angular/core';
import { RouterHistoryService } from '@services/router-history.service';
import { DOCUMENT } from '@angular/common';
import { UserPreferenceService } from '@users/user-preference.service';
import { getConfig, IqserPermissionsService } from '@iqser/common-ui';
import { getConfig } from '@iqser/common-ui';
import { AppConfig } from '@red/domain';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { Roles } from '@users/roles';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { filter, map, switchMap, take } from 'rxjs/operators';
function loadCustomTheme() {
const cssFileName = getConfig<AppConfig>().THEME;
@ -37,9 +34,8 @@ export class AppComponent {
userPreferenceService: UserPreferenceService,
renderer: Renderer2,
private readonly _router: Router,
iqserPermissionsService: IqserPermissionsService,
) {
renderer.addClass(inject(DOCUMENT).body, userPreferenceService.getTheme());
renderer.addClass(document.body, userPreferenceService.getTheme());
loadCustomTheme();
const removeQueryParams = _router.events.pipe(
@ -51,13 +47,9 @@ export class AppComponent {
);
removeQueryParams.subscribe();
const changeFavicon = iqserPermissionsService.has$(Roles.getRss).pipe(
tap(hasRss => {
const faviconUrl = hasRss ? 'assets/icons/documine-logo.ico' : 'favicon.ico';
document.getElementById('favicon').setAttribute('href', faviconUrl);
}),
);
changeFavicon.pipe(takeUntilDestroyed()).subscribe();
if (getConfig().IS_DOCUMINE) {
document.getElementById('favicon').setAttribute('href', 'assets/icons/documine-logo.ico');
}
}
#removeKeycloakQueryParams() {

View File

@ -131,11 +131,17 @@ export const appModuleFactory = (config: AppConfig) => {
features: {
ANNOTATIONS: {
color: 'aqua',
enabled: true,
enabled: false,
level: NgxLoggerLevel.DEBUG,
},
FILTERS: {
enabled: true,
enabled: false,
},
TENANTS: {
enabled: false,
},
ROUTES: {
enabled: false,
},
PDF: {
enabled: true,

View File

@ -11,7 +11,10 @@
<a [matTooltip]="'top-bar.navigation-items.back-to-dashboard' | translate" [routerLink]="['/'] | tenant" class="logo">
<div [attr.help-mode-key]="'home'" class="actions">
<iqser-logo (iqserHiddenAction)="userPreferenceService.toggleDevFeatures()" icon="red:logo"></iqser-logo>
<iqser-logo
(iqserHiddenAction)="userPreferenceService.toggleDevFeatures()"
[icon]="config.IS_DOCUMINE ? 'red:documine-logo' : 'red:logo'"
></iqser-logo>
<div class="app-name">{{ titleService.getTitle() }}</div>
</div>
</a>

View File

@ -6,7 +6,7 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-search-action';
import { filter, map, startWith } from 'rxjs/operators';
import { IqserPermissionsService } from '@iqser/common-ui';
import { getConfig, IqserPermissionsService } from '@iqser/common-ui';
import { BreadcrumbsService } from '@services/breadcrumbs.service';
import { FeaturesService } from '@services/features.service';
import { ARCHIVE_ROUTE, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE } from '@red/domain';
@ -33,6 +33,7 @@ export class BaseScreenComponent {
readonly roles = Roles;
readonly documentViewer = inject(REDDocumentViewer);
readonly currentUser = this.userService.currentUser;
readonly config = getConfig();
readonly searchActions: List<SpotlightSearchAction> = [
{
text: this._translateService.instant('search.this-dossier'),

View File

@ -4,7 +4,7 @@
</div>
<a class="logo">
<div class="actions">
<iqser-logo icon="red:logo"></iqser-logo>
<iqser-logo [icon]="config.IS_DOCUMINE ? 'red:documine-logo' : 'red:logo'"></iqser-logo>
<div class="app-name">{{ titleService.getTitle() }}</div>
</div>
</a>

View File

@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { getConfig } from '@iqser/common-ui';
@Component({
selector: 'redaction-skeleton-top-bar',
@ -8,5 +9,7 @@ import { Title } from '@angular/platform-browser';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SkeletonTopBarComponent {
readonly config = getConfig();
constructor(readonly titleService: Title) {}
}

View File

@ -0,0 +1,7 @@
import { CanDeactivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { DossierOverviewScreenComponent } from '../modules/dossier-overview/screen/dossier-overview-screen.component';
export const editAttributeGuard: CanDeactivateFn<DossierOverviewScreenComponent> = () =>
!inject(FileAttributesService).isEditingFileAttribute();

View File

@ -5,50 +5,40 @@ import { Dictionary } from '@red/domain';
export const canUndo = (annotation: AnnotationWrapper, isApprover: boolean) =>
!isApprover && (annotation.isSuggestion || annotation.pending);
export const canAcceptSuggestion = (annotation: AnnotationWrapper, isApprover: boolean, canProcessManualRedaction: boolean) => {
const can = isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion);
return annotation.isSuggestionAdd || annotation.isSuggestionRemoveDictionary ? can && canProcessManualRedaction : can;
};
export const canForceHint = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction && annotation.isIgnoredHint && !annotation.pending;
export const canRejectSuggestion = (annotation: AnnotationWrapper, isApprover: boolean, canProcessManualRedaction: boolean) => {
const can = isApprover && annotation.isSuggestion;
return annotation.isSuggestionAdd || annotation.isSuggestionRemoveDictionary ? can && canProcessManualRedaction : can;
};
export const canForceHint = (annotation: AnnotationWrapper, canAddOrRequestRedaction: boolean) =>
canAddOrRequestRedaction && annotation.isIgnoredHint && !annotation.pending;
export const canForceRedaction = (annotation: AnnotationWrapper, canAddOrRequestRedaction: boolean) =>
canAddOrRequestRedaction && annotation.isSkipped && !annotation.isFalsePositive && !annotation.pending;
export const canForceRedaction = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction && annotation.isSkipped && !annotation.isFalsePositive && !annotation.pending;
export const canAcceptRecommendation = (annotation: AnnotationWrapper) => annotation.isRecommendation && !annotation.pending;
export const canMarkAsFalsePositive = (annotation: AnnotationWrapper, annotationEntity: Dictionary) =>
annotation.canBeMarkedAsFalsePositive && annotationEntity.hasDictionary;
export const canRemoveOrSuggestToRemoveOnlyHere = (annotation: AnnotationWrapper, canAddOrRequestRedaction: boolean) =>
canAddOrRequestRedaction && !annotation.pending && (annotation.isRedacted || (annotation.isHint && !annotation.isImage));
export const canRemoveOnlyHere = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction && !annotation.pending && (annotation.isRedacted || (annotation.isHint && !annotation.isImage));
export const canRemoveOrSuggestToRemoveFromDictionary = (annotation: AnnotationWrapper) =>
export const canRemoveFromDictionary = (annotation: AnnotationWrapper) =>
annotation.isModifyDictionary &&
(annotation.isRedacted || annotation.isSkipped || annotation.isHint) &&
!annotation.pending &&
!annotation.hasBeenResized;
export const canRemoveOrSuggestToRemoveRedaction = (annotations: AnnotationWrapper[], permissions: AnnotationPermissions) =>
export const canRemoveRedaction = (annotations: AnnotationWrapper[], permissions: AnnotationPermissions) =>
annotations.length === 1 &&
(permissions.canRemoveOrSuggestToRemoveOnlyHere ||
permissions.canRemoveOrSuggestToRemoveFromDictionary ||
permissions.canMarkAsFalsePositive);
(permissions.canRemoveOnlyHere || permissions.canRemoveFromDictionary || permissions.canMarkAsFalsePositive);
export const canChangeLegalBasis = (annotation: AnnotationWrapper, canAddOrRequestRedaction: boolean) =>
canAddOrRequestRedaction && annotation.isRedacted && !annotation.pending;
export const canChangeLegalBasis = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction && annotation.isRedacted && !annotation.pending;
export const canRecategorizeImage = (annotation: AnnotationWrapper) =>
((annotation.isImage && !annotation.isSuggestion) || annotation.isSuggestionRecategorizeImage) && !annotation.pending;
export const canRecategorizeImage = (annotation: AnnotationWrapper, canRecategorize: boolean) =>
canRecategorize &&
((annotation.isImage && !annotation.isSuggestion) || annotation.isSuggestionRecategorizeImage) &&
!annotation.pending;
export const canResizeAnnotation = (annotation: AnnotationWrapper, canAddOrRequestRedaction: boolean) =>
canAddOrRequestRedaction &&
export const canResizeAnnotation = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction &&
(((annotation.isRedacted || annotation.isImage) && !annotation.isSuggestion) ||
annotation.isSuggestionResize ||
annotation.isRecommendation) &&

View File

@ -5,16 +5,14 @@ import { IqserPermissionsService } from '@iqser/common-ui';
import { Roles } from '@users/roles';
import {
canAcceptRecommendation,
canAcceptSuggestion,
canChangeLegalBasis,
canForceHint,
canForceRedaction,
canMarkAsFalsePositive,
canRecategorizeImage,
canRejectSuggestion,
canRemoveOrSuggestToRemoveFromDictionary,
canRemoveOrSuggestToRemoveOnlyHere,
canRemoveOrSuggestToRemoveRedaction,
canRemoveFromDictionary,
canRemoveOnlyHere,
canRemoveRedaction,
canResizeAnnotation,
canUndo,
} from './annotation-permissions.utils';
@ -23,11 +21,9 @@ export class AnnotationPermissions {
canUndo = true;
canAcceptRecommendation = true;
canMarkAsFalsePositive = true;
canRemoveOrSuggestToRemoveOnlyHere = true;
canRemoveOrSuggestToRemoveFromDictionary = true;
canRemoveOrSuggestToRemoveRedaction = true;
canAcceptSuggestion = true;
canRejectSuggestion = true;
canRemoveOnlyHere = true;
canRemoveFromDictionary = true;
canRemoveRedaction = true;
canForceRedaction = true;
canChangeLegalBasis = true;
canResizeAnnotation = true;
@ -46,27 +42,23 @@ export class AnnotationPermissions {
const summedPermissions: AnnotationPermissions = new AnnotationPermissions();
const canAddRedaction = permissionsService.has(Roles.redactions.write);
const canRequestRedaction = permissionsService.has(Roles.redactions.request);
const canProcessManualRedaction = permissionsService.has(Roles.redactions.processManualRequest);
const canAddOrRequestRedaction = canAddRedaction || canRequestRedaction;
const canDoAnyApproverAction = canAddRedaction && isApprover;
for (const annotation of annotations) {
const permissions: AnnotationPermissions = new AnnotationPermissions();
const annotationEntity = entities.find(entity => entity.type === annotation.type);
permissions.canUndo = canUndo(annotation, isApprover);
permissions.canAcceptSuggestion = canAcceptSuggestion(annotation, isApprover, canProcessManualRedaction);
permissions.canRejectSuggestion = canRejectSuggestion(annotation, isApprover, canProcessManualRedaction);
permissions.canForceHint = canForceHint(annotation, canAddOrRequestRedaction);
permissions.canForceRedaction = canForceRedaction(annotation, canAddOrRequestRedaction);
permissions.canForceHint = canForceHint(annotation, canDoAnyApproverAction);
permissions.canForceRedaction = canForceRedaction(annotation, canDoAnyApproverAction);
permissions.canAcceptRecommendation = canAcceptRecommendation(annotation);
permissions.canMarkAsFalsePositive = canMarkAsFalsePositive(annotation, annotationEntity);
permissions.canRemoveOrSuggestToRemoveOnlyHere = canRemoveOrSuggestToRemoveOnlyHere(annotation, canAddOrRequestRedaction);
permissions.canRemoveOrSuggestToRemoveFromDictionary = canRemoveOrSuggestToRemoveFromDictionary(annotation);
permissions.canRemoveOrSuggestToRemoveRedaction = canRemoveOrSuggestToRemoveRedaction(annotations, permissions);
permissions.canChangeLegalBasis = canChangeLegalBasis(annotation, canAddOrRequestRedaction);
permissions.canRecategorizeImage = canRecategorizeImage(annotation);
permissions.canResizeAnnotation = canResizeAnnotation(annotation, canAddOrRequestRedaction);
permissions.canRemoveOnlyHere = canRemoveOnlyHere(annotation, canAddRedaction);
permissions.canRemoveFromDictionary = canRemoveFromDictionary(annotation);
permissions.canRemoveRedaction = canRemoveRedaction(annotations, permissions);
permissions.canChangeLegalBasis = canChangeLegalBasis(annotation, canDoAnyApproverAction);
permissions.canRecategorizeImage = canRecategorizeImage(annotation, canDoAnyApproverAction);
permissions.canResizeAnnotation = canResizeAnnotation(annotation, canDoAnyApproverAction);
summedPermissions._merge(permissions);
}

View File

@ -371,6 +371,9 @@ export class AnnotationWrapper implements IListable {
if (redactionLogEntry.reason?.toLowerCase() === 'false positive') {
return annotationTypesTranslations[SuggestionAddFalsePositive];
}
if (annotation.superType === SuperTypes.ManualRedaction && annotation.hintDictionary) {
return _('annotation-type.manual-hint');
}
return annotationTypesTranslations[annotation.superType];
}

View File

@ -6,7 +6,7 @@ import { AccountSideNavComponent } from './account-side-nav/account-side-nav.com
import { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component';
import { NotificationPreferencesService } from './services/notification-preferences.service';
import { TranslateModule } from '@ngx-translate/core';
import { IconButtonComponent, IqserHelpModeModule } from '@iqser/common-ui';
import { IconButtonComponent, IqserAllowDirective, IqserHelpModeModule } from '@iqser/common-ui';
import { PreferencesComponent } from './screens/preferences/preferences.component';
import { SideNavComponent } from '@iqser/common-ui/lib/shared';
@ -20,6 +20,7 @@ import { SideNavComponent } from '@iqser/common-ui/lib/shared';
IqserHelpModeModule,
IconButtonComponent,
SideNavComponent,
IqserAllowDirective,
],
providers: [NotificationPreferencesService],
})

View File

@ -13,7 +13,7 @@
<div *ngFor="let key of notificationGroupsKeys; let i = index" class="group">
<div [translate]="translations[key]" class="group-title"></div>
<div class="iqser-input-group">
<ng-container *ngFor="let preference of notificationGroupsValues[i]">
<ng-container *ngFor="let preference of getRssFilteredSettings(notificationGroupsValues[i])">
<mat-checkbox
*ngIf="!skipPreference(preference)"
(change)="addRemovePreference($event.checked, category, preference)"

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { NotificationPreferencesService } from '../../../services/notification-preferences.service';
import { BaseFormComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { BaseFormComponent, IqserPermissionsService, LoadingService, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import {
NotificationCategoriesValues,
@ -13,6 +13,9 @@ import {
import { firstValueFrom } from 'rxjs';
import { notificationsSettingsTranslations } from '@translations/notifications-settings-translations';
import { getCurrentUser } from '@iqser/common-ui/lib/users';
import { Roles } from '@users/roles';
const RSS_EXCLUDED_SETTINGS = ['USER_PROMOTED_TO_APPROVER', 'USER_DEGRADED_TO_REVIEWER', 'ASSIGN_REVIEWER'];
@Component({
templateUrl: './notifications-screen.component.html',
@ -26,18 +29,24 @@ export class NotificationsScreenComponent extends BaseFormComponent implements O
readonly translations = notificationsSettingsTranslations;
readonly currentUser = getCurrentUser<User>();
constructor(
private readonly _toaster: Toaster,
private readonly _formBuilder: UntypedFormBuilder,
private readonly _loadingService: LoadingService,
private readonly _notificationPreferencesService: NotificationPreferencesService,
private readonly _changeRef: ChangeDetectorRef,
) {
readonly #toaster = inject(Toaster);
readonly #formBuilder = inject(UntypedFormBuilder);
readonly #loadingService = inject(LoadingService);
readonly #notificationPreferencesService = inject(NotificationPreferencesService);
readonly #cdRef = inject(ChangeDetectorRef);
readonly #iqserPermissionsService = inject(IqserPermissionsService);
readonly #isRss = this.#iqserPermissionsService.has(Roles.getRss);
constructor() {
super();
}
async ngOnInit(): Promise<void> {
await this._initializeForm();
await this.#initializeForm();
}
getRssFilteredSettings(settings: string[]) {
return settings.filter(s => (this.#isRss ? !RSS_EXCLUDED_SETTINGS.includes(s) : true));
}
isCategoryActive(category: string) {
@ -66,19 +75,19 @@ export class NotificationsScreenComponent extends BaseFormComponent implements O
}
async save() {
this._loadingService.start();
this.#loadingService.start();
try {
await firstValueFrom(this._notificationPreferencesService.update(this.form.value));
await firstValueFrom(this.#notificationPreferencesService.update(this.form.value));
} catch (e) {
this._toaster.error(_('notifications-screen.error.generic'));
this.#toaster.error(_('notifications-screen.error.generic'));
}
this.initialFormValue = JSON.parse(JSON.stringify(this.form.getRawValue()));
this._changeRef.markForCheck();
this._loadingService.stop();
this.#cdRef.markForCheck();
this.#loadingService.stop();
}
private _getForm(): UntypedFormGroup {
return this._formBuilder.group({
#getForm(): UntypedFormGroup {
return this.#formBuilder.group({
inAppNotificationsEnabled: [undefined],
emailNotificationsEnabled: [undefined],
emailNotificationType: [undefined],
@ -87,14 +96,14 @@ export class NotificationsScreenComponent extends BaseFormComponent implements O
});
}
private async _initializeForm() {
this._loadingService.start();
async #initializeForm() {
this.#loadingService.start();
this.form = this._getForm();
const notificationPreferences = await firstValueFrom(this._notificationPreferencesService.get());
this.form = this.#getForm();
const notificationPreferences = await firstValueFrom(this.#notificationPreferencesService.get());
this.form.patchValue(notificationPreferences);
this.initialFormValue = JSON.parse(JSON.stringify(this.form.getRawValue()));
this._loadingService.stop();
this.#loadingService.stop();
}
}

View File

@ -1,4 +1,4 @@
<form (submit)="save()" [formGroup]="form">
<form [formGroup]="form">
<div class="dialog-content">
<div *ngIf="currentScreen === screens.WARNING_PREFERENCES" class="content-delimiter"></div>
<div class="dialog-content-left">
@ -13,6 +13,11 @@
{{ 'preferences-screen.form.show-suggestions-in-preview' | translate }}
</mat-slide-toggle>
</div>
<div class="iqser-input-group" *allow="roles.getRss">
<mat-slide-toggle color="primary" formControlName="openStructuredComponentManagementDialogByDefault">
{{ 'preferences-screen.form.open-structured-view-by-default' | translate }}
</mat-slide-toggle>
</div>
</ng-container>
<ng-container *ngIf="currentScreen === screens.WARNING_PREFERENCES">
<p class="warnings-subtitle">{{ 'preferences-screen.warnings-subtitle' | translate }}</p>
@ -38,7 +43,6 @@
(action)="save()"
[disabled]="!valid || !changed"
[label]="'preferences-screen.actions.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
</div>

View File

@ -10,6 +10,7 @@ interface PreferencesForm {
// preferences
autoExpandFiltersOnActions: boolean;
displaySuggestionsInPreview: boolean;
openStructuredComponentManagementDialogByDefault: boolean;
// warnings preferences
unapprovedSuggestionsWarning: boolean;
loadAllAnnotationsWarning: boolean;
@ -35,6 +36,7 @@ export class PreferencesComponent extends BaseFormComponent {
readonly currentScreen: Screen;
readonly screens = Screens;
initialFormValue: PreferencesForm;
readonly roles = Roles;
constructor(
readonly userPreferenceService: UserPreferenceService,
@ -48,6 +50,9 @@ export class PreferencesComponent extends BaseFormComponent {
// preferences
autoExpandFiltersOnActions: [this.userPreferenceService.getAutoExpandFiltersOnActions()],
displaySuggestionsInPreview: [this.userPreferenceService.getDisplaySuggestionsInPreview()],
openStructuredComponentManagementDialogByDefault: [
this.userPreferenceService.getOpenStructuredComponentManagementDialogByDefault(),
],
// warnings preferences
unapprovedSuggestionsWarning: [this.userPreferenceService.getUnapprovedSuggestionsWarning()],
loadAllAnnotationsWarning: [this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)],
@ -68,6 +73,12 @@ export class PreferencesComponent extends BaseFormComponent {
if (this.form.controls.displaySuggestionsInPreview.value !== this.userPreferenceService.getDisplaySuggestionsInPreview()) {
await this.userPreferenceService.toggleDisplaySuggestionsInPreview();
}
if (
this.form.controls.openStructuredComponentManagementDialogByDefault.value !==
this.userPreferenceService.getOpenStructuredComponentManagementDialogByDefault()
) {
await this.userPreferenceService.toggleOpenStructuredComponentManagementDialogByDefault();
}
if (this.form.controls.unapprovedSuggestionsWarning.value !== this.userPreferenceService.getUnapprovedSuggestionsWarning()) {
await this.userPreferenceService.toggleUnapprovedSuggestionsWarning();
}
@ -83,12 +94,20 @@ export class PreferencesComponent extends BaseFormComponent {
}
await this.userPreferenceService.reload();
this.form.patchValue({
autoExpandFiltersOnActions: this.userPreferenceService.getAutoExpandFiltersOnActions(),
displaySuggestionsInPreview: this.userPreferenceService.getDisplaySuggestionsInPreview(),
unapprovedSuggestionsWarning: this.userPreferenceService.getUnapprovedSuggestionsWarning(),
});
this.#patchValues();
this.initialFormValue = this.form.getRawValue();
this._changeRef.markForCheck();
}
#patchValues() {
this.form.patchValue({
autoExpandFiltersOnActions: this.userPreferenceService.getAutoExpandFiltersOnActions(),
displaySuggestionsInPreview: this.userPreferenceService.getDisplaySuggestionsInPreview(),
openStructuredComponentManagementDialogByDefault:
this.userPreferenceService.getOpenStructuredComponentManagementDialogByDefault(),
unapprovedSuggestionsWarning: this.userPreferenceService.getUnapprovedSuggestionsWarning(),
loadAllAnnotationsWarning: this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning),
});
}
}

View File

@ -56,6 +56,7 @@ import { AuditInfoDialogComponent } from './dialogs/audit-info-dialog/audit-info
import { DossierTemplateActionsComponent } from './shared/components/dossier-template-actions/dossier-template-actions.component';
import { IqserUsersModule } from '@iqser/common-ui/lib/users';
import { TenantPipe } from '@iqser/common-ui/lib/tenants';
import { SelectComponent } from '@shared/components/select/select.component';
const dialogs = [
AddEditCloneDossierTemplateDialogComponent,
@ -123,6 +124,7 @@ const components = [
IqserAllowDirective,
IqserDenyDirective,
TenantPipe,
SelectComponent,
],
})
export class AdminModule {}

View File

@ -9,10 +9,11 @@ import { ChartConfiguration, ChartDataset } from 'chart.js';
export class ChartComponent implements OnChanges {
@Input({ required: true }) datasets: ChartDataset[];
@Input({ required: true }) labels: string[];
@Input() ticksCallback?: (value: number) => string;
@Input() valueFormatter?: (value: number) => string;
@Input() secondaryAxis = false;
@Input() yAxisLabel?: string;
@Input() yAxisLabelRight?: string;
@Input() reverseLegend = false;
chartData: ChartConfiguration['data'];
chartOptions: ChartConfiguration<'line'>['options'] = {};
@ -34,7 +35,7 @@ export class ChartComponent implements OnChanges {
scales: {
y: {
stacked: true,
ticks: { callback: this.ticksCallback ? (value: number) => this.ticksCallback(value) : undefined },
ticks: { callback: this.valueFormatter ? (value: number) => this.valueFormatter(value) : undefined },
title: {
display: !!this.yAxisLabel,
text: this.yAxisLabel,
@ -52,10 +53,10 @@ export class ChartComponent implements OnChanges {
},
},
plugins: {
legend: { position: 'right' },
legend: { position: 'right', reverse: this.reverseLegend },
tooltip: {
callbacks: {
label: this.ticksCallback ? item => `${item.dataset.label}: ${this.ticksCallback(item.parsed.y)}` : undefined,
label: this.valueFormatter ? item => `${item.dataset.label}: ${this.valueFormatter(item.parsed.y)}` : undefined,
},
},
},

View File

@ -42,6 +42,7 @@
*ngIf="data$ | async as data"
[datasets]="data.datasets"
[labels]="data.labels"
[ticksCallback]="formatSize"
[reverseLegend]="true"
[valueFormatter]="formatSize"
></redaction-chart>
</div>

View File

@ -5,7 +5,7 @@ import { LicenseService } from '@services/license.service';
import { map, tap } from 'rxjs/operators';
import type { DonutChartConfig, ILicenseReport } from '@red/domain';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartGrey, ChartRed } from '../../utils/constants';
import { ChartBlack, ChartBlue, ChartGreen, ChartGrey, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -71,21 +71,27 @@ export class LicenseStorageComponent {
{
data: monthlyData.flatMap(d => d.activeFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.storage.active-documents'),
...getLineConfig(ChartGreen, 'origin'),
...getLineConfig(ChartGreen, false, 'origin'),
stack: 'storage',
},
{
data: monthlyData.flatMap(d => d.archivedFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.storage.archived-documents'),
...getLineConfig(ChartBlue, '-1'),
...getLineConfig(ChartBlue, false, '-1'),
stack: 'storage',
},
{
data: monthlyData.flatMap(d => d.trashFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.storage.trash-documents'),
...getLineConfig(ChartRed, '-1'),
...getLineConfig(ChartRed, false, '-1'),
stack: 'storage',
},
{
data: monthlyData.flatMap(d => d.activeFilesUploadedBytes + d.archivedFilesUploadedBytes + d.trashFilesUploadedBytes),
label: this._translateService.instant('license-info-screen.storage.all-documents'),
...getLineConfig(ChartBlack, true, false),
borderWidth: 2,
},
];
}
}

View File

@ -35,7 +35,7 @@
[datasets]="data.datasets"
[labels]="data.labels"
[secondaryAxis]="true"
[yAxisLabelRight]="'license-info-screen.chart.total-pages' | translate"
[yAxisLabel]="'license-info-screen.chart.pages-per-month' | translate"
[yAxisLabelRight]="'license-info-screen.pages.total-pages' | translate"
[yAxisLabel]="'license-info-screen.pages.pages-per-month' | translate"
></redaction-chart>
</div>

View File

@ -5,6 +5,7 @@ import type { ILicenseReport } from '@red/domain';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'red-license-usage',
@ -21,7 +22,7 @@ export class LicenseUsageComponent {
})),
);
constructor(readonly licenseService: LicenseService) {}
constructor(readonly licenseService: LicenseService, private readonly _translateService: TranslateService) {}
getAnalysisPercentageOfLicense() {
const totalLicensedNumberOfPages = this.licenseService.totalLicensedNumberOfPages;
@ -35,16 +36,16 @@ export class LicenseUsageComponent {
return [
{
data: monthlyData.flatMap(d => d.numberOfAnalyzedPages),
label: 'Pages per Month',
label: this._translateService.instant('license-info-screen.pages.pages-per-month'),
type: 'bar',
backgroundColor: ChartBlue,
borderColor: ChartBlue,
order: 2,
},
{
data: monthlyData.flatMap(() => 200000),
label: 'Total Pages',
...getLineConfig(ChartRed, false),
data: monthlyData.flatMap(() => this.licenseService.totalLicensedNumberOfPages),
label: this._translateService.instant('license-info-screen.pages.total-pages'),
...getLineConfig(ChartRed, true, false),
yAxisID: 'y1',
order: 1,
},
@ -54,10 +55,10 @@ export class LicenseUsageComponent {
month.numberOfAnalyzedPages +
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + curr.numberOfAnalyzedPages, 0),
),
label: 'Cumulative Pages',
label: this._translateService.instant('license-info-screen.pages.cumulative'),
yAxisID: 'y1',
order: 1,
...getLineConfig(ChartGreen, false),
...getLineConfig(ChartGreen, true, false),
},
];
}

View File

@ -2,3 +2,4 @@ export const ChartRed = '#dd4d50';
export const ChartGreen = '#5ce594';
export const ChartBlue = '#0389ec';
export const ChartGrey = '#ccced3'; // grey-5
export const ChartBlack = '#283241'; // grey-1

View File

@ -10,6 +10,7 @@ export const verboseDate = (date: Dayjs) => `${monthNames[date.month()]} ${date.
export const getLineConfig: (
color: string,
displayLine: boolean,
target: FillTarget,
) => {
type: 'line';
@ -17,9 +18,9 @@ export const getLineConfig: (
backgroundColor: string;
pointBackgroundColor: string;
fill: ComplexFillTarget;
} = (color, target) => ({
} = (color, displayLine, target) => ({
type: 'line',
borderColor: hexToRgba(color, 1),
borderColor: hexToRgba(color, displayLine ? 1 : 0),
backgroundColor: hexToRgba(color, 1),
pointBackgroundColor: hexToRgba(color, 1),
fill: {
@ -27,6 +28,7 @@ export const getLineConfig: (
above: hexToRgba(color, 0.3),
below: hexToRgba(color, 0.3),
},
pointStyle: false,
});
export const getLabelsFromMonthlyData = (monthlyData: ILicenseData[]) => monthlyData.map(data => verboseDate(dayjs(data.startDate)));

View File

@ -107,7 +107,7 @@ export class AdminSideNavComponent implements OnInit {
screen: 'watermarks',
label: _('admin-side-nav.watermarks'),
helpModeKey: 'watermarks',
show: true,
show: this._permissionsService.has(Roles.watermarks.read),
},
{
screen: 'file-attributes',

View File

@ -40,7 +40,7 @@
<ng-template #input>
<div [ngClass]="{ 'workflow-edit-input': mode === 'workflow' }" class="edit-input" iqserStopPropagation>
<form [formGroup]="form">
<form (ngSubmit)="form.valid && save()" [formGroup]="form">
<iqser-dynamic-input
(closedDatepicker)="closedDatepicker = $event"
(keydown.escape)="close()"

View File

@ -183,3 +183,9 @@
.hide {
visibility: hidden;
}
@media screen and (max-width: 1395px) {
.file-attribute .edit-input form .workflow-input {
width: 63%;
}
}

View File

@ -2,7 +2,7 @@ import { Component, computed, effect, HostListener, Input, OnDestroy } from '@an
import { Dossier, File, FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain';
import { BaseFormComponent, HelpModeService, ListingService, Toaster } from '@iqser/common-ui';
import { PermissionsService } from '@services/permissions.service';
import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { FormBuilder, UntypedFormGroup } from '@angular/forms';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { firstValueFrom, Subscription } from 'rxjs';
import { FilesService } from '@services/files/files.service';
@ -11,6 +11,7 @@ import dayjs from 'dayjs';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, tap } from 'rxjs/operators';
import { ConfigService } from '../../config.service';
import { Debounce } from '@iqser/common-ui/lib/utils';
@Component({
selector: 'redaction-file-attribute [fileAttribute] [file] [dossier]',
@ -28,6 +29,8 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
readonly #shouldClose = computed(
() =>
this.fileAttributesService.isEditingFileAttribute() &&
!!this.file &&
!!this.fileAttribute &&
(this.fileAttribute.id !== this.fileAttributesService.openAttributeEdit() ||
this.file.fileId !== this.fileAttributesService.fileEdit()),
);
@ -71,6 +74,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id];
}
@Debounce(50)
@HostListener('document:click')
clickOutside() {
if (this.isInEditMode && this.closedDatepicker) {
@ -85,6 +89,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
async editFileAttribute($event: MouseEvent): Promise<void> {
if (!this.file.isInitialProcessing && this.permissionsService.canEditFileAttributes(this.file, this.dossier)) {
$event.stopPropagation();
this.fileAttributesService.openAttributeEdits.mutate(value => value.push(this.fileAttribute.id));
this.#toggleEdit();
this.fileAttributesService.setFileEdit(this.file.fileId);
this.fileAttributesService.setOpenAttributeEdit(this.fileAttribute.id);
@ -116,6 +121,13 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
if (this.isInEditMode) {
this.form = this.#getForm();
this.#toggleEdit();
this.fileAttributesService.openAttributeEdits.mutate(value => {
for (let index = 0; index < value.length; index++) {
if (value[index] === this.fileAttribute.id) {
value.splice(index, 1);
}
}
});
}
}
@ -133,16 +145,16 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
const fileAttributes = this.file.fileAttributes.attributeIdToValue;
Object.keys(fileAttributes).forEach(key => {
const attrValue = fileAttributes[key];
config[key] = [
dayjs(attrValue, 'YYYY-MM-DD', true).isValid() ? dayjs(attrValue).toDate() : attrValue,
Validators.pattern(/^(\s+\S+\s*)*(?!\s).*$/),
];
config[key] = [dayjs(attrValue, 'YYYY-MM-DD', true).isValid() ? dayjs(attrValue).toDate() : attrValue];
});
return this._formBuilder.group(config, {
validators: control =>
!control.get(this.fileAttribute.id).value?.trim().length && !this.fileAttributeValue ? { emptyString: true } : null,
});
return this._formBuilder.group(config);
}
#formatAttributeValue(attrValue) {
return this.isDate ? attrValue && dayjs(attrValue).format('YYYY-MM-DD') : attrValue.replaceAll(/\s\s+/g, ' ');
return this.isDate ? attrValue && dayjs(attrValue).format('YYYY-MM-DD') : attrValue.trim().replaceAll(/\s\s+/g, ' ');
}
#toggleEdit(): void {
@ -153,7 +165,6 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
}
this.isInEditMode = !this.isInEditMode;
this.fileAttributesService.isEditingFileAttribute.set(this.isInEditMode);
if (this.isInEditMode) {
this.#focusOnEditInput();

View File

@ -47,5 +47,5 @@
</iqser-page-header>
<ng-template #viewModeSelection>
<redaction-view-mode-selection></redaction-view-mode-selection>
<redaction-view-mode-selection iqserDisableStopPropagation></redaction-view-mode-selection>
</ng-template>

View File

@ -51,15 +51,6 @@
icon="iqser:check"
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.acceptSuggestion(annotations)"
*ngIf="annotationPermissions.canAcceptSuggestion"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.accept-suggestion.label' | translate"
[type]="buttonType"
icon="iqser:check"
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.convertHighlights(annotations)"
*ngIf="viewModeService.isEarmarks()"
@ -88,12 +79,12 @@
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.rejectSuggestion(annotations)"
*ngIf="annotationPermissions.canRejectSuggestion"
(action)="annotationActionsService.undoDirectAction(annotations)"
*allow="roles.redactions.deleteManual; if: annotationPermissions.canUndo"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.reject-suggestion' | translate"
[tooltip]="'annotation-actions.undo' | translate"
[type]="buttonType"
icon="iqser:close"
icon="red:undo"
></iqser-circle-button>
<iqser-circle-button
@ -152,7 +143,7 @@
<iqser-circle-button
(action)="removeOrSuggestRemoveRedaction()"
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveRedaction"
*ngIf="annotationPermissions.canRemoveRedaction"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.remove-annotation.remove-redaction' | translate"
[type]="buttonType"

View File

@ -1,7 +1,7 @@
<div
(click)="pageSelected.emit(number)"
(dblclick)="toggleReadState()"
*ngIf="componentContext$ | async"
*ngIf="assigneeChanged$ | async"
[class.active]="isActive"
[class.read]="read"
[id]="'quick-nav-page-' + number"

View File

@ -1,34 +1,31 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Component, effect, EventEmitter, inject, Input, OnChanges, Output } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { ViewedPagesService } from '@services/files/viewed-pages.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service';
import { getConfig } from '@iqser/common-ui';
import { map, tap } from 'rxjs/operators';
import { map, startWith, tap } from 'rxjs/operators';
import { AppConfig, ViewedPage } from '@red/domain';
import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service';
import { pairwise } from 'rxjs';
import { Observable, pairwise } from 'rxjs';
import { PdfViewer } from '../../../pdf-viewer/services/pdf-viewer.service';
import { ContextComponent } from '@iqser/common-ui/lib/utils';
interface PageIndicatorContext {
isRotated: boolean;
assigneeChanged: boolean;
}
import { NGXLogger } from 'ngx-logger';
@Component({
selector: 'redaction-page-indicator',
templateUrl: './page-indicator.component.html',
styleUrls: ['./page-indicator.component.scss'],
})
export class PageIndicatorComponent extends ContextComponent<PageIndicatorContext> implements OnChanges, OnInit {
export class PageIndicatorComponent implements OnChanges {
readonly #config = getConfig<AppConfig>();
readonly #logger = inject(NGXLogger);
@Input({ required: true }) number: number;
@Input() showDottedIcon = false;
@Input() activeSelection = false;
@Input() read = false;
@Output() readonly pageSelected = new EventEmitter<number>();
pageReadTimeout: number = null;
readonly assigneeChanged$: Observable<boolean>;
constructor(
private readonly _viewedPagesService: ViewedPagesService,
@ -38,38 +35,45 @@ export class PageIndicatorComponent extends ContextComponent<PageIndicatorContex
private readonly _pdf: PdfViewer,
readonly pageRotationService: PageRotationService,
) {
super();
this.assigneeChanged$ = this._state.file$.pipe(
pairwise(),
map(([prevFile, currFile]) => prevFile.assignee !== currFile.assignee),
tap(assigneeChanged => assigneeChanged && this.handlePageRead()),
startWith(true),
map(() => true),
);
effect(() => {
if (this.isActive) {
this.handlePageRead();
}
});
}
get isActive() {
return this.number === this._pdf.currentPage();
}
ngOnInit() {
const assigneeChanged$ = this._state.file$.pipe(
pairwise(),
map(([prevFile, currFile]) => prevFile.assignee !== currFile.assignee),
tap(assigneeChanged => assigneeChanged && this.handlePageRead()),
);
super._initContext({ assigneeChanged: assigneeChanged$ });
}
ngOnChanges() {
this.handlePageRead();
}
async toggleReadState() {
if (this._permissionService.canMarkPagesAsViewed(this._state.file())) {
if (this.read) {
await this.#markPageUnread();
} else {
await this.#markPageRead();
}
if (!this._permissionService.canMarkPagesAsViewed(this._state.file())) {
this.#logger.info('[PAGES] Cannot toggle read state');
return;
}
if (this.read) {
await this.#markPageUnread();
} else {
await this.#markPageRead();
}
}
handlePageRead(): void {
if (!this._permissionService.canMarkPagesAsViewed(this._state.file())) {
this.#logger.info('[PAGES] Cannot mark pages as read');
return;
}
@ -87,6 +91,8 @@ export class PageIndicatorComponent extends ContextComponent<PageIndicatorContex
}
async #markPageRead() {
this.#logger.info('[PAGES] Mark page read', this.number);
const fileId = this._state.fileId;
await this._viewedPagesService.add({ page: this.number }, this._state.dossierId, fileId);
const viewedPage = new ViewedPage({ page: this.number, fileId });
@ -94,6 +100,8 @@ export class PageIndicatorComponent extends ContextComponent<PageIndicatorContex
}
async #markPageUnread() {
this.#logger.info('[PAGES] Mark page unread', this.number);
const fileId = this._state.fileId;
await this._viewedPagesService.remove(this._state.dossierId, fileId, this.number);
this._viewedPagesMapService.delete(fileId, this.number);

View File

@ -177,7 +177,6 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
addRedactionRequest.addToDictionary = selectedType.hasDictionary;
} else {
addRedactionRequest.addToDictionary = this.isDictionaryRequest && addRedactionRequest.type !== 'dossier_redaction';
addRedactionRequest.addToDossierDictionary = this.isDictionaryRequest && addRedactionRequest.type === 'dossier_redaction';
}
if (!addRedactionRequest.reason) {

View File

@ -42,7 +42,7 @@
</ng-container>
<ng-container *deny="roles.getRss; if: dictionaryRequest || type === 'HINT'">
<div class="iqser-input-group required w-450" *ngIf="dictionaryRequest">
<div class="iqser-input-group required w-450">
<label [translate]="'redact-text.dialog.content.type'"></label>
<mat-form-field>

View File

@ -9,11 +9,7 @@ import { ActiveDossiersService } from '@services/dossiers/active-dossiers.servic
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import {
ManualRedactionEntryType,
ManualRedactionEntryTypes,
ManualRedactionEntryWrapper,
} from '@models/file/manual-redaction-entry.wrapper';
import { ManualRedactionEntryType, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { redactTextTranslations } from '@translations/redact-text-translations';
import { RedactTextOption, RedactTextOptions } from './redact-text-options';
import { tap } from 'rxjs/operators';
@ -29,6 +25,7 @@ interface RedactTextData {
file: File;
applyToAllDossiers: boolean;
isApprover: boolean;
hint: boolean;
}
interface DialogResult {
@ -59,6 +56,7 @@ export class RedactTextDialogComponent
readonly #translations = redactTextTranslations;
readonly #dossier: Dossier;
readonly #isRss = this._iqserPermissionsService.has(Roles.getRss);
readonly hint: boolean;
constructor(
@ -71,7 +69,7 @@ export class RedactTextDialogComponent
super();
this.#dossier = _activeDossiersService.find(this.data.dossierId);
this.type = this.data.manualRedactionEntryWrapper.type;
this.hint = this.type === ManualRedactionEntryTypes.HINT;
this.hint = this.data.hint;
this.#applyToAllDossiers = this.data.applyToAllDossiers ?? true;
this.#manualRedactionTypeExists = this._dictionaryService.hasManualType(this.#dossier.dossierTemplateId);
this.options = this.#options();
@ -83,6 +81,7 @@ export class RedactTextDialogComponent
.valueChanges.pipe(
tap((option: DetailsRadioOption<RedactTextOption>) => {
this.dictionaryRequest = option.value === RedactTextOptions.IN_DOSSIER;
this.#setDictionaries();
this.#resetValues();
}),
takeUntilDestroyed(),
@ -99,19 +98,14 @@ export class RedactTextDialogComponent
}
get disabled() {
if (this.dictionaryRequest || this.hint) {
if (this.dictionaryRequest || this.hint || this.#isRss) {
return !this.form.get('dictionary').value;
}
return !this.form.get('reason').value;
}
async ngOnInit(): Promise<void> {
this.dictionaries = this._dictionaryService.getPossibleDictionaries(
this.#dossier.dossierTemplateId,
this.hint,
!this.#applyToAllDossiers,
);
this.#setDictionaries();
const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this.#dossier.dossierTemplateId));
this.legalOptions = data.map(lbm => ({
legalBasis: lbm.reason,
@ -122,16 +116,13 @@ export class RedactTextDialogComponent
this.#selectReason();
this.#formatSelectedTextValue();
this.#resetValues();
}
extraOptionChanged(option: DetailsRadioOption<RedactTextOption>): void {
this.#applyToAllDossiers = option.extraOption.checked;
this.dictionaries = this._dictionaryService.getPossibleDictionaries(
this.#dossier.dossierTemplateId,
this.hint,
!this.#applyToAllDossiers,
);
this.#setDictionaries();
if (this.#applyToAllDossiers && this.form.get('dictionary').value) {
const selectedDictionaryLabel = this.form.get('dictionary').value;
const selectedDictionary = this.dictionaries.find(d => d.type === selectedDictionaryLabel);
@ -155,10 +146,18 @@ export class RedactTextDialogComponent
this.dialogRef.close({
redaction,
dictionary: this.dictionaries.find(d => d.type === this.form.get('dictionary').value),
applyToAllDossiers: this.dictionaryRequest ? this.#applyToAllDossiers : null,
});
}
#setDictionaries() {
this.dictionaries = this._dictionaryService.getPossibleDictionaries(
this.#dossier.dossierTemplateId,
this.hint,
!this.#applyToAllDossiers,
this.dictionaryRequest,
);
}
#getForm(): UntypedFormGroup {
return this._formBuilder.group({
selectedText: this.data?.manualRedactionEntryWrapper?.manualRedactionEntry?.value,
@ -194,12 +193,12 @@ export class RedactTextDialogComponent
addRedactionRequest.legalBasis = legalOption.legalBasis;
}
if (this._iqserPermissionsService.has(Roles.getRss)) {
const selectedType = this.dictionaries.find(d => d.type === addRedactionRequest.type);
const selectedType = this.dictionaries.find(d => d.type === addRedactionRequest.type);
if (selectedType) {
addRedactionRequest.addToDictionary = selectedType.hasDictionary;
} else {
addRedactionRequest.addToDictionary = this.dictionaryRequest && addRedactionRequest.type !== 'dossier_redaction';
addRedactionRequest.addToDossierDictionary = this.dictionaryRequest && addRedactionRequest.type === 'dossier_redaction';
}
if (!addRedactionRequest.reason) {
@ -223,7 +222,7 @@ export class RedactTextDialogComponent
value: RedactTextOptions.ONLY_HERE,
},
];
if (!this._iqserPermissionsService.has(Roles.getRss)) {
if (!this.#isRss) {
options.push({
label: this.#translations[this.type].inDossier.label,
description: this.#translations[this.type].inDossier.description,
@ -242,8 +241,10 @@ export class RedactTextDialogComponent
#resetValues() {
this.#applyToAllDossiers = this.data.applyToAllDossiers ?? true;
this.options[1].extraOption.checked = this.#applyToAllDossiers;
if (this.dictionaryRequest) {
if (!this.#isRss) {
this.options[1].extraOption.checked = this.#applyToAllDossiers;
}
if (this.dictionaryRequest || this.hint) {
this.form.get('reason').setValue(null);
this.form.get('dictionary').setValue(null);
return;

View File

@ -16,8 +16,8 @@ const FOLDER_ICON = 'red:folder';
const REMOVE_FROM_DICT_ICON = 'red:remove-from-dict';
export interface RemoveRedactionPermissions {
canRemoveOrSuggestToRemoveOnlyHere: boolean;
canRemoveOrSuggestToRemoveFromDictionary: boolean;
canRemoveOnlyHere: boolean;
canRemoveFromDictionary: boolean;
canMarkAsFalsePositive: boolean;
}
@ -49,6 +49,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
readonly options: DetailsRadioOption<RemoveRedactionOption>[];
form!: UntypedFormGroup;
hint: boolean;
readonly #redaction: AnnotationWrapper;
readonly #permissions: RemoveRedactionPermissions;
@ -58,6 +59,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
constructor(private readonly _formBuilder: FormBuilder, private readonly _permissionsService: PermissionsService) {
super();
this.#redaction = this.data.redaction;
this.hint = this.#redaction.hint;
this.#permissions = this.data.permissions;
this.options = this.#options();
this.form = this.#getForm();
@ -91,20 +93,20 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
#options() {
const options: DetailsRadioOption<RemoveRedactionOption>[] = [];
if (this.#permissions.canRemoveOrSuggestToRemoveOnlyHere) {
if (this.#permissions.canRemoveOnlyHere) {
options.push({
label: this.#translations.ONLY_HERE.label,
description: this.#translations.ONLY_HERE.description,
descriptionParams: { value: this.#redaction.value },
descriptionParams: { type: this.hint ? 'hint' : 'redact', value: this.#redaction.value },
icon: PIN_ICON,
value: RemoveRedactionOptions.ONLY_HERE,
});
}
if (this.#permissions.canRemoveOrSuggestToRemoveFromDictionary) {
if (this.#permissions.canRemoveFromDictionary) {
options.push({
label: this.#translations.IN_DOSSIER.label,
description: this.#translations.IN_DOSSIER.description,
descriptionParams: { value: this.#redaction.value },
descriptionParams: { type: this.hint ? 'hint' : 'redact', value: this.#redaction.value },
icon: FOLDER_ICON,
value: RemoveRedactionOptions.IN_DOSSIER,
extraOption: {

View File

@ -1,6 +1,9 @@
<section class="dialog">
<form (submit)="save()" [formGroup]="form">
<div [translate]="'remove-redaction.dialog.title'" class="dialog-header heading-l"></div>
<div
[innerHTML]="'remove-redaction.dialog.title' | translate : { type: hint ? 'hint' : 'redaction' }"
class="dialog-header heading-l"
></div>
<div class="dialog-content">
<iqser-details-radio [options]="options" formControlName="option"></iqser-details-radio>

View File

@ -4,10 +4,7 @@
<hr />
<div class="dialog-content">
<div *ngIf="rssData() | log as rssEntry" class="table output-data">
<div class="table-header">Component</div>
<div class="table-header">Value</div>
<div class="table-header">Transformation</div>
<div class="table-header">Annotations</div>
<div class="table-header" *ngFor="let cell of tableHeaderCells">{{ getHeaderCellTranslation(cell) | translate }}</div>
<ng-container *ngFor="let entry of rssEntry.result | keyvalue : originalOrder">
<div class="bold">{{ entry.key }}</div>
@ -80,6 +77,13 @@
></iqser-icon-button>
<div [translate]="'rss-dialog.actions.close'" class="all-caps-label cancel" mat-dialog-close></div>
<mat-checkbox
class="ml-auto"
color="primary"
(change)="toggleOpenStructuredComponentManagementDialogByDefault()"
[checked]="openStructuredComponentManagementDialogByDefault()"
>Display by default when opening documents</mat-checkbox
>
</div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>

View File

@ -63,3 +63,7 @@ ul {
.output-data > div:nth-child(8n + 12) {
background: var(--iqser-grey-8);
}
.ml-auto {
margin-left: auto;
}

View File

@ -17,8 +17,11 @@ interface RssData {
})
export class RssDialogComponent extends BaseDialogComponent implements OnInit {
readonly circleButtonTypes = CircleButtonTypes;
readonly rssData = signal<RssEntry | undefined>(undefined);
readonly openStructuredComponentManagementDialogByDefault = signal(
this.userPreferences.getOpenStructuredComponentManagementDialogByDefault(),
);
readonly tableHeaderCells = ['component', 'value', 'transformation-rule', 'annotation-references'] as const;
constructor(
protected readonly _dialogRef: MatDialogRef<RssDialogComponent>,
@ -34,6 +37,10 @@ export class RssDialogComponent extends BaseDialogComponent implements OnInit {
await this.#loadData();
}
getHeaderCellTranslation(cell: string) {
return `rss-dialog.table-header.${cell}`;
}
originalOrder = (): number => 0;
exportJSON() {
@ -56,6 +63,14 @@ export class RssDialogComponent extends BaseDialogComponent implements OnInit {
return this.exportJSON();
}
async toggleOpenStructuredComponentManagementDialogByDefault() {
await this.userPreferences.toggleOpenStructuredComponentManagementDialogByDefault();
await this.userPreferences.reload();
this.openStructuredComponentManagementDialogByDefault.set(
this.userPreferences.getOpenStructuredComponentManagementDialogByDefault(),
);
}
async undo(originalKey: string) {
this._loadingService.start();
await firstValueFrom(this._rssService.revertOverride(this.data.file.dossierId, this.data.file.fileId, [originalKey]));

View File

@ -25,7 +25,7 @@ import {
Toaster,
} from '@iqser/common-ui';
import { MatDialog } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.service';
import { AnnotationProcessingService } from './services/annotation-processing.service';
@ -317,6 +317,8 @@ export class FilePreviewScreenComponent
this.pdfProxyService.configureElements();
this.#restoreOldFilters();
document.documentElement.addEventListener('fullscreenchange', this.fullscreenListener);
this.#openRssDialogIfDefault();
}
ngAfterViewInit() {
@ -342,7 +344,7 @@ export class FilePreviewScreenComponent
result.annotations.map(w => w.manualRedactionEntry).filter(e => e.positions[0].page <= file.numberOfPages),
this.dossierId,
this.fileId,
result.dictionary?.label,
{ dictionaryLabel: result.dictionary?.label },
);
const addAndReload$ = add$.pipe(switchMap(() => this._filesService.reload(this.dossierId, file)));
@ -355,13 +357,16 @@ export class FilePreviewScreenComponent
const file = this.state.file();
const dossierTemplate = this._dossierTemplatesService.find(this.state.dossierTemplateId);
const isApprover = this.permissionsService.isApprover(this.state.dossier());
const applyDictionaryUpdatesToAllDossiersByDefault = dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault;
const hint = manualRedactionEntryWrapper.type === ManualRedactionEntryTypes.HINT;
const ref = this._iqserDialog.openDefault(RedactTextDialogComponent, {
data: {
manualRedactionEntryWrapper,
dossierId: this.dossierId,
file,
applyToAllDossiers: isApprover ? dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault : false,
applyToAllDossiers: isApprover ? applyDictionaryUpdatesToAllDossiersByDefault : false,
isApprover,
hint,
},
});
@ -370,13 +375,10 @@ export class FilePreviewScreenComponent
return;
}
const add$ = this._manualRedactionService.addAnnotation([result.redaction], this.dossierId, this.fileId, result.dictionary?.label);
if (isApprover && result.applyToAllDossiers !== null) {
const { ...body } = dossierTemplate;
body.applyDictionaryUpdatesToAllDossiersByDefault = result.applyToAllDossiers;
await this._dossierTemplatesService.createOrUpdate(body);
}
const add$ = this._manualRedactionService.addAnnotation([result.redaction], this.dossierId, this.fileId, {
hint,
dictionaryLabel: result.dictionary?.label,
});
const addAndReload$ = add$.pipe(
tap(() => this._documentViewer.clearSelection()),
@ -831,4 +833,13 @@ export class FilePreviewScreenComponent
}
});
}
#openRssDialogIfDefault() {
if (
this.permissionsService.canViewRssDialog() &&
this.userPreferenceService.getOpenStructuredComponentManagementDialogByDefault()
) {
this.openRSSView();
}
}
}

View File

@ -123,8 +123,8 @@ export class AnnotationActionsService {
async removeOrSuggestRemoveRedaction(redaction: AnnotationWrapper, permissions) {
const removePermissions: RemoveRedactionPermissions = {
canRemoveOrSuggestToRemoveOnlyHere: permissions.canRemoveOrSuggestToRemoveOnlyHere,
canRemoveOrSuggestToRemoveFromDictionary: permissions.canRemoveOrSuggestToRemoveFromDictionary,
canRemoveOnlyHere: permissions.canRemoveOnlyHere,
canRemoveFromDictionary: permissions.canRemoveFromDictionary,
canMarkAsFalsePositive: permissions.canMarkAsFalsePositive,
};
const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);
@ -149,12 +149,6 @@ export class AnnotationActionsService {
} else {
this.#removeRedaction(redaction, result);
}
if (isApprover && result.option.extraOption) {
const { ...body } = dossierTemplate;
body.applyDictionaryUpdatesToAllDossiersByDefault = result.option.extraOption.checked;
await this._dossierTemplatesService.createOrUpdate(body);
}
}
}

View File

@ -94,8 +94,15 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
return this.requestRecategorize(body, dossierId, fileId).pipe(this.#showToast('request-image-recategorization'));
}
addAnnotation(requests: List<IAddRedactionRequest>, dossierId: string, fileId: string, dictionaryLabel?: string) {
const toast = requests[0].addToDictionary ? this.#showAddToDictionaryToast(requests, dictionaryLabel) : this.#showToast('add');
addAnnotation(
requests: List<IAddRedactionRequest>,
dossierId: string,
fileId: string,
options?: { hint?: boolean; dictionaryLabel?: string },
) {
const toast = requests[0].addToDictionary
? this.#showAddToDictionaryToast(requests, options?.dictionaryLabel)
: this.#showToast(options?.hint ? 'force-hint' : 'add');
const canAddRedaction = this._iqaerPermissionsService.has(Roles.redactions.write);
if (canAddRedaction) {
return this.add(requests, dossierId, fileId).pipe(toast);

View File

@ -63,15 +63,6 @@ export class PdfAnnotationActionsService {
availableActions.push(recategorizeButton);
}
// if (permissions.canRemoveOrSuggestToRemoveFromDictionary) {
// const removeFromDictButton = this.#getButton(
// 'remove-from-dict',
// _('annotation-actions.remove-annotation.remove-from-dict'),
// () => this.#annotationActionsService.removeOrSuggestRemoveAnnotation(annotations, true),
// );
// availableActions.push(removeFromDictButton);
// }
if (permissions.canAcceptRecommendation) {
const acceptRecommendationButton = this.#getButton('check', _('annotation-actions.accept-recommendation.label'), () =>
this.#annotationActionsService.convertRecommendationToAnnotation(annotations),
@ -79,27 +70,6 @@ export class PdfAnnotationActionsService {
availableActions.push(acceptRecommendationButton);
}
if (permissions.canAcceptSuggestion) {
const acceptSuggestionButton = this.#getButton('check', _('annotation-actions.accept-suggestion.label'), () =>
this.#annotationActionsService.acceptSuggestion(annotations),
);
availableActions.push(acceptSuggestionButton);
}
if (permissions.canUndo) {
const undoButton = this.#getButton('undo', _('annotation-actions.undo'), () =>
this.#annotationActionsService.undoDirectAction(annotations),
);
availableActions.push(undoButton);
}
// if (permissions.canMarkAsFalsePositive) {
// const markAsFalsePositiveButton = this.#getButton('thumb-down', _('annotation-actions.remove-annotation.false-positive'), () =>
// this.#annotationActionsService.markAsFalsePositive(annotations),
// );
// availableActions.push(markAsFalsePositiveButton);
// }
if (permissions.canForceRedaction) {
const forceRedactionButton = this.#getButton('thumb-up', _('annotation-actions.force-redaction.label'), () =>
this.#annotationActionsService.forceAnnotation(annotations),
@ -114,27 +84,11 @@ export class PdfAnnotationActionsService {
availableActions.push(forceHintButton);
}
if (permissions.canRejectSuggestion) {
const rejectSuggestionButton = this.#getButton('close', _('annotation-actions.reject-suggestion'), () =>
this.#annotationActionsService.rejectSuggestion(annotations),
);
availableActions.push(rejectSuggestionButton);
}
// if (permissions.canRemoveOrSuggestToRemoveOnlyHere) {
// const removeOrSuggestToRemoveOnlyHereButton = this.#getButton(
// 'trash',
// _('annotation-actions.remove-annotation.only-here'),
// () => this.#annotationActionsService.removeOrSuggestRemoveAnnotation(annotations, false),
// );
// availableActions.push(removeOrSuggestToRemoveOnlyHereButton);
// }
if (permissions.canRemoveOrSuggestToRemoveRedaction) {
const removeOrSuggestToRemoveButton = this.#getButton('trash', _('annotation-actions.remove-annotation.remove-redaction'), () =>
if (permissions.canRemoveRedaction) {
const removeRedactionButton = this.#getButton('trash', _('annotation-actions.remove-annotation.remove-redaction'), () =>
this.#annotationActionsService.removeOrSuggestRemoveRedaction(annotations[0], permissions),
);
availableActions.push(removeOrSuggestToRemoveButton);
availableActions.push(removeRedactionButton);
}
return availableActions;
@ -158,21 +112,13 @@ export class PdfAnnotationActionsService {
canResize: permissions.length === 1 && permissions[0].canResizeAnnotation,
canChangeLegalBasis: permissions.reduce((acc, next) => acc && next.canChangeLegalBasis, true),
canRecategorizeImage: permissions.reduce((acc, next) => acc && next.canRecategorizeImage, true),
canRemoveOrSuggestToRemoveFromDictionary: permissions.reduce(
(acc, next) => acc && next.canRemoveOrSuggestToRemoveFromDictionary,
true,
),
canRemoveFromDictionary: permissions.reduce((acc, next) => acc && next.canRemoveFromDictionary, true),
canAcceptRecommendation: permissions.reduce((acc, next) => acc && next.canAcceptRecommendation, true),
canAcceptSuggestion: permissions.reduce((acc, next) => acc && next.canAcceptSuggestion, true),
canUndo:
this.#iqserPermissionsService.has(Roles.redactions.deleteManual) &&
permissions.reduce((acc, next) => acc && next.canUndo, true),
canMarkAsFalsePositive: permissions.reduce((acc, next) => acc && next.canMarkAsFalsePositive, true),
canForceRedaction: permissions.reduce((acc, next) => acc && next.canForceRedaction, true),
canForceHint: permissions.reduce((acc, next) => acc && next.canForceHint, true),
canRejectSuggestion: permissions.reduce((acc, next) => acc && next.canRejectSuggestion, true),
canRemoveOrSuggestToRemoveOnlyHere: permissions.reduce((acc, next) => acc && next.canRemoveOrSuggestToRemoveOnlyHere, true),
canRemoveOrSuggestToRemoveRedaction: permissions.reduce((acc, next) => acc && next.canRemoveOrSuggestToRemoveRedaction, true),
canRemoveOnlyHere: permissions.reduce((acc, next) => acc && next.canRemoveOnlyHere, true),
canRemoveRedaction: permissions.reduce((acc, next) => acc && next.canRemoveRedaction, true),
};
}
}

View File

@ -36,6 +36,7 @@ export class IconsModule {
'dictionary',
'denied',
'disable-analysis',
'documine-logo',
'double-chevron-right',
'enable-analysis',
'enter',

View File

@ -136,7 +136,7 @@ export class REDAnnotationManager {
#autoSelectRectangleAfterCreation() {
this.#manager.addEventListener('annotationChanged', (annotations: Annotation[], action: string, options) => {
this.#logger.info('[PDF] Annotations changed: ', annotations, action, options);
this.#logger.info('[ANNOTATIONS] Annotations changed: ', annotations, action, options);
// when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing

View File

@ -29,8 +29,9 @@ export function webViewerLoadedGuard(): CanActivateFn | ResolveFn<boolean> {
try {
instance = await pdf.init(inject(DOCUMENT).getElementById('viewer'));
} catch (e) {
logger.warn('[PDF] WebViewerGuard error: ', e, 'redirecting to', state.url);
return router.navigateByUrl(state.url);
const redirectUrl = state.url.split('/').slice(0, -2).join('/');
logger.warn('[PDF] WebViewerGuard error: ', e, 'redirecting to', redirectUrl);
return router.navigateByUrl(redirectUrl); // redirect to dissier page
}
annotationManager.init(instance.Core.annotationManager);

View File

@ -18,26 +18,26 @@
formControlName="downloadFileTypes"
></redaction-select>
</div>
<ng-container *allow="roles.watermarks.read">
<p class="heading">{{ 'dossier-watermark-selector.heading' | translate }}</p>
<ng-container *ngIf="ctx.existsWatermarks; else noWatermarks">
<redaction-watermark-selector
[dossierTemplateId]="dossier.dossierTemplateId"
[isReadonly]="!canEditDossier"
[label]="'dossier-watermark-selector.watermark' | translate"
formControlName="watermarkId"
>
</redaction-watermark-selector>
<p class="heading">{{ 'dossier-watermark-selector.heading' | translate }}</p>
<ng-container *ngIf="ctx.existsWatermarks; else noWatermarks">
<redaction-watermark-selector
[dossierTemplateId]="dossier.dossierTemplateId"
[isReadonly]="!canEditDossier"
[label]="'dossier-watermark-selector.watermark' | translate"
formControlName="watermarkId"
>
</redaction-watermark-selector>
<redaction-watermark-selector
[dossierTemplateId]="dossier.dossierTemplateId"
[isReadonly]="!canEditDossier"
[label]="'dossier-watermark-selector.preview' | translate"
formControlName="previewWatermarkId"
>
</redaction-watermark-selector>
<redaction-watermark-selector
[dossierTemplateId]="dossier.dossierTemplateId"
[isReadonly]="!canEditDossier"
[label]="'dossier-watermark-selector.preview' | translate"
formControlName="previewWatermarkId"
>
</redaction-watermark-selector>
</ng-container>
</ng-container>
<ng-template #noWatermarks>
<p [innerHTML]="'dossier-watermark-selector.no-watermark' | translate" class="no-watermark"></p>
</ng-template>

View File

@ -44,7 +44,7 @@ export class EditDossierTeamComponent implements EditDossierSectionInterface, On
}
get changed() {
const { owner, members, approvers } = this.form.value;
const { owner, members, approvers } = this.form.getRawValue();
if (this.dossier.ownerId !== owner) {
return true;
@ -66,7 +66,7 @@ export class EditDossierTeamComponent implements EditDossierSectionInterface, On
}
this.#updateLists();
}
const { owner, members, approvers } = this.form.value;
const { owner, members, approvers } = this.form.getRawValue();
const dossier = {
...this.dossier,
memberIds: members,
@ -129,7 +129,7 @@ export class EditDossierTeamComponent implements EditDossierSectionInterface, On
const possibleMembers = this.#userService.all.filter(user => user.isUser || user.isManager);
this.membersSelectOptions = possibleMembers
.filter(user => this.#userService.getName(user.id).toLowerCase().includes(value.toLowerCase()))
.filter(user => this.form.value.owner !== user.id)
.filter(user => this.form.getRawValue().owner !== user.id)
.map(user => user.id)
.sort((a, b) => this.#userService.getName(a).localeCompare(this.#userService.getName(b)));
}

View File

@ -28,6 +28,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { DossiersListingActionsComponent } from './components/dossiers-listing-actions/dossiers-listing-actions.component';
import { IqserUsersModule } from '@iqser/common-ui/lib/users';
import { SideNavComponent, SmallChipComponent, StatusBarComponent } from '@iqser/common-ui/lib/shared';
import { SelectComponent } from '@shared/components/select/select.component';
const components = [
FileActionsComponent,
@ -65,6 +66,7 @@ const dialogs = [EditDossierDialogComponent, AssignReviewerApproverDialogCompone
DynamicInputComponent,
IqserAllowDirective,
IqserDenyDirective,
SelectComponent,
],
})
export class SharedDossiersModule {}

View File

@ -39,7 +39,7 @@ export class FileDownloadBtnComponent implements OnChanges {
}
async downloadFiles() {
const ref = this._dialog.open(DownloadDialogComponent, {
const ref = this._dialog.openDefault(DownloadDialogComponent, {
data: { dossier: this.dossier, files: this.files },
});
const result = await ref.result();

View File

@ -79,7 +79,7 @@ export class ExpandableFileActionsComponent implements OnChanges {
}
async #downloadFiles(files: File[], dossier: Dossier) {
const ref = this._dialog.open(DownloadDialogComponent, {
const ref = this._dialog.openDefault(DownloadDialogComponent, {
data: { dossier, files },
});
const result = await ref.result();

View File

@ -1,11 +1,16 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostBinding, Input, TemplateRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatChipListbox, MatChipSelectionChange } from '@angular/material/chips';
import { MatChipListbox, MatChipSelectionChange, MatChipsModule } from '@angular/material/chips';
import { StopPropagationDirective } from '@iqser/common-ui';
import { NgForOf, NgTemplateOutlet } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
@Component({
selector: 'redaction-select',
templateUrl: './select.component.html',
styleUrls: ['./select.component.scss'],
standalone: true,
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -13,16 +18,17 @@ import { MatChipListbox, MatChipSelectionChange } from '@angular/material/chips'
multi: true,
},
],
imports: [StopPropagationDirective, NgTemplateOutlet, TranslateModule, NgForOf, MatChipsModule, MatIconModule],
})
export class SelectComponent implements AfterViewInit, ControlValueAccessor {
private _value: any[] = [];
private _onChange: (value: any[]) => void;
@Input() optionTemplate?: TemplateRef<{ option: any }>;
@Input() label: string;
@Input() options: any[];
@Input() disabled = false;
@Input() multiple = true;
@ViewChild(MatChipListbox) chipList: MatChipListbox;
private _value: any[] = [];
private _onChange: (value: any[]) => void;
constructor(private readonly _changeDetector: ChangeDetectorRef, private readonly _elementRef: ElementRef) {}

View File

@ -24,7 +24,7 @@
formControlName="downloadFileTypes"
></redaction-select>
<div class="iqser-input-group required">
<div class="iqser-input-group required" *deny="roles.getRss">
<label translate="download-dialog.form.redaction-preview-color"></label>
<input
[placeholder]="'download-dialog.form.redaction-preview-color-placeholder' | translate"

View File

@ -1,12 +1,18 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { Dossier, DownloadFileType, DownloadFileTypes, File, IReportTemplate, WorkflowFileStatuses } from '@red/domain';
import { downloadTypesForDownloadTranslations } from '@translations/download-types-translations';
import { ReportTemplateService } from '@services/report-template.service';
import { AbstractControl, FormBuilder } from '@angular/forms';
import { AbstractControl, FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, IqserDenyDirective, IqserDialogComponent } from '@iqser/common-ui';
import { Roles } from '@users/roles';
import { List } from '@iqser/common-ui/lib/utils';
import { NGXLogger } from 'ngx-logger';
import { AsyncPipe, NgIf } from '@angular/common';
import { SelectComponent } from '@shared/components/select/select.component';
import { TranslateModule } from '@ngx-translate/core';
import { ColorPickerModule } from 'ngx-color-picker';
import { MatIconModule } from '@angular/material/icon';
export interface DownloadDialogData {
readonly dossier: Dossier;
@ -22,12 +28,28 @@ export interface DownloadDialogResult {
@Component({
templateUrl: './download-dialog.component.html',
styleUrls: ['./download-dialog.component.scss'],
standalone: true,
imports: [
NgIf,
ReactiveFormsModule,
SelectComponent,
TranslateModule,
IqserDenyDirective,
ColorPickerModule,
MatIconModule,
IconButtonComponent,
CircleButtonComponent,
AsyncPipe,
],
})
export class DownloadDialogComponent extends IqserDialogComponent<DownloadDialogComponent, DownloadDialogData, DownloadDialogResult> {
readonly #logger = inject(NGXLogger);
readonly iconButtonTypes = IconButtonTypes;
readonly downloadTypes: { key: DownloadFileType; label: string }[] = this.#formDownloadTypes;
readonly hasApprovedFiles: boolean;
readonly downloadTypes: { key: DownloadFileType; label: string }[];
readonly availableReportTypes = this.#availableReportTypes;
readonly form = this.#getForm();
initialFormValue = this.form.getRawValue();
readonly roles = Roles;
@ -37,6 +59,8 @@ export class DownloadDialogComponent extends IqserDialogComponent<DownloadDialog
private readonly _formBuilder: FormBuilder,
) {
super();
this.hasApprovedFiles = this.data.files.some(file => file.workflowStatus === WorkflowFileStatuses.APPROVED);
this.downloadTypes = this.#formDownloadTypes;
}
get reportTypesLength() {
@ -52,12 +76,12 @@ export class DownloadDialogComponent extends IqserDialogComponent<DownloadDialog
}
get invalidDownload() {
return (
!this.form.controls.reportTemplateIds.value.length &&
!!this.form.controls.downloadFileTypes.value.length &&
this.form.controls.downloadFileTypes.value.every(type => type === DownloadFileTypes.REDACTED) &&
!this.data.files.some(file => file.workflowStatus === WorkflowFileStatuses.APPROVED)
);
const hasReportTypes = this.form.controls.reportTemplateIds.value.length;
const downloadFileTypes = this.form.controls.downloadFileTypes.value;
const onlyRedactedVersionSelected =
downloadFileTypes.length && downloadFileTypes.every(type => type === DownloadFileTypes.REDACTED);
return !hasReportTypes || (onlyRedactedVersionSelected && !this.hasApprovedFiles);
}
override get valid() {
@ -77,7 +101,7 @@ export class DownloadDialogComponent extends IqserDialogComponent<DownloadDialog
label: downloadTypesForDownloadTranslations[type],
}))
.filter(type => {
if (!this.data.files.some(file => file.workflowStatus === WorkflowFileStatuses.APPROVED)) {
if (!this.hasApprovedFiles) {
return type.key !== DownloadFileTypes.REDACTED;
}
return true;
@ -86,7 +110,7 @@ export class DownloadDialogComponent extends IqserDialogComponent<DownloadDialog
get #selectedDownloadTypes() {
return this.data.dossier.downloadFileTypes.filter(type => {
if (!this.data.files.some(file => file.workflowStatus === WorkflowFileStatuses.APPROVED)) {
if (!this.hasApprovedFiles) {
return type !== DownloadFileTypes.REDACTED;
}
return true;
@ -99,18 +123,20 @@ export class DownloadDialogComponent extends IqserDialogComponent<DownloadDialog
reportTemplateValueMapper = (reportTemplate: IReportTemplate) => reportTemplate.templateId;
close() {
override close() {
const result: DownloadDialogResult = {
reportTemplateIds: this.form.controls.reportTemplateIds.value,
downloadFileTypes: this.form.controls.downloadFileTypes.value,
redactionPreviewColor: this.form.controls.redactionPreviewColor.value,
};
this.#logger.info('[DOWNLOAD] Closing with result', result);
if (result.reportTemplateIds.length === 0) {
return this.dialogRef.close();
return super.close();
}
this.dialogRef.close(result);
super.close(result);
}
#hasReportTemplateOrDownloadType(control: AbstractControl) {

View File

@ -17,7 +17,6 @@ import {
IqserHelpModeModule,
StopPropagationDirective,
} from '@iqser/common-ui';
import { SelectComponent } from './components/select/select.component';
import { NavigateLastDossiersScreenDirective } from './directives/navigate-last-dossiers-screen.directive';
import { DictionaryManagerComponent } from './components/dictionary-manager/dictionary-manager.component';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
@ -43,17 +42,16 @@ import { AddEditEntityComponent } from './components/add-edit-entity/add-edit-en
import { ColorPickerModule } from 'ngx-color-picker';
import { WatermarkSelectorComponent } from './components/dossier-watermark-selector/watermark-selector.component';
import { OcrProgressBarComponent } from './components/ocr-progress-bar/ocr-progress-bar.component';
import { DownloadDialogComponent } from './dialogs/download-dialog/download-dialog.component';
import { CustomDateAdapter } from '@shared/CustomDateAdapter';
import { IqserUsersModule } from '@iqser/common-ui/lib/users';
import { SmallChipComponent } from '@iqser/common-ui/lib/shared';
import { SelectComponent } from '@shared/components/select/select.component';
const buttons = [FileDownloadBtnComponent];
const components = [
PaginationComponent,
AnnotationIconComponent,
SelectComponent,
DictionaryManagerComponent,
AssignUserDropdownComponent,
TypeFilterComponent,
@ -82,7 +80,7 @@ const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, Rea
const deleteThisWhenAllComponentsAreStandalone = [DonutChartComponent];
@NgModule({
declarations: [...components, ...utils, EditorComponent, DownloadDialogComponent],
declarations: [...components, ...utils, EditorComponent],
imports: [
CommonModule,
...modules,
@ -99,6 +97,7 @@ const deleteThisWhenAllComponentsAreStandalone = [DonutChartComponent];
HasScrollbarDirective,
IqserAllowDirective,
IqserDenyDirective,
SelectComponent,
],
exports: [...modules, ...components, ...utils, ...deleteThisWhenAllComponentsAreStandalone],
providers: [

View File

@ -12,6 +12,7 @@ import { DossierDictionariesMapService } from '@services/entity-services/dossier
import { List } from '@iqser/common-ui/lib/utils';
const MIN_WORD_LENGTH = 2;
const IMAGE_TYPES = ['image', 'formula', 'ocr'];
@Injectable({
providedIn: 'root',
@ -146,7 +147,12 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
return !!this._dictionariesMapService.get(dossierTemplateId).find(e => e.type === SuperTypes.ManualRedaction && !e.virtual);
}
getPossibleDictionaries(dossierTemplateId: string, hintTypes: boolean, dossierDictionaryOnly = false): Dictionary[] {
getPossibleDictionaries(
dossierTemplateId: string,
hintTypes: boolean,
dossierDictionaryOnly = false,
dictionaryRequest = false,
): Dictionary[] {
const possibleDictionaries: Dictionary[] = [];
this._dictionariesMapService.get(dossierTemplateId).forEach((d: Dictionary) => {
@ -157,8 +163,17 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
) {
possibleDictionaries.push(d);
}
} else if ((d.hint && d.hasDictionary && d.addToDictionaryAction) || (dossierDictionaryOnly && d.dossierDictionaryOnly)) {
possibleDictionaries.push(d);
} else if (
(d.hint && d.hasDictionary && d.addToDictionaryAction && d.type) ||
(dictionaryRequest && dossierDictionaryOnly && d.dossierDictionaryOnly)
) {
if (!dictionaryRequest) {
if (!IMAGE_TYPES.includes(d.type)) {
possibleDictionaries.push(d);
}
} else {
possibleDictionaries.push(d);
}
}
});

View File

@ -1,5 +1,5 @@
import { EntitiesService } from '@iqser/common-ui';
import { Injectable, signal } from '@angular/core';
import { computed, Injectable, signal } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { FileAttributeConfig, FileAttributes, IFileAttributeConfig, IFileAttributesConfig } from '@red/domain';
@ -12,8 +12,9 @@ export type FileAttributesConfigMap = Readonly<Record<string, IFileAttributesCon
})
export class FileAttributesService extends EntitiesService<IFileAttributeConfig, FileAttributeConfig> {
readonly fileAttributesConfig$ = new BehaviorSubject<FileAttributesConfigMap>({});
readonly isEditingFileAttribute = signal(false);
readonly isEditingFileAttribute = computed(() => this.openAttributeEdits().length > 0);
readonly openAttributeEdits = signal([]);
readonly openAttributeEdit = signal('');
readonly fileEdit = signal('');

View File

@ -1,10 +1,11 @@
import { inject, Injectable } from '@angular/core';
import { GenericService, QueryParam } from '@iqser/common-ui';
import { GenericService, IqserPermissionsService, QueryParam } from '@iqser/common-ui';
import { IWatermark, Watermark } from '@red/domain';
import { firstValueFrom, forkJoin, Observable } from 'rxjs';
import { firstValueFrom, forkJoin, Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { WatermarksMapService } from '@services/entity-services/watermarks-map.service';
import { mapEach } from '@iqser/common-ui/lib/utils';
import { Roles } from '@users/roles';
interface IsUsedResponse {
value: boolean;
@ -14,9 +15,11 @@ interface IsUsedResponse {
providedIn: 'root',
})
export class WatermarkService extends GenericService<IWatermark> {
readonly #watermarksMapService = inject(WatermarksMapService);
protected readonly _defaultModelPath = 'watermark';
readonly #watermarksMapService = inject(WatermarksMapService);
readonly #allowed = inject(IqserPermissionsService).has(Roles.watermarks.read);
async deleteWatermark(dossierTemplateId: string, watermarkId: number) {
await firstValueFrom(super.delete(null, `${this._defaultModelPath}/${watermarkId}`));
return firstValueFrom(this.loadForDossierTemplate(dossierTemplateId));
@ -37,6 +40,9 @@ export class WatermarkService extends GenericService<IWatermark> {
}
loadAll(dossierTemplateIds: string[]): Observable<Watermark[]> {
if (!this.#allowed) {
return of([]);
}
return forkJoin(dossierTemplateIds.map(id => this.loadForDossierTemplate(id))).pipe(map(arrays => [].concat(...arrays)));
}

View File

@ -31,7 +31,7 @@ export class FilesService extends EntitiesService<IFile, File> {
loadAll(dossierId: string) {
const files$ = this.getFor(dossierId).pipe(
mapEach(file => new File(file, this._userService.getName(file.assignee))),
tap(() => this._logger.info('[FILE] Loaded')),
tap(file => this._logger.info('[FILE] Loaded', file)),
);
const loadStats$ = files$.pipe(switchMap(files => this._dossierStatsService.getFor([dossierId]).pipe(map(() => files))));
return loadStats$.pipe(tap(files => this._filesMapService.set(dossierId, files)));
@ -41,7 +41,7 @@ export class FilesService extends EntitiesService<IFile, File> {
const _file = await firstValueFrom(super._getOne([dossierId, file.id]));
const reloadedFile = new File(_file, this._userService.getName(_file.assignee));
await firstValueFrom(this._dossierStatsService.getFor([dossierId]));
this._logger.info('[FILE] Reloaded');
this._logger.info('[FILE] Reloaded', reloadedFile);
return this._filesMapService.replace(dossierId, [reloadedFile]);
}

View File

@ -354,6 +354,10 @@ export class PermissionsService {
return this._iqserPermissionsService.has(Roles.rules.write) && this.isAdmin();
}
canViewRssDialog() {
return this._iqserPermissionsService.has(Roles.getRss);
}
#canDeleteEntity(entity: Dictionary): boolean {
return !entity.systemManaged;
}

View File

@ -10,6 +10,7 @@ export const PreferencesKeys = {
displaySuggestionsInPreview: 'Display-Suggestions-In-Preview',
unapprovedSuggestionsWarning: 'Unapproved-Suggestions-Warning',
loadAllAnnotationsWarning: 'Load-All-Annotations-Warning',
openStructuredComponentManagementDialogByDefault: 'Open-Structured-Component-Management-By-Default',
} as const;
@Injectable({
@ -53,6 +54,15 @@ export class UserPreferenceService extends IqserUserPreferenceService {
await this.save(PreferencesKeys.autoExpandFiltersOnActions, nextValue);
}
getOpenStructuredComponentManagementDialogByDefault(): boolean {
return this._getAttribute(PreferencesKeys.openStructuredComponentManagementDialogByDefault, 'false') === 'true';
}
async toggleOpenStructuredComponentManagementDialogByDefault(): Promise<void> {
const nextValue = (!this.getOpenStructuredComponentManagementDialogByDefault()).toString();
await this.save(PreferencesKeys.openStructuredComponentManagementDialogByDefault, nextValue);
}
getDisplaySuggestionsInPreview(): boolean {
return this._getAttribute(PreferencesKeys.displaySuggestionsInPreview, 'false') === 'true';
}

View File

@ -1,8 +1,9 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dev-04.iqser.cloud",
"APP_NAME": "RedactManager",
"API_URL": "https://dan.iqser.cloud",
"APP_NAME": "RedactManager very very very very long",
"IS_DOCUMINE": false,
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
"EULA_URL": "EULA_URL",
@ -11,7 +12,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dev-04.iqser.cloud/auth",
"OAUTH_URL": "https://dan.iqser.cloud/auth",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",

View File

@ -368,6 +368,7 @@
"declined-suggestion": "Abgelehnter Vorschlag",
"hint": "Hinweis",
"ignored-hint": "Ignorierter Hinweis",
"manual-hint": "",
"manual-redaction": "Manuelle Schwärzung",
"recommendation": "Empfehlung",
"redaction": "Schwärzung",
@ -1642,10 +1643,6 @@
},
"license-info-screen": {
"backend-version": "Backend-Version der Anwendung",
"chart": {
"pages-per-month": "Seiten pro Monat",
"total-pages": "Gesamtzahl der Seiten"
},
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)",
"copyright-claim-title": "Copyright",
"current-analyzed": "In aktuellem Lizenzzeitraum analysierte Seiten",
@ -1666,6 +1663,11 @@
"licensing-details": "Lizenzdetails",
"licensing-period": "Laufzeit der Lizenz",
"ocr-analyzed-pages": "Mit OCR konvertierte Seiten",
"pages": {
"cumulative": "Seiten insgesamt",
"pages-per-month": "Seiten pro Monat",
"total-pages": "Gesamtzahl der Seiten"
},
"status": {
"active": "Aktiv",
"inactive": ""
@ -1864,6 +1866,7 @@
"form": {
"auto-expand-filters-on-action": "",
"load-all-annotations-warning": "",
"open-structured-view-by-default": "",
"show-suggestions-in-preview": "",
"unapproved-suggestions-warning": ""
},

View File

@ -368,6 +368,7 @@
"declined-suggestion": "Declined Suggestion",
"hint": "Hint",
"ignored-hint": "Ignored Hint",
"manual-hint": "Manual Hint",
"manual-redaction": "Manual Redaction",
"recommendation": "Recommendation",
"redaction": "Redaction",
@ -1642,10 +1643,6 @@
},
"license-info-screen": {
"backend-version": "Backend Application Version",
"chart": {
"pages-per-month": "Pages per Month",
"total-pages": "Total Pages"
},
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)",
"copyright-claim-title": "Copyright Claim",
"current-analyzed": "Analyzed Pages in Licensing Period",
@ -1666,6 +1663,11 @@
"licensing-details": "Licensing Details",
"licensing-period": "Licensing Period",
"ocr-analyzed-pages": "OCR Processed Pages in Licensing Period",
"pages": {
"cumulative": "Cumulative Pages",
"pages-per-month": "Pages per Month",
"total-pages": "Total Pages"
},
"status": {
"active": "Active",
"inactive": "Inactive"
@ -1864,6 +1866,7 @@
"form": {
"auto-expand-filters-on-action": "Auto-expand filters on my actions",
"load-all-annotations-warning": "Warning regarding loading all annotations at once in file preview",
"open-structured-view-by-default": "Display structured component management modal by default",
"show-suggestions-in-preview": "Display suggestions in document preview",
"unapproved-suggestions-warning": "Warning regarding unapproved suggestions in document Preview mode"
},
@ -1959,11 +1962,11 @@
"label": "False positive"
},
"in-dossier": {
"description": "Do not redact \"{value}\" in any document of the current dossier.",
"label": "Remove in dossier"
"description": "Do not {type} \"{value}\" in any document of the current dossier.",
"label": "Remove from dossier"
},
"only-here": {
"description": "Do not redact \"{value}\" at this position in the current document.",
"description": "Do not {type} \"{value}\" at this position in the current document.",
"label": "Remove here"
},
"redact": {
@ -1976,7 +1979,7 @@
}
}
},
"title": "Remove redaction"
"title": "Remove {type}"
}
},
"report-type": {

View File

@ -208,9 +208,6 @@
"accept-recommendation": {
"label": "Empfehlung annehmen"
},
"accept-suggestion": {
"label": "Genehmigen und zum Wörterbuch hinzufügen"
},
"convert-highlights": {
"label": ""
},
@ -328,7 +325,6 @@
}
},
"recategorize-image": "neu kategorisieren",
"reject-suggestion": "Vorschlag ablehnen",
"remove-annotation": {
"remove-redaction": ""
},
@ -368,6 +364,7 @@
"declined-suggestion": "Abgelehnter Vorschlag",
"hint": "Hinweis",
"ignored-hint": "Ignorierter Hinweis",
"manual-hint": "",
"manual-redaction": "Manuelle Schwärzung",
"recommendation": "Empfehlung",
"redaction": "Schwärzung",
@ -1642,10 +1639,6 @@
},
"license-info-screen": {
"backend-version": "Backend-Version der Anwendung",
"chart": {
"pages-per-month": "Seiten pro Monat",
"total-pages": "Gesamtzahl der Seiten"
},
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)",
"copyright-claim-title": "Copyright",
"current-analyzed": "In aktuellem Lizenzzeitraum analysierte Seiten",
@ -1666,6 +1659,11 @@
"licensing-details": "Lizenzdetails",
"licensing-period": "Laufzeit der Lizenz",
"ocr-analyzed-pages": "Mit OCR konvertierte Seiten",
"pages": {
"cumulative": "Seiten insgesamt",
"pages-per-month": "Seiten pro Monat",
"total-pages": "Gesamtzahl der Seiten"
},
"status": {
"active": "Aktiv",
"inactive": ""
@ -1864,6 +1862,7 @@
"form": {
"auto-expand-filters-on-action": "",
"load-all-annotations-warning": "",
"open-structured-view-by-default": "",
"show-suggestions-in-preview": "",
"unapproved-suggestions-warning": ""
},
@ -2075,6 +2074,12 @@
"undo": ""
},
"annotations": "",
"table-header": {
"annotation": "",
"component": "",
"transformation": "",
"value": ""
},
"title": ""
},
"rules-screen": {

View File

@ -208,9 +208,6 @@
"accept-recommendation": {
"label": "Accept Recommendation"
},
"accept-suggestion": {
"label": "Approve Suggestion"
},
"convert-highlights": {
"label": "Convert Selected Earmarks"
},
@ -258,8 +255,8 @@
},
"manual-redaction": {
"add": {
"error": "Failed to save component: {error}",
"success": "Component added!"
"error": "Failed to save annotation: {error}",
"success": "Annotation added!"
},
"approve": {
"error": "Failed to approve suggestion: {error}",
@ -290,8 +287,8 @@
"success": "Hint removed!"
},
"remove": {
"error": "Failed to remove component: {error}",
"success": "Component removed!"
"error": "Failed to remove annotation: {error}",
"success": "Annotation removed!"
},
"request-change-legal-basis": {
"error": "Failed to request annotation reason change: {error}",
@ -328,7 +325,6 @@
}
},
"recategorize-image": "Recategorize",
"reject-suggestion": "Reject Suggestion",
"remove-annotation": {
"remove-redaction": "Remove"
},
@ -368,9 +364,10 @@
"declined-suggestion": "Declined Suggestion",
"hint": "Hint",
"ignored-hint": "Ignored Hint",
"manual-redaction": "Manual Component",
"manual-hint": "Manual Hint",
"manual-redaction": "Manual Annotation",
"recommendation": "Recommendation",
"redaction": "Component",
"redaction": "Annotation",
"skipped": "Skipped",
"suggestion-add": "Suggested component",
"suggestion-add-dictionary": "Suggested add to Dictionary",
@ -1424,7 +1421,7 @@
"text-highlights": "Earmarks",
"text-highlights-tooltip": "Shows all text-earmarks and allows removing or importing them as components",
"toggle-analysis": {
"disable": "Disable component",
"disable": "Disable extraction",
"enable": "Enable for component",
"only-managers": "Enabling / disabling is permitted only for managers"
}
@ -1642,10 +1639,6 @@
},
"license-info-screen": {
"backend-version": "Backend Application Version",
"chart": {
"pages-per-month": "Pages per Month",
"total-pages": "Total Pages"
},
"copyright-claim-text": "Copyright © 2020 - {currentYear} knecon AG (powered by IQSER)",
"copyright-claim-title": "Copyright Claim",
"current-analyzed": "Analyzed Pages in Licensing Period",
@ -1666,6 +1659,11 @@
"licensing-details": "Licensing Details",
"licensing-period": "Licensing Period",
"ocr-analyzed-pages": "OCR Processed Pages in Licensing Period",
"pages": {
"cumulative": "Cumulative Pages",
"pages-per-month": "Pages per Month",
"total-pages": "Total Pages"
},
"status": {
"active": "Active",
"inactive": "Inactive"
@ -1716,7 +1714,7 @@
"force-hint": "Force Hint",
"force-redaction": "Force Component",
"hint": "Add hint",
"redact": "Redact",
"redact": "Annotation",
"redaction": "Redaction"
}
}
@ -1724,9 +1722,9 @@
"minutes": "minutes",
"no-active-license": "Invalid or corrupt license Please contact your administrator",
"notification": {
"assign-approver": "You have been assigned as approver for <b>{fileHref, select, null{{fileName}} other{<a href=\"{fileHref}\" target=\"_blank\">{fileName}</a>}}</b> in dossier: <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>!",
"assign-approver": "You have been assigned to <b>{fileHref, select, null{{fileName}} other{<a href=\"{fileHref}\" target=\"_blank\">{fileName}</a>}}</b> in dossier: <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>!",
"assign-reviewer": "You have been assigned as reviewer for <b>{fileHref, select, null{{fileName}} other{<a href=\"{fileHref}\" target=\"_blank\">{fileName}</a>}}</b> in dossier: <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>!",
"document-approved": " <b>{fileHref, select, null{{fileName}} other{<a href=\"{fileHref}\" target=\"_blank\">{fileName}</a>}}</b> has been approved!",
"document-approved": " <b>{fileHref, select, null{{fileName}} other{<a href=\"{fileHref}\" target=\"_blank\">{fileName}</a>}}</b> has been moved to Done!",
"dossier-deleted": "Dossier: <b>{dossierName}</b> has been deleted!",
"dossier-owner-deleted": "The owner of dossier: <b>{dossierName}</b> has been deleted!",
"dossier-owner-removed": "You have been removed as dossier owner from <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>!",
@ -1754,9 +1752,9 @@
},
"options-title": "Choose on which action you want to be notified",
"options": {
"ASSIGN_APPROVER": "When I am assigned to a document as Approver",
"ASSIGN_APPROVER": "When I am assigned to a document",
"ASSIGN_REVIEWER": "When I am assigned to a document as Reviewer",
"DOCUMENT_APPROVED": "When the document status changes to Approved (only for dossier owners)",
"DOCUMENT_APPROVED": "When the document status changes to Done (only for dossier owners)",
"DOCUMENT_UNDER_APPROVAL": "When the document status changes to Under Approval",
"DOCUMENT_UNDER_REVIEW": "When the document status changes to In Progress",
"DOSSIER_DELETED": "When a dossier was deleted",
@ -1864,6 +1862,7 @@
"form": {
"auto-expand-filters-on-action": "Auto expand filters on my actions",
"load-all-annotations-warning": "Warning regarding loading all annotations at once in file preview",
"open-structured-view-by-default": "Display structured component management modal by default",
"show-suggestions-in-preview": "Display suggestions in document preview",
"unapproved-suggestions-warning": "Warning regarding unapproved suggestions in document Preview mode"
},
@ -1959,11 +1958,11 @@
"label": "False positive"
},
"in-dossier": {
"description": "Do not redact \"{value}\" in any document of the current dossier.",
"label": "Remove in dossier"
"description": "Do not {type} \"{value}\" in any document of the current dossier.",
"label": "Remove from dossier"
},
"only-here": {
"description": "Do not redact \"{value}\" at this position in the current document.",
"description": "Do not {type} \"{value}\" at this position in the current document.",
"label": "Remove here"
},
"redact": {
@ -1976,7 +1975,7 @@
}
}
},
"title": "Remove redaction"
"title": "Remove {type}"
}
},
"report-type": {
@ -2075,6 +2074,12 @@
"undo": "Undo to: {value}"
},
"annotations": "<strong>{type}</strong> found on {pageCount, plural, one{page} other{pages}} {pages} by rule #{ruleNumber}",
"table-header": {
"annotation-references": "Annotation references",
"component": "Component",
"transformation-rule": "Transformation rule",
"value": "Value"
},
"title": "Structured Component Management"
},
"rules-screen": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" id="Layer_1" version="1.1" x="0px" xml:space="preserve" y="0px" viewBox="398.3 453.2 132.2 117.1">
<style type="text/css">
.st0 {
opacity: 0.5;
fill: #504FDC;
}
.st1 {
fill: #504FDC;
}
</style>
<g>
<polygon class="st0" points="441.3,453.2 441.3,496.8 398.3,496.8 "/>
<path class="st1" d="M530.5,475.7c0,1.2-0.5,2.4-1.3,3.2c-0.8,0.8-1.9,1.3-3.2,1.3h-5.8c-2.5,0-4.5-2-4.5-4.5 c0-1.2,0.5-2.4,1.3-3.2c0.8-0.8,1.9-1.3,3.2-1.3h5.8C528.5,471.2,530.5,473.3,530.5,475.7z"/>
<path class="st1" d="M512.1,489.2h-32.2c-2.5,0-4.5-2-4.5-4.5c0-1.2,0.5-2.4,1.3-3.2c0.8-0.8,1.9-1.3,3.2-1.3h22.4 c1.2,0,2.4-0.5,3.2-1.3c0.8-0.8,1.3-1.9,1.3-3.2c0-2.5-2-4.5-4.5-4.5h-14.4c-2.5,0-4.5-2-4.5-4.5c0-1.2,0.5-2.4,1.3-3.2 c0.8-0.8,1.9-1.3,3.2-1.3h9.4c1.2,0,2.4-0.5,3.2-1.3c0.8-0.8,1.3-1.9,1.3-3.2c0-2.5-2-4.5-4.5-4.5h-56v43.6h-43v73.5h90 c1.2,0,2.4-0.5,3.2-1.3c0.8-0.8,1.3-1.9,1.3-3.2c0-2.5-2-4.5-4.5-4.5h-14.8l0,0h-0.6c-2.5,0-4.5-2-4.5-4.5c0-1.2,0.5-2.4,1.3-3.2 c0.8-0.8,1.9-1.3,3.2-1.3h14.4c1.2,0,2.4-0.5,3.2-1.3c0.8-0.8,1.3-1.9,1.3-3.2c0-2.5-2-4.5-4.5-4.5h-30.4c-2.5,0-4.5-2-4.5-4.5 c0-1.2,0.5-2.4,1.3-3.2c0.8-0.8,1.9-1.3,3.2-1.3h46.4c1.2,0,2.4-0.5,3.2-1.3c0.8-0.8,1.3-1.9,1.3-3.2c0-2.5-2-4.5-4.5-4.5h-38.4 c-2.5,0-4.5-2-4.5-4.5c0-1.2,0.5-2.4,1.3-3.2c0.8-0.8,1.9-1.3,3.2-1.3h22.4c1.2,0,2.4-0.5,3.2-1.3c0.8-0.8,1.3-1.9,1.3-3.2 c0-2.5-2-4.5-4.5-4.5h-14.4c-2.5,0-4.5-2-4.5-4.5c0-1.2,0.5-2.4,1.3-3.2c0.8-0.8,1.9-1.3,3.2-1.3h39.2c1.2,0,2.4-0.5,3.2-1.3 c0.8-0.8,1.3-1.9,1.3-3.2C516.6,491.3,514.5,489.2,512.1,489.2z M455.3,552.2c2.5,0,4.5,2,4.5,4.5c0,1.2-0.5,2.4-1.3,3.2 c-0.8,0.8-1.9,1.3-3.2,1.3h-5.8c-2.5,0-4.5-2-4.5-4.5c0-1.2,0.5-2.4,1.3-3.2c0.8-0.8,1.9-1.3,3.2-1.3H455.3z"/>
<path class="st1" d="M516.6,565.8c0,1.2-0.5,2.4-1.3,3.2c-0.8,0.8-1.9,1.3-3.2,1.3h-5.8c-2.5,0-4.5-2-4.5-4.5 c0-1.2,0.5-2.4,1.3-3.2c0.8-0.8,1.9-1.3,3.2-1.3h5.8C514.5,561.3,516.6,563.3,516.6,565.8z"/>
<path class="st1" d="M514.2,514.9c-0.8,0.8-1.9,1.3-3.2,1.3h-5.8c-2.5,0-4.5-2-4.5-4.5c0-1.2,0.5-2.4,1.3-3.2 c0.8-0.8,1.9-1.3,3.2-1.3h5.8c2.5,0,4.5,2,4.5,4.5C515.6,513,515.1,514.1,514.2,514.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -4,6 +4,7 @@ ADMIN_CONTACT_NAME="${ADMIN_CONTACT_NAME:-}"
ADMIN_CONTACT_URL="${ADMIN_CONTACT_URL:-}"
API_URL="${API_URL:-}"
APP_NAME="${APP_NAME:-}"
IS_DOCUMINE="${IS_DOCUMINE:-}"
AUTO_READ_TIME="${AUTO_READ_TIME:-1.5}"
BACKEND_APP_VERSION="${BACKEND_APP_VERSION:-4.7.0}"
EULA_URL="${EULA_URL:-}"
@ -25,13 +26,12 @@ BASE_TRANSLATIONS_DIRECTORY="${BASE_TRANSLATIONS_DIRECTORY:-/assets/i18n/redact/
THEME="${THEME:-theme-template}"
WATERMARK_PREVIEW_PAPER_FORMAT="${WATERMARK_PREVIEW_PAPER_FORMAT:-a4}"
echo '{
"ADMIN_CONTACT_NAME":"'"$ADMIN_CONTACT_NAME"'",
"ADMIN_CONTACT_URL":"'"$ADMIN_CONTACT_URL"'",
"API_URL":"'"$API_URL"'",
"APP_NAME":"'"$APP_NAME"'",
"IS_DOCUMINE":"'"$IS_DOCUMINE"'",
"AUTO_READ_TIME":"'"$AUTO_READ_TIME"'",
"BACKEND_APP_VERSION":"'"$BACKEND_APP_VERSION"'",
"EULA_URL":"'"$EULA_URL:"'",
@ -51,7 +51,7 @@ echo '{
"AVAILABLE_OLD_NOTIFICATIONS_MINUTES":"'"$AVAILABLE_OLD_NOTIFICATIONS_MINUTES"'",
"NOTIFICATIONS_THRESHOLD":"'"$NOTIFICATIONS_THRESHOLD"'",
"WATERMARK_PREVIEW_PAPER_FORMAT":"'"$WATERMARK_PREVIEW_PAPER_FORMAT"'"
}' > /usr/share/nginx/html/ui/assets/config/config.json
}' >/usr/share/nginx/html/ui/assets/config/config.json
echo 'Env variables: '
cat /usr/share/nginx/html/ui/assets/config/config.json

@ -1 +1 @@
Subproject commit a8f5fb2e25cd1f150c1099d387b4f9dece3b922c
Subproject commit e69f8b3f0df8701bbc3dbc79e2239aaad3acc889

View File

@ -4,7 +4,6 @@ import { DictionaryEntryType } from './dictionary-entry-types';
export interface IAddRedactionRequest {
addToDictionary?: boolean;
addToDossierDictionary?: boolean;
dictionaryEntryType?: DictionaryEntryType;
comment?: { text: string };
legalBasis?: string;

View File

@ -3,7 +3,6 @@ import { LogEntryStatus } from './types';
export interface IManualRedactionEntry {
addToDictionary?: boolean;
addToDossierDictionary?: boolean;
annotationId?: string;
fileId?: string;
legalBasis?: string;

View File

@ -1,4 +1,4 @@
import { IqserAppConfig } from '@iqser/common-ui/lib/utils';
import { IqserAppConfig } from '@common-ui/utils';
export interface AppConfig extends IqserAppConfig {
readonly ADMIN_CONTACT_NAME: string;

6
renovate.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}

View File

@ -24,6 +24,7 @@
"@i18n/*": ["apps/red-ui/src/app/i18n/*"],
"@iqser/common-ui": ["libs/common-ui/src/index.ts"],
"@iqser/common-ui/*": ["libs/common-ui/src/*"],
"@common-ui/*": ["libs/common-ui/src/lib/*"],
"@models/*": ["apps/red-ui/src/app/models/*"],
"@red/domain": ["libs/red-domain/src/index.ts"],
"@services/*": ["apps/red-ui/src/app/services/*"],