Compare commits

...

45 Commits

Author SHA1 Message Date
Nicoleta Panaghiu
bd9ae173f8 RED-10589 - removed file workload from workflow item on documine. 2024-12-12 10:31:41 +02:00
Nicoleta Panaghiu
d1421420f2 RED-10647: fixed documine helpmode links. 2024-12-11 18:31:08 +02:00
Valentin Mihai
2d5a6b474f RED-10634 - closed "edit component view" when component is deleted 2024-12-06 21:49:25 +02:00
Valentin Mihai
eb6aa43d48 RED-10600 - updated can assign reviewer logic when file is set to done to assign user when is moved back to "in review" 2024-12-05 16:21:33 +02:00
Valentin Mihai
f5d63c9174 RED-10593 - when legalBasisValue = 'n-a' set empty string as param to not display "Annotation based on rule n-a" 2024-12-05 16:13:05 +02:00
Valentin Mihai
2bd524842e RED-10612 - RED-10483 - stop making additional dossier requests which leads to "Dossier not found" error when dossier is already deleted 2024-12-05 12:17:46 +02:00
Nicoleta Panaghiu
1dc069a091 RED-10592: fixed listener to esc key in pdf viewer. 2024-12-04 13:23:46 +02:00
Nicoleta Panaghiu
c9bb6bc6a5 RED-10588: backported all the locked rules related changes. 2024-12-03 18:02:23 +02:00
Nicoleta Panaghiu
584a6b7bd0 RED-10563: fixed key actions not prevented when input is focused.
Also updated config.json with documine stack and properties.
2024-11-29 13:21:47 +02:00
Nicoleta Panaghiu
a890e4c5bc RED-10509: disabled stopPropagation when editing file attribute. 2024-11-25 10:56:37 +02:00
Valentin Mihai
4ec81ed0b3 RED-10396 - undefined checks 2024-11-20 11:48:07 +02:00
Nicoleta Panaghiu
489c3e4117 RED-10405: removed locked rules pending type and fixed pending count. 2024-11-18 11:41:59 +02:00
Nicoleta Panaghiu
2de02e95c8 RED-10405: check if file isError, indent pending type, remove unknown. 2024-11-13 15:08:10 +02:00
Nicoleta Panaghiu
d883472ed3 RED-10405: added labels and filtering for separate file pending types. 2024-11-13 11:02:20 +02:00
Adina Țeudan
de238c0a8e RED-10422 - improved number of requests with help of new changes endpoint 2024-11-11 18:21:24 +02:00
Adina Țeudan
67c9e54df0 RED-10414 Removed offset and fixed notification polling 2024-11-06 15:57:12 +02:00
Adina Țeudan
7ac3d3e5b6 Updated common-ui 2024-11-06 15:56:01 +02:00
Nicoleta Panaghiu
44050c0373 RED-10258: add all available fonts. 2024-11-05 11:17:06 +02:00
Nicoleta Panaghiu
67e50478e7 RED-9985: prevent filter categories from collapsing upon selection. 2024-09-26 11:55:52 +03:00
Nicoleta Panaghiu
edd709df6f RED-9993: fixed license labels. 2024-09-23 15:58:01 +03:00
Nicoleta Panaghiu
05b6f88d64 RED-9900: removed help mode helper elements from hidden buttons. 2024-09-19 17:36:31 +03:00
Nicoleta Panaghiu
7027de6f6e RED-10050: fixed navigating to doc with filtered annotations issue. 2024-09-18 13:54:39 +03:00
Nicoleta Panaghiu
7beddc6138 RED-9993: fixed license information. 2024-09-13 11:03:20 +03:00
Valentin Mihai
71980486a2 RED-10016 - DM: Permanent loading triggerd when trying to edit the component 2024-09-10 16:08:40 +03:00
Nicoleta Panaghiu
1081f8ad1c RED-9372: update common ui. 2024-09-10 11:43:26 +03:00
Nicoleta Panaghiu
435b9f16a9 RED-9372: fixed scrolling on dossier details. 2024-09-10 11:39:52 +03:00
Nicoleta Panaghiu
52f124adff RED-9372: update common ui. 2024-09-05 17:20:47 +03:00
Nicoleta Panaghiu
1171c0d78f RED-9372: fixed annotation details moving on hover. 2024-09-05 17:19:53 +03:00
Nicoleta Panaghiu
6cedf8afb0 RED-9950: increased component management section width by 5%. 2024-09-05 14:59:45 +03:00
Valentin Mihai
1b2f14ec89 RED-9962 - RM-163: Components values are not updated in the UI until page is refreshed 2024-09-04 16:22:30 +03:00
Nicoleta Panaghiu
dc66b8a708 RED-9950: decreased component management width. 2024-09-04 14:41:00 +03:00
Nicoleta Panaghiu
e2cd71dd24 RED-9950: removed more margins. 2024-09-03 12:24:04 +03:00
Nicoleta Panaghiu
3777c12134 RED-9950: increased document viewer space. 2024-08-29 17:29:14 +03:00
Nicoleta Panaghiu
3bfa4796b0 RED-9951: made document info scrollable & fixed paginator position. 2024-08-29 13:04:51 +03:00
Nicoleta Panaghiu
c0c6232f0a RED-9887: fixed save button being disabled. 2024-08-28 13:23:01 +03:00
Nicoleta Panaghiu
d52b8b0d87 RED-9946: added tooltip with valueDescription. 2024-08-27 15:10:36 +03:00
Nicoleta Panaghiu
61b869675e RED-9887: fixed component fields cancel and revert actions. 2024-08-27 14:58:43 +03:00
Nicoleta Panaghiu
be743fa9d7 RED-9824: fixed toaster notification message for edit annotation action. 2024-08-21 11:36:19 +03:00
Marius Schummer
e5a3366ec7 Correction of translation wording 2024-08-20 15:37:54 +02:00
Nicoleta Panaghiu
4a49e68c4a RED-9889: escape html in component values. 2024-08-20 15:13:14 +03:00
Marius Schummer
15a675515d Adjustments of default messages for empty annotation column (relating to RED-9788) 2024-08-15 16:10:27 +02:00
Valentin Mihai
3f8a53b86c RED-9260 - Component Management UI 2024-08-15 16:51:21 +03:00
Valentin Mihai
ae0510add8 RED-9201 - UI for Component Mapping Tables 2024-08-14 19:34:00 +03:00
Nicoleta Panaghiu
ca51e31de8 RED-9777: added help button on edit and remove annotation dialogs. 2024-08-14 11:48:46 +03:00
Nicoleta Panaghiu
02addbbc16 RED-9874: added user preference for overwrite file option. 2024-08-13 15:20:10 +03:00
372 changed files with 1914 additions and 877 deletions

View File

@ -258,6 +258,7 @@ export const appModuleFactory = (config: AppConfig) => {
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {
disableTooltipInteractivity: true,
showDelay: 1,
},
},
BaseDatePipe,

View File

@ -11,7 +11,9 @@ import { DictionaryService } from '@services/entity-services/dictionary.service'
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { WatermarkService } from '@services/entity-services/watermark.service';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { getConfig } from '@iqser/common-ui';
import { getConfig, Toaster } from '@iqser/common-ui';
import { RulesService } from '../modules/admin/services/rules.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export function templateExistsWhenEnteringAdmin(): CanActivateFn {
return async function (route: ActivatedRouteSnapshot): Promise<boolean> {
@ -21,12 +23,14 @@ export function templateExistsWhenEnteringAdmin(): CanActivateFn {
const defaultColorsService = inject(DefaultColorsService);
const watermarksService = inject(WatermarkService);
const router = inject(Router);
const rulesService = inject(RulesService);
const isDocumine = getConfig().IS_DOCUMINE;
const dossierTemplate = inject(DossierTemplateStatsService).get(dossierTemplateId);
await firstValueFrom(fileAttributesService.loadFileAttributesConfig(dossierTemplateId));
await firstValueFrom(dictionaryService.loadDictionaryDataForDossierTemplate(dossierTemplateId));
await firstValueFrom(defaultColorsService.loadForDossierTemplate(dossierTemplateId));
await firstValueFrom(rulesService.getFor(dossierTemplateId));
if (!isDocumine) {
await firstValueFrom(watermarksService.loadForDossierTemplate(dossierTemplateId));
}
@ -50,6 +54,8 @@ export function templateExistsWhenEnteringDossierList(): CanActivateFn {
const dictionaryService = inject(DictionaryService);
const defaultColorsService = inject(DefaultColorsService);
const watermarksService = inject(WatermarkService);
const rulesService = inject(RulesService);
const toaster = inject(Toaster);
const isDocumine = getConfig().IS_DOCUMINE;
await firstValueFrom(dashboardStatsService.loadForTemplate(dossierTemplateId));
@ -64,6 +70,10 @@ export function templateExistsWhenEnteringDossierList(): CanActivateFn {
await firstValueFrom(fileAttributesService.loadFileAttributesConfig(dossierTemplateId));
await firstValueFrom(dictionaryService.loadDictionaryDataForDossierTemplate(dossierTemplateId));
await firstValueFrom(defaultColorsService.loadForDossierTemplate(dossierTemplateId));
const rules = await firstValueFrom(rulesService.getFor(dossierTemplateId));
if (rules.timeoutDetected) {
toaster.error(_('dossier-listing.rules.timeoutError'));
}
if (!isDocumine) {
await firstValueFrom(watermarksService.loadForDossierTemplate(dossierTemplateId));
}

View File

@ -31,6 +31,11 @@
{{ 'preferences-screen.form.help-mode-dialog' | translate }}
</mat-checkbox>
</div>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="overwriteFileOption">
{{ 'preferences-screen.form.overwrite-file-option' | translate }}
</mat-checkbox>
</div>
</ng-container>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import {
@ -27,6 +27,7 @@ interface PreferencesForm {
// warnings preferences
loadAllAnnotationsWarning: boolean;
helpModeDialog: boolean;
overwriteFileOption: boolean;
[k: string]: any;
}
@ -57,6 +58,12 @@ const Screens = {
],
})
export class PreferencesComponent extends BaseFormComponent implements OnInit {
readonly #formBuilder = inject(FormBuilder);
readonly #permissionsService = inject(IqserPermissionsService);
readonly #changeRef = inject(ChangeDetectorRef);
readonly #loadingService = inject(LoadingService);
readonly #userPreferenceService = inject(UserPreferenceService);
readonly form: FormGroup<AsControl<PreferencesForm>>;
readonly currentScreen: Screen;
readonly screens = Screens;
@ -65,76 +72,87 @@ export class PreferencesComponent extends BaseFormComponent implements OnInit {
readonly config = getConfig();
readonly isIqserDevMode = isIqserDevMode();
constructor(
route: ActivatedRoute,
readonly userPreferenceService: UserPreferenceService,
private readonly _formBuilder: FormBuilder,
private readonly _permissionsService: IqserPermissionsService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _loadingService: LoadingService,
) {
get #isOverwriteFileOptionActive() {
return !(this.#userPreferenceService.getOverwriteFileOption() === 'undefined');
}
constructor(route: ActivatedRoute) {
super();
this.form = this._formBuilder.group({
this.form = this.#formBuilder.group({
// preferences
autoExpandFiltersOnActions: [this.userPreferenceService.getAutoExpandFiltersOnActions()],
tableExtractionType: [this.userPreferenceService.getTableExtractionType()],
autoExpandFiltersOnActions: [this.#userPreferenceService.getAutoExpandFiltersOnActions()],
tableExtractionType: [this.#userPreferenceService.getTableExtractionType()],
// warnings preferences
loadAllAnnotationsWarning: [this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)],
helpModeDialog: [this.userPreferenceService.getBool(KEYS.helpModeDialog)],
loadAllAnnotationsWarning: [this.#userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)],
helpModeDialog: [this.#userPreferenceService.getBool(KEYS.helpModeDialog)],
overwriteFileOption: [this.#isOverwriteFileOptionActive],
});
if (!this._permissionsService.has(Roles.managePreferences)) {
if (!this.#permissionsService.has(Roles.managePreferences)) {
this.form.disable();
}
if (!this._permissionsService.has(Roles.getTables)) {
if (!this.#permissionsService.has(Roles.getTables)) {
this.form.controls.tableExtractionType.disable();
}
if (!this.#isOverwriteFileOptionActive) {
this.form.controls.overwriteFileOption.disable();
}
this.initialFormValue = this.form.getRawValue();
this.currentScreen = route.snapshot.data.screen;
}
ngOnInit() {
this._loadingService.stop();
this.#loadingService.stop();
}
async save(): Promise<any> {
if (this.form.controls.autoExpandFiltersOnActions.value !== this.userPreferenceService.getAutoExpandFiltersOnActions()) {
await this.userPreferenceService.toggleAutoExpandFiltersOnActions();
if (this.form.controls.autoExpandFiltersOnActions.value !== this.#userPreferenceService.getAutoExpandFiltersOnActions()) {
await this.#userPreferenceService.toggleAutoExpandFiltersOnActions();
}
if (this.form.controls.tableExtractionType.value !== this.userPreferenceService.getTableExtractionType()) {
await this.userPreferenceService.save(PreferencesKeys.tableExtractionType, this.form.controls.tableExtractionType.value);
if (this.form.controls.tableExtractionType.value !== this.#userPreferenceService.getTableExtractionType()) {
await this.#userPreferenceService.save(PreferencesKeys.tableExtractionType, this.form.controls.tableExtractionType.value);
}
if (
this.form.controls.loadAllAnnotationsWarning.value !==
this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)
this.#userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)
) {
await this.userPreferenceService.save(
await this.#userPreferenceService.save(
PreferencesKeys.loadAllAnnotationsWarning,
String(this.form.controls.loadAllAnnotationsWarning.value),
);
}
if (this.form.controls.helpModeDialog.value !== this.userPreferenceService.getBool(KEYS.helpModeDialog)) {
await this.userPreferenceService.save(KEYS.helpModeDialog, String(this.form.controls.helpModeDialog.value));
if (this.form.controls.helpModeDialog.value !== this.#userPreferenceService.getBool(KEYS.helpModeDialog)) {
await this.#userPreferenceService.save(KEYS.helpModeDialog, String(this.form.controls.helpModeDialog.value));
}
await this.userPreferenceService.reload();
if (this.form.controls.overwriteFileOption.enabled && !this.form.controls.overwriteFileOption.value) {
await this.#userPreferenceService.saveOverwriteFileOption('undefined');
}
await this.#userPreferenceService.reload();
this.#patchValues();
this.initialFormValue = this.form.getRawValue();
this._changeRef.markForCheck();
this.#changeRef.markForCheck();
}
#patchValues() {
this.form.patchValue({
autoExpandFiltersOnActions: this.userPreferenceService.getAutoExpandFiltersOnActions(),
tableExtractionType: this.userPreferenceService.getTableExtractionType(),
loadAllAnnotationsWarning: this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning),
helpModeDialog: this.userPreferenceService.getBool(KEYS.helpModeDialog),
autoExpandFiltersOnActions: this.#userPreferenceService.getAutoExpandFiltersOnActions(),
tableExtractionType: this.#userPreferenceService.getTableExtractionType(),
loadAllAnnotationsWarning: this.#userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning),
helpModeDialog: this.#userPreferenceService.getBool(KEYS.helpModeDialog),
overwriteFileOption: this.#isOverwriteFileOptionActive,
});
if (!this.#isOverwriteFileOptionActive) {
this.form.controls.overwriteFileOption.disable();
}
}
}

View File

@ -50,9 +50,11 @@
icon="iqser:trash"
></iqser-circle-button>
}
@if (selectedComponent?.id === component.id) {
<mat-icon class="arrow-right" svgIcon="red:arrow-right"></mat-icon>
}
<mat-icon
[class.not-visible]="selectedComponent?.id !== component.id"
class="arrow-right"
svgIcon="red:arrow-right"
></mat-icon>
</div>
</div>
</div>

View File

@ -38,6 +38,10 @@
.arrow-right {
transform: scale(0.7);
&.not-visible {
visibility: hidden;
}
}
}
}
@ -45,6 +49,8 @@
.content-container {
background-color: var(--iqser-grey-6);
height: 100vh;
display: flex;
flex-direction: column;
.content-header {
display: flex;
@ -61,6 +67,7 @@
}
.content {
flex: 1;
display: flex;
gap: 20px;
overflow: hidden;
@ -68,7 +75,8 @@
.components-list {
flex: 1;
background-color: var(--iqser-white);
width: 100%;
display: flex;
flex-direction: column;
.header {
height: 30px;

View File

@ -103,6 +103,7 @@ export default class ComponentDefinitionsComponent extends BaseFormComponent imp
this._dialogService.openDialog('confirm', null, async () => {
await firstValueFrom(this._componentDefinitionsService.deleteComponentDefinitions(this.#dossierTemplateId, [componentId]));
await this.#loadData();
this.selectedComponent = null;
});
}

View File

@ -30,7 +30,7 @@
<div
class="row"
[matTooltip]="'add-edit-component-mapping.disabled-file-options' | translate"
[matTooltipDisabled]="!disabledFileOptions"
[matTooltipDisabled]="!form.get('encoding')?.disabled"
[matTooltipPosition]="'above'"
>
<div class="iqser-input-group required w-150">
@ -40,13 +40,12 @@
formControlName="delimiter"
name="delimiter"
type="text"
[class.disabled-file-options]="disabledFileOptions"
/>
</div>
<div class="iqser-input-group required w-150">
<label translate="add-edit-component-mapping.form.encoding-type"></label>
<mat-form-field [class.disabled-file-options]="disabledFileOptions">
<mat-form-field>
<mat-select formControlName="encoding">
<mat-option *ngFor="let type of encodingTypeOptions" [value]="type">
{{ translations[type] | translate }}

View File

@ -18,11 +18,6 @@
font-size: 15px;
}
}
.disabled-file-options {
opacity: 0.5;
pointer-events: none;
}
}
.row:last-child {

View File

@ -52,7 +52,6 @@ export class AddEditComponentMappingDialogComponent
{
protected readonly encodingTypeOptions = Object.keys(FileAttributeEncodingTypes);
protected readonly translations = fileAttributeEncodingTypesTranslations;
#fileChanged = false;
activeFile: File;
form!: UntypedFormGroup;
@ -73,13 +72,14 @@ export class AddEditComponentMappingDialogComponent
const file = new Blob([fileContent.body as Blob], { type: 'text/csv' });
this.form.get('file').setValue(file);
this.initialFormValue = this.form.getRawValue();
this.#disableEncodingAndDelimiter();
}
}
changeFile(file: File) {
this.#fileChanged = true;
this.form.get('file').setValue(file);
this.form.get('fileName').setValue(file?.name);
this.#enableEncodingAndDelimiter();
}
save() {
@ -96,7 +96,13 @@ export class AddEditComponentMappingDialogComponent
});
}
get disabledFileOptions() {
return this.initialFormValue?.file && !this.#fileChanged;
#disableEncodingAndDelimiter() {
this.form.get('encoding').disable();
this.form.get('delimiter').disable();
}
#enableEncodingAndDelimiter() {
this.form.get('encoding').enable();
this.form.get('delimiter').enable();
}
}

View File

@ -34,6 +34,21 @@
<span [innerHTML]="'dossier-template-info-screen.created-on' | translate: { date: createdOn }"></span>
</div>
<div *ngIf="areRulesLocked()">
<mat-icon
(click)="resetRules()"
[matTooltip]="
currentUser.isAdmin
? ('dossier-template-info-screen.rules-reset.tooltip' | translate)
: ('dossier-template-info-screen.rules-reset.disabled-action' | translate)
"
[class.cursor-pointer]="currentUser.isAdmin"
svgIcon="iqser:alert-circle"
class="action-icon"
></mat-icon>
<span class="error">{{ 'dossier-template-info-screen.rules-reset.label' | translate }}</span>
</div>
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ 'dossier-template-info-screen.entries' | translate: { count: ctx.stats.numberOfEntries } }}

View File

@ -18,3 +18,17 @@
padding-right: 24px;
margin-right: 24px;
}
.error {
color: var(--iqser-primary);
}
.action-icon {
::ng-deep svg g {
fill: var(--iqser-primary);
}
}
.cursor-pointer {
cursor: pointer;
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, computed, Input, OnInit } from '@angular/core';
import { ContextComponent } from '@iqser/common-ui/lib/utils';
import { type DossierTemplate, type DossierTemplateStats } from '@red/domain';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
@ -9,6 +9,12 @@ import { MatIcon } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
import { InitialsAvatarComponent } from '@common-ui/users';
import { DatePipe } from '@shared/pipes/date.pipe';
import { RulesService } from '../../../services/rules.service';
import { Toaster } from '@iqser/common-ui';
import { MatTooltip } from '@angular/material/tooltip';
import { firstValueFrom } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { getCurrentUser } from '@users/user.service';
interface Context {
readonly dossierTemplate: DossierTemplate;
@ -20,16 +26,22 @@ interface Context {
templateUrl: './dossier-template-details.component.html',
styleUrls: ['./dossier-template-details.component.scss'],
standalone: true,
imports: [NgIf, AsyncPipe, MatIcon, TranslateModule, DatePipe, InitialsAvatarComponent],
imports: [NgIf, AsyncPipe, MatIcon, TranslateModule, DatePipe, InitialsAvatarComponent, MatTooltip],
})
export class DossierTemplateDetailsComponent extends ContextComponent<Context> implements OnInit {
readonly translations = dossierTemplateStatusTranslations;
@Input({ required: true }) dossierTemplateId: string;
readonly areRulesLocked = computed(() => {
return this._rulesService.currentTemplateRules().timeoutDetected;
});
readonly currentUser = getCurrentUser();
constructor(
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dossierTemplateStatsService: DossierTemplateStatsService,
private readonly _rulesService: RulesService,
private readonly _toaster: Toaster,
) {
super();
}
@ -40,4 +52,11 @@ export class DossierTemplateDetailsComponent extends ContextComponent<Context> i
stats: this._dossierTemplateStatsService.watch$(this.dossierTemplateId),
});
}
async resetRules() {
if (!this.currentUser.isAdmin) return;
await firstValueFrom(this._rulesService.reset(this.dossierTemplateId));
this._toaster.success(_('dossier-template-info-screen.rules-reset.success'));
await firstValueFrom(this._rulesService.getFor(this.dossierTemplateId));
}
}

