Merge branch 'master' into VM/RED-7673

This commit is contained in:
Valentin Mihai 2023-10-23 11:38:37 +03:00
commit 72407fc2f6
41 changed files with 2457 additions and 2522 deletions

View File

@ -100,7 +100,7 @@
"selector": "memberLike",
"modifiers": ["protected"],
"format": ["camelCase"],
"leadingUnderscore": "require"
"leadingUnderscore": "allow"
},
{
"selector": "memberLike",

View File

@ -7,6 +7,7 @@ import { DossierTemplatesService } from '@services/dossier-templates/dossier-tem
import { DossierTemplateStatsService } from '@services/entity-services/dossier-template-stats.service';
import { NGXLogger } from 'ngx-logger';
import { firstValueFrom } from 'rxjs';
import { UserPreferenceService } from '@users/user-preference.service';
export function templateExistsWhenEnteringAdmin(): CanActivateFn {
return async function (route: ActivatedRouteSnapshot): Promise<boolean> {
@ -29,12 +30,14 @@ export function templateExistsWhenEnteringDossierList(): CanActivateFn {
const logger = inject(NGXLogger);
const router = inject(Router);
const tenantsService = inject(TenantsService);
const userPreferencesService = inject(UserPreferenceService);
await firstValueFrom(dashboardStatsService.loadAll());
await firstValueFrom(dossierTemplatesService.loadAll());
const dossierTemplateStats = dashboardStatsService.find(dossierTemplateId);
if (!dossierTemplateStats || dossierTemplateStats.isEmpty) {
logger.warn(`[ROUTES] Dossier template ${dossierTemplateId} not found, redirecting to main`);
await userPreferencesService.saveLastDossierTemplate(null);
await router.navigate([tenantsService.activeTenantId, 'main']);
return false;
}

View File

@ -43,9 +43,11 @@ export function ifLoggedIn(): AsyncGuard {
await licenseService.loadLicenses();
const token = await keycloakService.getToken();
const jwtToken = jwt_decode(token) as JwtToken;
const authTime = (jwtToken.auth_time || jwtToken.iat).toString();
localStorage.setItem('authTime', authTime);
if (token) {
const jwtToken = jwt_decode(token) as JwtToken;
const authTime = (jwtToken.auth_time || jwtToken.iat).toString();
localStorage.setItem('authTime', authTime);
}
}
const isLoggedIn = await keycloakService.isLoggedIn();

View File

@ -16,7 +16,7 @@
<div class="space-between flex-align-items-center">
<span>{{ license.name }}</span>
<div class="mr-10 flex-align-items-center">
<div [class.green]="license.id === licenseService.activeLicense.id" class="dot mr-4"></div>
<div [class.active]="license.id === licenseService.activeLicense.id" class="dot mr-4"></div>
<span class="small-label">{{ getStatus(license.id) | translate | uppercase }}</span>
</div>
</div>

View File

@ -1,15 +1,17 @@
.green {
background: var(--iqser-green-2);
}
.space-between {
justify-content: space-between;
}
.dot {
position: relative;
background-color: var(--iqser-red-1);
&.active {
background: var(--iqser-green-2);
}
}
.small-label {
font-weight: 600;
color: var(--iqser-text);
}

View File

@ -3,7 +3,7 @@ import { LicenseService } from '@services/license.service';
import { ILicense } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { map, tap } from 'rxjs/operators';
import { IqserPermissionsService } from '@iqser/common-ui';
import { LoadingService } from '@iqser/common-ui';
const translations = {
active: _('license-info-screen.status.active'),
@ -26,14 +26,19 @@ export class LicenseSelectComponent {
}),
);
constructor(readonly licenseService: LicenseService, private readonly _permissionsService: IqserPermissionsService) {}
constructor(
readonly licenseService: LicenseService,
private readonly _loadingService: LoadingService,
) {}
getStatus(id) {
getStatus(id: string): string {
return id === this.licenseService.activeLicense.id ? translations.active : translations.inactive;
}
async licenseChanged(license: ILicense) {
this._loadingService.start();
await this.licenseService.loadLicenseData(license);
this.licenseService.setSelectedLicense(license);
this._loadingService.stop();
}
}

View File

@ -10,12 +10,12 @@
<div *ngIf="licenseService.licenseData$ | async" class="grid-container">
<div class="row">
<div translate="license-info-screen.backend-version"></div>
<div>{{ configService.values.BACKEND_APP_VERSION || '-' }}</div>
<div>{{ config.BACKEND_APP_VERSION || '-' }}</div>
</div>
<div class="row">
<div translate="license-info-screen.custom-app-title"></div>
<div>{{ configService.values.APP_NAME || '-' }}</div>
<div>{{ config.APP_NAME || '-' }}</div>
</div>
<div class="row">

View File

@ -1,23 +1,24 @@
import { Component } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { TranslateService } from '@ngx-translate/core';
import { ButtonConfig, IconButtonTypes, IqserPermissionsService } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { RouterHistoryService } from '@services/router-history.service';
import { LicenseService } from '@services/license.service';
import { Roles } from '@users/roles';
import type { User } from '@red/domain';
import { List } from '@common-ui/utils';
import { ButtonConfig, getConfig, IconButtonTypes, IqserPermissionsService } from '@iqser/common-ui';
import { getCurrentUser } from '@iqser/common-ui/lib/users';
import { TranslateService } from '@ngx-translate/core';
import type { AppConfig, User } from '@red/domain';
import { LicenseService } from '@services/license.service';
import { RouterHistoryService } from '@services/router-history.service';
import { Roles } from '@users/roles';
@Component({
templateUrl: './license-screen.component.html',
styleUrls: ['./license-screen.component.scss'],
})
export class LicenseScreenComponent {
readonly roles = Roles;
readonly currentUser = getCurrentUser<User>();
readonly currentYear = new Date().getFullYear();
readonly buttonConfigs: readonly ButtonConfig[] = [
protected readonly config = getConfig<AppConfig>();
protected readonly roles = Roles;
protected readonly currentUser = getCurrentUser<User>();
protected readonly currentYear = new Date().getFullYear();
protected readonly buttonConfigs: List<ButtonConfig> = [
{
label: _('license-info-screen.email-report'),
action: (): void => this.sendMail(),
@ -28,7 +29,6 @@ export class LicenseScreenComponent {
];
constructor(
readonly configService: ConfigService,
readonly licenseService: LicenseService,
readonly permissionsService: IqserPermissionsService,
readonly routerHistoryService: RouterHistoryService,

View File

@ -1,19 +1,22 @@
<ng-container *ngIf="configService.listingMode$ | async as mode">
<div
(mousedown)="handleClick($event)"
(click)="editFileAttribute($event)"
(mousedown)="handleClick($event)"
[ngClass]="{ 'workflow-attribute': mode === 'workflow', 'file-name-column': fileNameColumn }"
class="file-attribute"
>
<div [ngClass]="{ 'workflow-value': mode === 'workflow' }" class="value">
<b *ngIf="mode === 'workflow' && !isInEditMode"> {{ fileAttribute.label }}: </b>
<span
*ngIf="!isDate; else date"
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
[matTooltip]="fileAttributeValue"
>
{{ fileAttributeValue || '-' }}</span
>
<mat-icon *ngIf="!fileAttribute.editable" [matTooltip]="'readonly' | translate" svgIcon="red:read-only"></mat-icon>
<div>
<b *ngIf="mode === 'workflow' && !isInEditMode"> {{ fileAttribute.label }}: </b>
<span
*ngIf="!isDate; else date"
[matTooltip]="fileAttributeValue"
[ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }"
>
{{ fileAttributeValue || '-' }}</span
>
</div>
<ng-template #date>
<span [ngClass]="{ hide: isInEditMode, 'clamp-3': mode !== 'workflow' }">
{{ fileAttributeValue ? (fileAttributeValue | date: 'd MMM yyyy') : '-' }}</span
@ -25,7 +28,8 @@
*ngIf="
(fileAttributesService.isEditingFileAttribute() === false || isInEditMode) &&
!file.isInitialProcessing &&
permissionsService.canEditFileAttributes(file, dossier)
permissionsService.canEditFileAttributes(file, dossier) &&
fileAttribute.editable
"
>
<div

View File

@ -25,6 +25,17 @@
.value {
z-index: 1;
display: flex;
align-items: center;
gap: 6px;
mat-icon {
min-width: 12px;
width: 12px;
height: 12px;
line-height: 12px;
opacity: 0.7;
}
}
.workflow-value {
@ -72,8 +83,10 @@
background: transparent;
}
&.file-name-column-input,
.workflow-edit-input {
&.workflow-edit-input {
justify-content: space-between;
left: 0;
top: -5px;
box-shadow: none;
width: 100%;
position: absolute;
@ -82,12 +95,6 @@
form {
width: 100%;
}
}
&.workflow-edit-input {
justify-content: space-between;
left: 0;
top: -5px;
iqser-circle-button {
margin: 0 5px;

View File

@ -22,9 +22,10 @@ import { Debounce } from '@iqser/common-ui/lib/utils';
import { AsyncPipe, DatePipe, NgClass, NgIf, NgTemplateOutlet } from '@angular/common';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'redaction-file-attribute [fileAttribute] [file] [dossier]',
selector: 'redaction-file-attribute',
templateUrl: './file-attribute.component.html',
styleUrls: ['./file-attribute.component.scss'],
standalone: true,
@ -40,9 +41,17 @@ import { MatIconModule } from '@angular/material/icon';
DynamicInputComponent,
CircleButtonComponent,
NgTemplateOutlet,
TranslateModule,
],
})
export class FileAttributeComponent extends BaseFormComponent implements OnDestroy {
isInEditMode = false;
closedDatepicker = true;
@Input({ required: true }) fileAttribute!: IFileAttributeConfig;
@Input({ required: true }) file!: File;
@Input({ required: true }) dossier!: Dossier;
@Input() fileNameColumn = false;
readonlyAttrs: string[] = [];
readonly #subscriptions = new Subscription();
readonly #shouldClose = computed(
() =>
@ -54,13 +63,21 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
!this.fileAttributesService.openAttributeEdits().length,
);
isInEditMode = false;
closedDatepicker = true;
get isDate(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.DATE;
}
@Input() fileAttribute!: IFileAttributeConfig;
@Input() file!: File;
@Input() dossier!: Dossier;
@Input() fileNameColumn = false;
get isNumber(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.NUMBER;
}
get isText(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.TEXT;
}
get fileAttributeValue(): string {
return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id];
}
constructor(
router: Router,
@ -93,14 +110,6 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
);
}
get isDate(): boolean {
return this.fileAttribute.type === FileAttributeConfigTypes.DATE;
}
get fileAttributeValue(): string {
return this.file.fileAttributes.attributeIdToValue[this.fileAttribute.id];
}
@Debounce(60)
@HostListener('document:click', ['$event'])
clickOutside($event: MouseEvent) {
@ -123,7 +132,12 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
}
async editFileAttribute($event: MouseEvent): Promise<void> {
if (!this.file.isInitialProcessing && this.permissionsService.canEditFileAttributes(this.file, this.dossier)) {
if (
!this.file.isInitialProcessing &&
this.permissionsService.canEditFileAttributes(this.file, this.dossier) &&
this.fileAttribute.editable &&
!this.isInEditMode
) {
$event.stopPropagation();
this.fileAttributesService.openAttributeEdits.mutate(value =>
value.push({ attribute: this.fileAttribute.id, file: this.file.id }),
@ -161,7 +175,13 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
this.#toggleEdit();
this.fileAttributesService.openAttributeEdits.mutate(value => {
for (let index = 0; index < value.length; index++) {
if (JSON.stringify(value[index]) === JSON.stringify({ attribute: this.fileAttribute.id, file: this.file.id })) {
if (
JSON.stringify(value[index]) ===
JSON.stringify({
attribute: this.fileAttribute.id,
file: this.file.id,
})
) {
value.splice(index, 1);
}
}
@ -171,6 +191,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
#initFileAttributes() {
const configs = this.fileAttributesService.getFileAttributeConfig(this.file.dossierTemplateId).fileAttributeConfigs;
this.readonlyAttrs = configs.filter(config => config.editable === false).map(config => config.id);
configs.forEach(config => {
if (!this.file.fileAttributes.attributeIdToValue[config.id]) {
this.file.fileAttributes.attributeIdToValue[config.id] = null;
@ -183,7 +204,12 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
const fileAttributes = this.file.fileAttributes.attributeIdToValue;
Object.keys(fileAttributes).forEach(key => {
const attrValue = fileAttributes[key];
config[key] = [dayjs(attrValue, 'YYYY-MM-DD', true).isValid() ? dayjs(attrValue).toDate() : attrValue];
config[key] = [
{
value: dayjs(attrValue, 'YYYY-MM-DD', true).isValid() ? dayjs(attrValue).toDate() : attrValue,
disabled: this.readonlyAttrs.includes(key),
},
];
});
return this._formBuilder.group(config, {
validators: [this.#checkEmptyInput(), this.#checkDate()],
@ -192,7 +218,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
#checkEmptyInput(): ValidatorFn {
return (control: AbstractControl) =>
(!this.isDate && !control.get(this.fileAttribute.id)?.value?.trim().length && !this.fileAttributeValue) ||
(this.isText && !control.get(this.fileAttribute.id)?.value?.trim().length && !this.fileAttributeValue) ||
control.get(this.fileAttribute.id)?.value === this.fileAttributeValue
? { emptyString: true }
: null;
@ -211,7 +237,15 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
}
#formatAttributeValue(attrValue) {
return this.isDate ? attrValue && dayjs(attrValue).format('YYYY-MM-DD') : attrValue.trim().replaceAll(/\s\s+/g, ' ');
if (this.isDate) {
return attrValue && dayjs(attrValue).format('YYYY-MM-DD');
}
if (this.isText) {
return attrValue.trim().replaceAll(/\s\s+/g, ' ');
}
return attrValue;
}
#toggleEdit(): void {
@ -229,11 +263,16 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
}
#focusOnEditInput(): void {
setTimeout(() => {
const input = document.getElementById(this.fileAttribute.id) as HTMLInputElement;
const end = input.value.length;
input?.setSelectionRange(end, end);
input?.focus();
}, 100);
if (this.isDate || this.isText) {
setTimeout(() => {
const input = document.getElementById(this.fileAttribute.id) as HTMLInputElement;
if (!input) {
return;
}
const end = input.value.length;
input.setSelectionRange(end, end);
input.focus();
}, 100);
}
}
}

View File

@ -1,6 +1,6 @@
<section>
<iqser-page-header [buttonConfigs]="buttonConfigs" [helpModeKey]="'dossier'">
<ng-container slot="beforeFilters">
<ng-container *ngIf="isArchiveEnabled" slot="beforeFilters">
<redaction-dossiers-type-switch></redaction-dossiers-type-switch>
</ng-container>
</iqser-page-header>
@ -12,6 +12,7 @@
<iqser-table
(noDataAction)="openAddDossierDialog()"
[hasScrollButton]="true"
[headerHelpModeKey]="'dossier_list'"
[helpModeKey]="'dossier'"
[itemSize]="85"
[noDataButtonLabel]="'dossier-listing.no-data.action' | translate"
@ -19,7 +20,6 @@
[noMatchText]="'dossier-listing.no-match.title' | translate"
[showNoDataButton]="permissionsService.canCreateDossier(dossierTemplate)"
[tableColumnConfigs]="tableColumnConfigs"
[headerHelpModeKey]="'dossier_list'"
noDataIcon="red:folder"
></iqser-table>
</div>

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier, DOSSIER_TEMPLATE_ID, DossierTemplate } from '@red/domain';
import { Dossier, DOSSIER_TEMPLATE_ID, DOSSIERS_ARCHIVE, DossierTemplate } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { ButtonConfig, ListingComponent, listingProvidersFactory, LoadingService, TableComponent } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -11,6 +11,7 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { SharedDialogService } from '@shared/services/dialog.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { OnAttach } from '@iqser/common-ui/lib/utils';
import { FeaturesService } from '@services/features.service';
@Component({
templateUrl: './dossiers-listing-screen.component.html',
@ -19,27 +20,29 @@ import { OnAttach } from '@iqser/common-ui/lib/utils';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DossiersListingScreenComponent extends ListingComponent<Dossier> implements OnInit, OnAttach {
readonly tableColumnConfigs = this._configService.tableConfig;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs: ButtonConfig[];
readonly dossierTemplate: DossierTemplate;
readonly computeFilters$ = this._activeDossiersService.all$.pipe(tap(() => this._computeAllFilters()));
readonly isArchiveEnabled = this._featuresService.isEnabled(DOSSIERS_ARCHIVE);
@ViewChild('needsWorkFilterTemplate', {
read: TemplateRef,
static: true,
})
private readonly _needsWorkFilterTemplate: TemplateRef<unknown>;
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<Dossier>;
readonly tableColumnConfigs = this._configService.tableConfig;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs: ButtonConfig[];
readonly dossierTemplate: DossierTemplate;
readonly computeFilters$ = this._activeDossiersService.all$.pipe(tap(() => this._computeAllFilters()));
constructor(
router: Router,
readonly router: Router,
private readonly _configService: ConfigService,
readonly permissionsService: PermissionsService,
private readonly _dialogService: SharedDialogService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _loadingService: LoadingService,
dossierTemplatesService: DossierTemplatesService,
readonly dossierTemplatesService: DossierTemplatesService,
private readonly _featuresService: FeaturesService,
) {
super();
const dossierTemplateId = router.routerState.snapshot.root.firstChild.firstChild.paramMap.get(DOSSIER_TEMPLATE_ID);

View File

@ -86,7 +86,7 @@
<mat-form-field>
<mat-select [placeholder]="'redact-text.dialog.content.type-placeholder' | translate" formControlName="dictionary">
<mat-select-trigger>{{ displayedDictionaryLabel }}</mat-select-trigger>
<mat-select-trigger>{{ displayedDictionaryLabel$ | async }}</mat-select-trigger>
<mat-option
(click)="typeChanged()"
*ngFor="let dictionary of dictionaries"
@ -115,7 +115,7 @@
<div class="dialog-actions">
<iqser-icon-button
[disabled]="disabled"
[disabled]="!form.valid"
[label]="'redact-text.dialog.actions.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"

View File

@ -1,6 +1,6 @@
import { Component, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder } from '@angular/forms';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DetailsRadioOption, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui';
import { Dictionary, IAddRedactionRequest, SuperTypes } from '@red/domain';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
@ -8,8 +8,8 @@ import { DictionaryService } from '@services/entity-services/dictionary.service'
import { JustificationsService } from '@services/entity-services/justifications.service';
import { Roles } from '@users/roles';
import { calcTextWidthInPixels } from '@utils/functions';
import { firstValueFrom } from 'rxjs';
import { tap } from 'rxjs/operators';
import { firstValueFrom, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { getRedactOrHintOptions, RedactOrHintOption, RedactOrHintOptions } from '../../utils/dialog-options';
import { RedactTextData, RedactTextResult } from '../../utils/dialog-types';
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
@ -24,20 +24,21 @@ export class RedactTextDialogComponent
extends IqserDialogComponent<RedactTextDialogComponent, RedactTextData, RedactTextResult>
implements OnInit
{
readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId);
readonly #manualRedactionTypeExists = inject(DictionaryService).hasManualType(this.#dossier.dossierTemplateId);
#applyToAllDossiers = this.data.applyToAllDossiers ?? true;
readonly roles = Roles;
readonly iconButtonTypes = IconButtonTypes;
readonly options = getRedactOrHintOptions(this.#dossier, this.#applyToAllDossiers, this.data.isApprover);
readonly initialText = this.data?.manualRedactionEntryWrapper?.manualRedactionEntry?.value;
readonly form = this.#getForm();
readonly form: FormGroup;
dictionaryRequest = false;
legalOptions: LegalBasisOption[] = [];
dictionaries: Dictionary[] = [];
isEditingSelectedText = false;
modifiedText = this.initialText;
selectedTextRows = 1;
readonly options: DetailsRadioOption<RedactOrHintOption>[];
readonly displayedDictionaryLabel$: Observable<string>;
readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId);
readonly #manualRedactionTypeExists = inject(DictionaryService).hasManualType(this.#dossier.dossierTemplateId);
#applyToAllDossiers = this.data.applyToAllDossiers ?? true;
constructor(
private readonly _justificationsService: JustificationsService,
@ -45,6 +46,10 @@ export class RedactTextDialogComponent
private readonly _formBuilder: FormBuilder,
) {
super();
this.options = getRedactOrHintOptions(this.#dossier, this.#applyToAllDossiers, this.data.isApprover);
this.form = this.#getForm();
this.form.controls.option.valueChanges
.pipe(
tap((option: DetailsRadioOption<RedactOrHintOption>) => {
@ -55,28 +60,18 @@ export class RedactTextDialogComponent
} else {
this.isEditingSelectedText = false;
this.modifiedText = this.form.controls.selectedText.value;
this.form.patchValue({ selectedText: this.initialText });
this.form.patchValue({ selectedText: this.initialText }, { emitEvent: true });
}
this.#setupValidators(option.value);
this.#resetValues();
}),
takeUntilDestroyed(),
)
.subscribe();
}
get displayedDictionaryLabel() {
const dictType = this.form.controls.dictionary.value;
if (dictType) {
return this.dictionaries.find(d => d.type === dictType)?.label ?? null;
}
return null;
}
get disabled() {
if (this.dictionaryRequest) {
return !this.form.controls.dictionary.value;
}
return !this.form.controls.dictionary.value;
this.displayedDictionaryLabel$ = this.form.controls.dictionary.valueChanges.pipe(
map(dictionary => this.dictionaries.find(d => d.type === dictionary)?.label ?? null),
);
}
async ngOnInit(): Promise<void> {
@ -134,14 +129,30 @@ export class RedactTextDialogComponent
}
}
#setupValidators(option: RedactOrHintOption) {
switch (option) {
case RedactOrHintOptions.IN_DOSSIER:
this.form.controls.reason.clearValidators();
this.form.controls.dictionary.addValidators(Validators.required);
break;
case RedactOrHintOptions.ONLY_HERE:
this.form.controls.dictionary.clearValidators();
this.form.controls.reason.addValidators(Validators.required);
break;
}
this.form.controls.reason.updateValueAndValidity();
this.form.controls.dictionary.updateValueAndValidity();
}
#setDictionaries() {
this.dictionaries = this._dictionaryService.getRedactTextDictionaries(this.#dossier.dossierTemplateId, !this.#applyToAllDossiers);
}
#getForm() {
#getForm(): FormGroup {
return this._formBuilder.group({
selectedText: this.data?.manualRedactionEntryWrapper?.manualRedactionEntry?.value,
reason: null as LegalBasisOption,
reason: [null as LegalBasisOption, Validators.required],
comment: [null],
dictionary: [this.#manualRedactionTypeExists ? SuperTypes.ManualRedaction : null],
option: this.options[0],

View File

@ -32,14 +32,6 @@
font-weight: 600;
}
.value-content {
.value {
}
.actions {
}
}
.table-header {
margin: 10px 0;
border-bottom: 1px solid var(--iqser-separator);

View File

@ -35,8 +35,10 @@ export class PdfViewer {
img: this.#convertPath('/assets/icons/general/pdftron-action-search.svg'),
title: inject(TranslateService).instant(_('pdf-viewer.text-popup.actions.search')),
onClick: () => {
setTimeout(() => this.#searchForSelectedText(), 250);
this.#focusSearch();
setTimeout(() => {
this.#searchForSelectedText();
this.#focusSearch();
}, 250);
},
};
readonly #destroyRef = inject(DestroyRef);
@ -62,6 +64,7 @@ export class PdfViewer {
searchUp: false, // search from the end of the document upwards
ambientString: true, // return ambient string as part of the result
};
searchedWord?: string;
constructor(
private readonly _logger: NGXLogger,
@ -263,14 +266,15 @@ export class PdfViewer {
#listenForCommandF() {
this.#instance.UI.hotkeys.on('command+f, ctrl+f', e => {
e.preventDefault();
if (!this.#isElementActive('searchPanel')) {
if (this.#isElementActive('searchPanel')) {
this.#updateSearchOptions();
} else {
this.activateSearch();
}
if (this.documentViewer.getSelectedText()) {
this.#updateSearchOptions();
this.#searchForSelectedText();
}
setTimeout(() => this.#focusSearch(), 30);
setTimeout(() => this.#focusSearch(), 40);
});
}
@ -309,7 +313,14 @@ export class PdfViewer {
}
#searchForSelectedText() {
const selected = [...new Set(this.documentViewer.getSelectedText().split('\n'))].join('\n');
const uniqueText = [...new Set(this.documentViewer.getSelectedText().split('\n'))];
const sameWord = [...new Set(uniqueText.map(text => text.toLowerCase()))].length === 1;
if (uniqueText.length === 1 && sameWord) {
this.searchedWord = uniqueText.join('\n');
} else if (!sameWord) {
this.searchedWord = this.documentViewer.getSelectedText();
}
const selected = this.searchedWord;
this.#instance.UI.searchTextFull(selected, this.searchOptions);
}
@ -376,7 +387,7 @@ export class PdfViewer {
if (input) {
input.focus();
}
if (input.value.length > 0) {
if (input?.value.length > 0) {
input.select();
}
}

View File

@ -179,6 +179,7 @@ export class FileActionsComponent implements OnChanges {
showDot: !!this.file.excludedPages?.length,
icon: 'red:exclude-pages',
show: !!this._excludedPagesService && this._permissionsService.canExcludePages(this.file, this.dossier),
helpModeKey: 'exclude_pages',
},
{
id: 'set-file-to-new-btn',

View File

@ -17,7 +17,7 @@
<div class="dialog-actions">
<iqser-icon-button
[disabled]="!form.valid || !changed"
[disabled]="!form.valid || !changed || !validOption"
[label]="'assign-owner.dialog.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"

View File

@ -66,6 +66,11 @@ export class AssignReviewerApproverDialogComponent extends IqserDialogComponent<
return false;
}
get validOption() {
const currentUser = this.form.get('user').value;
return !!this.userOptions.find(u => u === currentUser);
}
get #mode() {
const isUnderApproval = this.data.targetStatus === WorkflowFileStatuses.UNDER_APPROVAL;
const isApproved = this.data.targetStatus === WorkflowFileStatuses.APPROVED;

View File

@ -12,7 +12,6 @@
<iqser-dynamic-input
*ngFor="let attr of customAttributes"
[canEditInput]="!disabled"
[classList]="'w-full'"
[formControlName]="attr.id"
[id]="attr.id"

View File

@ -37,18 +37,18 @@
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
<ng-container *ngIf="activeEntryType === entryTypes.ENTRY">
<ng-container *ngIf="activeEntryType === entryTypes.ENTRY || selectedDictionary.hint">
{{
'edit-dossier-dialog.dictionary.entries'
| translate: { length: entriesToDisplay.length, hint: selectedDictionary.hint }
}}
</ng-container>
<ng-container *ngIf="activeEntryType === entryTypes.FALSE_POSITIVE">
<ng-container *ngIf="activeEntryType === entryTypes.FALSE_POSITIVE && !selectedDictionary.hint">
{{
'edit-dossier-dialog.dictionary.false-positive-entries' | translate: { length: entriesToDisplay.length }
}}
</ng-container>
<ng-container *ngIf="activeEntryType === entryTypes.FALSE_RECOMMENDATION">
<ng-container *ngIf="activeEntryType === entryTypes.FALSE_RECOMMENDATION && !selectedDictionary.hint">
{{
'edit-dossier-dialog.dictionary.false-recommendation-entries'
| translate: { length: entriesToDisplay.length }

View File

@ -8,7 +8,7 @@ import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import dayjs, { Dayjs } from 'dayjs';
import utc from 'dayjs/plugin/utc';
import localeData from 'dayjs/plugin/localeData';
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
export interface DayJsDateAdapterOptions {
@ -135,8 +135,7 @@ export class CustomDateAdapter extends DateAdapter<Dayjs> {
}
createDate(year: number, month: number, date: number): Dayjs {
const returnDayjs = this._dayJs().set('year', year).set('month', month).set('date', date);
return returnDayjs;
return this._dayJs().set('year', year).set('month', month).set('date', date);
}
today(): Dayjs {
@ -145,8 +144,9 @@ export class CustomDateAdapter extends DateAdapter<Dayjs> {
parse(value: any, parseFormat: string): Dayjs | null {
if (value && typeof value === 'string') {
return this._dayJs(value, dayjs().localeData().longDateFormat(parseFormat), this.locale);
return dayjs(value, parseFormat);
}
// todo: is this necessary?
return value ? this._dayJs(value).locale(this.locale) : null;
}
@ -229,7 +229,7 @@ export class CustomDateAdapter extends DateAdapter<Dayjs> {
dayjs.extend(utc);
}
dayjs.extend(LocalizedFormat);
dayjs.extend(localizedFormat);
dayjs.extend(customParseFormat);
dayjs.extend(localeData);

View File

@ -64,13 +64,16 @@
</div>
</ng-container>
<div *ngIf="!filterByDossierTemplate && activeDossiersService.all$ | async as dossiers" class="iqser-input-group w-200 mt-0">
<div *ngIf="!filterByDossierTemplate && !!dossiers.length" class="iqser-input-group w-200 mt-0">
<mat-form-field>
<mat-select [(ngModel)]="dossier" [disabled]="!compare">
<mat-option [value]="selectDossier">{{ selectDossier.dossierName | translate }}</mat-option>
<mat-option *ngFor="let dossier of dossiers" [value]="dossier">
{{ dossier.dossierName }}
</mat-option>
<ng-container *ngFor="let dossier of dossiers; let index = index">
<mat-option [value]="dossier">
{{ dossier.dossierName }}
</mat-option>
<mat-divider *ngIf="index === dossiers.length - 2"></mat-divider>
</ng-container>
</mat-select>
</mat-form-field>
</div>

View File

@ -8,6 +8,7 @@ import {
DictionaryType,
Dossier,
DossierTemplate,
IDictionary,
} from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DictionaryService } from '@services/entity-services/dictionary.service';
@ -50,6 +51,7 @@ export class DictionaryManagerComponent implements OnChanges {
@Output() readonly saveDictionary = new EventEmitter<string[]>();
@ViewChild(EditorComponent) readonly editor: EditorComponent;
readonly iconButtonTypes = IconButtonTypes;
readonly dossiers: Dossier[];
currentMatch = 0;
findMatches: FindMatch[] = [];
diffEditorText = '';
@ -71,7 +73,16 @@ export class DictionaryManagerComponent implements OnChanges {
private readonly _changeRef: ChangeDetectorRef,
readonly activeDossiersService: ActiveDossiersService,
readonly dossierTemplatesService: DossierTemplatesService,
) {}
) {
this.dossiers = activeDossiersService.all;
const templateDictionary = {
id: 'template',
dossierId: 'template',
dossierName: 'Template Dictionary',
dossierTemplateId: this.dossiers[0]?.dossierTemplateId,
} as Dossier;
this.dossiers.push(templateDictionary);
}
private _dossierTemplate = this.selectDossierTemplate;
@ -242,11 +253,16 @@ export class DictionaryManagerComponent implements OnChanges {
}
async #onDossierChanged(dossierTemplateId: string, dossierId?: string) {
const dictionary = (
await firstValueFrom(
this._dictionaryService.loadDictionaryEntriesByType([this.selectedDictionaryType], dossierTemplateId, dossierId),
)
)[0];
let dictionary: IDictionary;
if (dossierId === 'template') {
dictionary = await this._dictionaryService.getForType(dossierTemplateId, this.selectedDictionaryType);
} else {
dictionary = (
await firstValueFrom(
this._dictionaryService.loadDictionaryEntriesByType([this.selectedDictionaryType], dossierTemplateId, dossierId),
)
)[0];
}
const activeEntries =
this.activeEntryType === DictionaryEntryTypes.ENTRY || this.hint
? [...dictionary.entries]

View File

@ -20,7 +20,7 @@ const lineChangeToDecoration = ({ originalEndLineNumber, originalStartLineNumber
glyphMarginClassName: 'arrow-left',
isWholeLine: true,
},
} as IModelDeltaDecoration);
}) as IModelDeltaDecoration;
const isPositive = (lineChange: ILineChange) => lineChange.originalEndLineNumber - lineChange.originalStartLineNumber >= 0;
const notZero = (lineChange: ILineChange) => lineChange.originalEndLineNumber !== 0 && lineChange.originalStartLineNumber !== 0;
@ -47,7 +47,10 @@ export class EditorComponent implements OnInit, OnChanges {
codeEditor: ICodeEditor;
value: string;
constructor(private readonly _loadingService: LoadingService, private readonly _editorThemeService: EditorThemeService) {
constructor(
private readonly _loadingService: LoadingService,
private readonly _editorThemeService: EditorThemeService,
) {
const textChanged$ = this._editorTextChanged$.pipe(
debounceTime(300), // prevent race condition with onPaste event
takeUntilDestroyed(),

View File

@ -8,6 +8,6 @@ export class NavigateLastDossiersScreenDirective {
constructor(private readonly _routerHistoryService: RouterHistoryService) {}
@HostListener('click') onClick() {
this._routerHistoryService.navigateToLastDossiersScreen();
return this._routerHistoryService.navigateToLastDossiersScreen();
}
}

View File

@ -49,6 +49,7 @@ import { IqserUsersModule } from '@iqser/common-ui/lib/users';
import { SmallChipComponent } from '@iqser/common-ui/lib/shared';
import { SelectComponent } from '@shared/components/select/select.component';
import { FileAttributeComponent } from '../dossier-overview/components/file-attribute/file-attribute.component';
import { MatDividerModule } from '@angular/material/divider';
const buttons = [FileDownloadBtnComponent];
@ -103,6 +104,7 @@ const deleteThisWhenAllComponentsAreStandalone = [DonutChartComponent, FileAttri
SelectComponent,
RoundCheckboxComponent,
DynamicInputComponent,
MatDividerModule,
],
exports: [...modules, ...components, ...utils, ...deleteThisWhenAllComponentsAreStandalone],
providers: [

View File

@ -5,10 +5,9 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
import { filter, pluck } from 'rxjs/operators';
import { FilesMapService } from './files/files-map.service';
import { TranslateService } from '@ngx-translate/core';
import { BreadcrumbTypes, DOSSIER_ID, DOSSIER_TEMPLATE_ID, DOSSIERS_ARCHIVE, FILE_ID } from '@red/domain';
import { BreadcrumbTypes, DOSSIER_ID, DOSSIER_TEMPLATE_ID, FILE_ID } from '@red/domain';
import { DossiersService } from './dossiers/dossiers.service';
import { dossiersServiceResolver } from './entity-services/dossiers.service.provider';
import { FeaturesService } from './features.service';
import { DashboardStatsService } from './dossier-templates/dashboard-stats.service';
export type RouterLinkActiveOptions = { exact: boolean } | IsActiveMatchOptions;
@ -32,25 +31,8 @@ export type Breadcrumbs = List<Breadcrumb>;
providedIn: 'root',
})
export class BreadcrumbsService {
readonly #store$ = new BehaviorSubject<Breadcrumbs>([]);
readonly breadcrumbs$: Observable<Breadcrumbs>;
constructor(
private readonly _injector: Injector,
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
private readonly _dashboardStatsService: DashboardStatsService,
private readonly _featuresService: FeaturesService,
) {
this.breadcrumbs$ = this.#store$.asObservable();
_router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
const root = _router.routerState.snapshot.root;
this._clear();
this._addBreadcrumbs(root);
});
}
readonly #store$ = new BehaviorSubject<Breadcrumbs>([]);
get breadcrumbs() {
return this.#store$.value;
@ -71,6 +53,22 @@ export class BreadcrumbsService {
};
}
constructor(
private readonly _injector: Injector,
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
private readonly _dashboardStatsService: DashboardStatsService,
) {
this.breadcrumbs$ = this.#store$.asObservable();
_router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
const root = _router.routerState.snapshot.root;
this._clear();
this._addBreadcrumbs(root);
});
}
private _append(breadcrumb: Breadcrumb) {
this.#store$.next([...this.#store$.value, breadcrumb]);
}
@ -87,7 +85,7 @@ export class BreadcrumbsService {
const breadcrumbs = route.data.breadcrumbs || [];
if (breadcrumbs.length === 1 && this._featuresService.isEnabled(DOSSIERS_ARCHIVE)) {
if (breadcrumbs.length === 1) {
if (breadcrumbs[0] === BreadcrumbTypes.dossierTemplate) {
this._addDossierTemplateDropdown(params);
return;

View File

@ -1,10 +1,28 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { ErrorHandler, Inject, Injectable, Injector } from '@angular/core';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Injectable()
export class GlobalErrorHandler extends ErrorHandler {
constructor(@Inject(Injector) private _injector: Injector) {
super();
}
handleError(error: Error): void {
const chunkFailedMessage = /Loading chunk [\d]+ failed/;
console.write(error);
if (error.message.includes('HttpErrorResponse')) {
const err = JSON.parse(error.message.split('HttpErrorResponse:')[1]) as HttpErrorResponse;
if (err.status >= HttpStatusCode.BadRequest && err.status <= HttpStatusCode.InternalServerError) {
const toaster = this._injector.get(Toaster);
if (err.error.message) {
toaster.rawError(err.error.message);
} else if ([400, 403, 404, 409, 500].includes(err.status)) {
toaster.rawError(_(`generic-errors.${err.status}`));
}
}
}
if (error?.message?.includes('An error happened during access validation')) {
console.log('User is not authorized to access this page');
}

View File

@ -17,8 +17,8 @@ const defaultOnError: ILicenses = {
product: 'Error',
licensedTo: 'Error',
licensedToEmail: 'Error',
validFrom: '01-01-2023',
validUntil: '01-01-2024',
validFrom: '2023-01-01T00:00:00Z',
validUntil: '2023-12-31T00:00:00Z',
features: [
{
name: LicenseFeatures.PROCESSING_PAGES,

View File

@ -1,10 +1,10 @@
import { effect, Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
import { JwtToken } from '@guards/if-logged-in.guard';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
import jwt_decode from 'jwt-decode';
import { KeycloakService } from 'keycloak-angular';
import { JwtToken } from '@guards/if-logged-in.guard';
import { filter } from 'rxjs/operators';
const LAST_DOSSIERS_SCREEN = 'routerHistory_lastDossiersScreen';
@ -28,11 +28,15 @@ export class RouterHistoryService {
}
});
const ref = effect(async () => {
if (this._tenantsService.activeTenantId.length === 0) {
if (!(await _keycloakService.isLoggedIn())) {
return;
}
const token = await this._keycloakService.getToken();
if (this._tenantsService.activeTenantId.length === 0 || !token) {
return;
}
const jwtToken = jwt_decode(token) as JwtToken;
const authTime = (jwtToken.auth_time || jwtToken.iat).toString();
const localStorageAuthTime = localStorage.getItem('authTime');
@ -45,14 +49,14 @@ export class RouterHistoryService {
});
}
navigateToLastDossiersScreen(): void {
navigateToLastDossiersScreen() {
const lastDossiersScreen = localStorage.getItem(LAST_DOSSIERS_SCREEN);
if (this._router.url === decodeURI(lastDossiersScreen) || lastDossiersScreen === null) {
this._router.navigate(['/' + this._tenantsService.activeTenantId]);
} else {
const url = decodeURI(lastDossiersScreen).split('?')[0];
// todo links
this._router.navigate([url]);
return this._router.navigate(['/' + this._tenantsService.activeTenantId]);
}
const url = decodeURI(lastDossiersScreen).split('?')[0];
// todo links
return this._router.navigate([url]);
}
}

View File

@ -1,9 +1,9 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dan1.iqser.cloud",
"API_URL": "https://dan.iqser.cloud",
"APP_NAME": "RedactManager",
"IS_DOCUMINE": true,
"IS_DOCUMINE": false,
"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://dan1.iqser.cloud/auth",
"OAUTH_URL": "https://dan.iqser.cloud/auth",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",
"ANNOTATIONS_THRESHOLD": 1000,
"THEME": "scm",
"BASE_TRANSLATIONS_DIRECTORY": "/assets/i18n/scm/",
"THEME": "redact",
"BASE_TRANSLATIONS_DIRECTORY": "/assets/i18n/redact/",
"AVAILABLE_NOTIFICATIONS_DAYS": 30,
"AVAILABLE_OLD_NOTIFICATIONS_MINUTES": 60,
"NOTIFICATIONS_THRESHOLD": 1000,

View File

@ -612,5 +612,9 @@
{
"elementKey": "upload_report",
"documentKey": "upload_report"
},
{
"elementKey": "editor_exclude_pages",
"documentKey": "editor_exclude_pages"
}
]

View File

@ -2540,5 +2540,12 @@
"select": "Wählen"
}
},
"yesterday": "Gestern"
"yesterday": "Gestern",
"generic-errors": {
"400": "",
"403": "",
"404": "",
"409": "",
"500": ""
}
}

View File

@ -2545,5 +2545,12 @@
"select": "Select"
}
},
"yesterday": "Yesterday"
"yesterday": "Yesterday",
"generic-errors": {
"400": "The sent request is not valid.",
"403": "Access to the requested resource is not allowed.",
"404": "The requested resource could not be found.",
"409": "The request is incompatible with the current state.",
"500": "The server encountered an unexpected condition that prevented it from fulfilling the request."
}
}

View File

@ -2545,5 +2545,12 @@
"select": "Wählen"
}
},
"yesterday": "Gestern"
"yesterday": "Gestern",
"generic-errors": {
"400": "",
"403": "",
"404": "",
"409": "",
"500": ""
}
}

View File

@ -2545,5 +2545,12 @@
"select": "Select"
}
},
"yesterday": "Yesterday"
"yesterday": "Yesterday",
"generic-errors": {
"400": "The sent request is not valid.",
"403": "Access to the requested resource is not allowed.",
"404": "The requested resource could not be found.",
"409": "The request is incompatible with the current state.",
"500": "The server encountered an unexpected condition that prevented it from fulfilling the request."
}
}

@ -1 +1 @@
Subproject commit bd532cd28fe9b7fb57c3e92253df490f6efae6a1
Subproject commit a6383c1dbc840115897a31567c3f5633ba78b43a

View File

@ -23,98 +23,98 @@
"*.{ts,js,html}": "eslint --fix"
},
"dependencies": {
"@angular/animations": "16.2.2",
"@angular/cdk": "16.2.1",
"@angular/common": "16.2.2",
"@angular/compiler": "16.2.2",
"@angular/core": "16.2.2",
"@angular/forms": "16.2.2",
"@angular/material": "16.2.1",
"@angular/platform-browser": "16.2.2",
"@angular/platform-browser-dynamic": "16.2.2",
"@angular/router": "16.2.2",
"@angular/service-worker": "16.2.2",
"@angular/animations": "16.2.9",
"@angular/cdk": "16.2.8",
"@angular/common": "16.2.9",
"@angular/compiler": "16.2.9",
"@angular/core": "16.2.9",
"@angular/forms": "16.2.9",
"@angular/material": "16.2.8",
"@angular/platform-browser": "16.2.9",
"@angular/platform-browser-dynamic": "16.2.9",
"@angular/router": "16.2.9",
"@angular/service-worker": "16.2.9",
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@messageformat/core": "^3.1.0",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"@nx/angular": "16.7.4",
"@pdftron/webviewer": "10.3.0",
"@nx/angular": "16.10.0",
"@pdftron/webviewer": "10.4.0",
"chart.js": "4.4.0",
"dayjs": "1.11.9",
"dayjs": "1.11.10",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"jwt-decode": "^3.1.2",
"keycloak-angular": "14.1.0",
"keycloak-js": "22.0.1",
"keycloak-js": "22.0.4",
"lodash-es": "^4.17.21",
"monaco-editor": "0.41.0",
"monaco-editor": "0.43.0",
"ng2-charts": "5.0.3",
"ngx-color-picker": "^14.0.0",
"ngx-color-picker": "15.0.0",
"ngx-logger": "^5.0.11",
"ngx-toastr": "17.0.2",
"ngx-translate-messageformat-compiler": "6.5.0",
"object-hash": "^3.0.0",
"papaparse": "^5.4.0",
"rxjs": "7.8.1",
"sass": "1.66.1",
"scroll-into-view-if-needed": "^3.0.6",
"sass": "1.69.3",
"scroll-into-view-if-needed": "3.1.0",
"streamsaver": "^2.0.5",
"tslib": "2.6.2",
"zone.js": "0.13.1"
"zone.js": "0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "16.2.0",
"@angular-devkit/core": "16.2.0",
"@angular-devkit/schematics": "16.2.0",
"@angular-eslint/builder": "16.1.1",
"@angular-eslint/eslint-plugin": "16.1.1",
"@angular-eslint/eslint-plugin-template": "16.1.1",
"@angular-eslint/schematics": "16.1.1",
"@angular-eslint/template-parser": "16.1.1",
"@angular/cli": "~16.2.0",
"@angular/compiler-cli": "16.2.2",
"@angular/language-service": "16.2.2",
"@angular-devkit/build-angular": "16.2.6",
"@angular-devkit/core": "16.2.6",
"@angular-devkit/schematics": "16.2.6",
"@angular-eslint/builder": "16.2.0",
"@angular-eslint/eslint-plugin": "16.2.0",
"@angular-eslint/eslint-plugin-template": "16.2.0",
"@angular-eslint/schematics": "16.2.0",
"@angular-eslint/template-parser": "16.2.0",
"@angular/cli": "16.2.6",
"@angular/compiler-cli": "16.2.9",
"@angular/language-service": "16.2.9",
"@bartholomej/ngx-translate-extract": "^8.0.2",
"@localazy/ts-api": "^1.0.0",
"@nx/eslint-plugin": "16.7.4",
"@nx/jest": "16.7.4",
"@nx/linter": "16.7.4",
"@nx/workspace": "16.7.4",
"@schematics/angular": "16.2.0",
"@nx/eslint-plugin": "16.10.0",
"@nx/jest": "16.10.0",
"@nx/linter": "16.10.0",
"@nx/workspace": "16.10.0",
"@schematics/angular": "16.2.6",
"@types/file-saver": "^2.0.5",
"@types/jest": "29.5.4",
"@types/lodash-es": "4.17.8",
"@types/node": "20.5.6",
"@typescript-eslint/eslint-plugin": "6.4.1",
"@typescript-eslint/parser": "6.4.1",
"axios": "^1.3.4",
"@types/jest": "29.5.5",
"@types/lodash-es": "4.17.9",
"@types/node": "20.8.6",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"axios": "1.5.1",
"dotenv": "16.3.1",
"eslint": "8.48.0",
"eslint": "8.51.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-prettier": "5.0.1",
"eslint-plugin-rxjs": "^5.0.2",
"google-translate-api-browser": "^4.0.6",
"husky": "^8.0.3",
"jest": "29.6.4",
"jest-environment-jsdom": "29.6.4",
"jest-extended": "4.0.1",
"jest-preset-angular": "13.1.1",
"lint-staged": "14.0.1",
"nx": "16.7.4",
"nx-cloud": "16.3.0",
"postcss": "8.4.28",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-extended": "4.0.2",
"jest-preset-angular": "13.1.2",
"lint-staged": "15.0.1",
"nx": "16.10.0",
"nx-cloud": "16.5.2",
"postcss": "8.4.31",
"postcss-import": "15.1.0",
"postcss-preset-env": "9.1.1",
"postcss-preset-env": "9.2.0",
"postcss-url": "10.1.3",
"prettier": "3.0.2",
"sonarqube-scanner": "^3.0.1",
"prettier": "3.0.3",
"sonarqube-scanner": "3.1.0",
"superagent": "8.1.2",
"superagent-promise": "^1.1.0",
"ts-node": "10.9.1",
"typescript": "5.1.6",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "^4.8.0",
"webpack": "5.89.0",
"webpack-bundle-analyzer": "4.9.1",
"xliff": "^6.1.0"
}
}

4329
yarn.lock

File diff suppressed because it is too large Load Diff