Merge branch 'dan' into 'master'

RED-3800 ng update

See merge request redactmanager/red-ui!213
This commit is contained in:
Dan Percic 2023-12-01 09:32:33 +01:00
commit 0a32b8f5bd
15 changed files with 2311 additions and 2260 deletions

View File

@ -122,11 +122,11 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "red-ui:build"
"buildTarget": "red-ui:build"
},
"configurations": {
"production": {
"browserTarget": "red-ui:build:production"
"buildTarget": "red-ui:build:production"
}
}
}

View File

@ -0,0 +1,5 @@
export function getRouteTenant() {
const pathParams = location.pathname.split('/').filter(Boolean);
const uiPathIndex = pathParams.indexOf('ui');
return pathParams[uiPathIndex + 1];
}

View File

@ -1,12 +1,13 @@
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { inject } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { keycloakInitializer, KeycloakStatusService, TenantsService } from '@iqser/common-ui/lib/tenants';
import { KeycloakService } from 'keycloak-angular';
import { UserService } from '@users/user.service';
import { LicenseService } from '@services/license.service';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { getRouteTenant } from '@guards/guards-utils';
import { AsyncGuard } from '@iqser/common-ui';
import jwt_decode from 'jwt-decode';
import { keycloakInitializer, KeycloakStatusService, TenantsService } from '@iqser/common-ui/lib/tenants';
import { LicenseService } from '@services/license.service';
import { UserService } from '@users/user.service';
import { jwtDecode } from 'jwt-decode';
import { KeycloakService } from 'keycloak-angular';
import { NGXLogger } from 'ngx-logger';
export interface JwtToken {
auth_time: number;
@ -25,9 +26,7 @@ export function ifLoggedIn(): AsyncGuard {
const keycloakStatusService = inject(KeycloakStatusService);
const keycloakInstance = keycloakService.getKeycloakInstance();
const pathParams = location.pathname.split('/').filter(Boolean);
const uiPathIndex = pathParams.indexOf('ui');
const tenant = pathParams[uiPathIndex + 1];
const tenant = getRouteTenant();
const queryParams = new URLSearchParams(window.location.search);
const username = queryParams.get('username');
const router = inject(Router);
@ -48,15 +47,13 @@ export function ifLoggedIn(): AsyncGuard {
const token = await keycloakService.getToken();
if (token) {
const jwtToken = jwt_decode(token) as JwtToken;
const jwtToken = jwtDecode(token) as JwtToken;
const authTime = (jwtToken.auth_time || jwtToken.iat).toString();
localStorage.setItem('authTime', authTime);
}
}
const isLoggedIn = await keycloakService.isLoggedIn();
if (isLoggedIn) {
if (keycloakService.isLoggedIn()) {
logger.info('[ROUTES] Is logged in, continuing');
return true;
}

View File

@ -1,17 +1,24 @@
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { CanActivateFn, Router } from '@angular/router';
import { getRouteTenant } from '@guards/guards-utils';
import { KeycloakService } from 'keycloak-angular';
import { NGXLogger } from 'ngx-logger';
export function ifNotLoggedIn(): CanActivateFn {
return async () => {
const logger = inject(NGXLogger);
const router = inject(Router);
const keycloakService = inject(KeycloakService);
if (!keycloakService.getKeycloakInstance()) {
const tenant = getRouteTenant();
if (tenant) {
logger.warn('[ROUTES] Tenant ' + tenant + ' found in route, redirecting to /main');
await router.navigate(['main']);
return false;
}
}
const isLoggedIn = await keycloakService.isLoggedIn();
if (!isLoggedIn) {
if (!keycloakService.isLoggedIn()) {
logger.info('[ROUTES] Not logged in, continuing to selected route');
return true;
}

View File

@ -34,11 +34,15 @@ export const canChangeLegalBasis = (annotation: AnnotationWrapper, canAddRedacti
export const canRecategorizeAnnotation = (annotation: AnnotationWrapper, canRecategorize: boolean) =>
canRecategorize && (annotation.isImage || annotation.isDictBasedHint) && !annotation.pending;
export const canResizeAnnotation = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
export const canResizeAnnotation = (annotation: AnnotationWrapper, canAddRedaction: boolean, hasDictionary = false) =>
canAddRedaction &&
!annotation.isSkipped &&
!annotation.pending &&
(annotation.isRedacted || annotation.isImage || annotation.isDictBasedHint || annotation.isRecommendation);
(annotation.isRedacted ||
annotation.isImage ||
annotation.isDictBasedHint ||
annotation.isRecommendation ||
(hasDictionary && annotation.isRuleBased));
export const canEditAnnotation = (annotation: AnnotationWrapper) => (annotation.isRedacted || annotation.isSkipped) && !annotation.isImage;

View File

@ -62,7 +62,7 @@ export class AnnotationPermissions {
permissions.canRemoveRedaction = canRemoveRedaction(annotation, permissions);
permissions.canChangeLegalBasis = canChangeLegalBasis(annotation, canAddRedaction);
permissions.canRecategorizeAnnotation = canRecategorizeAnnotation(annotation, canAddRedaction);
permissions.canResizeAnnotation = canResizeAnnotation(annotation, canAddRedaction);
permissions.canResizeAnnotation = canResizeAnnotation(annotation, canAddRedaction, annotationEntity.hasDictionary);
permissions.canEditAnnotations = canEditAnnotation(annotation);
permissions.canEditHints = canEditHint(annotation);
permissions.canEditImages = canEditImage(annotation);

View File

@ -61,6 +61,10 @@ export class AnnotationWrapper implements IListable {
hasBeenRemovedByManualOverride: boolean;
isRemoved = false;
get isRuleBased() {
return this.engines.includes(LogEntryEngines.RULE);
}
get searchKey(): string {
return this.id;
}

View File

@ -1,13 +1,13 @@
import { ChangeDetectionStrategy, Component, HostListener, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { getConfig, IconButtonTypes } from '@iqser/common-ui';
import { IqserEventTarget } from '@iqser/common-ui/lib/utils';
import { Dictionary, DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { ActivatedRoute } from '@angular/router';
import { getCurrentUser } from '@users/user.service';
import { PermissionsService } from '@services/permissions.service';
import { AddEditEntityComponent } from '@shared/components/add-edit-entity/add-edit-entity.component';
import { getConfig, IconButtonTypes } from '@iqser/common-ui';
import { getCurrentUser } from '@users/user.service';
import { Observable } from 'rxjs';
import { IqserEventTarget } from '@iqser/common-ui/lib/utils';
@Component({
selector: 'redaction-entity-info',
@ -23,7 +23,11 @@ export class EntityInfoComponent {
readonly config = getConfig();
readonly iconButtonTypes = IconButtonTypes;
constructor(route: ActivatedRoute, dictionariesMapService: DictionariesMapService, readonly permissionsService: PermissionsService) {
constructor(
route: ActivatedRoute,
dictionariesMapService: DictionariesMapService,
readonly permissionsService: PermissionsService,
) {
this.dossierTemplateId = route.parent.snapshot.paramMap.get(DOSSIER_TEMPLATE_ID);
const entityType = route.parent.snapshot.paramMap.get(ENTITY_TYPE);
this.entity$ = dictionariesMapService.watch$(this.dossierTemplateId, entityType);

View File

@ -43,6 +43,15 @@ import Quad = Core.Math.Quad;
@Injectable()
export class PdfProxyService {
readonly #convertPath = inject(UI_ROOT_PATH_FN);
readonly #visibilityOffIcon = this.#convertPath('/assets/icons/general/visibility-off.svg');
readonly #visibilityIcon = this.#convertPath('/assets/icons/general/visibility.svg');
readonly #falsePositiveIcon = this.#convertPath('/assets/icons/general/pdftron-action-false-positive.svg');
readonly #addRedactionIcon = this._iqserPermissionsService.has(Roles.getRss)
? this.#convertPath('/assets/icons/general/pdftron-action-add-annotation.svg')
: this.#convertPath('/assets/icons/general/pdftron-action-add-redaction.svg');
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly #addHintIcon = this.#convertPath('/assets/icons/general/pdftron-action-add-hint.svg');
readonly annotationSelected$ = this.#annotationSelected$;
readonly manualAnnotationRequested$ = new Subject<ManualRedactionEntryWrapper>();
readonly redactTextRequested$ = new Subject<ManualRedactionEntryWrapper>();
@ -61,15 +70,6 @@ export class PdfProxyService {
const isAllowed = this._permissionsService.canPerformAnnotationActions(this._state.file(), this._state.dossier());
return isAllowed && isStandard;
});
readonly #convertPath = inject(UI_ROOT_PATH_FN);
readonly #visibilityOffIcon = this.#convertPath('/assets/icons/general/visibility-off.svg');
readonly #visibilityIcon = this.#convertPath('/assets/icons/general/visibility.svg');
readonly #falsePositiveIcon = this.#convertPath('/assets/icons/general/pdftron-action-false-positive.svg');
readonly #addRedactionIcon = this._iqserPermissionsService.has(Roles.getRss)
? this.#convertPath('/assets/icons/general/pdftron-action-add-annotation.svg')
: this.#convertPath('/assets/icons/general/pdftron-action-add-redaction.svg');
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly #addHintIcon = this.#convertPath('/assets/icons/general/pdftron-action-add-hint.svg');
constructor(
private readonly _translateService: TranslateService,
@ -351,6 +351,7 @@ export class PdfProxyService {
#configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) {
if (!this.canPerformActions()) {
this._pdf.instance.UI.annotationPopup.update([]);
return;
}

View File

@ -6,7 +6,7 @@ import { Dictionary, IDictionary } from '@red/domain';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { toSnakeCase } from '@utils/functions';
import { firstValueFrom, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
const REDACTION_FIELDS = ['defaultReason'];
@ -20,7 +20,7 @@ interface Color {
}
@Component({
selector: 'redaction-add-edit-entity [entity] [dossierTemplateId]',
selector: 'redaction-add-edit-entity',
templateUrl: './add-edit-entity.component.html',
styleUrls: ['./add-edit-entity.component.scss'],
})
@ -82,16 +82,13 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
async save(): Promise<void> {
this._loadingService.start();
const dictionary = this.#formToObject();
const isEditMode = !!this.entity;
try {
if (this.entity) {
// edit mode
await firstValueFrom(
this._dictionaryService.updateDictionary(dictionary, this.dossierTemplateId, dictionary.type, this.dossierId),
);
if (isEditMode) {
await this._dictionaryService.update(dictionary, this.dossierTemplateId, dictionary.type, this.dossierId);
this._toaster.success(_('add-edit-entity.success.edit'));
} else {
// create mode
await firstValueFrom(this._dictionaryService.addDictionary({ ...dictionary, dossierTemplateId: this.dossierTemplateId }));
await this._dictionaryService.add({ ...dictionary, dossierTemplateId: this.dossierTemplateId });
this._toaster.success(_('add-edit-entity.success.create'));
}
this.initialFormValue = this.form.getRawValue();
@ -149,7 +146,7 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
caseSensitive: [{ value: !this.entity?.caseInsensitive, disabled: this.#isSystemManaged }],
manageEntriesInDictionaryEditorOnly: [
{
value: !this.entity?.addToDictionaryAction,
value: this.entity?.addToDictionaryAction,
disabled: this.#isSystemManaged && !this.#isDossierRedaction,
},
],
@ -216,7 +213,7 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
#formToObject(): IDictionary {
// Fields which aren't set for hints, need additional check
const addToDictionaryAction = !this.form.get('manageEntriesInDictionaryEditorOnly')?.value;
const addToDictionaryAction = !!this.form.get('manageEntriesInDictionaryEditorOnly')?.value;
const hasDictionary = !!this.form.get('hasDictionary')?.value;
const dossierDictionaryOnly = !!this.form.get('dossierDictionaryOnly')?.value;

View File

@ -39,10 +39,6 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
return firstValueFrom(this._getOne([type, dossierTemplateId], this._defaultModelPath, queryParams));
}
/**
* Deletes entry types
*/
deleteDictionaries(dictionaryIds: List, dossierTemplateId: string, dossierId?: string): Observable<unknown> {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
const url = `${this._defaultModelPath}/type/${dossierTemplateId}/delete`;
@ -52,10 +48,6 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
);
}
/**
* Retrieve all entry types
*/
getAllDictionaries(dossierTemplateId: string, dossierId?: string) {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
return this._getOne<{ types: IDictionary[] }>(['type', dossierTemplateId], this._defaultModelPath, queryParams);
@ -65,27 +57,27 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
* Updates colors, hint and caseInsensitive of an entry type.
*/
updateDictionary(body: IUpdateDictionary, dossierTemplateId: string, type: string, dossierId?: string): Observable<unknown> {
async update(body: IUpdateDictionary, dossierTemplateId: string, type: string, dossierId?: string): Promise<unknown> {
const url = `${this._defaultModelPath}/type/${type}/${dossierTemplateId}`;
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
return this._post(body, url, queryParams).pipe(
const request = this._post(body, url, queryParams).pipe(
catchError((error: unknown) => this.#addUpdateDictionaryErrorToast(error)),
switchMap(() => this.loadDictionaryDataForDossierTemplate(dossierTemplateId)),
switchMap(() => this._dossierTemplateStatsService.getFor([dossierTemplateId])),
);
return await firstValueFrom(request);
}
/**
* Creates entry type with colors, hint and caseInsensitive
*/
addDictionary(dictionary: IDictionary, dossierId?: string): Observable<unknown> {
async add(dictionary: IDictionary, dossierId?: string): Promise<unknown> {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
return this._post(dictionary, `${this._defaultModelPath}/type`, queryParams).pipe(
const request = this._post(dictionary, `${this._defaultModelPath}/type`, queryParams).pipe(
catchError((error: unknown) => this.#addUpdateDictionaryErrorToast(error)),
switchMap(() => this.loadDictionaryDataForDossierTemplate(dictionary.dossierTemplateId)),
switchMap(() => this._dossierTemplateStatsService.getFor([dictionary.dossierTemplateId])),
);
return await firstValueFrom(request);
}
async saveEntries(
@ -118,10 +110,10 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
try {
if (deletedEntries.length) {
await this._deleteEntries(deletedEntries, dossierTemplateId, type, dictionaryEntryType, dossierId);
await this.#deleteEntries(deletedEntries, dossierTemplateId, type, dictionaryEntryType, dossierId);
}
if (entriesToAdd.length) {
await this._addEntries(entriesToAdd, dossierTemplateId, type, dictionaryEntryType, dossierId);
await this.#addEntries(entriesToAdd, dossierTemplateId, type, dictionaryEntryType, dossierId);
}
if (showToast) {
@ -284,13 +276,7 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
* Add dictionary entries with entry type.
*/
private _addEntries(
entries: List,
dossierTemplateId: string,
type: string,
dictionaryEntryType: DictionaryEntryType,
dossierId: string,
) {
#addEntries(entries: List, dossierTemplateId: string, type: string, dictionaryEntryType: DictionaryEntryType, dossierId: string) {
const queryParams: List<QueryParam> = [
{ key: 'dossierId', value: dossierId },
{ key: 'dictionaryEntryType', value: dictionaryEntryType },
@ -303,13 +289,7 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
* Delete dictionary entries with entry type.
*/
private _deleteEntries(
entries: List,
dossierTemplateId: string,
type: string,
dictionaryEntryType: DictionaryEntryType,
dossierId: string,
) {
#deleteEntries(entries: List, dossierTemplateId: string, type: string, dictionaryEntryType: DictionaryEntryType, dossierId: string) {
const queryParams = [
{ key: 'dossierId', value: dossierId },
{ key: 'dictionaryEntryType', value: dictionaryEntryType },

View File

@ -2,7 +2,7 @@ import { effect, Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { JwtToken } from '@guards/if-logged-in.guard';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
import jwt_decode from 'jwt-decode';
import { jwtDecode } from 'jwt-decode';
import { KeycloakService } from 'keycloak-angular';
import { filter } from 'rxjs/operators';
@ -37,7 +37,7 @@ export class RouterHistoryService {
return;
}
const jwtToken = jwt_decode(token) as JwtToken;
const jwtToken = jwtDecode(token) as JwtToken;
const authTime = (jwtToken.auth_time || jwtToken.iat).toString();
const localStorageAuthTime = localStorage.getItem('authTime');

View File

@ -1,7 +1,7 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dan.iqser.cloud",
"API_URL": "https://dan1.iqser.cloud",
"APP_NAME": "RedactManager",
"IS_DOCUMINE": false,
"RULE_EDITOR_DEV_ONLY": false,
@ -13,7 +13,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dan.iqser.cloud/auth",
"OAUTH_URL": "https://dan1.iqser.cloud/auth",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",

View File

@ -19,19 +19,19 @@
"*.{ts,js,html}": "eslint --fix"
},
"dependencies": {
"@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",
"@angular/animations": "17.0.5",
"@angular/cdk": "17.0.1",
"@angular/common": "17.0.5",
"@angular/compiler": "17.0.5",
"@angular/core": "17.0.5",
"@angular/forms": "17.0.5",
"@angular/material": "17.0.1",
"@angular/platform-browser": "17.0.5",
"@angular/platform-browser-dynamic": "17.0.5",
"@angular/router": "17.0.5",
"@angular/service-worker": "17.0.5",
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@messageformat/core": "^3.1.0",
"@messageformat/core": "^3.3.0",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"@pdftron/webviewer": "10.5.0",
@ -39,64 +39,64 @@
"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.4",
"jwt-decode": "^4.0.0",
"keycloak-angular": "15.0.0",
"keycloak-js": "23.0.1",
"lodash-es": "^4.17.21",
"monaco-editor": "0.43.0",
"monaco-editor": "0.44.0",
"ng2-charts": "5.0.3",
"ngx-color-picker": "15.0.0",
"ngx-color-picker": "16.0.0",
"ngx-logger": "^5.0.11",
"ngx-toastr": "17.0.2",
"ngx-toastr": "18.0.0",
"ngx-translate-messageformat-compiler": "6.5.0",
"object-hash": "^3.0.0",
"papaparse": "^5.4.0",
"rxjs": "7.8.1",
"sass": "1.69.3",
"sass": "1.69.5",
"scroll-into-view-if-needed": "3.1.0",
"streamsaver": "^2.0.5",
"tslib": "2.6.2",
"zone.js": "0.14.0"
"zone.js": "0.14.2"
},
"devDependencies": {
"@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",
"@angular-devkit/build-angular": "17.0.4",
"@angular-devkit/core": "17.0.4",
"@angular-devkit/schematics": "17.0.4",
"@angular-eslint/builder": "17.1.0",
"@angular-eslint/eslint-plugin": "17.1.0",
"@angular-eslint/eslint-plugin-template": "17.1.0",
"@angular-eslint/schematics": "17.1.0",
"@angular-eslint/template-parser": "17.1.0",
"@angular/cli": "17.0.4",
"@angular/compiler-cli": "17.0.5",
"@angular/language-service": "17.0.5",
"@bartholomej/ngx-translate-extract": "^8.0.2",
"@localazy/ts-api": "^1.0.0",
"@schematics/angular": "16.2.6",
"@types/file-saver": "^2.0.5",
"@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",
"eslint": "8.51.0",
"@schematics/angular": "17.0.4",
"@types/file-saver": "^2.0.7",
"@types/jest": "29.5.10",
"@types/lodash-es": "4.17.12",
"@types/node": "20.10.0",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"axios": "1.6.2",
"eslint": "^8.53.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.1",
"eslint-plugin-rxjs": "^5.0.2",
"google-translate-api-browser": "^4.0.6",
"google-translate-api-browser": "^4.1.9",
"husky": "^8.0.3",
"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",
"prettier": "3.0.3",
"sonarqube-scanner": "3.1.0",
"jest-preset-angular": "13.1.4",
"lint-staged": "15.1.0",
"prettier": "3.1.0",
"sonarqube-scanner": "3.3.0",
"ts-node": "10.9.1",
"typescript": "5.1.6",
"typescript": "5.2.2",
"webpack": "5.89.0",
"webpack-bundle-analyzer": "4.9.1",
"webpack-bundle-analyzer": "4.10.1",
"xliff": "^6.1.0"
}
}

4308
yarn.lock

File diff suppressed because it is too large Load Diff