View File

@ -3,7 +3,7 @@ import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonthAndYear } from '../../utils/functions';
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonth } from '../../utils/functions';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { size } from '@iqser/common-ui/lib/utils';
@ -43,7 +43,7 @@ export class LicenseAnalysisCapacityUsageComponent {
#getCapacityDatasets(): ChartDataset[] {
const monthlyData = [...this.licenseService.selectedLicenseReport.monthlyData];
const dataUntilCurrentMonth = getDataUntilCurrentMonth(monthlyData);
if (monthlyData.length === 1 || isCurrentMonthAndYear(monthlyData[0].startDate)) {
if (monthlyData.length === 1 || isCurrentMonth(monthlyData[0].startDate)) {
const empty = { analysedFilesBytes: null } as ILicenseData;
dataUntilCurrentMonth.splice(0, 0, empty);
monthlyData.splice(0, 0, empty);
@ -60,11 +60,8 @@ export class LicenseAnalysisCapacityUsageComponent {
},
{
data: dataUntilCurrentMonth.map((month, monthIndex) =>
month.analysedFilesBytes
? month.analysedFilesBytes +
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + (curr.analysedFilesBytes ?? 0), 0)
: 0,
data: dataUntilCurrentMonth.map((_, monthIndex) =>
monthlyData.slice(0, monthIndex + 1).reduce((acc, curr) => acc + (curr.analysedFilesBytes ?? 0), 0),
),
label: this._translateService.instant('license-info-screen.analysis-capacity-usage.analyzed-cumulative'),
yAxisID: 'y1',

View File

@ -3,7 +3,7 @@ import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonthAndYear } from '../../utils/functions';
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig, isCurrentMonth } from '../../utils/functions';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ILicenseData } from '@red/domain';
@ -40,7 +40,7 @@ export class LicensePageUsageComponent {
#getPagesDatasets(): ChartDataset[] {
const monthlyData = [...this.licenseService.selectedLicenseReport.monthlyData];
const dataUntilCurrentMonth = getDataUntilCurrentMonth(monthlyData);
if (monthlyData.length === 1 || isCurrentMonthAndYear(monthlyData[0].startDate)) {
if (monthlyData.length === 1 || isCurrentMonth(monthlyData[0].startDate)) {
const empty = { numberOfAnalyzedPages: null } as ILicenseData;
dataUntilCurrentMonth.splice(0, 0, empty);
monthlyData.splice(0, 0, empty);
@ -63,11 +63,8 @@ export class LicensePageUsageComponent {
order: 1,
},
{
data: dataUntilCurrentMonth.map((month, monthIndex) =>
month.numberOfAnalyzedPages
? month.numberOfAnalyzedPages +
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + (curr.numberOfAnalyzedPages ?? 0), 0)
: 0,
data: dataUntilCurrentMonth.map((_, monthIndex) =>
monthlyData.slice(0, monthIndex + 1).reduce((acc, curr) => acc + (curr.numberOfAnalyzedPages ?? 0), 0),
),
label: this._translateService.instant('license-info-screen.page-usage.cumulative-pages'),
yAxisID: 'y1',

View File

@ -6,7 +6,6 @@ import { ComplexFillTarget } from 'chart.js/dist/types';
const monthNames = dayjs.monthsShort();
const currentMonth = dayjs(Date.now()).month();
const currentYear = dayjs(Date.now()).year();
export const verboseDate = (date: Dayjs) => `${monthNames[date.month()]} ${date.year()}`;
@ -45,7 +44,7 @@ export const getLabelsFromLicense = (license: ILicenseReport) => {
monthIterator = monthIterator.add(1, 'month');
}
if (startMonth.month() === endMonth.month() || startMonth.month() === currentMonth) {
if (startMonth.isSame(endMonth, 'month') || isCurrentMonth(startMonth.toDate())) {
result.splice(0, 0, '');
}
@ -53,9 +52,9 @@ export const getLabelsFromLicense = (license: ILicenseReport) => {
};
export const getDataUntilCurrentMonth = (monthlyData: ILicenseData[]) => {
return monthlyData.filter(data => dayjs(data.startDate).month() <= currentMonth && dayjs(data.startDate).year() <= currentYear);
return monthlyData.filter(data => dayjs(data.startDate).isSameOrBefore(dayjs(Date.now()), 'month'));
};
export const isCurrentMonthAndYear = (date: Date | string) => {
return dayjs(date).month() === currentMonth && dayjs(date).year() === currentYear;
export const isCurrentMonth = (date: Date | string) => {
return dayjs(date).isSame(dayjs(Date.now()), 'month');
};

View File

@ -4,7 +4,6 @@ import { ChangeDetectorRef, Component, ElementRef, inject, OnInit, ViewChild } f
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatIcon } from '@angular/material/icon';
import { MatSlider, MatSliderThumb } from '@angular/material/slider';
import { MatTooltip } from '@angular/material/tooltip';
import { Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { environment } from '@environments/environment';
@ -46,6 +45,7 @@ import { ColorPickerModule } from 'ngx-color-picker';
import { BehaviorSubject, firstValueFrom, Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PaginatorComponent } from '../paginator/paginator.component';
import { MatTooltip } from '@angular/material/tooltip';
export const DEFAULT_WATERMARK: Partial<IWatermark> = {
text: 'Watermark',
@ -85,11 +85,11 @@ interface WatermarkForm {
HasScrollbarDirective,
NgForOf,
NgClass,
MatTooltip,
MatIcon,
MatSlider,
ColorPickerModule,
MatSliderThumb,
MatTooltip,
],
})
export class WatermarkScreenComponent implements OnInit {
@ -269,7 +269,7 @@ export class WatermarkScreenComponent implements OnInit {
});
if (environment.production) {
this.instance.Core.setCustomFontURL('https://' + window.location.host + this.#convertPath('/assets/pdftron'));
this.instance.Core.setCustomFontURL(window.location.origin + this.#convertPath('/assets/pdftron/fonts'));
}
this.#disableElements();

View File

@ -1,9 +1,25 @@
import { Injectable } from '@angular/core';
import { GenericService } from '@iqser/common-ui';
import { IRules } from '@red/domain';
import { EntitiesService, QueryParam } from '@iqser/common-ui';
import { IRules, Rules } from '@red/domain';
import { map, Observable, tap } from 'rxjs';
import { List } from '@common-ui/utils';
import { toSignal } from '@angular/core/rxjs-interop';
import { distinctUntilChanged, filter } from 'rxjs/operators';
@Injectable()
export class RulesService extends GenericService<IRules> {
@Injectable({ providedIn: 'root' })
export class RulesService extends EntitiesService<IRules, Rules> {
readonly currentTemplateRules = toSignal(
this.all$.pipe(
filter(all => !!all.length),
map(rules => rules[0]),
distinctUntilChanged(
(prev, curr) =>
prev.rules === curr.rules &&
prev.timeoutDetected === curr.timeoutDetected &&
prev.dossierTemplateId === curr.dossierTemplateId,
),
),
);
protected readonly _defaultModelPath = 'rules';
download(dossierTemplateId: string, ruleFileType: IRules['ruleFileType'] = 'ENTITY') {
@ -13,4 +29,12 @@ export class RulesService extends GenericService<IRules> {
uploadRules(body: IRules) {
return this._post<unknown>({ ...body, ruleFileType: body.ruleFileType ?? 'ENTITY' });
}
getFor<R = IRules>(entityId: string, queryParams?: List<QueryParam>): Observable<R> {
return super.getFor<R>(entityId, queryParams).pipe(tap(rules => this.setEntities([rules as Rules])));
}
reset(dossierTemplateId: string, ruleFileType: IRules['ruleFileType'] = 'ENTITY') {
return this._put(null, `${this._defaultModelPath}/${dossierTemplateId}/${ruleFileType}/reset`);
}
}

View File

@ -1,8 +1,8 @@
<ng-container (longPress)="forceReanalysisAction($event)" *ngIf="selectedFiles.length" redactionLongPress>
<ng-container (longPress)="forceReanalysisAction($event)" *ngIf="selectedFiles().length" redactionLongPress>
<redaction-expandable-file-actions
[actions]="buttons"
[buttonType]="buttonType"
[maxWidth]="maxWidth"
[actions]="buttons()"
[buttonType]="buttonType()"
[maxWidth]="maxWidth()"
[tooltipPosition]="IqserTooltipPositions.above"
>
</redaction-expandable-file-actions>

View File

@ -1,6 +1,6 @@
import { Component, Input, OnChanges } from '@angular/core';
import { Component, computed, input, signal } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { CircleButtonType, CircleButtonTypes } from '@iqser/common-ui';
import { CircleButtonType, CircleButtonTypes, Toaster } from '@iqser/common-ui';
import { Action, ActionTypes, Dossier, File, ProcessingFileStatuses } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { LongPressDirective, LongPressEvent } from '@shared/directives/long-press.directive';
@ -9,6 +9,8 @@ import { BulkActionsService } from '../../services/bulk-actions.service';
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
import { IqserTooltipPositions } from '@common-ui/utils';
import { NgIf } from '@angular/common';
import { RulesService } from '../../../admin/services/rules.service';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'redaction-dossier-overview-bulk-actions [dossier] [selectedFiles]',
@ -17,218 +19,211 @@ import { NgIf } from '@angular/common';
standalone: true,
imports: [LongPressDirective, ExpandableFileActionsComponent, NgIf],
})
export class DossierOverviewBulkActionsComponent implements OnChanges {
#analysisForced: boolean;
#canAssignToSelf: boolean;
#canAssign: boolean;
#canDelete: boolean;
#canReanalyse: boolean;
#canDisableAutoAnalysis: boolean;
#canEnableAutoAnalysis: boolean;
#canOcr: boolean;
#canSetToNew: boolean;
#canSetToUnderReview: boolean;
#canSetToUnderApproval: boolean;
#isReadyForApproval: boolean;
#canApprove: boolean;
#canUndoApproval: boolean;
#canToggleAnalysis: boolean;
#assignTooltip: string;
#toggleAnalysisTooltip: string;
#allFilesAreExcluded: boolean;
#canMoveToSameState: boolean;
@Input() dossier: Dossier;
@Input() selectedFiles: File[];
@Input() buttonType: CircleButtonType = CircleButtonTypes.default;
@Input() maxWidth: number;
buttons: Action[];
export class DossierOverviewBulkActionsComponent {
readonly dossier = input<Dossier>();
readonly selectedFiles = input<File[]>();
readonly buttonType = input<CircleButtonType>(CircleButtonTypes.default);
readonly maxWidth = input<number>();
readonly buttons = computed(() => this.#buttons);
readonly IqserTooltipPositions = IqserTooltipPositions;
readonly #areFilesInErrorState = computed(() => this.selectedFiles().some(file => file.isError));
readonly #areRulesLocked = computed(() => this._rulesService.currentTemplateRules().timeoutDetected);
readonly #allFilesAreUnderReviewOrUnassigned = computed(() =>
this.selectedFiles().reduce((acc, file) => acc && (file.isUnderReview || file.isNew), true),
);
readonly #allFilesAreUnderApproval = computed(() => this.selectedFiles().reduce((acc, file) => acc && file.isUnderApproval, true));
readonly #allFilesAreExcluded = computed(() => this.selectedFiles().reduce((acc, file) => acc && file.excluded, true));
readonly #allFilesAreApproved = computed(() => this.selectedFiles().reduce((acc, file) => acc && file.isApproved, true));
readonly #canMoveToSameState = computed(
() => this.#allFilesAreUnderReviewOrUnassigned() || this.#allFilesAreUnderApproval() || this.#allFilesAreApproved(),
);
readonly #canAssign = computed(
() =>
this.#canMoveToSameState() &&
(this._permissionsService.canAssignUser(this.selectedFiles(), this.dossier()) ||
this._permissionsService.canUnassignUser(this.selectedFiles(), this.dossier())),
);
readonly #canAssignToSelf = computed(
() => this.#canMoveToSameState() && this._permissionsService.canAssignToSelf(this.selectedFiles(), this.dossier()),
);
readonly #canDelete = computed(() => this._permissionsService.canSoftDeleteFile(this.selectedFiles(), this.dossier()));
readonly #canReanalyse = computed(() => this._permissionsService.canReanalyseFile(this.selectedFiles(), this.dossier()));
readonly #canDisableAutoAnalysis = computed(
() => this._permissionsService.canDisableAutoAnalysis(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #canEnableAutoAnalysis = computed(
() => this._permissionsService.canEnableAutoAnalysis(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #canToggleAnalysis = computed(() => this._permissionsService.canToggleAnalysis(this.selectedFiles(), this.dossier()));
readonly #canOcr = computed(
() => this._permissionsService.canOcrFile(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #canSetToNew = computed(
() => this._permissionsService.canSetToNew(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #canSetToUnderReview = computed(
() => this._permissionsService.canSetUnderReview(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #canSetToUnderApproval = computed(
() => this._permissionsService.canSetUnderApproval(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #isReadyForApproval = computed(
() => this._permissionsService.isReadyForApproval(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #canApprove = computed(
() => this._permissionsService.canBeApproved(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #canUndoApproval = computed(
() => this._permissionsService.canUndoApproval(this.selectedFiles(), this.dossier()) && !this.#areFilesInErrorState(),
);
readonly #assignTooltip = computed(() =>
this.#allFilesAreUnderApproval() ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer'),
);
readonly #toggleAnalysisTooltip = computed(() =>
this.#allFilesAreExcluded() ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable'),
);
readonly #analysisForced = signal(false);
constructor(
private readonly _permissionsService: PermissionsService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _bulkActionsService: BulkActionsService,
private readonly _rulesService: RulesService,
private readonly _toaster: Toaster,
) {}
private get _buttons(): Action[] {
get #buttons(): Action[] {
const actions: Action[] = [
{
id: 'delete-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.delete(this.selectedFiles),
action: () => this._bulkActionsService.delete(this.selectedFiles()),
tooltip: _('dossier-overview.bulk.delete'),
icon: 'iqser:trash',
show: this.#canDelete,
show: this.#canDelete(),
},
{
id: 'assign-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.assign(this.selectedFiles),
tooltip: this.#assignTooltip,
action: () => this._bulkActionsService.assign(this.selectedFiles()),
tooltip: this.#assignTooltip(),
icon: 'red:assign',
show: this.#canAssign,
show: this.#canAssign(),
},
{
id: 'assign-files-to-me-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.assignToMe(this.selectedFiles),
action: () => this._bulkActionsService.assignToMe(this.selectedFiles()),
tooltip: _('dossier-overview.assign-me'),
icon: 'red:assign-me',
show: this.#canAssignToSelf,
show: this.#canAssignToSelf(),
},
{
id: 'to-new-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToNew(this.selectedFiles),
action: () => this._bulkActionsService.setToNew(this.selectedFiles()),
tooltip: _('dossier-overview.back-to-new'),
icon: 'red:undo',
show: this.#canSetToNew,
show: this.#canSetToNew(),
},
{
id: 'to-under-approval-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles),
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles()),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:ready-for-approval',
show: this.#canSetToUnderApproval,
show: this.#canSetToUnderApproval(),
},
{
id: 'to-under-review-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.backToUnderReview(this.selectedFiles),
action: () => this._bulkActionsService.backToUnderReview(this.selectedFiles()),
tooltip: _('dossier-overview.under-review'),
icon: 'red:undo',
show: this.#canSetToUnderReview,
show: this.#canSetToUnderReview(),
},
{
id: 'download-files-btn',
type: ActionTypes.downloadBtn,
show: !this.selectedFiles.some(file => file.processingStatus === ProcessingFileStatuses.ERROR || !file.lastProcessed),
files: this.selectedFiles,
dossier: this.dossier,
show: !this.selectedFiles().some(file => file.processingStatus === ProcessingFileStatuses.ERROR || !file.lastProcessed),
files: this.selectedFiles(),
dossier: this.dossier(),
},
{
id: 'approve-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.approve(this.selectedFiles),
disabled: !this.#canApprove,
tooltip: this.#canApprove ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
action: () => this._bulkActionsService.approve(this.selectedFiles()),
disabled: !this.#canApprove(),
tooltip: this.#canApprove() ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
icon: 'red:approved',
show: this.#isReadyForApproval,
show: this.#isReadyForApproval(),
},
{
id: 'set-under-approval-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles),
action: () => this._bulkActionsService.setToUnderApproval(this.selectedFiles()),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:undo',
show: this.#canUndoApproval,
show: this.#canUndoApproval(),
},
{
id: 'ocr-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.ocr(this.selectedFiles),
action: () => this._bulkActionsService.ocr(this.selectedFiles()),
tooltip: _('dossier-overview.ocr-file'),
icon: 'iqser:ocr',
show: this.#canOcr,
show: this.#canOcr(),
},
{
id: 'reanalyse-files-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.reanalyse(this.selectedFiles),
tooltip: _('dossier-overview.bulk.reanalyse'),
action: () => this.#reanalyseBulk(this.selectedFiles()),
tooltip: this.#areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('dossier-overview.bulk.reanalyse'),
icon: 'iqser:refresh',
disabled: this.#areRulesLocked(),
show:
this.#canReanalyse &&
(this.#analysisForced || this.#canEnableAutoAnalysis || this.selectedFiles.every(file => file.isError)),
this.#canReanalyse() &&
(this.#analysisForced() || this.#canEnableAutoAnalysis() || this.selectedFiles().every(file => file.isError)),
},
{
id: 'stop-automatic-analysis-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles),
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles()),
tooltip: _('dossier-overview.stop-auto-analysis'),
icon: 'red:disable-analysis',
show: this.#canDisableAutoAnalysis,
show: this.#canDisableAutoAnalysis(),
},
{
id: 'start-automatic-analysis-btn',
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles),
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles()),
tooltip: _('dossier-overview.start-auto-analysis'),
icon: 'red:enable-analysis',
show: this.#canEnableAutoAnalysis,
show: this.#canEnableAutoAnalysis(),
},
{
id: 'toggle-analysis-btn',
type: ActionTypes.toggle,
action: () => this._bulkActionsService.toggleAnalysis(this.selectedFiles, !this.#allFilesAreExcluded),
tooltip: this.#toggleAnalysisTooltip,
checked: !this.#allFilesAreExcluded,
show: this.#canToggleAnalysis,
action: () => this._bulkActionsService.toggleAnalysis(this.selectedFiles(), !this.#allFilesAreExcluded()),
tooltip: this.#toggleAnalysisTooltip(),
checked: !this.#allFilesAreExcluded(),
show: this.#canToggleAnalysis(),
},
];
return actions.filter(btn => btn.show);
}
ngOnChanges() {
this._setup();
}
forceReanalysisAction($event: LongPressEvent) {
this.#analysisForced = !$event.touchEnd && this._userPreferenceService.isIqserDevMode;
this._setup();
this.#analysisForced.set(!$event.touchEnd && this._userPreferenceService.isIqserDevMode);
}
private _setup() {
if (!this.selectedFiles.length) {
async #reanalyseBulk(selectedFiles: File[]) {
const rules = await firstValueFrom(this._rulesService.getFor(this.dossier().dossierTemplateId));
if (rules.timeoutDetected) {
this._toaster.error(_('dossier-listing.rules.timeoutError'));
return;
}
const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce(
(acc, file) => acc && (file.isUnderReview || file.isNew),
true,
);
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true);
const allFilesAreApproved = this.selectedFiles.reduce((acc, file) => acc && file.isApproved, true);
this.#allFilesAreExcluded = this.selectedFiles.reduce((acc, file) => acc && file.excluded, true);
this.#canMoveToSameState = allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval || allFilesAreApproved;
this.#canAssign =
this.#canMoveToSameState &&
(this._permissionsService.canAssignUser(this.selectedFiles, this.dossier) ||
this._permissionsService.canUnassignUser(this.selectedFiles, this.dossier));
this.#canAssignToSelf = this.#canMoveToSameState && this._permissionsService.canAssignToSelf(this.selectedFiles, this.dossier);
this.#canDelete = this._permissionsService.canSoftDeleteFile(this.selectedFiles, this.dossier);
this.#canReanalyse = this._permissionsService.canReanalyseFile(this.selectedFiles, this.dossier);
this.#canDisableAutoAnalysis = this._permissionsService.canDisableAutoAnalysis(this.selectedFiles, this.dossier);
this.#canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis(this.selectedFiles, this.dossier);
this.#canToggleAnalysis = this._permissionsService.canToggleAnalysis(this.selectedFiles, this.dossier);
this.#canOcr = this._permissionsService.canOcrFile(this.selectedFiles, this.dossier);
this.#canSetToNew = this._permissionsService.canSetToNew(this.selectedFiles, this.dossier);
this.#canSetToUnderReview = this._permissionsService.canSetUnderReview(this.selectedFiles, this.dossier);
this.#canSetToUnderApproval = this._permissionsService.canSetUnderApproval(this.selectedFiles, this.dossier);
this.#isReadyForApproval = this._permissionsService.isReadyForApproval(this.selectedFiles, this.dossier);
this.#canApprove = this._permissionsService.canBeApproved(this.selectedFiles, this.dossier);
this.#canUndoApproval = this._permissionsService.canUndoApproval(this.selectedFiles, this.dossier);
this.#assignTooltip = allFilesAreUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer');
this.#toggleAnalysisTooltip = this.#allFilesAreExcluded
? _('file-preview.toggle-analysis.enable')
: _('file-preview.toggle-analysis.disable');
this.buttons = this._buttons;
await this._bulkActionsService.reanalyse(selectedFiles);
}
}

View File

@ -61,7 +61,8 @@
*ngFor="let config of statusConfig"
[attr.help-mode-key]="'dashboard_in_dossier'"
[config]="config"
filterKey="processingTypeFilters"
[class.indent]="!!PendingTypes[config.id]"
[filterKey]="PendingTypes[config.id] ? 'pendingTypeFilters' : 'processingTypeFilters'"
></iqser-progress-bar>
</div>

View File

@ -45,3 +45,7 @@
iqser-progress-bar:not(:last-child) {
margin-bottom: 10px;
}
.indent {
margin-left: 32px;
}

View File

@ -21,7 +21,9 @@ import {
DossierAttributeWithValue,
DossierStats,
File,
FileErrorCodes,
IDossierRequest,
PendingTypes,
ProcessingTypes,
StatusSorter,
User,
@ -58,7 +60,6 @@ interface DossierDetailsContext {
NgIf,
AsyncPipe,
CircleButtonComponent,
IqserAllowDirective,
TranslateModule,
DonutChartComponent,
IqserLoadingModule,
@ -68,12 +69,14 @@ interface DossierDetailsContext {
AssignUserDropdownComponent,
InitialsAvatarComponent,
TeamMembersComponent,
IqserAllowDirective,
],
})
export class DossierDetailsComponent extends ContextComponent<DossierDetailsContext> {
#currentChartSubtitleIndex = 0;
readonly #dossierId = getParam(DOSSIER_ID);
protected readonly circleButtonTypes = CircleButtonTypes;
protected readonly PendingTypes = PendingTypes;
@Input() dossierAttributes: DossierAttributeWithValue[];
@Output() readonly toggleCollapse = new EventEmitter();
editingOwner = false;
@ -97,14 +100,17 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
super();
const dossier$ = _dossiersService.getEntityChanged$(this.#dossierId).pipe(shareLast());
const filesChanged$ = _filesMapService.watchChanged$(this.#dossierId).pipe(shareLast());
const files$ = _filesMapService.get$(this.#dossierId).pipe(shareLast());
const dossierStats$ = dossierStatsService.watch$(this.#dossierId).pipe(shareLast());
const dossierStatsWithEffects$ = dossierStats$.pipe(
combineLatestWith(filesChanged$),
tap(([stats]) => this.#calculateChartConfig(stats)),
map(([stats]) => stats),
);
const statusConfig$ = dossierStats$.pipe(map(stats => this.#calculateStatusConfig(stats)));
const statusConfig$ = dossierStats$.pipe(
combineLatestWith(files$),
map(([stats, files]) => this.#calculateStatusConfig(stats, files)),
);
super._initContext({
needsWorkFilters: filterService.getFilterModels$('needsWorkFilters'),
dossier: dossier$,
@ -152,7 +158,8 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
.reduce((sum: number, file: File) => sum + file.numberOfPages, 0);
}
#calculateStatusConfig(stats: DossierStats): ProgressBarConfigModel[] {
#calculateStatusConfig(stats: DossierStats, files: File[]): ProgressBarConfigModel[] {
const numberOfTimeoutFiles = files.filter(file => file.errorCode === FileErrorCodes.RULES_EXECUTION_TIMEOUT).length;
return [
{
id: ProcessingTypes.pending,
@ -161,6 +168,13 @@ export class DossierDetailsComponent extends ContextComponent<DossierDetailsCont
count: stats.processingStats.pending,
icon: 'red:reanalyse',
},
{
id: PendingTypes.timeout,
label: _('processing-status.pending-timeout'),
total: stats.numberOfFiles,
count: numberOfTimeoutFiles,
icon: 'red:reanalyse',
},
{
id: ProcessingTypes.ocr,
label: _('processing-status.ocr'),

View File

@ -1,6 +1,6 @@
<iqser-page-header
(closeAction)="router.navigate([dossier.dossiersListRouterLink])"
[actionConfigs]="actionConfigs"
(closeAction)="router.navigate([dossier().dossiersListRouterLink])"
[actionConfigs]="actionConfigs()"
[helpModeKey]="'document'"
[showCloseButton]="true"
[viewModeSelection]="viewModeSelection"
@ -10,36 +10,44 @@
[attr.help-mode-key]="isDocumine ? 'dossier_download_dossier' : 'download_dossier_in_dossier'"
[buttonId]="'download-files-btn'"
[disabled]="downloadFilesDisabled$ | async"
[dossier]="dossier"
[dossier]="dossier()"
[files]="entitiesService.all$ | async"
[iqserDisableStopPropagation]="shouldDisableStopPropagation()"
[stopMenuImmediatePropagation]="shouldDisableStopPropagation()"
dossierDownload
></redaction-file-download-btn>
<iqser-circle-button
(action)="downloadDossierAsCSV()"
*ngIf="permissionsService.canDownloadCsvReport(dossier)"
*ngIf="permissionsService.canDownloadCsvReport(dossier())"
[attr.help-mode-key]="'download_csv'"
[disabled]="listingService.areSomeSelected$ | async"
[icon]="'iqser:csv'"
[iqserDisableStopPropagation]="shouldDisableStopPropagation()"
[tooltip]="'dossier-overview.header-actions.download-csv' | translate"
></iqser-circle-button>
<iqser-circle-button
(action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn(dossier)"
[disabled]="listingService.areSomeSelected$ | async"
*ngIf="permissionsService.displayReanalyseBtn(dossier())"
[disabled]="(listingService.areSomeSelected$ | async) || areRulesLocked()"
[icon]="'iqser:refresh'"
[iqserDisableStopPropagation]="shouldDisableStopPropagation()"
[tooltipClass]="'small warn'"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
[tooltip]="
(areRulesLocked() ? 'dossier-listing.rules.timeoutError' : 'dossier-overview.new-rule.toast.actions.reanalyse-all')
| translate
"
[type]="circleButtonTypes.warn"
></iqser-circle-button>
<iqser-circle-button
(action)="upload.emit()"
*ngIf="permissionsService.canUploadFiles(dossier)"
*ngIf="permissionsService.canUploadFiles(dossier())"
[attr.help-mode-key]="'upload_document'"
[buttonId]="'upload-document-btn'"
[icon]="'iqser:upload'"
[iqserDisableStopPropagation]="shouldDisableStopPropagation()"
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
[type]="circleButtonTypes.primary"
class="ml-14"

View File

@ -1,6 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, EventEmitter, input, OnInit, Output } from '@angular/core';
import {
ActionConfig,
CircleButtonComponent,
CircleButtonTypes,
DisableStopPropagationDirective,
@ -24,13 +23,14 @@ import { PrimaryFileAttributeService } from '@services/primary-file-attribute.se
import { Router } from '@angular/router';
import { Roles } from '@users/roles';
import { SortingService } from '@iqser/common-ui/lib/sorting';
import { List, some } from '@iqser/common-ui/lib/utils';
import { ComponentLogService } from '@services/files/component-log.service';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { some } from '@iqser/common-ui/lib/utils';
import { AsyncPipe, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { FileDownloadBtnComponent } from '@shared/components/buttons/file-download-btn/file-download-btn.component';
import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-selection.component';
import { toSignal } from '@angular/core/rxjs-interop';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { RulesService } from '../../../admin/services/rules.service';
@Component({
selector: 'redaction-dossier-overview-screen-header [dossier] [upload]',
@ -39,27 +39,30 @@ import { ViewModeSelectionComponent } from '../view-mode-selection/view-mode-sel
imports: [
IqserListingModule,
CircleButtonComponent,
MatMenuTrigger,
IqserAllowDirective,
AsyncPipe,
TranslateModule,
FileDownloadBtnComponent,
NgIf,
ViewModeSelectionComponent,
DisableStopPropagationDirective,
MatMenu,
MatMenuItem,
IqserAllowDirective,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossierOverviewScreenHeaderComponent implements OnInit {
@Input() dossier: Dossier;
export class DossierOverviewScreenHeaderComponent {
readonly dossier = input<Dossier>();
@Output() readonly upload = new EventEmitter<void>();
readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles;
actionConfigs: List<ActionConfig>;
readonly downloadFilesDisabled$: Observable<boolean>;
readonly downloadComponentLogsDisabled$: Observable<boolean>;
readonly isDocumine = getConfig().IS_DOCUMINE;
readonly areRulesLocked = computed(() => {
return this._rulesService.currentTemplateRules().timeoutDetected;
});
readonly areSomeSelected = toSignal(this.listingService.areSomeSelected$);
readonly actionConfigs = computed(() => this.configService.actionConfig(this.dossier().id, this.areSomeSelected()));
readonly shouldDisableStopPropagation = computed(() => this._fileAttributesService.isEditingFileAttribute());
constructor(
private readonly _toaster: Toaster,
@ -73,6 +76,8 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
private readonly _reanalysisService: ReanalysisService,
private readonly _loadingService: LoadingService,
private readonly _primaryFileAttributeService: PrimaryFileAttributeService,
private readonly _rulesService: RulesService,
private readonly _fileAttributesService: FileAttributesService,
) {
const someNotProcessed$ = this.entitiesService.all$.pipe(some(file => !file.lastProcessed));
this.downloadFilesDisabled$ = combineLatest([this.listingService.areSomeSelected$, someNotProcessed$]).pipe(
@ -83,14 +88,16 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
);
}
ngOnInit() {
this.actionConfigs = this.configService.actionConfig(this.dossier.id, this.listingService.areSomeSelected$);
}
async reanalyseDossier() {
this._loadingService.start();
const rules = await firstValueFrom(this._rulesService.getFor(this.dossier().dossierTemplateId));
if (rules.timeoutDetected) {
this._toaster.error(_('dossier-listing.rules.timeoutError'));
this._loadingService.stop();
return;
}
try {
await this._reanalysisService.reanalyzeDossier(this.dossier, true);
await this._reanalysisService.reanalyzeDossier(this.dossier(), true);
this._toaster.success(_('dossier-overview.reanalyse-dossier.success'));
} catch (e) {
this._toaster.error(_('dossier-overview.reanalyse-dossier.error'));
@ -101,12 +108,12 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
async downloadDossierAsCSV() {
const displayedEntities = await firstValueFrom(this.listingService.displayed$);
const entities = this.sortingService.defaultSort(displayedEntities);
const fileName = this.dossier.dossierName + '.export.csv';
const fileName = this.dossier().dossierName + '.export.csv';
const mapper = (file?: File) => ({
...file,
hasAnnotations: file.hasRedactions,
assignee: this._userService.getName(file.assignee) || '-',
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier.dossierTemplateId),
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier().dossierTemplateId),
});
const documineOnlyFields = ['hasAnnotations'];
const redactionOnlyFields = ['hasHints', 'hasImages', 'hasUpdates', 'hasRedactions'];

View File

@ -23,11 +23,13 @@
<div class="cell" *ngIf="!isDocumine">
<redaction-file-workload *ngIf="!file.excluded" [file]="file"></redaction-file-workload>
</div>
</ng-container>
<div class="user-column cell">
<iqser-initials-avatar [user]="file.assignee" [withName]="true"></iqser-initials-avatar>
</div>
<div class="user-column cell">
<iqser-initials-avatar [user]="file.assignee" [withName]="true"></iqser-initials-avatar>
</div>
<ng-container *ngIf="!file.isError">
<div class="cell">
<div class="small-label stats-subtitle">
<div>
@ -39,7 +41,12 @@
</ng-container>
<div [class.extend-cols]="file.isError" class="status-container cell">
<div *ngIf="file.isError" class="small-label error" translate="dossier-overview.file-listing.file-entry.file-error"></div>
<div
*ngIf="file.isError"
class="small-label error"
translate="dossier-overview.file-listing.file-entry.file-error"
[translateParams]="{ errorCode: file.errorCode }"
></div>
<div *ngIf="file.isUnprocessed" class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div>

View File

@ -5,7 +5,7 @@
}
.extend-cols {
grid-column-end: span 3;
grid-column-end: span 2;
align-items: flex-end;
}

View File

@ -27,7 +27,9 @@
<redaction-file-attribute [dossier]="dossier" [fileAttribute]="config" [file]="file"></redaction-file-attribute>
</div>
<redaction-file-workload [file]="file"></redaction-file-workload>
@if (!isDocumine) {
<redaction-file-workload [file]="file"></redaction-file-workload>
}
<div class="file-actions overflow-visible">
<redaction-processing-indicator [file]="file" class="mr-8"></redaction-processing-indicator>

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, computed, ElementRef, Input, OnInit, Optional, ViewChild } from '@angular/core';
import { DisableStopPropagationDirective, HelpModeService } from '@iqser/common-ui';
import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, Optional, ViewChild } from '@angular/core';
import { DisableStopPropagationDirective, getConfig, HelpModeService } from '@iqser/common-ui';
import { Debounce, trackByFactory } from '@iqser/common-ui/lib/utils';
import { Dossier, File, IFileAttributeConfig } from '@red/domain';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
@ -37,6 +37,7 @@ import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
export class WorkflowItemComponent implements OnInit {
@ViewChild('actionsWrapper', { static: true }) private _actionsWrapper: ElementRef;
width: number;
readonly isDocumine = getConfig().IS_DOCUMINE;
readonly trackBy = trackByFactory();
@Input({ required: true }) file: File;
@Input({ required: true }) dossier: Dossier;

View File

@ -24,6 +24,7 @@ import {
FileAttributeConfigType,
FileAttributeConfigTypes,
IFileAttributeConfig,
PendingType,
ProcessingType,
StatusSorter,
User,
@ -46,6 +47,7 @@ import { annotationFilterChecker, RedactionFilterSorter, sortArray, sortByName }
import { EditDossierDialogComponent } from '../shared-dossiers/dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
import { DossiersDialogService } from '../shared-dossiers/services/dossiers-dialog.service';
import { BulkActionsService } from './services/bulk-actions.service';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
@Injectable()
export class ConfigService {
@ -67,6 +69,7 @@ export class ConfigService {
private readonly _userPreferenceService: UserPreferenceService,
private readonly _dossiersService: DossiersService,
private readonly _iqserPermissionsService: IqserPermissionsService,
private readonly _fileAttributesService: FileAttributesService,
) {
const previousListingMode = this._userPreferenceService.getFilesListingMode();
const listingMode = previousListingMode ? previousListingMode : ListingModes.table;
@ -112,7 +115,7 @@ export class ConfigService {
};
}
actionConfig(dossierId: string, disabled$: Observable<boolean>): List<ActionConfig> {
actionConfig(dossierId: string, disabled: boolean): List<ActionConfig> {
return [
{
id: 'editDossier',
@ -121,7 +124,8 @@ export class ConfigService {
icon: 'iqser:edit',
hide: !this.#currentUser.isManager && !this._iqserPermissionsService.has(Roles.dossiers.edit),
helpModeKey: 'edit_dossier',
disabled$,
disableStopPropagation: this._fileAttributesService.isEditingFileAttribute(),
disabled,
},
];
}
@ -184,6 +188,7 @@ export class ConfigService {
const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctProcessingTypes = new Set<ProcessingType>();
const allDistinctPendingTypes = new Set<PendingType>();
const dynamicFilters = new Map<string, { type: FileAttributeConfigType; filterValue: Set<string> }>();
@ -216,6 +221,7 @@ export class ConfigService {
}
allDistinctProcessingTypes.add(file.processingType);
allDistinctPendingTypes.add(file.pendingType);
// extract values for dynamic filters
fileAttributeConfigs.forEach(config => {
@ -317,6 +323,14 @@ export class ConfigService {
hide: true,
});
const pendingTypesFilters = [...allDistinctPendingTypes].map(item => new NestedFilter({ id: item, label: item }));
filterGroups.push({
slug: 'pendingTypeFilters',
filters: pendingTypesFilters,
checker: (file: File, filter: INestedFilter) => file.pendingType === filter.id,
hide: true,
});
dynamicFilters.forEach((value: { filterValue: Set<string>; type: FileAttributeConfigType }, filterKey: string) => {
const id = filterKey.split(':')[0];
const key = filterKey.split(':')[1];

View File

@ -56,7 +56,7 @@
</iqser-workflow>
</div>
<div *ngIf="dossierAttributes$ | async" [class.collapsed]="collapsedDetails" class="right-container" iqserHasScrollbar>
<div *ngIf="dossierAttributes$ | async" [class.collapsed]="collapsedDetails" class="right-container">
<redaction-dossier-details
(toggleCollapse)="collapsedDetails = !collapsedDetails"
[dossierAttributes]="dossierAttributes"

View File

@ -25,10 +25,7 @@
width: 375px;
min-width: 375px;
padding: 16px 24px 16px 24px;
&.has-scrollbar:hover {
padding-right: 13px;
}
overflow-y: auto;
redaction-dossier-details {
width: 100%;

View File

@ -41,7 +41,7 @@ import { Roles } from '@users/roles';
import { UserPreferenceService } from '@users/user-preference.service';
import { convertFiles, Files, handleFileDrop } from '@utils/index';
import { merge, Observable } from 'rxjs';
import { filter, skip, switchMap, tap } from 'rxjs/operators';
import { filter, map, skip, switchMap, tap } from 'rxjs/operators';
import { ConfigService } from '../config.service';
import { BulkActionsService } from '../services/bulk-actions.service';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
@ -145,8 +145,9 @@ export default class DossierOverviewScreenComponent extends ListingComponent<Fil
get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId && !!this._dossiersCacheService.get(dossierId)),
switchMap(dossierId => this._filesService.loadAll(dossierId)),
map(changes => changes[this.dossierId]),
filter(changes => !!changes && !!this._dossiersCacheService.get(this.dossierId)),
switchMap(changes => this._filesService.loadByIds({ [this.dossierId]: changes }).pipe(map(files => files[this.dossierId]))),
);
}

View File

@ -2,7 +2,7 @@
display: flex;
position: absolute;
top: 6px;
right: 19px;
right: 8px;
}
.popover {

View File

@ -96,7 +96,7 @@ export class AnnotationDetailsComponent implements OnChanges {
icon: 'red:rule',
description: _('annotation-engines.rule'),
show: isBasedOn(annotation, LogEntryEngines.RULE),
translateParams: { rule: annotation.legalBasisValue || '' },
translateParams: { rule: annotation.legalBasisValue === 'n-a' ? '' : annotation.legalBasisValue || '' },
},
{
icon: 'red:import_redactions',

View File

@ -3,21 +3,13 @@
:host {
width: 100%;
position: relative;
overflow: hidden;
&:hover {
overflow-y: auto;
@include common-mixins.scroll-bar;
}
overflow-y: auto;
@include common-mixins.scroll-bar;
&.has-scrollbar:hover redaction-annotation-wrapper::ng-deep,
&::ng-deep.documine-wrapper {
.annotation {
padding-right: 5px;
}
redaction-annotation-details {
right: 8px;
}
}
}

View File

@ -1,7 +1,6 @@
import { Component, computed, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import { getConfig, HasScrollbarDirective } from '@iqser/common-ui';
import { FilterService } from '@iqser/common-ui/lib/filtering';
import { IqserEventTarget } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ListItem } from '@models/file/list-item';
import { EarmarkGroup } from '@red/domain';
@ -15,6 +14,7 @@ import { NgForOf, NgIf } from '@angular/common';
import { HighlightsSeparatorComponent } from '../highlights-separator/highlights-separator.component';
import { AnnotationWrapperComponent } from '../annotation-wrapper/annotation-wrapper.component';
import { AnnotationReferencesListComponent } from '../annotation-references-list/annotation-references-list.component';
import { isTargetInput } from '@utils/functions';
@Component({
selector: 'redaction-annotations-list',
@ -52,7 +52,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective {
console.log('Selected Annotation:', annotation);
}
if (($event?.target as IqserEventTarget)?.localName === 'input') {
if (isTargetInput($event)) {
return;
}

View File

@ -18,13 +18,10 @@
.right-content {
flex-direction: column;
height: calc(100% - 71px);
@include common-mixins.scroll-bar;
overflow: hidden;
&:hover {
overflow: auto;
}
overflow-y: auto;
&.has-scrollbar .section {
padding-right: 13px;

View File

@ -3,12 +3,16 @@
@if (!editing) {
<div class="value">
<div class="text">
@for (componentValue of entry.componentValues; track componentValue) {
<span [innerHTML]="transformNewLines(componentValue.value ?? componentValue.originalValue)"></span>
@for (componentValue of currentEntry().componentValues; track componentValue) {
<span
[innerHTML]="transformNewLines(componentValue.value ?? componentValue.originalValue) | replaceNbsp"
[matTooltip]="componentValue.valueDescription"
[matTooltipPositionAtOrigin]="true"
></span>
}
</div>
<div class="actions">
@if (canEdit) {
@if (canEdit()) {
<iqser-circle-button
(action)="edit()"
[tooltip]="'component-management.actions.edit' | translate"
@ -16,7 +20,7 @@
[class.help-mode]="helpModeService.isHelpModeActive()"
icon="iqser:edit"
></iqser-circle-button>
@if (hasUpdatedValues) {
@if (hasUpdatedValues()) {
<div class="changes-dot"></div>
}
}
@ -24,23 +28,23 @@
</div>
} @else {
<div (cdkDropListDropped)="drop($event)" cdkDropList>
@for (value of entry.componentValues; track value) {
@for (value of currentEntry().componentValues; track value) {
<div cdkDrag class="editing-value">
<mat-icon
[class.hidden-button]="entry.componentValues.length === 1"
[attr.help-mode-key]="'change_component_order'"
[class.hidden-button]="currentEntry().componentValues.length === 1"
[attr.help-mode-key]="currentEntry().componentValues.length > 1 ? 'change_component_order' : null"
cdkDragHandle
class="draggable"
svgIcon="red:draggable-dots"
></mat-icon>
<div [attr.help-mode-key]="'edit_component'" class="iqser-input-group w-full">
<div [attr.help-mode-key]="'editor_edit_component'" class="iqser-input-group w-full">
<textarea [id]="'value-input-' + $index" [(ngModel)]="value.value" rows="1" type="text"></textarea>
</div>
<iqser-circle-button
(action)="removeValue($index)"
[tooltip]="'component-management.actions.delete' | translate"
[class.hidden-button]="entry.componentValues.length === 1"
[attr.help-mode-key]="'remove_component_value'"
[class.hidden-button]="currentEntry().componentValues.length === 1"
[attr.help-mode-key]="currentEntry().componentValues.length > 1 ? 'remove_component_value' : null"
class="remove-value"
icon="iqser:trash"
></iqser-circle-button>
@ -54,9 +58,9 @@
[label]="'component-management.actions.save' | translate"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<div (click)="deselect($event)" class="all-caps-label cancel" translate="component-management.actions.cancel"></div>
<div (click)="cancel($event)" class="all-caps-label cancel" translate="component-management.actions.cancel"></div>
<div class="flex right">
@if (hasUpdatedValues && canEdit) {
@if (hasUpdatedValues() && canEdit()) {
<iqser-circle-button
(action)="undo()"
[tooltip]="'component-management.actions.undo' | translate"

View File

@ -2,12 +2,13 @@
display: flex;
flex-direction: row;
padding: 10px 0 10px 0;
margin-left: 26px;
margin-right: 26px;
margin-left: 14px;
margin-right: 20px;
position: relative;
.component {
width: 40%;
margin-right: 8px;
}
.value {
@ -72,13 +73,13 @@
}
.value {
margin-right: 26px;
margin-right: 20px;
}
}
&:hover {
.component {
margin-left: 26px;
margin-left: 14px;
}
.value {
@ -94,7 +95,7 @@
border-left: 4px solid var(--iqser-primary);
.component {
margin-left: 22px;
margin-left: 10px;
}
.arrow-right {

View File

@ -1,6 +1,6 @@
import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
import { AsyncPipe, KeyValuePipe, NgClass, NgForOf, NgIf } from '@angular/common';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, computed, input, OnInit, output, signal, WritableSignal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatIcon } from '@angular/material/icon';
import { CircleButtonComponent, HelpModeService, IconButtonComponent, IconButtonTypes, IqserDialog } from '@iqser/common-ui';
@ -8,6 +8,9 @@ import { TranslateModule } from '@ngx-translate/core';
import { IComponentLogEntry, IComponentValue } from '@red/domain';
import { RevertValueDialogComponent } from '../../dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { escapeHtml } from '@common-ui/utils';
import { ReplaceNbspPipe } from '@common-ui/pipes/replace-nbsp.pipe';
import { MatTooltip } from '@angular/material/tooltip';
@Component({
selector: 'redaction-editable-structured-component-value [entry] [canEdit]',
@ -28,20 +31,24 @@ import { FilePreviewStateService } from '../../services/file-preview-state.servi
CdkDragHandle,
FormsModule,
AsyncPipe,
ReplaceNbspPipe,
MatTooltip,
],
})
export class EditableStructuredComponentValueComponent implements OnInit {
readonly entry = input<IComponentLogEntry>();
currentEntry: WritableSignal<IComponentLogEntry>;
readonly canEdit = input<boolean>();
readonly deselectLast = output();
readonly overrideValue = output<IComponentLogEntry>();
readonly revertOverride = output<string>();
hasUpdatedValues = computed(() => this.currentEntry().overridden);
selected = false;
valueBeforeCurrentEdit: IComponentValue[];
protected entryLabel: string;
protected editing = false;
protected hasUpdatedValues = false;
protected initialEntry: IComponentLogEntry;
protected readonly iconButtonTypes = IconButtonTypes;
@Input() entry: IComponentLogEntry;
@Input() canEdit: boolean;
@Output() readonly deselectLast = new EventEmitter();
@Output() readonly overrideValue = new EventEmitter<IComponentLogEntry>();
@Output() readonly revertOverride = new EventEmitter<string>();
selected = false;
constructor(
readonly helpModeService: HelpModeService,
@ -50,38 +57,26 @@ export class EditableStructuredComponentValueComponent implements OnInit {
) {}
get disabled() {
for (let i = 0; i < this.entry.componentValues.length; i++) {
if (this.entry.componentValues[i].value !== this.initialEntry.componentValues[i]?.value) {
for (let i = 0; i < this.currentEntry().componentValues.length; i++) {
if (this.currentEntry().componentValues[i].value !== this.initialEntry.componentValues[i]?.value) {
return false;
}
}
return this.entry.componentValues.length === this.initialEntry.componentValues.length;
}
get #hasUpdatedValues() {
for (const value of this.entry.componentValues) {
if (value.originalValue === null && value.value === '') {
continue;
}
if (value.originalValue !== value.value) {
return true;
}
}
return false;
return this.currentEntry().componentValues.length === this.initialEntry.componentValues.length;
}
get #initialEntry() {
return JSON.parse(JSON.stringify(this.entry));
return JSON.parse(JSON.stringify(this.entry()));
}
ngOnInit() {
this.currentEntry = signal(this.entry());
this.reset();
}
reset() {
this.initialEntry = this.#initialEntry;
this.hasUpdatedValues = this.#hasUpdatedValues;
this.entryLabel = this.parseName(this.entry.name);
this.entryLabel = this.parseName(this.currentEntry().name);
this.deselect();
}
@ -93,11 +88,12 @@ export class EditableStructuredComponentValueComponent implements OnInit {
}
this.deselectLast.emit();
this.selected = true;
this._state.componentReferenceIds = this.#getUniqueReferencesIds(this.entry.componentValues);
this._state.componentReferenceIds = this.#getUniqueReferencesIds(this.currentEntry().componentValues);
}
}
edit() {
this.valueBeforeCurrentEdit = JSON.parse(JSON.stringify([...this.currentEntry().componentValues]));
this.deselectLast.emit();
this.selected = true;
this.editing = true;
@ -111,37 +107,48 @@ export class EditableStructuredComponentValueComponent implements OnInit {
this._state.componentReferenceIds = null;
}
cancel($event?: MouseEvent) {
this.currentEntry.update(value => ({ ...value, componentValues: this.valueBeforeCurrentEdit }));
this.deselect($event);
}
removeValue(index: number) {
this.entry.componentValues.splice(index, 1);
this.currentEntry.update(value => ({ ...value, componentValues: value.componentValues.filter((_, i) => i !== index) }));
}
save() {
this.entry.overridden = true;
this.overrideValue.emit(this.entry);
this.currentEntry.update(value => ({ ...value, overridden: true }));
this.overrideValue.emit(this.currentEntry());
this.reset();
}
async undo() {
const dialog = this._iqserDialog.openDefault(RevertValueDialogComponent, { data: { entry: this.entry }, width: '800px' });
const dialog = this._iqserDialog.openDefault(RevertValueDialogComponent, { data: { entry: this.currentEntry() }, width: '800px' });
const result = await dialog.result();
if (result) {
this.revertOverride.emit(this.entry.name);
this.revertOverride.emit(this.currentEntry().name);
this.reset();
}
}
add() {
this.entry.componentValues.push({
componentRuleId: null,
entityReferences: [],
originalValue: null,
value: '',
valueDescription: '',
});
this.currentEntry.update(value => ({
...value,
componentValues: [
...value.componentValues,
{
componentRuleId: null,
entityReferences: [],
originalValue: null,
value: '',
valueDescription: '',
},
],
}));
}
drop(event: CdkDragDrop<string>) {
moveItemInArray(this.entry.componentValues, event.previousIndex, event.currentIndex);
moveItemInArray(this.currentEntry().componentValues, event.previousIndex, event.currentIndex);
}
parseName(name: string) {
@ -149,7 +156,7 @@ export class EditableStructuredComponentValueComponent implements OnInit {
}
transformNewLines(value: string) {
return value.replace(/\n/g, '<br>');
return escapeHtml(value).replace(/\n/g, '<br>');
}
#getUniqueReferencesIds(values: IComponentValue[]) {
@ -164,7 +171,7 @@ export class EditableStructuredComponentValueComponent implements OnInit {
#updateTextAreaHeight() {
setTimeout(() => {
for (let i = 0; i < this.entry.componentValues.length; i++) {
for (let i = 0; i < this.currentEntry().componentValues.length; i++) {
const textArea = document.getElementById(`value-input-${i}`);
textArea.style.height = 'auto';
textArea.style.height = `${textArea.scrollHeight}px`;

View File

@ -4,7 +4,6 @@ import {
Component,
computed,
ElementRef,
HostListener,
Input,
NgZone,
OnDestroy,
@ -19,7 +18,6 @@ import {
getConfig,
HelpModeService,
IqserAllowDirective,
IqserDialog,
IqserPermissionsService,
isIqserDevMode,
LoadingService,
@ -51,6 +49,7 @@ import { ALL_HOTKEYS } from '../../utils/constants';
import { AnnotationDrawService } from '../../../pdf-viewer/services/annotation-draw.service';
import { FileManagementService } from '@services/files/file-management.service';
import { MatDialog } from '@angular/material/dialog';
import { isTargetInput, isTargetTextArea } from '@utils/functions';
@Component({
selector: 'redaction-file-header',
@ -105,6 +104,7 @@ export class FileHeaderComponent implements OnInit, AfterViewInit, OnDetach, OnD
ngOnInit() {
document.documentElement.addEventListener('fullscreenchange', this.fullscreenListener);
this._pdf.instance.UI.iframeWindow.addEventListener('keyup', this.handleKeyEvent);
}
ngAfterViewInit() {
@ -116,10 +116,12 @@ export class FileHeaderComponent implements OnInit, AfterViewInit, OnDetach, OnD
ngOnDetach() {
document.documentElement.removeEventListener('fullscreenchange', this.fullscreenListener);
this._pdf.instance.UI.iframeWindow.removeEventListener('keyup', this.handleKeyEvent);
}
ngOnDestroy() {
document.documentElement.removeEventListener('fullscreenchange', this.fullscreenListener);
this._pdf.instance.UI.iframeWindow.removeEventListener('keyup', this.handleKeyEvent);
}
async downloadOriginalFile({ cacheIdentifier, dossierId, fileId, filename }: File) {
@ -177,7 +179,7 @@ export class FileHeaderComponent implements OnInit, AfterViewInit, OnDetach, OnD
}
}
@HostListener('document:keyup', ['$event'])
@Bind()
handleKeyEvent($event: KeyboardEvent) {
if (this._router.url.indexOf('/file/') < 0) {
return;
@ -208,19 +210,20 @@ export class FileHeaderComponent implements OnInit, AfterViewInit, OnDetach, OnD
this._changeRef.markForCheck();
}
if ($event.key === 'F5') {
window.location.reload();
}
if (isTargetInput($event) || isTargetTextArea($event)) {
return;
}
if (!$event.ctrlKey && !$event.metaKey && ['f', 'F'].includes($event.key)) {
// if you type in an input, don't toggle full-screen
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
return;
}
this.toggleFullScreen();
return;
}
if (['h', 'H'].includes($event.key)) {
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
return;
}
this._ngZone.run(() => {
window.focus();
this._helpModeService.activateHelpMode(false);

View File

@ -43,7 +43,7 @@
flex-direction: column;
&.documine-width {
width: calc(var(--documine-workload-content-width));
width: calc(var(--documine-workload-content-width) - 55px);
border-right: 1px solid var(--iqser-separator);
z-index: 1;
@ -79,7 +79,7 @@
.quick-navigation {
height: 100%;
border-right: 1px solid var(--iqser-separator);
min-width: var(--qiuck-navigation-width);
min-width: var(--quick-navigation-width);
overflow: hidden;
display: flex;
flex-direction: column;

View File

@ -27,7 +27,7 @@ import {
PreventDefaultDirective,
} from '@iqser/common-ui';
import { FilterService, INestedFilter, PopupFilterComponent } from '@iqser/common-ui/lib/filtering';
import { AutoUnsubscribe, Debounce, IqserEventTarget } from '@iqser/common-ui/lib/utils';
import { AutoUnsubscribe, Debounce } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ListItem } from '@models/file/list-item';
import { TranslateModule } from '@ngx-translate/core';
@ -36,7 +36,7 @@ import { workloadTranslations } from '@translations/workload-translations';
import { UserPreferenceService } from '@users/user-preference.service';
import { getLocalStorageDataByFileId } from '@utils/local-storage';
import { combineLatest, delay, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { filter, map, tap } from 'rxjs/operators';
import scrollIntoView from 'scroll-into-view-if-needed';
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
import { REDDocumentViewer } from '../../../pdf-viewer/services/document-viewer.service';
@ -57,6 +57,7 @@ import { PageExclusionComponent } from '../page-exclusion/page-exclusion.compone
import { PagesComponent } from '../pages/pages.component';
import { ReadonlyBannerComponent } from '../readonly-banner/readonly-banner.component';
import { DocumentInfoComponent } from '../document-info/document-info.component';
import { isTargetInput } from '@utils/functions';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -89,10 +90,7 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
],
})
export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
private readonly _annotationsElement = viewChild<ElementRef>('annotationsElement');
private readonly _quickNavigationElement = viewChild<ElementRef>('quickNavigation');
readonly multiSelectTemplate = viewChild<TemplateRef<any>>('multiSelect');
readonly #isIqserDevMode = this._userPreferenceService.isIqserDevMode;
protected readonly iconButtonTypes = IconButtonTypes;
protected readonly circleButtonTypes = CircleButtonTypes;
protected readonly displayedAnnotations$: Observable<Map<number, ListItem<AnnotationWrapper>[]>>;
@ -106,6 +104,9 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
protected displayedPages: number[] = [];
protected pagesPanelActive = true;
protected enabledFilters = [];
private readonly _annotationsElement = viewChild<ElementRef>('annotationsElement');
private readonly _quickNavigationElement = viewChild<ElementRef>('quickNavigation');
readonly #isIqserDevMode = this._userPreferenceService.isIqserDevMode;
#displayedPagesChanged = false;
constructor(
@ -187,6 +188,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
const secondary$ = this.filterService.getFilterModels$('secondaryFilters');
return combineLatest([
this._documentViewer.loaded$,
this.fileDataService.all$,
primary$,
secondary$,
@ -195,7 +197,8 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
this._pageRotationService.rotations$,
]).pipe(
delay(0),
map(([annotations, primary, secondary, componentReferenceIds]) =>
filter(([loaded]) => loaded),
map(([, annotations, primary, secondary, componentReferenceIds]) =>
this.#filterAnnotations(annotations, primary, secondary, componentReferenceIds),
),
map(annotations => this.#mapListItemsFromAnnotationWrapperArray(annotations)),
@ -243,11 +246,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
@HostListener('window:keyup', ['$event'])
handleKeyEvent($event: KeyboardEvent): void {
if (
!ALL_HOTKEY_ARRAY.includes($event.key) ||
this._dialog.openDialogs.length ||
($event.target as IqserEventTarget).localName === 'input'
) {
if (!ALL_HOTKEY_ARRAY.includes($event.key) || this._dialog.openDialogs.length || isTargetInput($event)) {
return;
}
@ -329,7 +328,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
}
preventKeyDefault($event: KeyboardEvent): void {
if (COMMAND_KEY_ARRAY.includes($event.key) && !(($event.target as any).localName === 'input')) {
if (COMMAND_KEY_ARRAY.includes($event.key) && !isTargetInput($event)) {
$event.preventDefault();
}
}

View File

@ -1,3 +1,5 @@
@use 'common-mixins';
.components-header {
display: flex;
flex-direction: row;
@ -24,18 +26,19 @@ mat-icon {
display: flex;
flex-direction: column;
font-size: 12px;
overflow: scroll;
overflow-x: hidden;
overflow-y: scroll;
height: calc(100% - 40px);
@include common-mixins.scroll-bar;
.component-row {
display: flex;
flex-direction: column;
margin-left: 13px;
margin-right: 13px;
.header {
display: flex;
padding: 10px 26px;
padding: 10px 14px;
font-weight: 600;
:first-child {
@ -47,9 +50,9 @@ mat-icon {
&:not(:last-child) {
border-bottom: 1px solid var(--iqser-separator);
}
border-bottom: 1px solid var(--iqser-separator);
margin-left: 26px;
margin-right: 26px;
margin-right: 13px;
}
}
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit, signal, ViewChildren } from '@angular/core';
import { Component, effect, Input, OnInit, signal, ViewChildren } from '@angular/core';
import { List } from '@common-ui/utils';
import { IconButtonTypes, LoadingService } from '@iqser/common-ui';
import { ComponentLogEntry, Dictionary, File, IComponentLogEntry, WorkflowFileStatuses } from '@red/domain';
@ -11,6 +11,7 @@ import { map } from 'rxjs/operators';
import { toObservable } from '@angular/core/rxjs-interop';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
@Component({
selector: 'redaction-structured-component-management',
@ -33,7 +34,13 @@ export class StructuredComponentManagementComponent implements OnInit {
private readonly _loadingService: LoadingService,
private readonly _componentLogFilterService: ComponentLogFilterService,
private readonly _filterService: FilterService,
) {}
private readonly _state: FilePreviewStateService,
) {
effect(async () => {
this._state.file();
await this.#loadData();
});
}
get canEdit() {
return this.file.workflowStatus !== WorkflowFileStatuses.APPROVED;
@ -75,7 +82,6 @@ export class StructuredComponentManagementComponent implements OnInit {
}
async #loadData(): Promise<void> {
this._loadingService.start();
const componentLogData = await firstValueFrom(
this._componentLogService.getComponentLogData(
this.file.dossierTemplateId,

View File

@ -52,6 +52,7 @@
</iqser-icon-button>
<div [translate]="'edit-redaction.dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
<iqser-help-button></iqser-help-button>
</div>
</form>

View File

@ -1,6 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { CircleButtonComponent, HasScrollbarDirective, IconButtonComponent, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui';
import {
CircleButtonComponent,
HasScrollbarDirective,
HelpButtonComponent,
IconButtonComponent,
IconButtonTypes,
IqserDialogComponent,
} from '@iqser/common-ui';
import { Dictionary, Dossier, SuperTypes } from '@red/domain';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
@ -33,6 +40,7 @@ import { MatDialogClose } from '@angular/material/dialog';
IconButtonComponent,
CircleButtonComponent,
MatDialogClose,
HelpButtonComponent,
],
})
export class EditAnnotationDialogComponent

View File

@ -60,6 +60,7 @@
</iqser-icon-button>
<div [translate]="'remove-annotation.dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
<iqser-help-button></iqser-help-button>
</div>
</form>

View File

@ -6,7 +6,14 @@ import { MatDialogClose } from '@angular/material/dialog';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
import { ReplaceNbspPipe } from '@common-ui/pipes/replace-nbsp.pipe';
import { CircleButtonComponent, HasScrollbarDirective, IconButtonComponent, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui';
import {
CircleButtonComponent,
HasScrollbarDirective,
HelpButtonComponent,
IconButtonComponent,
IconButtonTypes,
IqserDialogComponent,
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { getRemoveRedactionOptions } from '../../../utils/dialog-options';
import { RemoveAnnotationData, RemoveAnnotationOption, RemoveAnnotationOptions, RemoveAnnotationResult } from '../../../utils/dialog-types';
@ -28,6 +35,7 @@ import { RemoveAnnotationData, RemoveAnnotationOption, RemoveAnnotationOptions,
CircleButtonComponent,
MatDialogClose,
NgIf,
HelpButtonComponent,
],
})
export class RemoveAnnotationDialogComponent extends IqserDialogComponent<

View File

@ -39,7 +39,7 @@
}
&.documine-container {
width: 60%;
width: calc(100% - var(--structured-component-management-width));
}
}

View File

@ -1,9 +1,8 @@
import { ActivatedRouteSnapshot, NavigationExtras, Router, RouterLink } from '@angular/router';
import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
import {
CircleButtonComponent,
CircleButtonTypes,
ConfirmOption,
ConfirmOptions,
@ -12,13 +11,12 @@ import {
ErrorService,
getConfig,
IConfirmationDialogData,
IqserAllowDirective,
IqserDialog,
LoadingService,
Toaster,
} from '@iqser/common-ui';
import { copyLocalStorageFiltersValues, FilterService, NestedFilter, processFilters } from '@iqser/common-ui/lib/filtering';
import { AutoUnsubscribe, Bind, bool, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils';
import { AutoUnsubscribe, bool, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { Dictionary, File, ViewModes } from '@red/domain';
@ -60,12 +58,7 @@ import { ViewModeService } from './services/view-mode.service';
import { RedactTextData } from './utils/dialog-types';
import { MultiSelectService } from './services/multi-select.service';
import { NgIf } from '@angular/common';
import { ViewSwitchComponent } from './components/view-switch/view-switch.component';
import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component';
import { UserManagementComponent } from './components/user-management/user-management.component';
import { TranslateModule } from '@ngx-translate/core';
import { InitialsAvatarComponent } from '@common-ui/users';
import { FileActionsComponent } from '../shared-dossiers/components/file-actions/file-actions.component';
import { FilePreviewRightContainerComponent } from './components/right-container/file-preview-right-container.component';
import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component';
import { FileHeaderComponent } from './components/file-header/file-header.component';
@ -79,16 +72,8 @@ import { DocumentInfoService } from './services/document-info.service';
standalone: true,
imports: [
NgIf,
ViewSwitchComponent,
ProcessingIndicatorComponent,
UserManagementComponent,
TranslateModule,
InitialsAvatarComponent,
CircleButtonComponent,
IqserAllowDirective,
FileActionsComponent,
DisableStopPropagationDirective,
RouterLink,
FilePreviewRightContainerComponent,
TypeFilterComponent,
FileHeaderComponent,
@ -303,7 +288,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
super.ngOnDestroy();
}
@Bind()
handleEscInsideViewer($event: KeyboardEvent) {
$event.preventDefault();
if (!!this._annotationManager.selected[0]) {
@ -356,7 +340,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.pdfProxyService.configureElements();
this.#restoreOldFilters();
this.pdf.instance.UI.hotkeys.on('esc', this.handleEscInsideViewer);
this.pdf.instance.UI.hotkeys.on('esc', {
keydown: (e: KeyboardEvent) => this.pdf.escKeyHandler.keydown(e),
keyup: (e: KeyboardEvent) => {
this.pdf.escKeyHandler.keyup(e);
this.handleEscInsideViewer(e);
},
});
this._viewerHeaderService.resetLayers();
}

View File

@ -32,6 +32,10 @@ export class DocumentInfoService {
},
{ allowSignalWrites: true },
);
effect(() => {
document.body.style.setProperty('--quick-navigation-width', this.shown() ? '350px' : '61px');
});
}
fileAttributes$(fileId: string, dossierId: string, dossierTemplateId: string) {

View File

@ -270,7 +270,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const viewTime = timestampOf(viewedPage.viewedTime) - DELTA_VIEW_TIME;
let changeOccurredAfterPageIsViewed = lastChange && timestampOf(lastChange.dateTime) > viewTime;
if (changeOccurredAfterPageIsViewed) {
if (changeOccurredAfterPageIsViewed !== undefined) {
this.#markPageAsUnseenIfNeeded(viewedPage, lastChange.dateTime);
return lastChange?.type;
}
@ -281,7 +281,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const processedTime = lastManualChange?.processedDate;
changeOccurredAfterPageIsViewed = processedTime && timestampOf(processedTime) > viewTime;
if (changeOccurredAfterPageIsViewed) {
if (changeOccurredAfterPageIsViewed !== undefined) {
this.#markPageAsUnseenIfNeeded(viewedPage, processedTime);
return ChangeTypes.CHANGED;
}

View File

@ -128,7 +128,8 @@ export class FilePreviewStateService {
get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
map(changes => changes[this.dossierId]),
filter(fileIds => fileIds && fileIds.length > 0),
map(() => true),
);
}

View File

@ -14,7 +14,7 @@
align-items: center;
&.documine-pagination {
left: calc(100% - (var(--documine-viewer-width) / 2) - var(--qiuck-navigation-width));
left: calc(100% - (var(--documine-viewer-width) / 2) - var(--quick-navigation-width));
}
> div {

View File

@ -14,6 +14,7 @@ import { PdfViewer } from './pdf-viewer.service';
import Color = Core.Annotations.Color;
import DocumentViewer = Core.DocumentViewer;
import Quad = Core.Math.Quad;
import { isTargetInput } from '@utils/functions';
@Injectable()
export class REDDocumentViewer {
@ -71,12 +72,12 @@ export class REDDocumentViewer {
return fromEvent<KeyboardEvent>(this.#document, 'keyUp').pipe(
tap(stopAndPreventIfNotAllowed),
filter($event => {
if (($event.target as HTMLElement)?.tagName?.toLowerCase() === 'input') {
if (isTargetInput($event)) {
if ($event.key === 'Escape') {
return true;
}
}
return ($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input';
return isTargetInput($event);
}),
filter($event => $event.key.startsWith('Arrow') || ['f', 'h', 'H', 'Escape'].includes($event.key)),
tap<KeyboardEvent>(stopAndPrevent),

View File

@ -65,6 +65,21 @@ export class PdfViewer {
};
selectedText = '';
readonly escKeyHandler = {
keydown: (e: KeyboardEvent) => {
e.preventDefault();
this.#clickSelectToolButton();
},
keyup: (e: KeyboardEvent) => {
e.preventDefault();
if (this.#isElementActive('searchPanel') && !this._annotationManager.resizingAnnotationId) {
this.#focusViewer();
this.deactivateSearch();
}
this.#clickSelectToolButton();
},
};
constructor(
private readonly _logger: NGXLogger,
private readonly _errorService: ErrorService,
@ -147,7 +162,7 @@ export class PdfViewer {
this.#instance = await this.#getInstance(htmlElement);
if (environment.production) {
this.#instance.Core.setCustomFontURL('https://' + window.location.host + this.#convertPath('/assets/pdftron'));
this.#instance.Core.setCustomFontURL(window.location.origin + this.#convertPath('/assets/pdftron/fonts'));
}
await this.runWithCleanup(async () => {
@ -163,7 +178,6 @@ export class PdfViewer {
this.#disableHotkeys();
this.#getSelectedText();
this.#listenForCommandF();
this.#listenForEsc();
this.#clearSearchResultsWhenVisibilityChanged();
});
@ -270,23 +284,6 @@ export class PdfViewer {
});
}
#listenForEsc() {
this.#instance.UI.hotkeys.on('esc', {
keydown: e => {
e.preventDefault();
this.#clickSelectToolButton();
},
keyup: e => {
e.preventDefault();
if (this.#isElementActive('searchPanel') && !this._annotationManager.resizingAnnotationId) {
this.#focusViewer();
this.deactivateSearch();
}
this.#clickSelectToolButton();
},
});
}
#getSearchOption(optionId: string): boolean {
const iframeWindow = this.#instance.UI.iframeWindow;
const checkbox = iframeWindow.document.getElementById(optionId) as HTMLInputElement;

View File

@ -1,26 +1,26 @@
<div *ngIf="isDossierOverviewList && fileAttributesService.isEditingFileAttribute() === false" class="action-buttons">
<div *ngIf="isDossierOverviewList() && fileAttributesService.isEditingFileAttribute() === false" class="action-buttons">
<ng-container *ngTemplateOutlet="actions"></ng-container>
<redaction-processing-indicator *ngIf="showStatusBar" [file]="file"></redaction-processing-indicator>
<redaction-processing-indicator *ngIf="showStatusBar()" [file]="file()"></redaction-processing-indicator>
<iqser-status-bar *ngIf="showStatusBar" [configs]="[{ color: file.workflowStatus, length: 1 }]"></iqser-status-bar>
<iqser-status-bar *ngIf="showStatusBar()" [configs]="[{ color: file().workflowStatus, length: 1 }]"></iqser-status-bar>
</div>
<ng-container *ngIf="isFilePreview || isDossierOverviewWorkflow">
<ng-container *ngIf="isFilePreview() || isDossierOverviewWorkflow()">
<ng-container *ngTemplateOutlet="actions"></ng-container>
</ng-container>
<ng-template #actions (longPress)="forceReanalysisAction($event)" redactionLongPress>
<div class="file-actions">
<redaction-expandable-file-actions
[actions]="buttons"
[id]="'actions-for-' + file.fileId"
[maxWidth]="maxWidth"
[minWidth]="minWidth"
[actions]="buttons()"
[id]="'actions-for-' + file().fileId"
[maxWidth]="maxWidth()"
[minWidth]="minWidth()"
[tooltipPosition]="tooltipPosition"
[helpModeKeyPrefix]="helpModeKeyPrefix"
[isDossierOverviewWorkflow]="isDossierOverviewWorkflow"
[singleEntityAction]="singleEntityAction"
[helpModeKeyPrefix]="helpModeKeyPrefix()"
[isDossierOverviewWorkflow]="isDossierOverviewWorkflow()"
[singleEntityAction]="singleEntityAction()"
></redaction-expandable-file-actions>
</div>
</ng-template>

View File

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, HostBinding, Injector, Input, OnChanges, Optional, ViewChild } from '@angular/core';
import { Component, computed, HostBinding, Injector, input, Optional, signal, ViewChild } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -36,6 +36,7 @@ import { FileAssignService } from '../../services/file-assign.service';
import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component';
import { StatusBarComponent } from '@common-ui/shared';
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { RulesService } from '../../../admin/services/rules.service';
@Component({
selector: 'redaction-file-actions',
@ -44,52 +45,113 @@ import { NgIf, NgTemplateOutlet } from '@angular/common';
standalone: true,
imports: [ProcessingIndicatorComponent, StatusBarComponent, LongPressDirective, ExpandableFileActionsComponent, NgTemplateOutlet, NgIf],
})
export class FileActionsComponent implements OnChanges {
@Input({ required: true }) file: File;
@Input({ required: true }) dossier: Dossier;
@Input({ required: true }) type: 'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow';
@Input() maxWidth: number;
@Input() minWidth: number;
@Input() helpModeKeyPrefix: 'dossier' | 'editor' = 'dossier';
@Input() singleEntityAction = false;
readonly currentUser = getCurrentUser<User>();
toggleTooltip?: string;
assignTooltip?: string;
showDownload = false;
showSetToNew = false;
showUndoApproval = false;
showAssignToSelf = false;
showImportRedactions = false;
showAssign = false;
showDelete = false;
showOCR = false;
canReanalyse = false;
canDisableAutoAnalysis = false;
canEnableAutoAnalysis = false;
showUnderReview = false;
showUnderApproval = false;
showApprove = false;
canToggleAnalysis = false;
showToggleAnalysis = false;
showStatusBar = false;
showReanalyseFilePreview = false;
showReanalyseDossierOverview = false;
analysisForced = false;
isDossierOverview = false;
isDossierOverviewList = false;
isDossierOverviewWorkflow = false;
isFilePreview = false;
isDossierMember = false;
tooltipPosition = IqserTooltipPositions.above;
buttons: Action[];
export class FileActionsComponent {
@ViewChild(ExpandableFileActionsComponent)
private readonly _expandableActionsComponent: ExpandableFileActionsComponent;
readonly file = input.required<File>();
readonly dossier = input.required<Dossier>();
readonly type = input.required<'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow'>();
readonly maxWidth = input<number>();
readonly minWidth = input<number>();
readonly helpModeKeyPrefix = input<'dossier' | 'editor'>('dossier');
readonly singleEntityAction = input(false);
readonly currentUser = getCurrentUser<User>();
readonly tooltipPosition = IqserTooltipPositions.above;
readonly isDossierOverview = computed(() => this.type().startsWith('dossier-overview'));
readonly isDossierOverviewList = computed(() => this.type() === 'dossier-overview-list');
readonly isDossierOverviewWorkflow = computed(() => this.type() === 'dossier-overview-workflow');
readonly isFilePreview = computed(() => this.type() === 'file-preview');
readonly buttons = computed(() => this.#buttons);
readonly showStatusBar = computed(() => !this.file().isError && !this.file().isUnprocessed && this.isDossierOverviewList());
readonly #assignTooltip? = computed(() =>
this.file().isUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer'),
);
readonly #showSetToNew = computed(
() =>
this._permissionsService.canSetToNew(this.file(), this.dossier()) && !this.isDossierOverviewWorkflow() && !this.file().isError,
);
readonly #showUndoApproval = computed(
() =>
this._permissionsService.canUndoApproval(this.file(), this.dossier()) &&
!this.isDossierOverviewWorkflow() &&
!this.file().isError,
);
readonly #showAssignToSelf = computed(
() => this._permissionsService.canAssignToSelf(this.file(), this.dossier()) && this.isDossierOverview(),
);
readonly #showImportRedactions = computed(
() => this._permissionsService.canImportRedactions(this.file(), this.dossier()) && !this.file().isError,
);
readonly #showAssign = computed(
() =>
(this._permissionsService.canAssignUser(this.file(), this.dossier()) ||
this._permissionsService.canUnassignUser(this.file(), this.dossier())) &&
this.isDossierOverview(),
);
readonly #showDelete = computed(() => this._permissionsService.canSoftDeleteFile(this.file(), this.dossier()));
readonly #showOCR = computed(() => this._permissionsService.canOcrFile(this.file(), this.dossier()) && !this.file().isError);
readonly #canReanalyse = computed(() => this._permissionsService.canReanalyseFile(this.file(), this.dossier()));
readonly #canEnableAutoAnalysis = computed(
() => this._permissionsService.canEnableAutoAnalysis([this.file()], this.dossier()) && !this.file().isError,
);
readonly #showUnderReview = computed(
() =>
this._permissionsService.canSetUnderReview(this.file(), this.dossier()) &&
!this.isDossierOverviewWorkflow() &&
!this.file().isError,
);
readonly #showUnderApproval = computed(
() =>
this._permissionsService.canSetUnderApproval(this.file(), this.dossier()) &&
!this.isDossierOverviewWorkflow() &&
!this.file().isError,
);
readonly #showApprove = computed(
() =>
this._permissionsService.isReadyForApproval(this.file(), this.dossier()) &&
!this.isDossierOverviewWorkflow() &&
!this.file().isError,
);
readonly #canToggleAnalysis = computed(() => this._permissionsService.canToggleAnalysis(this.file(), this.dossier()));
readonly #toggleTooltip? = computed(() => {
if (!this.#canToggleAnalysis()) {
return _('file-preview.toggle-analysis.only-managers');
}
return this.file()?.excluded ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable');
});
readonly #showToggleAnalysis = computed(
() => !!this.file().lastProcessed && this._permissionsService.showToggleAnalysis(this.dossier()),
);
readonly #analysisForced = signal(false);
readonly #showReanalyse = computed(
() => (this.#canReanalyse() || this.file().excludedFromAutomaticAnalysis || this.#analysisForced()) && !this.file().dossierArchived,
);
readonly #isDossierMember = computed(() => this._permissionsService.isDossierMember(this.dossier()));
readonly #showDownload = computed(
() =>
this._permissionsService.canDownloadRedactedFile() &&
!!this.file().lastProcessed &&
!this.file().isError &&
this.#isDossierMember(),
);
readonly #showReanalyseFilePreview = computed(
() => this.#showReanalyse() && this.isFilePreview() && !this.file().isApproved && this.#isDossierMember(),
);
readonly #showReanalyseDossierOverview = computed(
() => this.#showReanalyse() && this.isDossierOverview() && !this.file().isApproved && this.#isDossierMember(),
);
readonly #ariaExpanded$ = toObservable(this._documentInfoService?.shown);
readonly #areRulesLocked = computed(() => this._rulesService.currentTemplateRules().timeoutDetected);
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly #canDisableAutoAnalysis = computed(
() => !this.#isDocumine && this._permissionsService.canDisableAutoAnalysis([this.file()], this.dossier()),
);
constructor(
private readonly _injector: Injector,
private readonly _filesService: FilesService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _loadingService: LoadingService,
private readonly _dialogService: DossiersDialogService,
private readonly _iqserDialog: IqserDialog,
@ -100,6 +162,8 @@ export class FileActionsComponent implements OnChanges {
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _fileManagementService: FileManagementService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _rulesService: RulesService,
private readonly _toasterService: Toaster,
readonly fileAttributesService: FileAttributesService,
@Optional() private readonly _documentInfoService: DocumentInfoService,
@Optional() private readonly _excludedPagesService: ExcludedPagesService,
@ -110,24 +174,15 @@ export class FileActionsComponent implements OnChanges {
return !!this._expandableActionsComponent?.expanded;
}
private get _toggleTooltip(): string {
if (!this.canToggleAnalysis) {
return _('file-preview.toggle-analysis.only-managers');
}
return this.file?.excluded ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable');
}
private get _buttons(): Action[] {
get #buttons() {
const actions: Action[] = [
{
id: 'btn-download_file',
type: ActionTypes.downloadBtn,
files: [this.file],
dossier: this.dossier,
files: [this.file()],
dossier: this.dossier(),
tooltipClass: 'small',
show: this.showDownload,
disabled: this.file.processingStatus === ProcessingFileStatuses.ERROR,
show: this.#showDownload(),
helpModeKey: this.#isDocumine ? 'download_document' : 'download',
},
{
@ -136,16 +191,16 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#openDeleteFileDialog(),
tooltip: _('dossier-overview.delete.action'),
icon: 'iqser:trash',
show: this.showDelete,
show: this.#showDelete(),
helpModeKey: 'delete_file',
},
{
id: 'btn-assign',
type: ActionTypes.circleBtn,
action: () => this.#assign(),
tooltip: this.assignTooltip,
tooltip: this.#assignTooltip(),
icon: 'red:assign',
show: this.showAssign,
show: this.#showAssign(),
helpModeKey: 'assign_user',
},
{
@ -154,7 +209,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#assignToMe(),
tooltip: _('dossier-overview.assign-me'),
icon: 'red:assign-me',
show: this.showAssignToSelf,
show: this.#showAssignToSelf(),
helpModeKey: 'assign_user',
},
{
@ -163,7 +218,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#openImportRedactionsDialog(),
tooltip: _('dossier-overview.import-redactions'),
icon: 'red:import_redactions',
show: this.showImportRedactions && !this._iqserPermissionsService.has(Roles.getRss),
show: this.#showImportRedactions() && !this._iqserPermissionsService.has(Roles.getRss),
helpModeKey: 'import_redactions',
},
{
@ -171,7 +226,7 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn,
action: () => this.#toggleDocumentInfo(),
tooltip: _('file-preview.document-info'),
ariaExpanded: toObservable(this._documentInfoService?.shown, { injector: this._injector }),
ariaExpanded: this.#ariaExpanded$,
icon: 'red:status-info',
show: !!this._documentInfoService,
helpModeKey: 'document_info',
@ -181,10 +236,10 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn,
action: () => this.#toggleExcludePages(),
tooltip: _('file-preview.exclude-pages'),
ariaExpanded: toObservable(this._excludedPagesService?.shown, { injector: this._injector }),
showDot: !!this.file.excludedPages?.length,
ariaExpanded: this.#ariaExpanded$,
showDot: !!this.file().excludedPages?.length,
icon: 'red:exclude-pages',
show: !!this._excludedPagesService && this._permissionsService.canExcludePages(this.file, this.dossier),
show: !!this._excludedPagesService && this._permissionsService.canExcludePages(this.file(), this.dossier()),
helpModeKey: 'exclude_pages',
},
{
@ -193,7 +248,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#setToNew(),
tooltip: _('dossier-overview.back-to-new'),
icon: 'red:undo',
show: this.showSetToNew,
show: this.#showSetToNew(),
helpModeKey: 'change_status',
},
{
@ -202,7 +257,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#setFileUnderApproval(),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:ready-for-approval',
show: this.showUnderApproval,
show: this.#showUnderApproval(),
helpModeKey: 'change_status',
},
{
@ -211,17 +266,17 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#setFileUnderReview(),
tooltip: _('dossier-overview.under-review'),
icon: 'red:undo',
show: this.showUnderReview,
show: this.#showUnderReview(),
helpModeKey: 'change_status',
},
{
id: 'btn-set_file_approved',
type: ActionTypes.circleBtn,
action: () => this.setFileApproved(),
tooltip: this.file.canBeApproved ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
tooltip: this.file().canBeApproved ? _('dossier-overview.approve') : _('dossier-overview.approve-disabled'),
icon: 'red:approved',
disabled: !this.file.canBeApproved,
show: this.showApprove,
disabled: !this.file().canBeApproved,
show: this.#showApprove(),
helpModeKey: 'change_status',
},
{
@ -230,28 +285,27 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#toggleAutomaticAnalysis(),
tooltip: _('dossier-overview.stop-auto-analysis'),
icon: 'red:disable-analysis',
show: this.canDisableAutoAnalysis,
show: this.#canDisableAutoAnalysis(),
helpModeKey: 'stop_analysis',
},
{
id: 'btn-reanalyse_file_preview',
type: ActionTypes.circleBtn,
action: () => this.#reanalyseFile(),
tooltip: _('file-preview.reanalyse-notification'),
tooltipClass: 'small',
tooltip: this.#areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('file-preview.reanalyse-notification'),
icon: 'iqser:refresh',
show: this.showReanalyseFilePreview,
disabled: this.file.isProcessing,
helpModeKey: 'stop_analysis',
show: this.#showReanalyseFilePreview(),
disabled: this.file().isProcessing || this.#areRulesLocked(),
helpModeKey: this.#isDocumine ? 'analyze_file' : 'stop_analysis',
},
{
id: 'btn-toggle_automatic_analysis',
type: ActionTypes.circleBtn,
action: () => this.#toggleAutomaticAnalysis(),
tooltip: _('dossier-overview.start-auto-analysis'),
buttonType: this.isFilePreview ? CircleButtonTypes.warn : CircleButtonTypes.default,
buttonType: this.isFilePreview() ? CircleButtonTypes.warn : CircleButtonTypes.default,
icon: 'red:enable-analysis',
show: this.canEnableAutoAnalysis,
show: this.#canEnableAutoAnalysis(),
helpModeKey: 'stop_analysis',
},
{
@ -260,7 +314,7 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#setFileUnderApproval(),
tooltip: _('dossier-overview.under-approval'),
icon: 'red:undo',
show: this.showUndoApproval,
show: this.#showUndoApproval(),
helpModeKey: 'change_status',
},
{
@ -269,27 +323,28 @@ export class FileActionsComponent implements OnChanges {
action: () => this.#ocrFile(),
tooltip: _('dossier-overview.ocr-file'),
icon: 'iqser:ocr',
show: this.showOCR,
show: this.#showOCR(),
helpModeKey: 'automatic_text_recognition',
},
{
id: 'btn-reanalyse_file',
type: ActionTypes.circleBtn,
action: () => this.#reanalyseFile(),
tooltip: _('dossier-overview.reanalyse.action'),
tooltip: this.#areRulesLocked() ? _('dossier-listing.rules.timeoutError') : _('dossier-overview.reanalyse.action'),
icon: 'iqser:refresh',
show: this.showReanalyseDossierOverview,
helpModeKey: 'stop_analysis',
show: this.#showReanalyseDossierOverview(),
disabled: this.#areRulesLocked(),
helpModeKey: this.#isDocumine ? 'analyze_file' : 'stop_analysis',
},
{
id: 'btn-toggle_analysis',
type: ActionTypes.toggle,
action: () => this.#toggleAnalysis(),
disabled: !this.canToggleAnalysis,
tooltip: this.toggleTooltip,
class: { 'mr-24': this.isDossierOverviewList },
checked: !this.file.excluded,
show: this.showToggleAnalysis && this.isDossierMember,
disabled: !this.#canToggleAnalysis(),
tooltip: this.#toggleTooltip(),
class: { 'mr-24': this.isDossierOverviewList() },
checked: !this.file().excluded,
show: this.#showToggleAnalysis() && this.#isDossierMember(),
helpModeKey: 'disable_extraction',
},
];
@ -297,33 +352,28 @@ export class FileActionsComponent implements OnChanges {
return actions.filter(btn => btn.show);
}
ngOnChanges() {
this.#setup();
}
async setFileApproved() {
if (!this.file.analysisRequired && !this.file.hasUpdates) {
if (!this.file().analysisRequired && !this.file().hasUpdates) {
await this.#setFileApproved();
return;
}
const data: IConfirmationDialogData = {
title: this.file.analysisRequired
title: this.file().analysisRequired
? _('confirmation-dialog.approve-file-without-analysis.title')
: _('confirmation-dialog.approve-file.title'),
question: this.file.analysisRequired
question: this.file().analysisRequired
? _('confirmation-dialog.approve-file-without-analysis.question')
: _('confirmation-dialog.approve-file.question'),
confirmationText: this.file.analysisRequired ? _('confirmation-dialog.approve-file-without-analysis.confirmationText') : null,
denyText: this.file.analysisRequired ? _('confirmation-dialog.approve-file-without-analysis.denyText') : null,
confirmationText: this.file().analysisRequired ? _('confirmation-dialog.approve-file-without-analysis.confirmationText') : null,
denyText: this.file().analysisRequired ? _('confirmation-dialog.approve-file-without-analysis.denyText') : null,
};
this._dialogService.openDialog('confirm', data, () => this.#setFileApproved());
}
forceReanalysisAction($event: LongPressEvent) {
this.analysisForced = !$event.touchEnd && this._userPreferenceService.isIqserDevMode;
this.#setup();
this.#analysisForced.set(!$event.touchEnd && this._userPreferenceService.isIqserDevMode);
}
#showOCRConfirmationDialog(): Observable<boolean> {
@ -338,7 +388,7 @@ export class FileActionsComponent implements OnChanges {
}
#openImportRedactionsDialog() {
this._dialogService.openDialog('importRedactions', { dossierId: this.file.dossierId, fileId: this.file.fileId });
this._dialogService.openDialog('importRedactions', { dossierId: this.file().dossierId, fileId: this.file().fileId });
}
#openDeleteFileDialog() {
@ -351,8 +401,8 @@ export class FileActionsComponent implements OnChanges {
async () => {
this._loadingService.start();
try {
const dossier = this._activeDossiersService.find(this.file.dossierId);
await firstValueFrom(this._fileManagementService.delete([this.file], this.file.dossierId));
const dossier = this._activeDossiersService.find(this.file().dossierId);
await firstValueFrom(this._fileManagementService.delete([this.file()], this.file().dossierId));
await this._injector.get(Router).navigate([dossier.routerLink]);
} catch (error) {
this._injector.get(Toaster).error(_('error.http.generic'), { params: error });
@ -363,8 +413,8 @@ export class FileActionsComponent implements OnChanges {
}
#assign() {
const files = [this.file];
const targetStatus = this.file.workflowStatus;
const files = [this.file()];
const targetStatus = this.file().workflowStatus;
const withCurrentUserAsDefault = true;
const withUnassignedOption = true;
this._iqserDialog.openDefault(AssignReviewerApproverDialogComponent, {
@ -378,29 +428,34 @@ export class FileActionsComponent implements OnChanges {
}
async #assignToMe() {
await this._fileAssignService.assignToMe([this.file]);
await this._fileAssignService.assignToMe([this.file()]);
}
async #reanalyseFile() {
const rules = await firstValueFrom(this._rulesService.getFor(this.dossier().dossierTemplateId));
if (rules.timeoutDetected) {
this._toasterService.error(_('dossier-listing.rules.timeoutError'));
return;
}
const params: ReanalyzeQueryParams = {
force: true,
triggeredByUser: true,
};
await this._reanalysisService.reanalyzeFilesForDossier([this.file], this.file.dossierId, params);
await this._reanalysisService.reanalyzeFilesForDossier([this.file()], this.file().dossierId, params);
}
async #toggleAutomaticAnalysis() {
this._loadingService.start();
await firstValueFrom(this._reanalysisService.toggleAutomaticAnalysis(this.file.dossierId, [this.file]));
await firstValueFrom(this._reanalysisService.toggleAutomaticAnalysis(this.file().dossierId, [this.file()]));
this._loadingService.stop();
}
async #setFileUnderApproval() {
await this._fileAssignService.assignApprover(this.file, true);
await this._fileAssignService.assignApprover(this.file(), true);
}
async #ocrFile() {
if (this.file.lastManualChangeDate) {
if (this.file().lastManualChangeDate) {
const confirm = await firstValueFrom(this.#showOCRConfirmationDialog());
if (!confirm) {
return;
@ -413,93 +468,47 @@ export class FileActionsComponent implements OnChanges {
viewerHeaderService.disableRotationButtons();
this._loadingService.start();
await this._reanalysisService.ocrFiles([this.file], this.file.dossierId);
await this._reanalysisService.ocrFiles([this.file()], this.file().dossierId);
this._loadingService.stop();
}
async #setFileUnderReview() {
await this._fileAssignService.assignReviewer(this.file, true);
await this._fileAssignService.assignReviewer(this.file(), true);
}
async #toggleAnalysis() {
this._loadingService.start();
await this._reanalysisService.toggleAnalysis(this.file.dossierId, [this.file], !this.file.excluded);
await this._reanalysisService.toggleAnalysis(this.file().dossierId, [this.file()], !this.file().excluded);
this._loadingService.stop();
}
#setup() {
this.isDossierOverviewList = this.type === 'dossier-overview-list';
this.isDossierOverviewWorkflow = this.type === 'dossier-overview-workflow';
this.isDossierOverview = this.type.startsWith('dossier-overview');
this.isFilePreview = this.type === 'file-preview';
this.assignTooltip = this.file.isUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer');
this.showAssign =
(this._permissionsService.canAssignUser(this.file, this.dossier) ||
this._permissionsService.canUnassignUser(this.file, this.dossier)) &&
this.isDossierOverview;
this.showAssignToSelf = this._permissionsService.canAssignToSelf(this.file, this.dossier) && this.isDossierOverview;
this.showImportRedactions = this._permissionsService.canImportRedactions(this.file, this.dossier);
this.showSetToNew = this._permissionsService.canSetToNew(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.showUndoApproval = this._permissionsService.canUndoApproval(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.showUnderReview = this._permissionsService.canSetUnderReview(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.showUnderApproval = this._permissionsService.canSetUnderApproval(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.showApprove = this._permissionsService.isReadyForApproval(this.file, this.dossier) && !this.isDossierOverviewWorkflow;
this.canToggleAnalysis = this._permissionsService.canToggleAnalysis(this.file, this.dossier);
this.showToggleAnalysis = !!this.file.lastProcessed && this._permissionsService.showToggleAnalysis(this.dossier);
this.toggleTooltip = this._toggleTooltip;
this.isDossierMember = this._permissionsService.isDossierMember(this.dossier);
this.showDelete = this._permissionsService.canSoftDeleteFile(this.file, this.dossier);
this.showOCR = this._permissionsService.canOcrFile(this.file, this.dossier);
this.canReanalyse = this._permissionsService.canReanalyseFile(this.file, this.dossier);
this.canDisableAutoAnalysis = !this.#isDocumine && this._permissionsService.canDisableAutoAnalysis([this.file], this.dossier);
this.canEnableAutoAnalysis = this._permissionsService.canEnableAutoAnalysis([this.file], this.dossier);
this.showStatusBar = !this.file.isError && !this.file.isUnprocessed && this.isDossierOverviewList;
const showReanalyse =
(this.canReanalyse || this.file.excludedFromAutomaticAnalysis || this.analysisForced) && !this.file.dossierArchived;
this.showReanalyseFilePreview = showReanalyse && this.isFilePreview && !this.file.isApproved && this.isDossierMember;
this.showReanalyseDossierOverview = showReanalyse && this.isDossierOverview && !this.file.isApproved && this.isDossierMember;
this.showDownload = this._permissionsService.canDownloadRedactedFile() && !!this.file.lastProcessed && this.isDossierMember;
this.buttons = this._buttons;
this._changeRef.markForCheck();
}
async #setFileApproved() {
this._loadingService.start();
await this._filesService.setApproved(this.file);
await this._filesService.setApproved(this.file());
this._loadingService.stop();
}
async #setToNew() {
this._loadingService.start();
await this._filesService.setToNew(this.file);
await this._filesService.setToNew(this.file());
this._loadingService.stop();
}
#toggleExcludePages() {
this._excludedPagesService.toggle();
const shown = this._excludedPagesService.shown();
setLocalStorageDataByFileId(this.file.id, 'show-exclude-pages', shown);
setLocalStorageDataByFileId(this.file().id, 'show-exclude-pages', shown);
if (shown) {
setLocalStorageDataByFileId(this.file.id, 'show-document-info', false);
setLocalStorageDataByFileId(this.file().id, 'show-document-info', false);
}
}
#toggleDocumentInfo() {
this._documentInfoService.toggle();
const shown = this._documentInfoService.shown();
setLocalStorageDataByFileId(this.file.id, 'show-document-info', shown);
setLocalStorageDataByFileId(this.file().id, 'show-document-info', shown);
if (shown) {
setLocalStorageDataByFileId(this.file.id, 'show-exclude-pages', false);
setLocalStorageDataByFileId(this.file().id, 'show-exclude-pages', false);
}
}
}

View File

@ -10,6 +10,7 @@ import utc from 'dayjs/plugin/utc';
import localeData from 'dayjs/plugin/localeData';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
export interface DayJsDateAdapterOptions {
/**
@ -232,6 +233,7 @@ export class CustomDateAdapter extends DateAdapter<Dayjs> {
dayjs.extend(localizedFormat);
dayjs.extend(customParseFormat);
dayjs.extend(localeData);
dayjs.extend(isSameOrBefore);
this.setLocale(dateLocale);
}

View File

@ -1,6 +1,6 @@
@if (dropdownButton()) {
<iqser-circle-button
(click)="$event.stopImmediatePropagation()"
(click)="stopImmediatePropagation($event)"
[matMenuTriggerFor]="downloadMenu"
[tooltipClass]="tooltipClass()"
[tooltipPosition]="tooltipPosition()"

View File

@ -28,6 +28,7 @@ export class FileDownloadBtnComponent implements OnChanges {
readonly singleFileDownload = input<boolean>(false);
readonly dossierDownload = input(false, { transform: booleanAttribute });
readonly dropdownButton = computed(() => this.isDocumine && (this.dossierDownload() || this.singleFileDownload()));
readonly stopMenuImmediatePropagation = input(false);
tooltip: string;
canDownloadFiles: boolean;
invalidDownload = false;
@ -96,4 +97,10 @@ export class FileDownloadBtnComponent implements OnChanges {
const fileToDownload = !this.dossierDownload() ? this.files()[0] : null;
return firstValueFrom(this._componentLogService.exportXML(this.dossier().dossierTemplateId, this.dossier().id, fileToDownload));
}
stopImmediatePropagation($event: MouseEvent) {
if (!this.stopMenuImmediatePropagation()) {
$event.stopImmediatePropagation();
}
}
}

View File

@ -15,6 +15,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HeadersConfiguration } from '@iqser/common-ui/lib/utils';
import { LicenseService } from '@services/license.service';
import { LicenseFeatures } from '../../admin/screens/license/utils/constants';
import { UserPreferenceService } from '@users/user-preference.service';
export interface ActiveUpload {
subscription: Subscription;
@ -43,6 +44,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
private readonly _errorMessageService: ErrorMessageService,
private readonly _licenseService: LicenseService,
private readonly _toaster: Toaster,
private readonly _userPreferenceService: UserPreferenceService,
) {
super();
const fileFetch$ = this.#fetchFiles$.pipe(
@ -76,7 +78,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
const maxSizeBytes = maxSizeMB * 1024 * 1024;
const dossierFiles = this._filesMapService.get(dossierId);
const supportMsOfficeFormats = this._licenseService.getFeature(LicenseFeatures.SUPPORT_MS_OFFICE_FORMATS)?.value as boolean;
let option: OverwriteFileOption = localStorage.getItem('overwriteFileOption') as OverwriteFileOption;
let option: OverwriteFileOption | 'undefined' = this._userPreferenceService.getOverwriteFileOption();
for (let idx = 0; idx < files.length; ++idx) {
const file = files[idx];
let currentOption = option;
@ -95,14 +97,14 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
}
}
} else if (dossierFiles.find(pf => pf.filename === file.file.name)) {
if (!option) {
if (option === 'undefined') {
const res = await this._dialogService.openOverwriteFileDialog(file.file.name);
if (res.cancel) {
return;
}
if (res.rememberChoice) {
localStorage.setItem('overwriteFileOption', res.option);
await this._userPreferenceService.saveOverwriteFileOption(res.option);
}
currentOption = res.option;

View File

@ -1,17 +1,16 @@
import { GenericService, LAST_CHECKED_OFFSET, QueryParam, ROOT_CHANGES_KEY } from '@iqser/common-ui';
import { Dossier, DossierStats, IDossierChanges } from '@red/domain';
import { forkJoin, Observable, of, Subscription, throwError, timer } from 'rxjs';
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { GenericService, ROOT_CHANGES_KEY } from '@iqser/common-ui';
import { Dossier, IChangesDetails, IDossierChanges } from '@red/domain';
import { forkJoin, Observable, Subscription, timer } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { NGXLogger } from 'ngx-logger';
import { ActiveDossiersService } from './active-dossiers.service';
import { ArchivedDossiersService } from './archived-dossiers.service';
import { inject, Injectable, OnDestroy } from '@angular/core';
import { DashboardStatsService } from '../dossier-templates/dashboard-stats.service';
import { CHANGED_CHECK_INTERVAL } from '@utils/constants';
import { List } from '@iqser/common-ui/lib/utils';
import { Router } from '@angular/router';
import { filterEventsOnPages } from '@utils/operators';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
@Injectable({ providedIn: 'root' })
export class DossiersChangesService extends GenericService<Dossier> implements OnDestroy {
@ -19,40 +18,40 @@ export class DossiersChangesService extends GenericService<Dossier> implements O
readonly #activeDossiersService = inject(ActiveDossiersService);
readonly #archivedDossiersService = inject(ArchivedDossiersService);
readonly #dashboardStatsService = inject(DashboardStatsService);
readonly #dossierStatsService = inject(DossierStatsService);
readonly #logger = inject(NGXLogger);
readonly #router = inject(Router);
protected readonly _defaultModelPath = 'dossier';
loadOnlyChanged(): Observable<IDossierChanges> {
const removeIfNotFound = (id: string) =>
catchError((error: unknown) => {
if (error instanceof HttpErrorResponse && error.status === HttpStatusCode.NotFound) {
this.#activeDossiersService.remove(id);
this.#archivedDossiersService.remove(id);
return of([]);
}
return throwError(() => error);
});
loadOnlyChanged(): Observable<IChangesDetails> {
const load = (changes: IChangesDetails) => this.#load(this.#mapDossierChanges(changes.dossierChanges));
const load = (changes: IDossierChanges) =>
changes.map(change => this.#load(change.dossierId).pipe(removeIfNotFound(change.dossierId)));
const loadStats = (change: IChangesDetails) => {
const dossierStatsToLoad = new Set<string>();
change.dossierChanges.forEach(dossierChange => dossierStatsToLoad.add(dossierChange.dossierId));
change.fileChanges.forEach(fileChange => dossierStatsToLoad.add(fileChange.dossierId));
const dossierIds = this.#filterDossierIds(Array.from(dossierStatsToLoad));
return this.#dossierStatsService.getFor(dossierIds);
};
return this.hasChangesDetails$().pipe(
tap(changes => this.#logger.info('[DOSSIERS_CHANGES] Found changes', changes)),
tap(changes => console.log('[DOSSIERS_CHANGES] Found changes', changes)),
switchMap(dossierChanges =>
forkJoin([...load(dossierChanges), this.#dashboardStatsService.loadAll().pipe(take(1))]).pipe(map(() => dossierChanges)),
forkJoin([load(dossierChanges), loadStats(dossierChanges), this.#dashboardStatsService.loadAll().pipe(take(1))]).pipe(
map(() => dossierChanges),
),
),
);
}
hasChangesDetails$(): Observable<IDossierChanges> {
hasChangesDetails$(): Observable<IChangesDetails> {
const body = { value: this._lastCheckedForChanges.get(ROOT_CHANGES_KEY) };
const dateBeforeRequest = new Date(Date.now() - LAST_CHECKED_OFFSET).toISOString();
const dateBeforeRequest = new Date().toISOString();
this.#logger.info('[DOSSIERS_CHANGES] Check with Last Checked Date', body.value);
return this._post<IDossierChanges>(body, `${this._defaultModelPath}/changes/details`).pipe(
filter(changes => changes.length > 0),
return this._post<IChangesDetails>(body, `${this._defaultModelPath}/changes/details/v2`).pipe(
filter(changes => changes.dossierChanges.length > 0 || changes.fileChanges.length > 0),
tap(() => this._lastCheckedForChanges.set(ROOT_CHANGES_KEY, dateBeforeRequest)),
tap(() => this.#logger.info('[DOSSIERS_CHANGES] Save Last Checked Date value', dateBeforeRequest)),
);
@ -75,18 +74,37 @@ export class DossiersChangesService extends GenericService<Dossier> implements O
this.#subscription.unsubscribe();
}
#load(id: string): Observable<DossierStats[]> {
const queryParams: List<QueryParam> = [{ key: 'includeArchived', value: true }];
return super._getOne([id], this._defaultModelPath, queryParams).pipe(
map(entity => new Dossier(entity)),
switchMap((dossier: Dossier) => {
if (dossier.isArchived) {
this.#activeDossiersService.remove(dossier.id);
return this.#archivedDossiersService.updateDossier(dossier);
}
this.#archivedDossiersService.remove(dossier.id);
return this.#activeDossiersService.updateDossier(dossier);
getByIds(ids: string[]) {
return super._post<Record<string, Dossier>>({ value: ids }, `${this._defaultModelPath}/by-id`);
}
#load(ids: string[]): Observable<Dossier[]> {
return this.getByIds(ids).pipe(
map(entity => {
return Object.values(entity).map(dossier => new Dossier(dossier));
}),
map((dossiers: Dossier[]) => {
const archivedDossiers = dossiers.filter(dossier => dossier.isArchived);
const deletedDossiers = dossiers.filter(dossier => dossier.isSoftDeleted);
const activeDossiers = dossiers.filter(dossier => !dossier.isArchived && !dossier.isSoftDeleted);
archivedDossiers.forEach(dossier => this.#activeDossiersService.remove(dossier.id));
activeDossiers.forEach(dossier => this.#archivedDossiersService.remove(dossier.id));
deletedDossiers.forEach(dossier => this.#activeDossiersService.remove(dossier.id));
this.#activeDossiersService.updateDossiers(activeDossiers);
this.#archivedDossiersService.updateDossiers(archivedDossiers);
return dossiers;
}),
);
}
#filterDossierIds(ids: string[]) {
return ids.filter(id => this.#dashboardStatsService.all.find(stats => stats.dossiersInTemplate.includes(id)));
}
#mapDossierChanges(dossierChanges: IDossierChanges | string[]): string[] {
const dossierIds = dossierChanges.map(change => change.dossierId);
return this.#filterDossierIds(dossierIds);
}
}

View File

@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core';
import { StatsService } from '@iqser/common-ui';
import { DOSSIER_ID, DossierStats, IDossierStats } from '@red/domain';
import { DashboardStats, DOSSIER_ID, DossierStats, IDossierStats } from '@red/domain';
import { Observable, of } from 'rxjs';
import { UserService } from '@users/user.service';
import { NGXLogger } from 'ngx-logger';

View File

@ -1,5 +1,5 @@
import { EntitiesService, Toaster } from '@iqser/common-ui';
import { Dossier, DossierStats, IDossier, IDossierChanges, IDossierRequest } from '@red/domain';
import { Dossier, DossierFileChanges, DossierStats, IChangesDetails, IDossier, IDossierRequest } from '@red/domain';
import { Observable, of, Subject } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { inject } from '@angular/core';
@ -17,7 +17,7 @@ export abstract class DossiersService extends EntitiesService<IDossier, Dossier>
protected readonly _toaster = inject(Toaster);
protected readonly _entityClass = Dossier;
protected abstract readonly _defaultModelPath: string;
readonly dossierFileChanges$ = new Subject<string>();
readonly dossierFileChanges$ = new Subject<DossierFileChanges>();
abstract readonly routerPath: string;
createOrUpdate(dossier: IDossierRequest): Observable<Dossier> {
@ -52,7 +52,18 @@ export abstract class DossiersService extends EntitiesService<IDossier, Dossier>
return this._dossierStatsService.getFor([dossier.id]);
}
emitFileChanges(dossierChanges: IDossierChanges): void {
dossierChanges.filter(change => change.fileChanges).forEach(change => this.dossierFileChanges$.next(change.dossierId));
updateDossiers(dossier: Dossier[]): void {
dossier.forEach(d => this.replace(d));
}
emitFileChanges(changes: IChangesDetails): void {
const changeModel: DossierFileChanges = {};
changes.fileChanges.forEach(change => {
if (!changeModel[change.dossierId]) {
changeModel[change.dossierId] = [];
}
changeModel[change.dossierId].push(change.fileId);
});
this.dossierFileChanges$.next(changeModel);
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { EntitiesService, isArray, QueryParam } from '@iqser/common-ui';
import { List, mapEach } from '@iqser/common-ui/lib/utils';
import { File, IFile } from '@red/domain';
import { DossierFileChanges, File, IFile } from '@red/domain';
import { UserService } from '@users/user.service';
import { NGXLogger } from 'ngx-logger';
import { firstValueFrom } from 'rxjs';
@ -27,6 +27,32 @@ export class FilesService extends EntitiesService<IFile, File> {
super();
}
loadByIds(dossierFileChanges: DossierFileChanges) {
const filesByDossier$ = super
._post<{ value: Record<string, IFile[]> }>({ value: dossierFileChanges }, `${this._defaultModelPath}/by-id`)
.pipe(
map(response => {
const filesByDossier = response.value;
const result: Record<string, File[]> = {};
for (const key of Object.keys(filesByDossier)) {
result[key] = filesByDossier[key].map(file => new File(file, this._userService.getName(file.assignee)));
result[key].forEach(file => this._logger.info('[FILE] Loaded', file));
}
return result;
}),
);
return filesByDossier$.pipe(
tap(files => {
for (const key of Object.keys(files)) {
const notDeletedFiles = files[key].filter(file => !file.deleted);
const deletedFiles = files[key].filter(file => file.deleted);
this._filesMapService.replace(key, notDeletedFiles);
deletedFiles.map(file => file.id).forEach(id => this._filesMapService.delete(key, id));
}
}),
);
}
/** Reload dossier files + stats. */
loadAll(dossierId: string) {
const files$ = this.getFor(dossierId).pipe(

View File

@ -1,7 +1,7 @@
import { inject, Injectable, OnDestroy } from '@angular/core';
import { EntitiesService, getConfig, QueryParam } from '@iqser/common-ui';
import { TranslateService } from '@ngx-translate/core';
import { EMPTY, firstValueFrom, iif, merge, Observable, of, Subscription, timer } from 'rxjs';
import { EMPTY, firstValueFrom, merge, Observable, of, Subscription, timer } from 'rxjs';
import { AppConfig, Dossier, INotification, Notification, NotificationTypes } from '@red/domain';
import { map, switchMap, tap } from 'rxjs/operators';
import { notificationsTranslations } from '@translations/notifications-translations';
@ -141,6 +141,14 @@ export class NotificationsService extends EntitiesService<INotification, Notific
}
#loadNotificationsIfChanged(): Observable<Notification[]> {
return this.hasChanges$().pipe(switchMap(changed => iif(() => changed, this.loadAll(), EMPTY)));
return this.hasChanges$().pipe(
switchMap(changed => {
if (changed) {
return this.loadAll();
} else {
return EMPTY;
}
}),
);
}
}

View File

@ -473,7 +473,7 @@ export class PermissionsService {
}
#canAssignToSelf(file: File, dossier: Dossier): boolean {
const precondition = this.#fileIsOk(file, dossier) && !this.isFileAssignee(file);
const precondition = (this.#fileIsOk(file, dossier) || file.isError) && !this.isFileAssignee(file);
return precondition && (this.isApprover(dossier) || (this.isDossierMember(dossier) && (file.isNew || file.isUnderReview)));
}
@ -500,7 +500,7 @@ export class PermissionsService {
}
#canAssignReviewer(file: File, dossier: Dossier) {
const fileStatesForReviewer = file.isNew || file.isUnderReview || file.isUnderApproval;
const fileStatesForReviewer = file.isNew || file.isUnderReview || file.isUnderApproval || file.isApproved;
return fileStatesForReviewer && this.isDossierMember(dossier);
}

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { IqserUserPreferenceService, ListingMode } from '@iqser/common-ui';
import { SystemDefaultOption, SystemDefaultType } from '../modules/account/utils/dialog-defaults';
import { RedactOrHintOption, RemoveRedactionOption } from '../modules/file-preview/utils/dialog-types';
import { OverwriteFileOption } from '@red/domain';
export const PreferencesKeys = {
dossierRecent: 'Dossier-Recent',
@ -21,6 +22,7 @@ export const PreferencesKeys = {
removeRedactionDefaultExtraOption: 'Remove-Redaction-Default-Extra',
removeRecommendationDefaultExtraOption: 'Remove-Recommendation-Default-Extra',
removeHintDefaultExtraOption: 'Remove-Hint-Default-Extra',
rememberOverwriteFileOption: 'Remember-Overwrite-File',
} as const;
@Injectable({
@ -169,4 +171,12 @@ export class UserPreferenceService extends IqserUserPreferenceService {
async saveRemoveRecommendationDefaultExtraOption(defaultOption: boolean | string): Promise<void> {
await this.save(PreferencesKeys.removeRecommendationDefaultExtraOption, defaultOption.toString());
}
getOverwriteFileOption(): OverwriteFileOption | 'undefined' {
return this._getAttribute(PreferencesKeys.rememberOverwriteFileOption, 'undefined') as OverwriteFileOption | 'undefined';
}
async saveOverwriteFileOption(preference: OverwriteFileOption | 'undefined') {
await this.save(PreferencesKeys.rememberOverwriteFileOption, preference);
}
}

View File

@ -1,5 +1,5 @@
import { ITrackable } from '@iqser/common-ui';
import type { List } from '@iqser/common-ui/lib/utils';
import type { IqserEventTarget, List } from '@iqser/common-ui/lib/utils';
import type { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Dayjs } from 'dayjs';
@ -143,3 +143,11 @@ export function urlFileId() {
const fileId = splitUrl[splitUrl.length - 1];
return fileId.split('?')[0];
}
export function isTargetInput(event: Event) {
return (event?.target as IqserEventTarget)?.localName === 'input';
}
export function isTargetTextArea(event: Event) {
return (event?.target as IqserEventTarget)?.localName === 'textarea';
}

View File

@ -1,9 +1,9 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dan2.iqser.cloud",
"API_URL": "https://frontend2.iqser.cloud",
"APP_NAME": "RedactManager",
"IS_DOCUMINE": false,
"IS_DOCUMINE": true,
"RULE_EDITOR_DEV_ONLY": false,
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
@ -13,13 +13,13 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dan2.iqser.cloud/auth",
"OAUTH_URL": "https://frontend2.iqser.cloud/auth",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",
"ANNOTATIONS_THRESHOLD": 1000,
"THEME": "redact",
"BASE_TRANSLATIONS_DIRECTORY": "/assets/i18n/redact/",
"THEME": "scm",
"BASE_TRANSLATIONS_DIRECTORY": "/assets/i18n/scm/",
"AVAILABLE_NOTIFICATIONS_DAYS": 30,
"AVAILABLE_OLD_NOTIFICATIONS_MINUTES": 60,
"NOTIFICATIONS_THRESHOLD": 1000,

View File

@ -520,6 +520,11 @@
"documentKey": "dossier_stop_analysis",
"scrollableParentView": "VIRTUAL_SCROLL"
},
{
"elementKey": "dossier_analyze_file",
"documentKey": "analyze_file",
"scrollableParentView": "VIRTUAL_SCROLL"
},
{
"elementKey": "dossier_automatic_text_recognition",
"documentKey": "dossier_automatic_text_recognition",
@ -569,7 +574,6 @@
"elementKey": "workload_page_list",
"documentKey": "workload_page_list"
},
{
"elementKey": "editor_delete_file",
"documentKey": "editor_delete_file"
@ -735,10 +739,6 @@
"elementKey": "components_table",
"documentKey": "components_table"
},
{
"elementKey": "components_table",
"documentKey": "components_table"
},
{
"elementKey": "remove_annotation_DIALOG",
"documentKey": "remove_annotation"

View File

@ -273,9 +273,6 @@
"watermarks": "Wasserzeichen"
},
"analysis-disabled": "",
"annotation": {
"pending": "(Analyse steht aus)"
},
"annotation-actions": {
"accept-recommendation": {
"label": "Empfehlung annehmen"
@ -331,14 +328,14 @@
"error": "Rekategorisierung des Bilds fehlgeschlagen: {error}",
"success": "Bild wurde einer neuen Kategorie zugeordnet."
},
"remove": {
"error": "Entfernen der Schwärzung fehlgeschlagen: {error}",
"success": "Schwärzung wurde entfernt"
},
"remove-hint": {
"error": "Entfernen des Hinweises fehlgeschlagen: {error}",
"success": "Hinweis wurde entfernt"
},
"remove": {
"error": "Entfernen der Schwärzung fehlgeschlagen: {error}",
"success": "Schwärzung wurde entfernt"
},
"undo": {
"error": "Die Aktion konnte nicht rückgängig gemacht werden. Fehler: {error}",
"success": "Rücksetzung erfolgreich"
@ -351,15 +348,15 @@
"remove-highlights": {
"label": "Ausgewählte Markierungen entfernen"
},
"resize": {
"label": "Größe ändern"
},
"resize-accept": {
"label": "Neue Größe speichern"
},
"resize-cancel": {
"label": "Größenänderung abbrechen"
},
"resize": {
"label": "Größe ändern"
},
"see-references": {
"label": "Referenzen anzeigen"
},
@ -393,6 +390,9 @@
"skipped": "Ignorierte Schwärzung",
"text-highlight": "Markierung"
},
"annotation": {
"pending": "(Analyse steht aus)"
},
"annotations": "Annotationen",
"archived-dossiers-listing": {
"no-data": {
@ -587,8 +587,7 @@
"success": {
"generic": ""
},
"title": "",
"warning-text": ""
"title": ""
},
"configurations": "Konfiguration",
"confirm-archive-dossier": {
@ -637,18 +636,14 @@
"warning": "Warnung: Wiederherstellung des Benutzers nicht möglich."
},
"confirmation-dialog": {
"approve-file": {
"question": "Dieses Dokument enthält ungesehene Änderungen, die sich durch die Reanalyse ergeben haben.<br><br>Möchten Sie es trotzdem freigeben?",
"title": "Warnung!"
},
"approve-file-without-analysis": {
"confirmationText": "Ohne Analyse freigeben",
"denyText": "Abbrechen",
"question": "Analyse zur Erkennung neuer Schwärzungen erforderlich.",
"title": "Warnung!"
},
"approve-multiple-files": {
"question": "Mindestens eine der ausgewählten Dateien enthält ungesehene Änderungen, die im Zuge einer Reanalyse hinzugefügt wurden.<br><br>Möchen Sie die Dateien wirklich freigeben?",
"approve-file": {
"question": "Dieses Dokument enthält ungesehene Änderungen, die sich durch die Reanalyse ergeben haben.<br><br>Möchten Sie es trotzdem freigeben?",
"title": "Warnung!"
},
"approve-multiple-files-without-analysis": {
@ -657,6 +652,10 @@
"question": "Für mindestens eine Datei ist ein Analyselauf zur Erkennung neuer Schwärzungen erforderlich.",
"title": "Warnung"
},
"approve-multiple-files": {
"question": "Mindestens eine der ausgewählten Dateien enthält ungesehene Änderungen, die im Zuge einer Reanalyse hinzugefügt wurden.<br><br>Möchen Sie die Dateien wirklich freigeben?",
"title": "Warnung!"
},
"assign-file-to-me": {
"question": {
"multiple": "Dieses Dokument wird gerade von einer anderen Person geprüft.<br><br>Möchten Sie sich die Datei dennoch zuweisen?",
@ -934,6 +933,9 @@
"reanalyse": {
"action": "Ganzes Dossier analysieren"
},
"rules": {
"timeoutError": "Regeln für Dossier-Vorlagen gesperrt!"
},
"stats": {
"analyzed-pages": "{count, plural, one{Seite} other{Seiten}}",
"total-people": "Benutzer"
@ -990,7 +992,7 @@
"download-file-disabled": "Download: Sie müssen Genehmiger im Dossier sein und die initiale Verarbeitung {count, plural, one{der Datei} other{der Dateien}} muss abgeschlossen sein.",
"file-listing": {
"file-entry": {
"file-error": "Reanalyse erforderlich",
"file-error": "Reanalyse erforderlich {errorCode, select, RULES_EXECUTION_TIMEOUT{(Zeitlimit für Regeln)} other{}}",
"file-pending": "Ausstehend ..."
}
},
@ -1026,13 +1028,13 @@
"recent": "Neu ({hours} h)",
"unassigned": "Keinem Bearbeiter zugewiesen"
},
"reanalyse": {
"action": "Datei analysieren"
},
"reanalyse-dossier": {
"error": "Einplanung der Dateien für die Reanalyse fehlgeschlagen. Bitte versuchen Sie es noch einmal.",
"success": "Dateien für Reanalyse vorgesehen."
},
"reanalyse": {
"action": "Datei analysieren"
},
"report-download": "",
"start-auto-analysis": "Auto-Analyse aktivieren",
"stop-auto-analysis": "Auto-Analyse anhalten",
@ -1066,8 +1068,7 @@
"dossier-states": "{count, plural, one{Dossier-Status} other{Dossier-Status}}"
},
"error": {
"conflict": "Es gibt bereits einen Dossier-Status mit diesem Namen.",
"generic": "Speichern des Dossier-Status fehlgeschlagen."
"conflict": "Es gibt bereits einen Dossier-Status mit diesem Namen."
},
"no-data": {
"title": "Es wurde noch kein Dossier-Status angelegt."
@ -1091,6 +1092,12 @@
"entities": "{count} {count, plural, one{Entität} other{Entitäten}}",
"entries": "{count} {count, plural, one{Eintrag} other{Einträge}}",
"modified-on": "Geändert am: {date}",
"rules-reset": {
"disabled-action": "Bitte wenden Sie sich an Ihren Administrator, um die Regeln freizuschalten.",
"label": "Regeln gesperrt",
"success": "Die Regeln der Dossier-Vorlage wurden erfolgreich zurückgesetzt.",
"tooltip": "Klicken Sie hier, um die Regeln zurückzusetzen"
},
"title": "Dossier-Vorlage bearbeiten",
"valid-from": "Gültig ab: {date}",
"valid-to": "Gültig bis: {Datum}"
@ -1103,14 +1110,6 @@
"total-documents": "Dokumente",
"total-people": "<strong>{count}</strong> {count, plural, one{Benutzer} other {Benutzer}}"
},
"dossier-templates": {
"label": "Dossier-Vorlagen",
"status": {
"active": "Aktiv",
"inactive": "Inaktiv",
"incomplete": "Unvollständig"
}
},
"dossier-templates-listing": {
"action": {
"clone": "Vorlage klonen",
@ -1145,6 +1144,14 @@
"title": "{length} {length, plural, one{Dossier-Vorlage} other{Dossier-Vorlagen}}"
}
},
"dossier-templates": {
"label": "Dossier-Vorlagen",
"status": {
"active": "Aktiv",
"inactive": "Inaktiv",
"incomplete": "Unvollständig"
}
},
"dossier-watermark-selector": {
"heading": "Wasserzeichen auf Dokumenten",
"no-watermark": "Kein Wasserzeichen in der Dossier-Vorlage verfügbar:<br>Bitten Sie Ihren Admin, eines zu konfigurieren.",
@ -1340,15 +1347,6 @@
"title": "{length} {length, plural, one{Wörterbuch} other{Wörterbücher}}"
}
},
"entity": {
"info": {
"actions": {
"revert": "Zurücksetzen",
"save": "Änderungen speichern"
},
"heading": "Entität bearbeiten"
}
},
"entity-rules-screen": {
"error": {
"generic": "Fehler: Aktualisierung der Entitätsregeln fehlgeschlagen."
@ -1360,22 +1358,30 @@
"generic": "Die Entitätsregeln wurden aktualisiert."
},
"title": "Entitätsregeln-Editor",
"warning-text": "Warnung: experimentelle Funktion!",
"warnings-found": "{warnings, plural, one{A warning} other{{warnings} warnings}} in Regeln gefunden"
},
"entity": {
"info": {
"actions": {
"revert": "Zurücksetzen",
"save": "Änderungen speichern"
},
"heading": "Entität bearbeiten"
}
},
"error": {
"deleted-entity": {
"dossier": {
"action": "Zurück zur Übersicht",
"label": "Dieses Dossier wurde gelöscht!"
},
"file": {
"action": "Zurück zum Dossier",
"label": "Diese Datei wurde gelöscht!"
},
"file-dossier": {
"action": "Zurück zur Übersicht",
"label": "Das Dossier dieser Datei wurde gelöscht!"
},
"file": {
"action": "Zurück zum Dossier",
"label": "Diese Datei wurde gelöscht!"
}
},
"file-preview": {
@ -1393,12 +1399,6 @@
},
"exact-date": "{day}. {month} {year} um {hour}:{minute} Uhr",
"file": "Datei",
"file-attribute": {
"update": {
"error": "Aktualisierung des Werts für das Datei-Attribut fehlgeschlagen. Bitte versuchen Sie es noch einmal.",
"success": "Der Wert für das Dateiattribut wurde erfolgreich aktualisiert."
}
},
"file-attribute-encoding-types": {
"ascii": "ASCII",
"iso": "ISO-8859-1",
@ -1409,6 +1409,12 @@
"number": "Nummer",
"text": "Freier Text"
},
"file-attribute": {
"update": {
"error": "Aktualisierung des Werts für das Datei-Attribut fehlgeschlagen. Bitte versuchen Sie es noch einmal.",
"success": "Der Wert für das Dateiattribut wurde erfolgreich aktualisiert."
}
},
"file-attributes-configurations": {
"cancel": "Abbrechen",
"form": {
@ -1453,7 +1459,7 @@
"save": {
"error": "Erstellung der Datei-Attribute fehlgeschlagen.",
"label": "Attribute speichern",
"success": "{count} Datei-{count, plural, one{Attribut} other{Attribute}} erfolgreich erstellt!"
"success": "{count} Datei-{count, plural, one{Attribut} other{Attribute}} erfolgreich erstellt."
},
"search": {
"placeholder": "Nach Spaltennamen suchen..."
@ -1626,15 +1632,6 @@
"csv": "Die Datei-Attribute wurden erfolgreich aus der hochgeladenen CSV-Datei importiert."
}
},
"filter": {
"analysis": "Analyse erforderlich",
"comment": "Kommentare",
"hint": "Nur Hinweise",
"image": "Bilder",
"none": "Keine Annotationen",
"redaction": "Schwärzungen",
"updated": "Aktualisiert"
},
"filter-menu": {
"filter-options": "Filteroptionen",
"filter-types": "Filter",
@ -1644,6 +1641,15 @@
"unseen-pages": "Nur Annotationen auf ungesehenen Seiten",
"with-comments": "Nur Annotationen mit Kommentaren"
},
"filter": {
"analysis": "Analyse erforderlich",
"comment": "Kommentare",
"hint": "Nur Hinweise",
"image": "Bilder",
"none": "Keine Annotationen",
"redaction": "Schwärzungen",
"updated": "Aktualisiert"
},
"filters": {
"assigned-people": "Bearbeiter",
"documents-status": "Dokumentenstatus",
@ -1922,13 +1928,6 @@
"user-promoted-to-approver": "<b>{user}</b> wurde im Dossier <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b> zum Genehmiger ernannt!",
"user-removed-as-dossier-member": "<b>{user}</b> wurde als Mitglied von: <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b> entfernt!"
},
"notifications": {
"button-text": "Benachrichtigungen",
"deleted-dossier": "Gelöschtes Dossier",
"label": "Benachrichtigungen",
"mark-all-as-read": "Alle als gelesen markieren",
"mark-as": "Als {type, select, read{gelesen} unread{ungelesen} other{}} markieren"
},
"notifications-screen": {
"category": {
"email-notifications": "E-Mail-Benachrichtigungen",
@ -1942,6 +1941,7 @@
"dossier": "Benachrichtigungen zu Dossiers",
"other": "Andere Benachrichtigungen"
},
"options-title": "Wählen Sie aus, bei welchen Aktivitäten Sie benachrichtigt werden möchten",
"options": {
"ASSIGN_APPROVER": "Wenn ich einem Dokument als Genehmiger zugewiesen werde",
"ASSIGN_REVIEWER": "Wenn ich einem Dokument als Prüfer zugewiesen werde",
@ -1959,7 +1959,6 @@
"USER_PROMOTED_TO_APPROVER": "Wenn ich Genehmiger in einem Dossier werde",
"USER_REMOVED_AS_DOSSIER_MEMBER": "Wenn ich die Dossier-Mitgliedschaft verliere"
},
"options-title": "Wählen Sie aus, bei welchen Aktivitäten Sie benachrichtigt werden möchten",
"schedule": {
"daily": "Tägliche Zusammenfassung",
"instant": "Sofort",
@ -1967,6 +1966,13 @@
},
"title": "Benachrichtigungseinstellungen"
},
"notifications": {
"button-text": "Benachrichtigungen",
"deleted-dossier": "Gelöschtes Dossier",
"label": "Benachrichtigungen",
"mark-all-as-read": "Alle als gelesen markieren",
"mark-as": "Als {type, select, read{gelesen} unread{ungelesen} other{}} markieren"
},
"ocr": {
"confirmation-dialog": {
"cancel": "Abbrechen",
@ -2049,7 +2055,7 @@
"auto-expand-filters-on-action": "Filter ausgehend von meinen Aktionen automatisch anpassen",
"help-mode-dialog": "Dialog zur Aktivierung des Hilfemodus",
"load-all-annotations-warning": "Warnung bei gleichzeitigem Laden aller Annotationen in der Miniaturansicht",
"open-structured-view-by-default": "Strukturierte Komponentenansicht standardmäßig anzeigen",
"overwrite-file-option": "",
"table-extraction-type": "Art der Tabellenextraktion"
},
"label": "Präferenzen",
@ -2058,16 +2064,17 @@
"warnings-label": "Dialoge und Meldungen",
"warnings-subtitle": "„Nicht mehr anzeigen“-Optionen"
},
"processing": {
"basic": "Verarbeitung läuft",
"ocr": "OCR"
},
"processing-status": {
"ocr": "OCR",
"pending": "Ausstehend",
"pending-timeout": "Ausstehend (Zeitlimit für Regeln)",
"processed": "Verarbeitet",
"processing": "Verarbeitung läuft"
},
"processing": {
"basic": "Verarbeitung läuft",
"ocr": "OCR"
},
"readonly": "Lesemodus",
"readonly-archived": "Lesemodus (archiviert)",
"redact-text": {
@ -2297,18 +2304,12 @@
"inactive": "Inaktiv",
"manager-admin": "Manager & Admin",
"no-role": "Keine Rolle definiert",
"red-admin": "Anwendungsadmin",
"red-manager": "Manager",
"red-admin": "{count, plural, one{Anwendungsadmin} other{Anwendungsadmins}}",
"red-manager": "{count, plural, one{Manager} other{Manager}}",
"red-user": "Benutzer",
"red-user-admin": "Benutzeradmin",
"regular": "regulärer Benutzer"
},
"search": {
"active-dossiers": "Dokumente in aktiven Dossiers",
"all-dossiers": "Alle Dokumente",
"placeholder": "Dokumente durchsuchen...",
"this-dossier": "In diesem Dossier"
},
"search-screen": {
"cols": {
"assignee": "Bearbeiter",
@ -2332,6 +2333,12 @@
"no-match": "Der Suchbegriff wurde in keinem der Dokumente gefunden.",
"table-header": "{length} {length, plural, one{Suchergebnis} other{Suchergebnisse}}"
},
"search": {
"active-dossiers": "Dokumente in aktiven Dossiers",
"all-dossiers": "Alle Dokumente",
"placeholder": "Dokumente durchsuchen...",
"this-dossier": "In diesem Dossier"
},
"seconds": "Sekunden",
"size": "Größe",
"smtp-auth-config": {
@ -2587,4 +2594,4 @@
}
},
"yesterday": "Gestern"
}
}

View File

@ -933,6 +933,9 @@
"reanalyse": {
"action": "Analyze entire dossier"
},
"rules": {
"timeoutError": "Dossier template rules locked!"
},
"stats": {
"analyzed-pages": "{count, plural, one{Page} other{Pages}}",
"total-people": "Total users"
@ -989,7 +992,7 @@
"download-file-disabled": "To download, ensure you are an approver in the dossier, and the {count, plural, one{file has undergone} other{files have undergone}} initial processing.",
"file-listing": {
"file-entry": {
"file-error": "Re-processing required",
"file-error": "Re-processing required {errorCode, select, RULES_EXECUTION_TIMEOUT{(Rules timeout)} other{}}",
"file-pending": "Pending..."
}
},
@ -1089,6 +1092,12 @@
"entities": "{count} {count, plural, one{entity} other{entities}}",
"entries": "{count} {count, plural, one{entry} other{entries}}",
"modified-on": "Modified on: {date}",
"rules-reset": {
"disabled-action": "Please contact your administrator to unlock the rules.",
"label": "Rules locked",
"success": "Dossier template rules successfully reset.",
"tooltip": "Click to reset rules"
},
"title": "Edit dossier template",
"valid-from": "Valid from: {date}",
"valid-to": "Valid to: {date}"
@ -2046,6 +2055,7 @@
"auto-expand-filters-on-action": "Auto-expand filters on my actions",
"help-mode-dialog": "Help mode activation dialog",
"load-all-annotations-warning": "Warning regarding simultaneous loading of all annotations in thumbnails",
"overwrite-file-option": "Preferred action when re-uploading an already existing file",
"table-extraction-type": "Table extraction type"
},
"label": "Preferences",
@ -2057,6 +2067,7 @@
"processing-status": {
"ocr": "OCR",
"pending": "Pending",
"pending-timeout": "Pending (Rules timeout)",
"processed": "Processed",
"processing": "Processing"
},
@ -2583,4 +2594,4 @@
}
},
"yesterday": "Yesterday"
}
}

View File

@ -273,9 +273,6 @@
"watermarks": "Watermarks"
},
"analysis-disabled": "Analysis disabled",
"annotation": {
"pending": "(Pending analysis)"
},
"annotation-actions": {
"accept-recommendation": {
"label": "Empfehlung annehmen"
@ -324,21 +321,21 @@
"success": "Schwärzung eingefügt!"
},
"recategorize-annotation": {
"error": "",
"success": ""
"error": "Bearbeiten des Typs fehlgeschlagen: {error}",
"success": "Annotation wurde bearbeitet: {changes} geändert."
},
"recategorize-image": {
"error": "Rekategorisierung des Bildes gescheitert: {error}",
"success": "Bild wurde einer neuen Kategorie zugeordnet."
},
"remove": {
"error": "Fehler beim Entfernen der Schwärzung: {error}",
"success": "Schwärzung entfernt!"
},
"remove-hint": {
"error": "Failed to remove hint: {error}",
"success": "Hint removed!"
},
"remove": {
"error": "Fehler beim Entfernen der Schwärzung: {error}",
"success": "Schwärzung entfernt!"
},
"undo": {
"error": "Die Aktion konnte nicht rückgängig gemacht werden. Fehler: {error}",
"success": "erfolgreich Rückgängig gemacht"
@ -351,15 +348,15 @@
"remove-highlights": {
"label": "Remove selected earmarks"
},
"resize": {
"label": "Größe ändern"
},
"resize-accept": {
"label": "Größe speichern"
},
"resize-cancel": {
"label": "Größenänderung abbrechen"
},
"resize": {
"label": "Größe ändern"
},
"see-references": {
"label": "See references"
},
@ -393,6 +390,9 @@
"skipped": "Übersprungen",
"text-highlight": "Earmark"
},
"annotation": {
"pending": "(Pending analysis)"
},
"annotations": "Annotations",
"archived-dossiers-listing": {
"no-data": {
@ -587,8 +587,7 @@
"success": {
"generic": "Component rules updated!"
},
"title": "Component rule editor",
"warning-text": "Warning: experimental feature!"
"title": "Component rule editor"
},
"configurations": "Einstellungen",
"confirm-archive-dossier": {
@ -637,18 +636,14 @@
"warning": "Achtung: Diese Aktion kann nicht rückgängig gemacht werden!"
},
"confirmation-dialog": {
"approve-file": {
"question": "Dieses Dokument enthält ungesehene Änderungen. Möchten Sie es trotzdem genehmigen?",
"title": "Warnung!"
},
"approve-file-without-analysis": {
"confirmationText": "Approve without analysis",
"denyText": "Cancel",
"question": "Analysis required to detect new components.",
"title": "Warning!"
},
"approve-multiple-files": {
"question": "Mindestens eine der ausgewählten Dateien enthält ungesehene Änderungen. Möchten Sie sie trotzdem genehmigen?",
"approve-file": {
"question": "Dieses Dokument enthält ungesehene Änderungen. Möchten Sie es trotzdem genehmigen?",
"title": "Warnung!"
},
"approve-multiple-files-without-analysis": {
@ -657,6 +652,10 @@
"question": "Analysis required to detect new components for at least one file.",
"title": "Warning"
},
"approve-multiple-files": {
"question": "Mindestens eine der ausgewählten Dateien enthält ungesehene Änderungen. Möchten Sie sie trotzdem genehmigen?",
"title": "Warnung!"
},
"assign-file-to-me": {
"question": {
"multiple": "Dieses Dokument wird gerade von einer anderen Person geprüft. Möchten Sie Reviewer werden und sich selbst dem Dokument zuweisen?",
@ -934,6 +933,9 @@
"reanalyse": {
"action": "Gesamtes Dossier analysieren"
},
"rules": {
"timeoutError": "Regeln für Dossier-Vorlagen gesperrt!"
},
"stats": {
"analyzed-pages": "{count, plural, one{Page} other{Pages}}",
"total-people": "Anzahl der Benutzer"
@ -990,7 +992,7 @@
"download-file-disabled": "You need to be approver in the dossier and the {count, plural, one{file needs} other{files need}} to be initially processed in order to download.",
"file-listing": {
"file-entry": {
"file-error": "Reanalyse erforderlich",
"file-error": "Reanalyse erforderlich {errorCode, select, RULES_EXECUTION_TIMEOUT{(Zeitlimit für Regeln)} other{}}",
"file-pending": "Ausstehend ..."
}
},
@ -1026,13 +1028,13 @@
"recent": "Neu ({hours} h)",
"unassigned": "Niemandem zugewiesen"
},
"reanalyse": {
"action": "Datei analysieren"
},
"reanalyse-dossier": {
"error": "Die Dateien konnten nicht für eine Reanalyse eingeplant werden. Bitte versuchen Sie es erneut.",
"success": "Dateien für Reanalyse vorgesehen."
},
"reanalyse": {
"action": "Datei analysieren"
},
"report-download": "Report download",
"start-auto-analysis": "Enable auto-analysis",
"stop-auto-analysis": "Stop auto-analysis",
@ -1066,8 +1068,7 @@
"dossier-states": "{count, plural, one{Dossier state} other{Dossier states}}"
},
"error": {
"conflict": "Dossier state with this name already exists!",
"generic": "Failed to save dossier state!"
"conflict": "Dossier state with this name already exists!"
},
"no-data": {
"title": "There are no dossier states."
@ -1091,6 +1092,12 @@
"entities": "{count} {count, plural, one{entity} other{entities}}",
"entries": "{count} {count, plural, one{entry} other{entries}}",
"modified-on": "Modified on: {date}",
"rules-reset": {
"disabled-action": "Bitte wenden Sie sich an Ihren Administrator, um die Regeln freizuschalten.",
"label": "Regeln gesperrt",
"success": "Die Regeln der Dossier-Vorlage wurden erfolgreich zurückgesetzt.",
"tooltip": "Klicken Sie hier, um die Regeln zurückzusetzen"
},
"title": "Edit dossier template",
"valid-from": "Valid from: {date}",
"valid-to": "Valid to: {date}"
@ -1103,14 +1110,6 @@
"total-documents": "Anzahl der Dokumente",
"total-people": "<strong>{count}</strong> {count, plural, one{user} other {users}}"
},
"dossier-templates": {
"label": "Dossier-Vorlagen",
"status": {
"active": "Active",
"inactive": "Inactive",
"incomplete": "Incomplete"
}
},
"dossier-templates-listing": {
"action": {
"clone": "Clone template",
@ -1145,6 +1144,14 @@
"title": "{length} dossier {length, plural, one{template} other{templates}}"
}
},
"dossier-templates": {
"label": "Dossier-Vorlagen",
"status": {
"active": "Active",
"inactive": "Inactive",
"incomplete": "Incomplete"
}
},
"dossier-watermark-selector": {
"heading": "Watermarks on documents",
"no-watermark": "There is no watermark defined for the dossier template.<br>Contact your app admin to define one.",
@ -1340,15 +1347,6 @@
"title": "{length} {length, plural, one{entity} other{entities}}"
}
},
"entity": {
"info": {
"actions": {
"revert": "Revert",
"save": "Save changes"
},
"heading": "Edit entity"
}
},
"entity-rules-screen": {
"error": {
"generic": "Something went wrong... Entity rules update failed!"
@ -1360,22 +1358,30 @@
"generic": "Entity rules updated!"
},
"title": "Entity rule editor",
"warning-text": "Warning: experimental feature!",
"warnings-found": "{warnings, plural, one{A warning} other{{warnings} warnings}} found in rules"
},
"entity": {
"info": {
"actions": {
"revert": "Revert",
"save": "Save changes"
},
"heading": "Edit entity"
}
},
"error": {
"deleted-entity": {
"dossier": {
"action": "Zurück zur Übersicht",
"label": "Dieses Dossier wurde gelöscht!"
},
"file": {
"action": "Zurück zum Dossier",
"label": "Diese Datei wurde gelöscht!"
},
"file-dossier": {
"action": "Zurück zur Übersicht",
"label": "Das Dossier dieser Datei wurde gelöscht!"
},
"file": {
"action": "Zurück zum Dossier",
"label": "Diese Datei wurde gelöscht."
}
},
"file-preview": {
@ -1393,12 +1399,6 @@
},
"exact-date": "{day} {month} {year} um {hour}:{minute} Uhr",
"file": "Datei",
"file-attribute": {
"update": {
"error": "Failed to update file attribute value!",
"success": "File attribute value has been updated successfully!"
}
},
"file-attribute-encoding-types": {
"ascii": "ASCII",
"iso": "ISO-8859-1",
@ -1409,6 +1409,12 @@
"number": "Nummer",
"text": "Freier Text"
},
"file-attribute": {
"update": {
"error": "Failed to update file attribute value!",
"success": "File attribute value has been updated successfully!"
}
},
"file-attributes-configurations": {
"cancel": "Cancel",
"form": {
@ -1626,15 +1632,6 @@
"csv": "File attributes were imported successfully from uploaded CSV file."
}
},
"filter": {
"analysis": "Analyse erforderlich",
"comment": "Kommentare",
"hint": "Nut Hinweise",
"image": "Bilder",
"none": "Keine Anmerkungen",
"redaction": "Geschwärzt",
"updated": "Aktualisiert"
},
"filter-menu": {
"filter-options": "Filteroptionen",
"filter-types": "Filter",
@ -1644,6 +1641,15 @@
"unseen-pages": "Nur Anmerkungen auf unsichtbaren Seiten",
"with-comments": "Nur Anmerkungen mit Kommentaren"
},
"filter": {
"analysis": "Analyse erforderlich",
"comment": "Kommentare",
"hint": "Nut Hinweise",
"image": "Bilder",
"none": "Keine Anmerkungen",
"redaction": "Geschwärzt",
"updated": "Aktualisiert"
},
"filters": {
"assigned-people": "Beauftragt",
"documents-status": "Documents state",
@ -1922,13 +1928,6 @@
"user-promoted-to-approver": "<b>{user}</b> wurde im Dossier <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b> zum Genehmiger ernannt!",
"user-removed-as-dossier-member": "<b>{user}</b> wurde als Mitglied von: <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b> entfernt!"
},
"notifications": {
"button-text": "Notifications",
"deleted-dossier": "Deleted dossier",
"label": "Benachrichtigungen",
"mark-all-as-read": "Alle als gelesen markieren",
"mark-as": "Mark as {type, select, read{read} unread{unread} other{}}"
},
"notifications-screen": {
"category": {
"email-notifications": "E-Mail Benachrichtigungen",
@ -1942,6 +1941,7 @@
"dossier": "Dossierbezogene Benachrichtigungen",
"other": "Andere Benachrichtigungen"
},
"options-title": "Wählen Sie aus, in welcher Kategorie Sie benachrichtigt werden möchten",
"options": {
"ASSIGN_APPROVER": "Wenn ich einem Dokument als Genehmiger zugewiesen bin",
"ASSIGN_REVIEWER": "Wenn ich einem Dokument als Überprüfer zugewiesen bin",
@ -1959,7 +1959,6 @@
"USER_PROMOTED_TO_APPROVER": "Wenn ich Genehmiger in einem Dossier werde",
"USER_REMOVED_AS_DOSSIER_MEMBER": "Wenn ich die Dossier-Mitgliedschaft verliere"
},
"options-title": "Wählen Sie aus, in welcher Kategorie Sie benachrichtigt werden möchten",
"schedule": {
"daily": "Tägliche Zusammenfassung",
"instant": "Sofortig",
@ -1967,6 +1966,13 @@
},
"title": "Benachrichtigungseinstellungen"
},
"notifications": {
"button-text": "Notifications",
"deleted-dossier": "Deleted dossier",
"label": "Benachrichtigungen",
"mark-all-as-read": "Alle als gelesen markieren",
"mark-as": "Mark as {type, select, read{read} unread{unread} other{}}"
},
"ocr": {
"confirmation-dialog": {
"cancel": "Cancel",
@ -2049,7 +2055,7 @@
"auto-expand-filters-on-action": "Auto expand filters on my actions",
"help-mode-dialog": "Help Mode Dialog",
"load-all-annotations-warning": "Warning regarding loading all annotations at once in file preview",
"open-structured-view-by-default": "Display Component View by default when opening a document",
"overwrite-file-option": "",
"table-extraction-type": "Table extraction type"
},
"label": "Preferences",
@ -2058,16 +2064,17 @@
"warnings-label": "Prompts and dialogs",
"warnings-subtitle": "Do not show again options"
},
"processing-status": {
"ocr": "OCR",
"pending": "Ausstehend",
"pending-timeout": "Ausstehend (Zeitlimit für Regeln)",
"processed": "Verarbeitet",
"processing": "Verarbeitung läuft"
},
"processing": {
"basic": "Processing",
"ocr": "OCR"
},
"processing-status": {
"ocr": "OCR",
"pending": "Pending",
"processed": "Processed",
"processing": "Processing"
},
"readonly": "Lesemodus",
"readonly-archived": "Read only (archived)",
"redact-text": {
@ -2220,7 +2227,7 @@
}
}
},
"invalid-upload": "Ungültiges Upload-Format ausgewählt! Unterstützt werden Dokumente im .xlsx- und im .docx-Format",
"invalid-upload": "Ungültiges Upload-Format ausgewählt. Unterstützte Formate: .xlsx- und .docx",
"multi-file-report": "(Mehrere Dateien)",
"report-documents": "Dokumente für den Bericht",
"setup": "Click the upload button on the right to upload your component report templates.",
@ -2303,12 +2310,6 @@
"red-user-admin": "Benutzer-Admin",
"regular": "Regulär"
},
"search": {
"active-dossiers": "ganze Plattform",
"all-dossiers": "all documents",
"placeholder": "Nach Dokumenten oder Dokumenteninhalt suchen",
"this-dossier": "in diesem Dossier"
},
"search-screen": {
"cols": {
"assignee": "Bevollmächtigter",
@ -2332,6 +2333,12 @@
"no-match": "Keine Dokumente entsprechen Ihren aktuellen Filtern.",
"table-header": "{length} search {length, plural, one{result} other{results}}"
},
"search": {
"active-dossiers": "ganze Plattform",
"all-dossiers": "all documents",
"placeholder": "Nach Dokumenten oder Dokumenteninhalt suchen",
"this-dossier": "in diesem Dossier"
},
"seconds": "seconds",
"size": "Size",
"smtp-auth-config": {
@ -2587,4 +2594,4 @@
}
},
"yesterday": "Gestern"
}
}

View File

@ -321,8 +321,8 @@
"success": "Annotation added!"
},
"recategorize-annotation": {
"error": "",
"success": ""
"error": "Failed to edit type: {error}",
"success": "Annotation was edited: Changed {changes}."
},
"recategorize-image": {
"error": "Failed to recategorize image: {error}",
@ -933,6 +933,9 @@
"reanalyse": {
"action": "Analyze entire dossier"
},
"rules": {
"timeoutError": "Dossier template rules locked!"
},
"stats": {
"analyzed-pages": "{count, plural, one{Page} other{Pages}}",
"total-people": "Total users"
@ -989,7 +992,7 @@
"download-file-disabled": "You need to be approver in the dossier and the {count, plural, one{file needs} other{files need}} to be initially processed in order to download.",
"file-listing": {
"file-entry": {
"file-error": "Re-processing required",
"file-error": "Re-processing required {errorCode, select, RULES_EXECUTION_TIMEOUT{(Rules timeout)} other{}}",
"file-pending": "Pending..."
}
},
@ -1089,6 +1092,12 @@
"entities": "{count} {count, plural, one{entity} other{entities}}",
"entries": "{count} {count, plural, one{entry} other{entries}}",
"modified-on": "Modified on: {date}",
"rules-reset": {
"disabled-action": "Please contact your administrator to unlock the rules.",
"label": "Rules locked",
"success": "Dossier template rules successfully reset.",
"tooltip": "Click to reset rules"
},
"title": "Edit dossier template",
"valid-from": "Valid from: {date}",
"valid-to": "Valid to: {date}"
@ -1531,7 +1540,7 @@
},
"last-assignee": "Last assignee",
"no-data": {
"title": "There are no annotations on this page."
"title": "This page does not contain annotations for the selected component or filter."
},
"quick-nav": {
"jump-first": "Jump to first page",
@ -1548,7 +1557,7 @@
"jump-to-next": "Jump to next",
"jump-to-previous": "Jump to previous",
"label": "Workload",
"no-annotations": "There are no annotations on the selected page or for the selected component.",
"no-annotations": "There are no annotations for the selected component.",
"page-is": "This page is",
"reset": "reset",
"select": "Select",
@ -1556,7 +1565,7 @@
"select-none": "None",
"show-skipped": "Show skipped in document",
"the-filters": "the filters",
"wrong-filters": "The selected filter combination is not possible. Please adjust or"
"wrong-filters": "No annotations for the selected filter combination. Please adjust or"
},
"document-info": {
"close": "Close document info",
@ -2046,6 +2055,7 @@
"auto-expand-filters-on-action": "Auto expand filters on my actions",
"help-mode-dialog": "Help Mode Dialog",
"load-all-annotations-warning": "Warning regarding loading all annotations at once in file preview",
"overwrite-file-option": "Preferred action when re-uploading of an already existing file",
"table-extraction-type": "Table extraction type"
},
"label": "Preferences",
@ -2057,6 +2067,7 @@
"processing-status": {
"ocr": "OCR",
"pending": "Pending",
"pending-timeout": "Pending (Rules timeout)",
"processed": "Processed",
"processing": "Processing"
},
@ -2583,4 +2594,4 @@
}
},
"yesterday": "Yesterday"
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,205 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.-------------------------------------------------------------------
------------[ Log Start: 2019-Oct-10 19:32:29.143159]--------------
System: (Windows, AMD64), PDFNetVersion: 7.1.0.71627
-------------------------------------------------------------------
[ INFO 19:32:29.143159, fontapp, WebFontCreator.cpp(679)]: logging enabled

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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