Compare commits

..

5 Commits

Author SHA1 Message Date
Valentin Mihai
f6242136b7 RED-10556 - changed 'label' with 'csvColumnHeader' 2024-12-05 15:07:22 +02:00
Valentin Mihai
84cdb2f567 RED-10557 - updated logic for file attributes mapping 2024-12-05 15:05:00 +02:00
Valentin Mihai
8505d500d3 Merge branch 'master' into VM/RED-10557 2024-12-05 12:56:51 +02:00
Valentin Mihai
402be3dadf Merge branch 'master' into VM/RED-10557 2024-12-05 12:42:04 +02:00
Valentin Mihai
4b5f3ab969 WIP on master
RED-10557 - included file attributes in CSV export + setting
2024-11-28 11:26:04 +02:00
118 changed files with 1507 additions and 2159 deletions

View File

@ -4,60 +4,41 @@ variables:
PROJECT: red-ui
DOCKERFILELOCATION: 'docker/$PROJECT/Dockerfile'
include:
- project: 'gitlab/gitlab'
ref: 'main'
file: 'ci-templates/docker_build_nexus_v2.yml'
rules:
- if: $CI_PIPELINE_SOURCE != "schedule"
sonarqube:
stage: test
image:
name: sonarsource/sonar-scanner-cli:11.1
entrypoint:
- ''
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: '0'
cache:
key: "${CI_JOB_NAME}"
paths:
- ".sonar/cache"
script:
- sonar-scanner
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
- if: "$CI_COMMIT_BRANCH =~ /^release/"
- if: $CI_PIPELINE_SOURCE != "schedule"
localazy update:
image: node:20.5
cache:
- key:
files:
- yarn.lock
paths:
- .yarn-cache/
script:
# - git config user.email "${CI_EMAIL}"
# - git config user.name "${CI_USERNAME}"
# - git remote add gitlab_origin https://${CI_USERNAME}:${CI_ACCESS_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
- git push https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
- cd tools/localazy
- yarn install --cache-folder .yarn-cache
- yarn start
- cd ../..
- git add .
- |-
CHANGES=$(git status --porcelain | wc -l)
if [ "$CHANGES" -gt "0" ]
then
git status
git commit -m "push back localazy update"
git push gitlab_origin HEAD:${CI_COMMIT_REF_NAME}
# git push https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
# git push
fi
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
image: node:20.5
cache:
- key:
files:
- yarn.lock
paths:
- .yarn-cache/
script:
# - git config user.email "${CI_EMAIL}"
# - git config user.name "${CI_USERNAME}"
# - git remote add gitlab_origin https://${CI_USERNAME}:${CI_ACCESS_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
- git push https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
- cd tools/localazy
- yarn install --cache-folder .yarn-cache
- yarn start
- cd ../..
- git add .
- |-
CHANGES=$(git status --porcelain | wc -l)
if [ "$CHANGES" -gt "0" ]
then
git status
git commit -m "push back localazy update"
git push gitlab_origin HEAD:${CI_COMMIT_REF_NAME}
# git push https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
# git push
fi
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"

View File

@ -135,20 +135,20 @@ export const appModuleFactory = (config: AppConfig) => {
features: {
ANNOTATIONS: {
color: 'aqua',
enabled: false,
enabled: true,
level: NgxLoggerLevel.DEBUG,
},
FILTERS: {
enabled: false,
},
TENANTS: {
enabled: false,
enabled: true,
},
ROUTES: {
enabled: false,
enabled: true,
},
PDF: {
enabled: false,
enabled: true,
},
FILE: {
enabled: false,
@ -171,9 +171,6 @@ export const appModuleFactory = (config: AppConfig) => {
DOSSIERS_CHANGES: {
enabled: false,
},
GUARDS: {
enabled: false,
},
},
} as ILoggerConfig,
},

View File

@ -40,7 +40,8 @@ export function ifLoggedIn(): AsyncGuard {
logger.info('[KEYCLOAK] Keycloak init...');
await keycloakInitializer(tenant);
logger.info('[KEYCLOAK] Keycloak init done for tenant: ', { tenant });
logger.info('[KEYCLOAK] Keycloak init done!');
console.log({ tenant });
await tenantsService.selectTenant(tenant);
await usersService.initialize();
await licenseService.loadLicenses();
@ -50,7 +51,6 @@ export function ifLoggedIn(): AsyncGuard {
const jwtToken = jwtDecode(token) as JwtToken;
const authTime = (jwtToken.auth_time || jwtToken.iat).toString();
localStorage.setItem('authTime', authTime);
localStorage.setItem('token', token);
}
}

View File

@ -69,5 +69,3 @@ export const canEditHint = (annotation: AnnotationWrapper) =>
((annotation.isHint && !annotation.isRuleBased) || annotation.isIgnoredHint) && !annotation.isImage;
export const canEditImage = (annotation: AnnotationWrapper) => annotation.isImage;
export const canRevertChanges = (annotation: AnnotationWrapper) => annotation.hasRedactionChanges;

View File

@ -17,7 +17,6 @@ import {
canRemoveRedaction,
canResizeAnnotation,
canResizeInDictionary,
canRevertChanges,
canUndo,
} from './annotation-permissions.utils';
import { AnnotationWrapper } from './annotation.wrapper';
@ -38,7 +37,6 @@ export class AnnotationPermissions {
canEditAnnotations = true;
canEditHints = true;
canEditImages = true;
canRevertChanges = true;
static forUser(
isApprover: boolean,
@ -77,7 +75,6 @@ export class AnnotationPermissions {
permissions.canEditAnnotations = canEditAnnotation(annotation);
permissions.canEditHints = canEditHint(annotation);
permissions.canEditImages = canEditImage(annotation);
permissions.canRevertChanges = canRevertChanges(annotation);
summedPermissions._merge(permissions);
}
return summedPermissions;
@ -100,7 +97,6 @@ export class AnnotationPermissions {
result.canEditAnnotations = permissions.reduce((acc, next) => acc && next.canEditAnnotations, true);
result.canEditHints = permissions.reduce((acc, next) => acc && next.canEditHints, true);
result.canEditImages = permissions.reduce((acc, next) => acc && next.canEditImages, true);
result.canRevertChanges = permissions.reduce((acc, next) => acc && next.canRevertChanges, true);
return result;
}

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit, ViewContainerRef } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { accountTranslations } from '@translations/account-translations';
import { NgClass } from '@angular/common';
import { NgClass, NgIf } from '@angular/common';
import { AccountSideNavComponent } from '../account-side-nav/account-side-nav.component';
import { TranslateModule } from '@ngx-translate/core';
@ -10,7 +10,7 @@ import { TranslateModule } from '@ngx-translate/core';
templateUrl: './base-account-screen-component.html',
styleUrls: ['./base-account-screen-component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgClass, RouterOutlet, AccountSideNavComponent, TranslateModule],
imports: [NgClass, NgIf, RouterOutlet, AccountSideNavComponent, TranslateModule],
})
export class BaseAccountScreenComponent implements OnInit {
readonly translations = accountTranslations;

View File

@ -17,6 +17,7 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { UserService } from '@users/user.service';
import { firstValueFrom } from 'rxjs';
import { UserProfileDialogService } from '../services/user-profile-dialog.service';
import { NgForOf, NgIf } from '@angular/common';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { MatSlideToggle } from '@angular/material/slide-toggle';
@ -38,9 +39,11 @@ interface UserProfileForm {
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReactiveFormsModule,
NgIf,
MatFormField,
MatSelect,
MatOption,
NgForOf,
TranslateModule,
MatSlideToggle,
IconButtonComponent,

View File

@ -1,24 +1,22 @@
import { inject, provideEnvironmentInitializer } from '@angular/core';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { templateExistsWhenEnteringAdmin } from '@guards/dossier-template-exists.guard';
import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { entityExistsGuard } from '@guards/entity-exists-guard.service';
import { PermissionsGuard } from '@guards/permissions-guard';
import { CompositeRouteGuard, IqserPermissionsGuard, IqserRoutes } from '@iqser/common-ui';
import { IqserAuthGuard } from '@iqser/common-ui/lib/users';
import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain';
import { CopilotService } from '@services/copilot.service';
import { RedRoleGuard } from '@users/red-role.guard';
import { Roles } from '@users/roles';
import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component';
import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component';
import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component';
import { AuditScreenComponent } from './screens/audit/audit-screen.component';
import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component';
import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain';
import { templateExistsWhenEnteringAdmin } from '@guards/dossier-template-exists.guard';
import { entityExistsGuard } from '@guards/entity-exists-guard.service';
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { AuditScreenComponent } from './screens/audit/audit-screen.component';
import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component';
import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component';
import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component';
import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component';
import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component';
import { PermissionsGuard } from '@guards/permissions-guard';
import { Roles } from '@users/roles';
import { IqserAuthGuard } from '@iqser/common-ui/lib/users';
import { AdminDialogService } from './services/admin-dialog.service';
import { AuditService } from './services/audit.service';
import { DigitalSignatureService } from './services/digital-signature.service';
@ -80,12 +78,7 @@ const dossierTemplateIdRoutes: IqserRoutes = [
},
type: 'ENTITY',
},
providers: [
RulesService,
provideEnvironmentInitializer(() => {
return inject(CopilotService).connectAsync('/api/llm/llm-websocket');
}),
],
providers: [RulesService],
},
{
path: 'component-rules',

View File

@ -1,18 +1,12 @@
import { Component, OnInit, signal } from '@angular/core';
import {
BaseFormComponent,
CircleButtonComponent,
HasScrollbarDirective,
IconButtonComponent,
listingProvidersFactory,
LoadingService,
} from '@iqser/common-ui';
import { BaseFormComponent, CircleButtonComponent, IconButtonComponent, listingProvidersFactory, LoadingService } from '@iqser/common-ui';
import { ComponentDefinitionsService } from '@services/entity-services/component-definitions.service';
import { firstValueFrom } from 'rxjs';
import { getParam } from '@common-ui/utils';
import { DOSSIER_TEMPLATE_ID, IComponentDefinition } from '@red/domain';
import { toObservable } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
import { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component';
import { CommonModule, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { PermissionsService } from '@services/permissions.service';
import { MatIcon } from '@angular/material/icon';
@ -26,6 +20,8 @@ import { AdminDialogService } from '../../services/admin-dialog.service';
providers: listingProvidersFactory(ComponentDefinitionsComponent),
imports: [
IconButtonComponent,
InputWithActionComponent,
NgIf,
TranslateModule,
CommonModule,
MatIcon,
@ -35,7 +31,6 @@ import { AdminDialogService } from '../../services/admin-dialog.service';
FormsModule,
ReactiveFormsModule,
CircleButtonComponent,
HasScrollbarDirective,
],
})
export default class ComponentDefinitionsComponent extends BaseFormComponent implements OnInit {
@ -107,7 +102,6 @@ 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

@ -7,7 +7,7 @@ import { NgForOf, NgIf } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDialogModule } from '@angular/material/dialog';
import { MatOption } from '@angular/material/autocomplete';
import { MatSelect } from '@angular/material/select';
import { MatSelect, MatSelectTrigger } from '@angular/material/select';
import { fileAttributeEncodingTypesTranslations } from '@translations/file-attribute-encoding-types-translations';
import { firstValueFrom } from 'rxjs';
import { ComponentMappingsService } from '@services/entity-services/component-mappings.service';
@ -40,6 +40,7 @@ interface DialogResult {
CircleButtonComponent,
MatDialogModule,
MatOption,
MatSelectTrigger,
MatSelect,
IconButtonComponent,
UploadFileComponent,

View File

@ -1,5 +1,6 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { RouterLink } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component';
import { getCurrentUser } from '@common-ui/users';
@ -24,6 +25,9 @@ import { Roles } from '@users/roles';
import { combineLatest, firstValueFrom } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { AdminSideNavComponent } from '../../shared/components/admin-side-nav/admin-side-nav.component';
import { DossierTemplateActionsComponent } from '../../shared/components/dossier-template-actions/dossier-template-actions.component';
import { DossierTemplateBreadcrumbsComponent } from '../../shared/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { AddEditComponentMappingDialogComponent } from './add-edit-component-mapping-dialog/add-edit-component-mapping-dialog.component';
import { download } from '@utils/file-download-utils';
import { MatTooltip } from '@angular/material/tooltip';
@ -33,10 +37,14 @@ import { MatTooltip } from '@angular/material/tooltip';
styleUrls: ['./component-mappings-screen.component.scss'],
providers: listingProvidersFactory(ComponentMappingsScreenComponent),
imports: [
DossierTemplateBreadcrumbsComponent,
AsyncPipe,
NgIf,
DossierTemplateActionsComponent,
CircleButtonComponent,
TranslateModule,
RouterLink,
AdminSideNavComponent,
IqserListingModule,
InputWithActionComponent,
IconButtonComponent,

View File

@ -11,7 +11,7 @@ import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.se
import { TranslateModule } from '@ngx-translate/core';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect } from '@angular/material/select';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { NgForOf, NgIf } from '@angular/common';
export interface ConfirmDeleteDossierStateDialogData {
@ -28,6 +28,7 @@ export interface ConfirmDeleteDossierStateDialogData {
ReactiveFormsModule,
MatCheckbox,
MatFormField,
MatSelectTrigger,
MatSelect,
MatOption,
NgForOf,

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, HostListener, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { getConfig, HasScrollbarDirective, IconButtonComponent, IconButtonTypes } from '@iqser/common-ui';
import { getConfig, HasScrollbarDirective, HelpButtonComponent, IconButtonComponent, 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';
@ -17,7 +17,16 @@ import { TranslateModule } from '@ngx-translate/core';
templateUrl: './entity-info.component.html',
styleUrls: ['./entity-info.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [HasScrollbarDirective, MatIcon, NgIf, TranslateModule, AsyncPipe, IconButtonComponent, AddEditEntityComponent],
imports: [
HasScrollbarDirective,
MatIcon,
NgIf,
TranslateModule,
AsyncPipe,
IconButtonComponent,
AddEditEntityComponent,
HelpButtonComponent,
],
})
export class EntityInfoComponent {
@ViewChild(AddEditEntityComponent) private readonly _addEditEntityComponent: AddEditEntityComponent;

View File

@ -77,6 +77,14 @@
</mat-checkbox>
</div>
</div>
<div class="options-wrapper">
<div class="iqser-input-group mt-0">
<mat-checkbox color="primary" formControlName="includeInCsvExport" name="includeInCsvExport">
{{ 'add-edit-file-attribute.form.includeInCsvExport' | translate }}
</mat-checkbox>
</div>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button

View File

@ -80,6 +80,7 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
primaryAttribute: [fileAttribute?.primaryAttribute],
filterable: { value: fileAttribute?.filterable, disabled: !this.canSetFilterable },
displayedInFileList: { value: fileAttribute?.displayedInFileList, disabled: !this.canSetDisplayed },
includeInCsvExport: [fileAttribute?.includeInCsvExport],
});
}
}

View File

@ -57,7 +57,6 @@ export class FileAttributesConfigurationsDialogComponent extends BaseDialogCompo
if (supportCsvMapping) {
return {
...this.#configuration,
keyColumn: this.form.get('keyColumn').value,
filenameMappingColumnHeaderName: this.form.get('keyColumn').value,
delimiter: this.form.get('delimiter').value,
encoding: this.form.get('encodingType').value,
@ -67,14 +66,13 @@ export class FileAttributesConfigurationsDialogComponent extends BaseDialogCompo
return {
...this.#configuration,
filenameMappingColumnHeaderName: '',
keyColumn: this.form.get('keyColumn').value,
};
}
#getForm() {
return this._formBuilder.group({
supportCsvMapping: [!!this.#configuration.filenameMappingColumnHeaderName],
keyColumn: [this.#configuration.filenameMappingColumnHeaderName || this.#configuration.keyColumn || '', [Validators.required]],
keyColumn: [this.#configuration.filenameMappingColumnHeaderName || '', [Validators.required]],
delimiter: [this.#configuration.delimiter || '', [Validators.required]],
encodingType: [this.#configuration.encoding || FileAttributeEncodingTypes['UTF-8'], [Validators.required]],
});

View File

@ -2,8 +2,8 @@ import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatFormField } from '@angular/material/form-field';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { MatOption, MatSelect } from '@angular/material/select';
import { MatMenu, MatMenuTrigger } from '@angular/material/menu';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ChevronButtonComponent } from '@common-ui/buttons/chevron-button';
@ -29,6 +29,7 @@ import { fileAttributeTypesTranslations } from '@translations/file-attribute-typ
MatMenu,
EditableInputComponent,
MatFormField,
MatSelectTrigger,
MatSelect,
MatOption,
FormsModule,
@ -36,7 +37,6 @@ import { fileAttributeTypesTranslations } from '@translations/file-attribute-typ
RoundCheckboxComponent,
NgForOf,
NgIf,
MatMenuItem,
],
})
export class ActiveFieldsListingComponent extends ListingComponent<IField> implements OnChanges {

View File

@ -4,7 +4,7 @@ import { AbstractControl, ReactiveFormsModule, UntypedFormBuilder, UntypedFormGr
import { MatAutocomplete, MatAutocompleteTrigger, MatOption } from '@angular/material/autocomplete';
import { MAT_DIALOG_DATA, MatDialogClose, MatDialogRef } from '@angular/material/dialog';
import { MatFormField } from '@angular/material/form-field';
import { MatSelect } from '@angular/material/select';
import { MatSelect, MatSelectTrigger } from '@angular/material/select';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component';
import {
@ -45,6 +45,7 @@ export interface IFileAttributesCSVImportData {
MatAutocomplete,
AsyncPipe,
MatOption,
MatSelectTrigger,
MatSelect,
CircleButtonComponent,
NgIf,

View File

@ -104,6 +104,10 @@
<iqser-round-checkbox *ngIf="attribute.primaryAttribute" [size]="18" active></iqser-round-checkbox>
</div>
<div class="center cell">
<iqser-round-checkbox *ngIf="attribute.includeInCsvExport" [size]="18" active></iqser-round-checkbox>
</div>
<div class="cell">
<div *allow="roles.fileAttributes.writeConfig; if: currentUser.isAdmin" class="action-buttons">
<div [attr.help-mode-key]="'edit_delete_file_attribute'">

View File

@ -83,9 +83,9 @@ export default class FileAttributesListingScreenComponent extends ListingCompone
rightIcon: 'red:status-info',
rightIconTooltip: _('file-attributes-listing.table-col-names.primary-info-tooltip'),
},
{ label: _('file-attributes-listing.table-col-names.include-in-csv-export'), class: 'flex-center' },
];
readonly roles = Roles;
keyColumnValue: string = '';
constructor(
readonly permissionsService: PermissionsService,
@ -172,13 +172,13 @@ export default class FileAttributesListingScreenComponent extends ListingCompone
FileAttributesConfigurationsDialogComponent,
{
...defaultDialogConfig,
data: { ...this.#existingConfiguration, keyColumn: this.keyColumnValue },
data: this.#existingConfiguration,
},
);
const configuration = await firstValueFrom(ref.afterClosed());
if (configuration) {
this.keyColumnValue = configuration.keyColumn;
await this.#setConfigAndLoadData(configuration);
}
}

View File

@ -22,6 +22,7 @@ import { Observable } from 'rxjs';
import { DossierTemplateDetailsComponent } from '../dossier-template-details/dossier-template-details.component';
import { TranslateModule } from '@ngx-translate/core';
import { MatCheckbox } from '@angular/material/checkbox';
import { NgIf } from '@angular/common';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatIcon } from '@angular/material/icon';
import { SelectComponent } from '@shared/components/select/select.component';
@ -41,6 +42,7 @@ const downloadTypes = ['ORIGINAL', 'PREVIEW', 'OPTIMIZED_PREVIEW', 'DELTA_PREVIE
DossierTemplateDetailsComponent,
TranslateModule,
MatCheckbox,
NgIf,
MatDatepickerModule,
SelectComponent,
IconButtonComponent,

View File

@ -1,57 +1,8 @@
<div class="header-container">
<div [translate]="translations[type()]['title']" class="heading-l"></div>
<div [translate]="translations[this.type]['title']" class="heading-l"></div>
</div>
<div class="flex" style="height: 100%">
<ngx-monaco-editor (init)="onCodeEditorInit($event)" [(ngModel)]="codeEditorText" [options]="editorOptions"></ngx-monaco-editor>
<div [class.collapsed]="collapsed()" class="right-container flex-column">
<div class="collapsed-wrapper">
<ng-container
*ngTemplateOutlet="collapsible; context: { action: 'expand', tooltip: ('copilot.label' | translate) }"
></ng-container>
<div class="all-caps-label" translate="copilot.label"></div>
</div>
<div class="header-wrapper flex mt-8">
<div class="heading-xl flex-1">{{ 'copilot.label' | translate | titlecase }}</div>
<ng-container
*ngTemplateOutlet="collapsible; context: { action: 'collapse', tooltip: ('copilot.label' | translate) }"
></ng-container>
</div>
<div class="mt-24">
@for (comment of conversation(); track comment) {
<div class="comment">
<div class="comment-details-wrapper">
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
{{ comment.date | date: 'sophisticatedDate' }}
</div>
</div>
<pre class="text-auto">{{ comment.text }}</pre>
</div>
}
<iqser-input-with-action
(action)="add($event)"
[placeholder]="'comments.add-comment' | translate"
autocomplete="off"
icon="iqser:collapse"
width="full"
></iqser-input-with-action>
</div>
</div>
</div>
<ng-template #collapsible let-action="action" let-tooltip="tooltip">
<iqser-circle-button
(action)="toggleCollapse()"
[icon]="'iqser:' + action"
[tooltipPosition]="IqserTooltipPositions.before"
[tooltip]="tooltip"
></iqser-circle-button>
</ng-template>
<ngx-monaco-editor (init)="onCodeEditorInit($event)" [(ngModel)]="codeEditorText" [options]="editorOptions"></ngx-monaco-editor>
<div *ngIf="changed && permissionsService.canEditRules() && !isLeaving" class="changes-box">
<div (click)="goToErrors()" *ngIf="numberOfErrors() || numberOfWarnings()" class="errors">
@ -61,15 +12,15 @@
<span
*ngIf="numberOfErrors()"
[translateParams]="{ errors: numberOfErrors() }"
[translate]="translations[type()]['errors-found']"
[translate]="translations[this.type]['errors-found']"
>
</span>
<span
*ngIf="numberOfWarnings()"
[class.only-warning]="!numberOfErrors()"
[translateParams]="{ warnings: numberOfWarnings() }"
[translate]="translations[type()]['warnings-found']"
[translate]="translations[this.type]['warnings-found']"
class="warning"
[class.only-warning]="!numberOfErrors()"
></span>
</div>
</span>
@ -78,11 +29,11 @@
<div class="actions">
<iqser-icon-button
(action)="save()"
[label]="translations[type()]['save-changes'] | translate"
[label]="translations[this.type]['save-changes'] | translate"
[type]="iconButtonTypes.primary"
icon="iqser:check"
></iqser-icon-button>
<div (click)="revert()" [translate]="translations[type()]['revert-changes']" class="all-caps-label cancel"></div>
<div (click)="revert()" [translate]="translations[this.type]['revert-changes']" class="all-caps-label cancel"></div>
</div>
</div>

View File

@ -81,22 +81,3 @@ ngx-monaco-editor {
gap: 24px;
}
}
.right-container {
display: flex;
width: 750px;
min-width: 375px;
padding: 16px 24px 16px 24px;
&.has-scrollbar:hover {
padding-right: 13px;
}
redaction-dossier-details {
width: 100%;
}
}
.text-auto {
text-wrap: auto;
}

View File

@ -1,40 +1,22 @@
import { NgIf, NgTemplateOutlet, TitleCasePipe } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
DestroyRef,
inject,
input,
OnInit,
signal,
viewChild,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component';
import { TenantsService } from '@common-ui/tenants';
import { getCurrentUser } from '@common-ui/users';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { Debounce, IqserTooltipPositions } from '@iqser/common-ui/lib/utils';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { TranslateModule } from '@ngx-translate/core';
import { DroolsKeywords, IRules } from '@red/domain';
import { CopilotService } from '@services/copilot.service';
import { EditorThemeService } from '@services/editor-theme.service';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, OnInit, signal } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { DatePipe } from '@shared/pipes/date.pipe';
import { firstValueFrom } from 'rxjs';
import { map } from 'rxjs/operators';
import { IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { RulesService } from '../../../services/rules.service';
import { firstValueFrom } from 'rxjs';
import { DOSSIER_TEMPLATE_ID, DroolsKeywords, IRules } from '@red/domain';
import { EditorThemeService } from '@services/editor-theme.service';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
import { Debounce, getParam } from '@iqser/common-ui/lib/utils';
import { ActivatedRoute } from '@angular/router';
import { rulesScreenTranslations } from '../../../translations/rules-screen-translations';
import ICodeEditor = monaco.editor.ICodeEditor;
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { MatIcon } from '@angular/material/icon';
import { FormsModule } from '@angular/forms';
import { NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
interface SyntaxError {
line: number;
@ -49,57 +31,18 @@ interface UploadResponse {
deprecatedWarnings: SyntaxError[];
}
export const SentenceTypes = {
question: 'question',
answer: 'answer',
} as const;
export type SentenceType = keyof typeof SentenceTypes;
interface Sentence {
text: string | null;
date: string;
type: SentenceType;
}
const endingSentence: Sentence = { text: null, date: new Date().toISOString(), type: SentenceTypes.answer };
const RULE_VALIDATION_TIMEOUT = 2000;
@Component({
templateUrl: './rules-screen.component.html',
styleUrls: ['./rules-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MonacoEditorModule,
MatIcon,
FormsModule,
IconButtonComponent,
NgIf,
TranslateModule,
CircleButtonComponent,
InputWithActionComponent,
MatTooltip,
NgTemplateOutlet,
DatePipe,
TitleCasePipe,
],
imports: [MonacoEditorModule, MatIcon, FormsModule, IconButtonComponent, NgIf, TranslateModule],
})
export default class RulesScreenComponent implements OnInit, ComponentCanDeactivate {
readonly #errorGlyphs = signal<string[]>([]);
#codeEditor: ICodeEditor;
#decorations: string[] = [];
readonly #errors = signal<SyntaxError[]>([]);
#ruleValidationTimeout: number = null;
readonly #copilotService = inject(CopilotService);
readonly #currentUser = getCurrentUser();
readonly #conversation = signal<Sentence[]>([endingSentence]);
protected readonly collapsed = signal(true);
protected readonly IqserTooltipPositions = IqserTooltipPositions;
readonly dossierTemplateId = input.required<string>();
readonly translations = rulesScreenTranslations;
readonly iconButtonTypes = IconButtonTypes;
readonly inputWithAction = viewChild(InputWithActionComponent);
readonly editorOptions: IStandaloneEditorConstructionOptions = {
theme: 'vs',
language: 'java',
@ -111,43 +54,19 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
initialLines: string[] = [];
currentLines: string[] = [];
isLeaving = false;
readonly type = input.required<IRules['ruleFileType']>();
readonly type: IRules['ruleFileType'];
readonly #errorGlyphs = signal<string[]>([]);
readonly numberOfErrors = computed(() => this.#errors().filter(e => !e.warning).length);
readonly numberOfWarnings = computed(() => this.#errors().filter(e => e.warning).length);
readonly conversation = computed(() => this.#conversation().filter(r => !!r.text));
constructor(
readonly permissionsService: PermissionsService,
private readonly _rulesService: RulesService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _toaster: Toaster,
private readonly _loadingService: LoadingService,
private readonly _editorThemeService: EditorThemeService,
) {
const username = this.#currentUser.id;
const tenant = inject(TenantsService).activeTenantId;
inject(DestroyRef).onDestroy(() => this.#copilotService.deactivate());
this.#copilotService
.listen<{ token?: string }>('/user/' + username + '/queue/' + tenant + '/rules-copilot')
.pipe(
takeUntilDestroyed(),
map(res => res?.token),
)
.subscribe(token => {
if (token === null) {
this.#conversation.update(responses => [...responses, { ...endingSentence }]);
return;
}
this.#conversation.update(responses => {
const last = responses.pop();
return [...responses, { ...last, text: (last.text ?? '') + token }];
});
});
}
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
#codeEditor: ICodeEditor;
#decorations: string[] = [];
#errors = signal<SyntaxError[]>([]);
#ruleValidationTimeout: number = null;
set isLeavingPage(isLeaving: boolean) {
this.isLeaving = isLeaving;
this._changeDetectorRef.markForCheck();
this._changeDetectorRef.detectChanges();
}
get changed(): boolean {
@ -165,23 +84,16 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
this.#closeProblemsView();
}
toggleCollapse() {
this.collapsed.update(collapsed => !collapsed);
if (this.#conversation().length === 1) {
this.#copilotService.send('Hello!');
}
}
add(question: string) {
this.#conversation.update(responses => {
const last = responses.pop();
last.text = question;
last.type = SentenceTypes.question;
last.date = new Date().toISOString();
return [...responses, last, { ...endingSentence }];
});
this.inputWithAction().reset();
this.#copilotService.send(question);
constructor(
readonly permissionsService: PermissionsService,
private readonly _rulesService: RulesService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _toaster: Toaster,
private readonly _loadingService: LoadingService,
private readonly _editorThemeService: EditorThemeService,
private readonly _route: ActivatedRoute,
) {
this.type = this._route.snapshot.data.type;
}
async ngOnInit() {
@ -209,7 +121,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
}
(window as any).monaco.editor.setTheme(this._editorThemeService.getTheme(true));
await this.#configureSyntaxHighlighting();
this._changeDetectorRef.markForCheck();
this._changeDetectorRef.detectChanges();
}
@Debounce()
@ -229,20 +141,12 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
await this.#uploadRules();
}
revert(): void {
this.currentLines = this.initialLines;
this.#decorations = this.#codeEditor?.deltaDecorations(this.#decorations, []) || [];
this.#removeErrorMarkers();
this._changeDetectorRef.markForCheck();
this._loadingService.stop();
}
async #uploadRules(dryRun = false) {
return firstValueFrom(
this._rulesService.uploadRules({
rules: this.#getValue(),
dossierTemplateId: this.dossierTemplateId(),
ruleFileType: this.type(),
dossierTemplateId: this.#dossierTemplateId,
ruleFileType: this.type,
dryRun,
}),
).then(
@ -251,7 +155,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
this.#drawErrorMarkers(errors);
if (!dryRun) {
await this.#initialize();
this._toaster.success(rulesScreenTranslations[this.type()]['success.generic']);
this._toaster.success(rulesScreenTranslations[this.type]['success.generic']);
}
},
error => {
@ -268,12 +172,20 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
this.#drawErrorMarkers(errors);
this._loadingService.stop();
if (!dryRun) {
this._toaster.error(rulesScreenTranslations[this.type()]['error.generic']);
this._toaster.error(rulesScreenTranslations[this.type]['error.generic']);
}
},
);
}
revert(): void {
this.currentLines = this.initialLines;
this.#decorations = this.#codeEditor?.deltaDecorations(this.#decorations, []) || [];
this.#removeErrorMarkers();
this._changeDetectorRef.detectChanges();
this._loadingService.stop();
}
#mapErrors(response: UploadResponse, dryRun = false) {
const warnings = response.deprecatedWarnings.map(w => ({ ...w, warning: true }));
if (dryRun) {
@ -383,7 +295,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
async #initialize() {
this._loadingService.start();
await firstValueFrom(this._rulesService.download(this.dossierTemplateId(), this.type())).then(
await firstValueFrom(this._rulesService.download(this.#dossierTemplateId, this.type)).then(
rules => {
this.currentLines = this.initialLines = rules.rules.split('\n');
this.revert();

View File

@ -22,8 +22,6 @@ import { ConfigureCertificateDialogComponent } from '../dialogs/configure-digita
import { EditColorDialogComponent } from '../dialogs/edit-color-dialog/edit-color-dialog.component';
import { SmtpAuthDialogComponent } from '../dialogs/smtp-auth-dialog/smtp-auth-dialog.component';
import { UploadDictionaryDialogComponent } from '../dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component';
import { UserStatsService } from './user-stats.service';
import { result } from 'lodash-es';
type DialogType =
| 'confirm'
@ -75,26 +73,19 @@ export class AdminDialogService extends DialogService<DialogType> {
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _loadingService: LoadingService,
private readonly _userService: UserService,
private readonly _userStatsService: UserStatsService,
private readonly _reportTemplateService: ReportTemplateService,
) {
super(_dialog);
}
async deleteUsers(userIds: string[], cb?: () => Promise<void> | void): Promise<void> {
const userStats = await firstValueFrom(this._userStatsService.getOne(userIds[0]));
deleteUsers(userIds: string[], cb?: () => Promise<void> | void): void {
const data: IConfirmationDialogData = {
title: _('confirm-delete-users.title'),
question: _('confirm-delete-users.warning'),
confirmationText: _('confirm-delete-users.delete'),
denyText: _('confirm-delete-users.cancel'),
titleColor: TitleColors.WARN,
translateParams: {
usersCount: 1,
dossiersCount: userStats.numberOfDossierOwnerships,
documentsCount: userStats.numberOfAssignedFiles,
},
translateParams: { usersCount: 1, dossiersCount: this._getUsersDossiersCount(userIds) },
checkboxes: [
{ value: false, label: _('confirm-delete-users.impacted-dossiers') },
{ value: false, label: _('confirm-delete-users.impacted-documents') },

View File

@ -1,10 +0,0 @@
import { StatsService } from '@iqser/common-ui';
import { IUserStats, USER_ID, UserStats } from '@red/domain';
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UserStatsService extends StatsService<UserStats, IUserStats> {
protected readonly _primaryKey = USER_ID;
protected readonly _entityClass = UserStats;
protected readonly _defaultModelPath = 'user-stats';
}

View File

@ -1,6 +1,6 @@
import { NgIf } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { CircleButtonComponent, LoadingService } from '@iqser/common-ui';
import { getCurrentUser } from '@iqser/common-ui/lib/users';
import { TranslateModule } from '@ngx-translate/core';
@ -13,7 +13,7 @@ import { AdminDialogService } from '../../../services/admin-dialog.service';
selector: 'redaction-dossier-template-actions',
templateUrl: './dossier-template-actions.component.html',
styleUrls: ['./dossier-template-actions.component.scss'],
imports: [NgIf, CircleButtonComponent, TranslateModule],
imports: [NgIf, CircleButtonComponent, TranslateModule, RouterLink],
})
export class DossierTemplateActionsComponent implements OnInit {
@Input() dossierTemplateId: string;

View File

@ -1,4 +1,4 @@
import { AsyncPipe, NgClass, NgIf } from '@angular/common';
import { AsyncPipe, NgClass, NgIf, NgTemplateOutlet } from '@angular/common';
import { Component, computed, effect, HostListener, input, Input, OnDestroy } from '@angular/core';
import { AbstractControl, FormBuilder, FormsModule, ReactiveFormsModule, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
@ -41,6 +41,7 @@ import { ConfigService } from '../../config.service';
ReactiveFormsModule,
DynamicInputComponent,
CircleButtonComponent,
NgTemplateOutlet,
TranslateModule,
StopPropagationDirective,
],
@ -89,11 +90,14 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
);
this.#subscriptions.add(sub2.subscribe());
effect(() => {
if (this.#shouldClose()) {
this.close();
}
});
effect(
() => {
if (this.#shouldClose()) {
this.close();
}
},
{ allowSignalWrites: true },
);
}
get isDate(): boolean {
@ -127,6 +131,7 @@ export class FileAttributeComponent extends BaseFormComponent implements OnDestr
handleClick($event: MouseEvent) {
$event.stopPropagation();
$event.preventDefault();
}
ngOnDestroy() {

View File

@ -105,12 +105,19 @@ export class DossierOverviewScreenHeaderComponent {
const displayedEntities = await firstValueFrom(this.listingService.displayed$);
const entities = this.sortingService.defaultSort(displayedEntities);
const fileName = this.dossier().dossierName + '.export.csv';
const configNames = this._fileAttributesService
.getFileAttributeConfig(this.dossier().dossierTemplateId)
.fileAttributeConfigs.filter(a => a.includeInCsvExport)
.map(c => ({ csvColumnHeader: c.csvColumnHeader, id: c.id }));
const mapper = (file?: File) => ({
...file,
hasAnnotations: file.hasRedactions,
assignee: this._userService.getName(file.assignee) || '-',
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier().dossierTemplateId),
...this.#mapFileAttributes(file, displayedEntities, configNames),
});
const documineOnlyFields = ['hasAnnotations'];
const redactionOnlyFields = ['hasHints', 'hasImages', 'hasUpdates', 'hasRedactions'];
const fileFields = [
@ -127,7 +134,25 @@ export class DossierOverviewScreenHeaderComponent {
'lastProcessed',
...(this.isDocumine ? documineOnlyFields : redactionOnlyFields),
'excluded',
...configNames.map(c => c.csvColumnHeader),
];
saveAsCSV(fileName, entities, fileFields, mapper);
}
#mapFileAttributes(file: File, displayedEntities: File[], configNames: { csvColumnHeader: string; id: string }[]) {
const entity = displayedEntities.find(e => e.id === file.id);
if (!entity) return {};
return Object.keys(entity.fileAttributes.attributeIdToValue).reduce(
(result, key) => {
const match = configNames.find(item => item.id === key);
if (match) {
result[match.csvColumnHeader] = entity.fileAttributes.attributeIdToValue[key];
}
return result;
},
{} as Record<string, any>,
);
}
}

View File

@ -1,7 +1,7 @@
@use 'common-mixins';
.error {
color: #dd4d50;
color: var(--iqser-primary);
}
.extend-cols {

View File

@ -27,9 +27,7 @@
<redaction-file-attribute [dossier]="dossier" [fileAttribute]="config" [file]="file"></redaction-file-attribute>
</div>
@if (!isDocumine) {
<redaction-file-workload [file]="file"></redaction-file-workload>
}
<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, ElementRef, Input, OnInit, Optional, ViewChild } from '@angular/core';
import { DisableStopPropagationDirective, getConfig, HelpModeService } from '@iqser/common-ui';
import { ChangeDetectorRef, Component, computed, ElementRef, Input, OnInit, Optional, ViewChild } from '@angular/core';
import { DisableStopPropagationDirective, 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';
@ -36,7 +36,6 @@ 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

@ -1,4 +1,3 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component, ElementRef, HostListener, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import {
@ -17,7 +16,6 @@ import {
} from '@iqser/common-ui';
import { NestedFilter } from '@iqser/common-ui/lib/filtering';
import { getParam, OnAttach, OnDetach, shareLast } from '@iqser/common-ui/lib/utils';
import { TranslateModule } from '@ngx-translate/core';
import {
Dossier,
DOSSIER_ID,
@ -28,7 +26,6 @@ import {
WorkflowFileStatus,
} from '@red/domain';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { DossierAttributesService } from '@services/entity-services/dossier-attributes.service';
import { dossiersServiceProvider } from '@services/entity-services/dossiers.service.provider';
@ -36,7 +33,6 @@ import { FileAttributesService } from '@services/entity-services/file-attributes
import { FilesMapService } from '@services/files/files-map.service';
import { FilesService } from '@services/files/files.service';
import { PermissionsService } from '@services/permissions.service';
import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component';
import { FileUploadModel } from '@upload-download/model/file-upload.model';
import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service';
import { FileUploadService } from '@upload-download/services/file-upload.service';
@ -46,13 +42,17 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { convertFiles, Files, handleFileDrop } from '@utils/index';
import { merge, Observable } from 'rxjs';
import { filter, map, skip, switchMap, tap } from 'rxjs/operators';
import { DossierOverviewBulkActionsComponent } from '../components/bulk-actions/dossier-overview-bulk-actions.component';
import { DossierDetailsComponent } from '../components/dossier-details/dossier-details.component';
import { DossierOverviewScreenHeaderComponent } from '../components/screen-header/dossier-overview-screen-header.component';
import { TableItemComponent } from '../components/table-item/table-item.component';
import { WorkflowItemComponent } from '../components/workflow-item/workflow-item.component';
import { ConfigService } from '../config.service';
import { BulkActionsService } from '../services/bulk-actions.service';
import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service';
import { AsyncPipe, NgIf } from '@angular/common';
import { DossierOverviewScreenHeaderComponent } from '../components/screen-header/dossier-overview-screen-header.component';
import { TranslateModule } from '@ngx-translate/core';
import { WorkflowItemComponent } from '../components/workflow-item/workflow-item.component';
import { DossierDetailsComponent } from '../components/dossier-details/dossier-details.component';
import { DossierOverviewBulkActionsComponent } from '../components/bulk-actions/dossier-overview-bulk-actions.component';
import { TableItemComponent } from '../components/table-item/table-item.component';
import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component';
@Component({
templateUrl: './dossier-overview-screen.component.html',

View File

@ -83,15 +83,14 @@
icon="iqser:trash"
></iqser-circle-button>
<ng-template [allow]="roles.redactions.deleteManual" [allowIf]="annotationPermissions().canUndo">
<iqser-circle-button
(action)="annotationActionsService.undoDirectAction(annotations())"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.undo' | translate"
[type]="buttonType"
icon="red:undo"
></iqser-circle-button>
</ng-template>
<iqser-circle-button
(action)="annotationActionsService.undoDirectAction(annotations())"
*allow="roles.redactions.deleteManual; if: annotationPermissions().canUndo"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.undo' | translate"
[type]="buttonType"
icon="red:undo"
></iqser-circle-button>
<iqser-circle-button
(action)="annotationReferencesService.show(annotations()[0])"
@ -151,15 +150,6 @@
icon="iqser:visibility"
></iqser-circle-button>
<iqser-circle-button
(action)="revertChanges()"
*ngIf="canRevertChanges() && devMode"
[buttonId]="annotations().length === 1 ? 'annotation-' + annotations()[0].id + '-undo' : 'annotations-undo'"
[type]="buttonType"
[iconSize]="16"
icon="red:revert-changes"
></iqser-circle-button>
<iqser-circle-button
(action)="removeRedaction()"
*ngIf="canRemoveRedaction()"

View File

@ -1,9 +1,7 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component, computed, input, Input, untracked } from '@angular/core';
import { CircleButtonComponent, getConfig, HelpModeService, IqserAllowDirective, IqserPermissionsService } from '@iqser/common-ui';
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { TranslateModule } from '@ngx-translate/core';
import { PermissionsService } from '@services/permissions.service';
import { Roles } from '@users/roles';
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
@ -11,9 +9,10 @@ import { AnnotationActionsService } from '../../services/annotation-actions.serv
import { AnnotationReferencesService } from '../../services/annotation-references.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { SkippedService } from '../../services/skipped.service';
import { ViewModeService } from '../../services/view-mode.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { SkippedService } from '../../services/skipped.service';
import { AsyncPipe, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
export const AnnotationButtonTypes = {
default: 'default',
@ -29,14 +28,12 @@ export type AnnotationButtonType = keyof typeof AnnotationButtonTypes;
imports: [CircleButtonComponent, NgIf, TranslateModule, AsyncPipe, IqserAllowDirective],
})
export class AnnotationActionsComponent {
readonly #isDocumine = getConfig().IS_DOCUMINE;
@Input() buttonType: AnnotationButtonType = AnnotationButtonTypes.default;
@Input() tooltipPosition: 'before' | 'above' = 'before';
@Input() canPerformAnnotationActions: boolean;
@Input() alwaysVisible: boolean;
@Input() actionsHelpModeKey: string;
readonly roles = Roles;
readonly devMode = this._userPreferences.isIqserDevMode;
readonly annotations = input.required<AnnotationWrapper[], (AnnotationWrapper | undefined)[]>({
transform: value => value.filter(a => a !== undefined),
});
@ -70,10 +67,6 @@ export class AnnotationActionsComponent {
readonly hideSkipped = computed(() => this.skippedService.hideSkipped() && this.annotations().some(a => a.isSkipped));
readonly isImageHint = computed(() => this.annotations().every(a => a.IMAGE_HINT));
readonly isImage = computed(() => this.annotations().reduce((acc, a) => acc && a.isImage, true));
readonly canRevertChanges = computed(() => this.annotationPermissions().canRevertChanges);
readonly annotationChangesAllowed = computed(
() => (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.somePending(),
);
readonly canRemoveRedaction = computed(
() => this.annotationChangesAllowed() && this.annotationPermissions().canRemoveRedaction && this.sameType(),
);
@ -82,6 +75,10 @@ export class AnnotationActionsComponent {
readonly canAcceptRecommendation = computed(
() => this.annotationChangesAllowed() && this.annotationPermissions().canAcceptRecommendation,
);
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly annotationChangesAllowed = computed(
() => (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.somePending(),
);
readonly canResize = computed(
() => this.annotationChangesAllowed() && this.annotationPermissions().canResizeAnnotation && this.annotations().length === 1,
);
@ -107,14 +104,13 @@ export class AnnotationActionsComponent {
readonly viewModeService: ViewModeService,
readonly helpModeService: HelpModeService,
readonly multiSelectService: MultiSelectService,
readonly skippedService: SkippedService,
readonly annotationActionsService: AnnotationActionsService,
readonly annotationReferencesService: AnnotationReferencesService,
private readonly _state: FilePreviewStateService,
private readonly _permissionsService: PermissionsService,
private readonly _iqserPermissionsService: IqserPermissionsService,
private readonly _annotationManager: REDAnnotationManager,
private readonly _userPreferences: UserPreferenceService,
readonly skippedService: SkippedService,
readonly annotationActionsService: AnnotationActionsService,
readonly annotationReferencesService: AnnotationReferencesService,
) {}
get resized(): boolean {
@ -132,11 +128,6 @@ export class AnnotationActionsComponent {
await this.annotationActionsService.convertRecommendationToAnnotation(annotations, 'accept');
}
async revertChanges() {
const annotations = untracked(this.annotations);
await this.annotationActionsService.revertChanges(annotations);
}
hideAnnotation() {
const viewerAnnotations = untracked(this.viewerAnnotations);
this._annotationManager.hide(viewerAnnotations);

View File

@ -7,7 +7,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { annotationChangesTranslations } from '@translations/annotation-changes-translations';
import { MultiSelectService } from '../../services/multi-select.service';
import { LogEntryEngine, LogEntryEngines } from '@red/domain';
import { NgForOf } from '@angular/common';
import { NgForOf, NgIf } from '@angular/common';
import { MatTooltip } from '@angular/material/tooltip';
import { MatIcon } from '@angular/material/icon';
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
@ -36,7 +36,7 @@ const changesProperties: KeysOf<AnnotationWrapper>[] = [
selector: 'redaction-annotation-details',
templateUrl: './annotation-details.component.html',
styleUrls: ['./annotation-details.component.scss'],
imports: [MatTooltip, MatIcon, CdkOverlayOrigin, NgForOf, CdkConnectedOverlay, TranslateModule],
imports: [NgIf, MatTooltip, MatIcon, CdkOverlayOrigin, NgForOf, CdkConnectedOverlay, TranslateModule],
})
export class AnnotationDetailsComponent {
readonly annotation = input.required<ListItem<AnnotationWrapper>>();

View File

@ -1,30 +1,30 @@
import { Clipboard } from '@angular/cdk/clipboard';
import { NgIf } from '@angular/common';
import { Component, computed, ElementRef, 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';
import { UserPreferenceService } from '@users/user-preference.service';
import { isTargetInput } from '@utils/functions';
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
import { AnnotationReferencesService } from '../../services/annotation-references.service';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { ViewModeService } from '../../services/view-mode.service';
import { AnnotationReferencesListComponent } from '../annotation-references-list/annotation-references-list.component';
import { AnnotationWrapperComponent } from '../annotation-wrapper/annotation-wrapper.component';
import { JsonPipe, 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 { Clipboard } from '@angular/cdk/clipboard';
import { isTargetInput } from '@utils/functions';
@Component({
selector: 'redaction-annotations-list',
templateUrl: './annotations-list.component.html',
styleUrls: ['./annotations-list.component.scss'],
imports: [NgIf, AnnotationReferencesListComponent, HighlightsSeparatorComponent, AnnotationWrapperComponent],
imports: [NgForOf, NgIf, HighlightsSeparatorComponent, AnnotationWrapperComponent, AnnotationReferencesListComponent, JsonPipe],
})
export class AnnotationsListComponent extends HasScrollbarDirective {
protected readonly isDocumine = getConfig().IS_DOCUMINE;
readonly annotations = input.required<ListItem<AnnotationWrapper>[]>();
readonly pagesPanelActive = output<boolean>();
readonly displayedHighlightGroups = computed(() => {
@ -44,6 +44,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective {
return result;
});
protected readonly isDocumine = getConfig().IS_DOCUMINE;
constructor(
protected readonly _elementRef: ElementRef,

View File

@ -1,15 +1,16 @@
import { Component, input } from '@angular/core';
import { Component, input, Input } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { Dossier, File } from '@red/domain';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { ComponentLogService } from '@services/entity-services/component-log.service';
import { StopPropagationDirective } from '@iqser/common-ui';
@Component({
selector: 'redaction-documine-export',
templateUrl: './documine-export.component.html',
imports: [MatTooltip, TranslateModule, MatMenuTrigger, MatMenu, MatMenuItem],
imports: [MatTooltip, TranslateModule, MatMenuTrigger, MatMenu, MatMenuItem, StopPropagationDirective],
})
export class DocumineExportComponent {
readonly dossier = input<Dossier>();

View File

@ -37,7 +37,7 @@
class="draggable"
svgIcon="red:draggable-dots"
></mat-icon>
<div [attr.help-mode-key]="'editor_edit_component'" class="iqser-input-group w-full">
<div [attr.help-mode-key]="'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

View File

@ -1,5 +1,5 @@
import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
import { NgClass } from '@angular/common';
import { AsyncPipe, KeyValuePipe, NgClass, NgForOf, NgIf } from '@angular/common';
import { Component, computed, input, OnInit, output, signal, WritableSignal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatIcon } from '@angular/material/icon';
@ -20,12 +20,16 @@ import { MatTooltip } from '@angular/material/tooltip';
CircleButtonComponent,
NgClass,
TranslateModule,
KeyValuePipe,
CdkDropList,
MatIcon,
IconButtonComponent,
CdkDrag,
NgIf,
NgForOf,
CdkDragHandle,
FormsModule,
AsyncPipe,
ReplaceNbspPipe,
MatTooltip,
],

View File

@ -1,38 +0,0 @@
<div [ngStyle]="{ height: redactedTextsAreaHeight() }" class="table-container">
<table mat-table [dataSource]="source()" multiTemplateDataRows>
@for (column of config().columns; track column.label) {
<ng-container matColumnDef="{{ column.label }}">
<th mat-header-cell class="cell" *matHeaderCellDef>
<label>{{ column.value }}</label>
</th>
<td mat-cell class="cell" [ngStyle]="{ width: column.width ?? 'unset' }" *matCellDef="let cell">
{{ cell[column.label] }}
</td>
</ng-container>
}
<ng-container matColumnDef="expand-icon">
<td mat-cell class="expand-icon" *matCellDef="let element" [class.expanded-by-default]="shouldBeExpanded()">
@if (!shouldBeExpanded()) {
@if (expandedElement === element) {
<mat-icon svgIcon="red:arrow-up" (click)="close($event)"></mat-icon>
} @else {
<mat-icon svgIcon="red:arrow-down" (click)="expand($event, element)"></mat-icon>
}
}
</td>
<th mat-header-cell *matHeaderCellDef aria-label="row actions"></th>
</ng-container>
<ng-container matColumnDef="expandedDetail">
<td mat-cell class="expanded" *matCellDef="let element" [attr.colspan]="columnsToDisplay().length">
<div class="expanded-component" [class.expanded-by-default]="shouldBeExpanded()">
<ng-container #detailsComponent></ng-container>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToDisplay(); sticky: true"></tr>
<tr mat-row *matRowDef="let element; columns: columnsToDisplay()"></tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="component-row"></tr>
</table>
</div>

View File

@ -1,101 +0,0 @@
table {
width: 100%;
td {
max-width: 0;
}
}
.table-container {
overflow-y: auto;
transition: height 0.25s ease-in-out;
}
.mat-mdc-row {
height: 20px;
max-height: 20px;
font-size: 13px;
&:nth-child(4n - 3),
&:nth-child(4n - 2) {
background-color: var(--iqser-alt-background);
}
.mdc-data-table__cell {
height: 20px;
padding: 0 8px 0 8px;
border: none;
&.expand-icon {
margin-right: 8px;
max-width: 28px;
width: 28px;
background: white;
}
&.expanded-by-default {
width: 0;
margin: 0;
max-width: 0;
padding: 0;
}
&.expanded {
width: 100%;
background: white;
padding: 0;
height: 0;
}
.expanded-component {
width: 100%;
background: var(--iqser-alt-background);
}
}
&.component-row {
height: 0;
}
}
.mat-mdc-header-row {
height: 20px;
font-size: 13px;
.mat-mdc-header-cell {
padding: 0;
border: none;
}
}
mat-icon {
width: 20px;
height: 20px;
font-size: 13px;
}
.expanded-component {
overflow: hidden;
display: flex;
&:not(.expanded-by-default) {
margin-left: 28px;
}
}
label {
opacity: 0.7;
font-weight: normal;
padding-left: 8px;
}
.cell {
text-align: start;
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 8px;
line-height: 1.5;
}

View File

@ -1,117 +0,0 @@
import { Component, computed, effect, input, signal, Type, viewChildren, ViewContainerRef } from '@angular/core';
import { NgStyle } from '@angular/common';
import {
MatCell,
MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
MatTable,
} from '@angular/material/table';
import { MatIcon } from '@angular/material/icon';
import { isJustOne } from '@common-ui/utils';
export interface Data {
styles?: string;
component?: Type<unknown>;
componentInputs?: { [key: string]: unknown };
expanded: boolean;
values: Record<string, string>;
}
export interface Column {
label: string;
value: string;
width?: string;
}
export interface Config {
columns: Column[];
data: Data[];
}
const TABLE_ROW_SIZE = 24;
const MAX_ITEMS_DISPLAY = 10;
@Component({
selector: 'redaction-expandable-row-table',
standalone: true,
imports: [
NgStyle,
MatTable,
MatColumnDef,
MatHeaderCell,
MatCell,
MatIcon,
MatHeaderRow,
MatRow,
MatHeaderCellDef,
MatCellDef,
MatHeaderRowDef,
MatRowDef,
],
templateUrl: './expandable-row-table.component.html',
styleUrl: './expandable-row-table.component.scss',
})
export class ExpandableRowTableComponent {
readonly config = input.required<Config>();
readonly source = computed(() => this.config().data.map(row => row.values));
readonly columnsToDisplay = computed(() => ['expand-icon', ...this.config().columns.map(column => column.label)]);
readonly detailsComponentRef = viewChildren('detailsComponent', { read: ViewContainerRef });
readonly shouldBeExpanded = computed(() => isJustOne(this.config().data));
readonly redactedTextsAreaHeight = computed(() =>
this.shouldBeExpanded()
? 'unset'
: `${
(this.config().data.length <= MAX_ITEMS_DISPLAY
? TABLE_ROW_SIZE * this.config().data.length + (this.#currentExpandedComponentHeight() ?? 0)
: TABLE_ROW_SIZE * MAX_ITEMS_DISPLAY) + 20
}px`,
);
expandedElement: Record<string, string>;
#expandedComponentRef = null;
readonly #currentExpandedComponentHeight = signal(null);
constructor() {
effect(() => {
if (this.shouldBeExpanded()) {
this.#initializeDetailsComponent(this.source()[0]);
}
});
}
expand($event: MouseEvent, element: Record<string, string>) {
this.expandedElement = element;
this.#initializeDetailsComponent(element);
$event.stopPropagation();
}
close($event: MouseEvent) {
this.expandedElement = null;
this.#currentExpandedComponentHeight.set(null);
this.detailsComponentRef().forEach(ref => ref.clear());
$event.stopPropagation();
}
#initializeDetailsComponent(element: Record<string, string>) {
this.detailsComponentRef().forEach(ref => ref.clear());
const expandedIndex = this.source().indexOf(element);
if (this.detailsComponentRef()[expandedIndex]) {
this.#currentExpandedComponentHeight.set(null);
const config = this.config().data.find(row => row.values['id'] === element['id']);
this.#expandedComponentRef = this.detailsComponentRef()[expandedIndex].createComponent(config.component);
if (config.componentInputs) {
for (const [key, value] of Object.entries(config.componentInputs)) {
(this.#expandedComponentRef.instance as any)[key] = value;
}
}
setTimeout(() =>
this.#currentExpandedComponentHeight.set(this.#expandedComponentRef.instance.elementRef.nativeElement.clientHeight),
);
}
}
}

View File

@ -1,8 +0,0 @@
<div [class.full-width]="isExpanded()" class="container">
<p><strong [translate]="'revert-manual-changes-dialog.details.title'"></strong></p>
<redaction-selected-annotations-table
[data]="data()"
[columns]="columns"
[headerHasBackground]="true"
></redaction-selected-annotations-table>
</div>

View File

@ -1,50 +0,0 @@
:host {
width: 100%;
}
.container {
padding: 8px;
width: calc(100% - 28px);
&.full-width {
width: 100%;
}
}
p {
margin: 4px 0;
}
.original-container {
display: inline-flex;
width: calc(100% - 16px);
p:nth-child(n + 2) {
padding-left: 8px;
}
p {
flex: 1;
text-align: start;
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 8px;
line-height: 1.5;
width: 25%;
}
p:last-child {
padding-right: 0;
width: 50%;
}
}
redaction-selected-annotations-table {
::ng-deep .table {
padding: 0;
width: calc(100% - 16px);
}
}

View File

@ -1,29 +0,0 @@
import { Component, computed, ElementRef, inject, input } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { SelectedAnnotationsTableComponent, ValueColumn } from '../selected-annotations-table/selected-annotations-table.component';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'redaction-manual-changes',
standalone: true,
imports: [SelectedAnnotationsTableComponent, TranslateModule],
templateUrl: './manual-changes.component.html',
styleUrl: './manual-changes.component.scss',
})
export class ManualChangesComponent {
readonly redaction = input<AnnotationWrapper>();
readonly isExpanded = input<boolean>();
readonly columns = [{ label: 'Action' }, { label: 'Date' }, { label: 'User' }];
readonly data = computed(() =>
this.redaction().entry.manualChanges.map(
change =>
[
{ label: change.manualRedactionType.toLowerCase().capitalize() },
{ label: change.requestedDate, componentType: 'date' },
{ label: change.userId, componentType: 'avatar' },
] as ValueColumn[],
),
);
readonly elementRef = inject(ElementRef);
}

View File

@ -22,7 +22,7 @@
.ocr-indicator {
display: flex;
margin-right: 40px;
margin-right: 5px;
align-items: center;
}

View File

@ -1,6 +1,7 @@
import { Component, inject, Input } from '@angular/core';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { OcrProgressBarComponent } from '@shared/components/ocr-progress-bar/ocr-progress-bar.component';
import { NgIf } from '@angular/common';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MatIcon } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
@ -10,7 +11,7 @@ import { MatTooltip } from '@angular/material/tooltip';
selector: 'redaction-readonly-banner',
templateUrl: './readonly-banner.component.html',
styleUrls: ['./readonly-banner.component.scss'],
imports: [OcrProgressBarComponent, MatProgressBar, MatIcon, TranslateModule, MatTooltip],
imports: [OcrProgressBarComponent, NgIf, MatProgressBar, MatIcon, TranslateModule, MatTooltip],
})
export class ReadonlyBannerComponent {
protected readonly _state = inject(FilePreviewStateService);

View File

@ -1,19 +1,13 @@
<div [ngStyle]="gridConfig()" class="table">
@for (column of _columns(); track column.label) {
<div [ngClass]="{ hide: !!column.hide, background: headerHasBackground() }" class="col cell">
<div [ngClass]="{ hide: !!column.hide }" class="col cell">
<label>{{ column.label }}</label>
</div>
}
@for (row of _data(); track $index) {
@for (cell of row; track cell.label) {
<div [ngClass]="{ background: _data().indexOf(row) % 2 === 0, hide: !!cell.hide, bold: cell.bold }" class="cell">
@if (cell.componentType === 'date') {
<span>{{ cell.label | date }}</span>
} @else if (cell.componentType === 'avatar') {
<iqser-initials-avatar [user]="cell.label" [withName]="true" [size]="'extra-small'"></iqser-initials-avatar>
} @else {
{{ cell.label }}
}
{{ cell.label }}
</div>
}
}

View File

@ -11,10 +11,7 @@
position: sticky;
top: 0;
z-index: 1;
&:not(.background) {
background: white;
}
background: white;
label {
opacity: 0.7;
@ -29,10 +26,6 @@
list-style-position: inside;
overflow: hidden;
&:not(.background) {
background: white;
}
padding-right: 8px;
line-height: 1.5;
}

View File

@ -1,14 +1,11 @@
import { Component, computed, input } from '@angular/core';
import { NgClass, NgStyle } from '@angular/common';
import { InitialsAvatarComponent } from '@common-ui/users';
import { DatePipe } from '@shared/pipes/date.pipe';
import { NgClass, NgForOf, NgStyle } from '@angular/common';
export interface ValueColumn {
label: string;
hide?: boolean;
bold?: boolean;
width?: string;
componentType?: 'string' | 'avatar' | 'date';
}
const TABLE_ROW_SIZE = 20;
@ -16,13 +13,13 @@ const MAX_ITEMS_DISPLAY = 10;
@Component({
selector: 'redaction-selected-annotations-table',
imports: [NgClass, NgStyle, InitialsAvatarComponent, DatePipe],
imports: [NgForOf, NgClass, NgStyle],
templateUrl: './selected-annotations-table.component.html',
styleUrl: './selected-annotations-table.component.scss',
})
export class SelectedAnnotationsTableComponent {
readonly defaultColumnWidth = input(false);
readonly headerHasBackground = input(false);
readonly columns = input.required<ValueColumn[]>();
readonly _columns = computed(() => this.columns().filter(item => !this.defaultColumnWidth() || !item.hide));

View File

@ -3,7 +3,7 @@
<iqser-popup-filter [primaryFiltersSlug]="'componentLogFilters'" [attr.help-mode-key]="'filter_components'"></iqser-popup-filter>
</div>
<div class="components-container" id="components-view">
<div *ngIf="componentLogService.all$ | async as components" class="components-container" id="components-view">
<div class="component-row">
<div class="header">
<div class="component">{{ 'component-management.table-header.component' | translate }}</div>
@ -12,7 +12,7 @@
<div class="row-separator"></div>
</div>
<div *ngFor="let entry of filteredComponents()" class="component-row">
<div *ngFor="let entry of components" class="component-row">
<redaction-editable-structured-component-value
#editableComponent
[entry]="entry"

View File

@ -1,22 +1,21 @@
import { Component, computed, effect, Input, OnInit, 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';
import { firstValueFrom, map } from 'rxjs';
import { combineLatest, firstValueFrom, Observable } from 'rxjs';
import { EditableStructuredComponentValueComponent } from '../editable-structured-component-value/editable-structured-component-value.component';
import { FilterService, PopupFilterComponent } from '@common-ui/filtering';
import { ComponentLogFilterService } from '../../services/component-log-filter.service';
import { NgForOf } from '@angular/common';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { ComponentLogService } from '@services/entity-services/component-log.service';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'redaction-structured-component-management',
templateUrl: './structured-component-management.component.html',
styleUrls: ['./structured-component-management.component.scss'],
imports: [PopupFilterComponent, TranslateModule, NgForOf, EditableStructuredComponentValueComponent],
imports: [PopupFilterComponent, NgIf, AsyncPipe, TranslateModule, NgForOf, EditableStructuredComponentValueComponent],
})
export class StructuredComponentManagementComponent implements OnInit {
protected readonly iconButtonTypes = IconButtonTypes;
@ -24,12 +23,6 @@ export class StructuredComponentManagementComponent implements OnInit {
@Input() dictionaries: Dictionary[];
@ViewChildren('editableComponent') editableComponents: List<EditableStructuredComponentValueComponent>;
readonly filteredComponents = computed(() => this.#filteredComponents);
readonly #displayedComponents = toSignal(this.componentLogService.all$);
readonly #filterModel = toSignal(
this._filterService.getFilterModels$('componentLogFilters').pipe(map(filters => (filters ? [...filters] : []))),
);
constructor(
private readonly _loadingService: LoadingService,
private readonly _componentLogFilterService: ComponentLogFilterService,
@ -47,13 +40,6 @@ export class StructuredComponentManagementComponent implements OnInit {
return this.file.workflowStatus !== WorkflowFileStatuses.APPROVED;
}
get #filteredComponents() {
if (this.#filterModel() && this.#displayedComponents()) {
return this._componentLogFilterService.filterComponents(this.#displayedComponents(), this.#filterModel());
}
return this.#displayedComponents();
}
async ngOnInit(): Promise<void> {
await this.#loadData();
}

View File

@ -41,7 +41,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="assignToMe(file)"
(action)="fileAssignService.assignToMe([file])"
*ngIf="_canAssignToSelf()"
[icon]="'red:assign-me'"
[tooltip]="'file-preview.assign-me' | translate"

View File

@ -1,22 +1,21 @@
import { NgIf } from '@angular/common';
import { Component, computed, HostListener, inject, NgZone, OnDestroy, OnInit } from '@angular/core';
import { Component, computed, HostListener, NgZone, OnDestroy, OnInit } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { StatusBarComponent } from '@common-ui/shared';
import { Bind } from '@common-ui/utils';
import { CircleButtonComponent, LoadingService, StopPropagationDirective, Toaster } from '@iqser/common-ui';
import { getCurrentUser, InitialsAvatarComponent } from '@iqser/common-ui/lib/users';
import { TranslateModule } from '@ngx-translate/core';
import { File, User } from '@red/domain';
import { FilesService } from '@services/files/files.service';
import { PermissionsService } from '@services/permissions.service';
import { AssignUserDropdownComponent } from '@shared/components/assign-user-dropdown/assign-user-dropdown.component';
import { workflowFileStatusTranslations } from '@translations/file-status-translations';
import { UserService } from '@users/user.service';
import { moveElementInArray } from '@utils/functions';
import { PdfViewer } from '../../../pdf-viewer/services/pdf-viewer.service';
import { FileAssignService } from '../../../shared-dossiers/services/file-assign.service';
import { FileDataService } from '../../services/file-data.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { PdfViewer } from '../../../pdf-viewer/services/pdf-viewer.service';
import { Bind } from '@common-ui/utils';
import { AssignUserDropdownComponent } from '@shared/components/assign-user-dropdown/assign-user-dropdown.component';
import { NgIf } from '@angular/common';
import { StatusBarComponent } from '@common-ui/shared';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'redaction-user-management',
@ -49,7 +48,6 @@ export class UserManagementComponent implements OnInit, OnDestroy {
: this.#customSort([...dossier.memberIds, ...unassignUser]);
});
protected readonly _currentUserId = getCurrentUser().id;
protected readonly fileDataService = inject(FileDataService);
readonly translations = workflowFileStatusTranslations;
readonly statusBarConfig = computed(() => [{ length: 1, color: this.state.file().workflowStatus }]);
readonly assignTooltip = computed(() => {
@ -73,13 +71,6 @@ export class UserManagementComponent implements OnInit, OnDestroy {
readonly ngZone: NgZone,
) {}
async assignToMe(file: File) {
await this.fileAssignService.assignToMe([file]);
//TODO: check which one to call
// await firstValueFrom(this.fileDataService.updateAnnotations(file, file.numberOfAnalyses));
await this.fileDataService.loadEntityLog();
}
async assignReviewer(file: File, user: User | string) {
const assigneeId = typeof user === 'string' ? user : user?.id;
@ -93,11 +84,6 @@ export class UserManagementComponent implements OnInit, OnDestroy {
await this.filesService.setUnderApproval(file, assigneeId);
}
if (assigneeId === this._currentUserId) {
// await firstValueFrom(this.fileDataService.updateAnnotations(file, file.numberOfAnalyses));
await this.fileDataService.loadEntityLog();
}
this.loadingService.stop();
const translateParams = { reviewerName: this.userService.getName(assigneeId), filename: file.filename };

View File

@ -7,7 +7,7 @@ import { Dossier } from '@red/domain';
import { JustificationsService } from '@services/entity-services/justifications.service';
import { firstValueFrom } from 'rxjs';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect } from '@angular/material/select';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { NgForOf, NgIf } from '@angular/common';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
@ -18,6 +18,7 @@ import { LegalBasisOption } from '../../utils/dialog-types';
imports: [
ReactiveFormsModule,
MatFormField,
MatSelectTrigger,
MatSelect,
MatOption,
NgForOf,

View File

@ -3,7 +3,7 @@ import { CircleButtonComponent, ConfirmOptions, IconButtonComponent, IqserDialog
import { MatDialogClose } from '@angular/material/dialog';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { IComponentLogEntry } from '@red/domain';
import { NgFor } from '@angular/common';
import { NgFor, NgIf } from '@angular/common';
interface RevertValueData {
entry: IComponentLogEntry;
@ -13,7 +13,7 @@ interface RevertValueResult {}
@Component({
templateUrl: 'revert-value-dialog.component.html',
styleUrls: ['./revert-value-dialog.component.scss'],
imports: [CircleButtonComponent, IconButtonComponent, MatDialogClose, TranslateModule, NgFor],
imports: [CircleButtonComponent, IconButtonComponent, MatDialogClose, TranslateModule, NgFor, NgIf],
})
export class RevertValueDialogComponent extends IqserDialogComponent<RevertValueDialogComponent, RevertValueData, RevertValueResult> {
protected readonly entry = this.data.entry;

View File

@ -15,10 +15,12 @@ import { JustificationsService } from '@services/entity-services/justifications.
import { Roles } from '@users/roles';
import { firstValueFrom } from 'rxjs';
import { ManualRedactionService } from '../../services/manual-redaction.service';
import { NgForOf, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect } from '@angular/material/select';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
import {
LegalBasisOption,
@ -39,14 +41,18 @@ export const NON_READABLE_CONTENT = 'non-readable content';
styleUrls: ['./rectangle-annotation-dialog.component.scss'],
imports: [
ReactiveFormsModule,
NgIf,
CircleButtonComponent,
TranslateModule,
HasScrollbarDirective,
MatFormField,
MatSelectTrigger,
MatSelect,
MatOption,
NgForOf,
MatTooltip,
IqserDenyDirective,
MatCheckbox,
IconButtonComponent,
DetailsRadioComponent,
],

View File

@ -1,22 +0,0 @@
<section class="dialog">
<div class="dialog-header heading-l">
<span [translate]="'revert-manual-changes-dialog.title'"></span>
</div>
<div class="dialog-content" [class.is-expanded]="data.redactions.length === 1">
<div class="iqser-input-group">
<redaction-expandable-row-table [config]="config"></redaction-expandable-row-table>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button
[label]="'revert-manual-changes-dialog.actions.save' | translate"
[type]="IconButtonTypes.primary"
(click)="save()"
></iqser-icon-button>
<div [translate]="'revert-manual-changes-dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
</div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close" mat-dialog-close />
</section>

View File

@ -1,3 +0,0 @@
.dialog-content:not(.is-expanded) {
padding-left: 20px;
}

View File

@ -1,50 +0,0 @@
import { Component, signal } from '@angular/core';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui';
import { RevertManualChangesData } from '../../utils/dialog-types';
import { TranslateModule } from '@ngx-translate/core';
import { Config, ExpandableRowTableComponent } from '../../components/expandable-row-table/expandable-row-table.component';
import { ManualChangesComponent } from '../../components/manual-changes/manual-changes.component';
import { MatDialogClose } from '@angular/material/dialog';
import { isJustOne } from '@common-ui/utils';
@Component({
selector: 'redaction-revert-manual-changes-dialog',
standalone: true,
imports: [IconButtonComponent, TranslateModule, CircleButtonComponent, ExpandableRowTableComponent, MatDialogClose],
templateUrl: './revert-manual-changes-dialog.component.html',
styleUrl: './revert-manual-changes-dialog.component.scss',
})
export class RevertManualChangesDialogComponent extends IqserDialogComponent<RevertManualChangesData, RevertManualChangesData, boolean> {
protected readonly IconButtonTypes = IconButtonTypes;
readonly config: Config = {
columns: [
{ label: 'value', value: 'Value', width: '25%' },
{ label: 'type', value: 'Type', width: '25%' },
{
label: 'reason',
value: 'Reason',
width: '50%',
},
],
data: this.data.redactions.map(redaction => ({
values: {
id: redaction.id,
value: redaction.value,
type: redaction.typeLabel,
reason: redaction.legalBasisValue,
},
expanded: false,
component: ManualChangesComponent,
componentInputs: { redaction: signal(redaction), isExpanded: signal(isJustOne(this.data.redactions)) },
})),
};
constructor() {
super();
}
save() {
this.close(true);
}
}

View File

@ -6,7 +6,6 @@ import { AnnotationActionsService } from './services/annotation-actions.service'
import { AnnotationProcessingService } from './services/annotation-processing.service';
import { AnnotationReferencesService } from './services/annotation-references.service';
import { AnnotationsListingService } from './services/annotations-listing.service';
import { ComponentLogFilterService } from './services/component-log-filter.service';
import { DocumentInfoService } from './services/document-info.service';
import { ExcludedPagesService } from './services/excluded-pages.service';
import { FileDataService } from './services/file-data.service';
@ -17,6 +16,7 @@ import { PdfProxyService } from './services/pdf-proxy.service';
import { SkippedService } from './services/skipped.service';
import { StampService } from './services/stamp.service';
import { ViewModeService } from './services/view-mode.service';
import { ComponentLogFilterService } from './services/component-log-filter.service';
export const filePreviewScreenProviders = [
FilterService,

View File

@ -58,9 +58,9 @@ redaction-pdf-viewer.hidden {
}
.resize {
background: var(--iqser-grey-4);
background: #444857;
height: 100%;
width: 10px;
width: 14px;
cursor: col-resize;
position: relative;
z-index: 10;
@ -74,7 +74,7 @@ redaction-pdf-viewer.hidden {
transform: translate(-50%, -50%);
width: 3px;
height: 15px;
border-inline: 1px solid var(--iqser-grey-1);
border-inline: 1px solid #fff;
}
@media only screen and (max-width: 1015px) {

View File

@ -1,12 +1,10 @@
import { CdkDrag, CdkDragEnd, CdkDragMove, CdkDragStart } from '@angular/cdk/drag-drop';
import { NgIf } from '@angular/common';
import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
effect,
ElementRef,
inject,
NgZone,
OnDestroy,
OnInit,
@ -15,7 +13,6 @@ import {
viewChild,
ViewChild,
} from '@angular/core';
import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
import {
@ -32,25 +29,21 @@ import {
Toaster,
} from '@iqser/common-ui';
import { copyLocalStorageFiltersValues, FilterService, INestedFilter, NestedFilter, processFilters } from '@iqser/common-ui/lib/filtering';
import { AutoUnsubscribe, Bind, bool, List, log, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils';
import { AutoUnsubscribe, Bind, 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 { TranslateModule } from '@ngx-translate/core';
import { AnalyseStatuses, AnalysisEvent, File, ViewModes, WsTopics } from '@red/domain';
import { File, ViewModes } from '@red/domain';
import { ConfigService } from '@services/config.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { ComponentLogService } from '@services/entity-services/component-log.service';
import { FilesMapService } from '@services/files/files-map.service';
import { FilesService } from '@services/files/files.service';
import { PermissionsService } from '@services/permissions.service';
import { ReanalysisService } from '@services/reanalysis.service';
import { WebSocketService } from '@services/web-socket.service';
import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component';
import { Roles } from '@users/roles';
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
import { NGXLogger } from 'ngx-logger';
import { combineLatest, first, firstValueFrom, Observable, of, pairwise, Subscription } from 'rxjs';
import { combineLatest, first, firstValueFrom, Observable, of, pairwise } from 'rxjs';
import { catchError, filter, map, startWith, switchMap, tap } from 'rxjs/operators';
import { byId, byPage, handleFilterDelta, hasChanges } from '../../utils';
import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.service';
@ -61,28 +54,33 @@ import { PdfViewer } from '../pdf-viewer/services/pdf-viewer.service';
import { ReadableRedactionsService } from '../pdf-viewer/services/readable-redactions.service';
import { ViewerHeaderService } from '../pdf-viewer/services/viewer-header.service';
import { ROTATION_ACTION_BUTTONS, ViewerEvents } from '../pdf-viewer/utils/constants';
import { FileHeaderComponent } from './components/file-header/file-header.component';
import { FilePreviewRightContainerComponent } from './components/right-container/file-preview-right-container.component';
import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component';
import { AddHintDialogComponent } from './dialogs/add-hint-dialog/add-hint-dialog.component';
import { AddAnnotationDialogComponent } from './dialogs/docu-mine/add-annotation-dialog/add-annotation-dialog.component';
import { RectangleAnnotationDialog } from './dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component';
import { RedactTextDialogComponent } from './dialogs/redact-text-dialog/redact-text-dialog.component';
import { filePreviewScreenProviders } from './file-preview-providers';
import { AnnotationProcessingService } from './services/annotation-processing.service';
import { AnnotationsListingService } from './services/annotations-listing.service';
import { DocumentInfoService } from './services/document-info.service';
import { FileDataService } from './services/file-data.service';
import { FilePreviewDialogService } from './services/file-preview-dialog.service';
import { FilePreviewStateService } from './services/file-preview-state.service';
import { ManualRedactionService } from './services/manual-redaction.service';
import { MultiSelectService } from './services/multi-select.service';
import { PdfProxyService } from './services/pdf-proxy.service';
import { SkippedService } from './services/skipped.service';
import { StampService } from './services/stamp.service';
import { ViewModeService } from './services/view-mode.service';
import { ANNOTATION_ACTION_ICONS, ANNOTATION_ACTIONS } from './utils/constants';
import { RedactTextData } from './utils/dialog-types';
import { MultiSelectService } from './services/multi-select.service';
import { NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
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';
import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component';
import { DocumentInfoService } from './services/document-info.service';
import { RectangleAnnotationDialog } from './dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component';
import { ANNOTATION_ACTION_ICONS, ANNOTATION_ACTIONS } from './utils/constants';
import { ComponentLogService } from '@services/entity-services/component-log.service';
import { CdkDrag, CdkDragEnd, CdkDragMove, CdkDragStart } from '@angular/cdk/drag-drop';
@Component({
templateUrl: './file-preview-screen.component.html',
@ -101,28 +99,18 @@ import { RedactTextData } from './utils/dialog-types';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach, ComponentCanDeactivate {
readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles;
readonly fileId = this.state.fileId;
readonly dossierId = this.state.dossierId;
readonly resizeHandle = viewChild<ElementRef>('resize');
protected readonly isDocumine = getConfig().IS_DOCUMINE;
@ViewChild('annotationFilterTemplate', {
read: TemplateRef,
static: false,
})
private readonly _filterTemplate: TemplateRef<unknown>;
#loadAllAnnotationsEnabled = false;
readonly #wsConnection$ = inject(WebSocketService)
.listen<AnalysisEvent>(WsTopics.ANALYSIS)
.pipe(
log('[WS] Analysis events'),
filter(event => event.analyseStatus === AnalyseStatuses.FINISHED),
switchMap(event => this._fileDataService.updateAnnotations(this.state.file(), event.analysisNumber)),
log('[WS] Annotations updated'),
);
#wsConnectionSub: Subscription;
protected readonly isDocumine = getConfig().IS_DOCUMINE;
readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles;
readonly fileId = this.state.fileId;
readonly dossierId = this.state.dossierId;
readonly resizeHandle = viewChild<ElementRef>('resize');
#overlayElement: HTMLDivElement | null = null;
constructor(
readonly pdf: PdfViewer,
@ -164,6 +152,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _componentLogService: ComponentLogService,
) {
super();
effect(() => {
const file = this.state.file();
this._fileDataService.loadAnnotations(file).then();
});
effect(() => {
const file = this.state.file();
if (file.analysisRequired && !file.excludedFromAutomaticAnalysis) {
@ -171,20 +164,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
});
const file = this.state.file();
if (this._fileDataService.annotations().length) {
firstValueFrom(this._fileDataService.updateAnnotations(file, file.numberOfAnalyses)).then();
} else {
this._fileDataService.loadAnnotations(file).then();
}
effect(() => {
if (this._documentViewer.loaded()) {
this._pageRotationService.clearRotations();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
this.viewerReady().then();
}
});
effect(
() => {
if (this._documentViewer.loaded()) {
this._pageRotationService.clearRotations();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
this.viewerReady().then();
}
},
{ allowSignalWrites: true },
);
effect(() => {
this.state.updateExcludedPagesStyle();
@ -250,7 +239,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
onDragStart(event: CdkDragStart) {
event.event.preventDefault();
if (!this.isDocumine) return;
this.#createDragOverlay();
const contentInnerElement = document.body.getElementsByClassName('content-inner').item(0) as HTMLElement;
if (contentInnerElement) {
contentInnerElement.classList.add('dragging');
}
}
onDragMove(event: CdkDragMove) {
@ -264,7 +256,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
onDragEnd(event: CdkDragEnd) {
event.event.preventDefault();
if (!this.isDocumine) return;
this.#removeDragOverlay();
const contentInnerElement = document.body.getElementsByClassName('content-inner').item(0) as HTMLElement;
if (contentInnerElement) {
contentInnerElement.classList.remove('dragging');
}
}
deleteEarmarksOnViewChange$() {
@ -291,19 +286,17 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.pdf.instance.UI.hotkeys.off('esc');
this.pdf.instance.UI.iframeWindow.document.removeEventListener('click', this.handleViewerClick);
this._changeRef.markForCheck();
this.#wsConnectionSub.unsubscribe();
}
ngOnDestroy() {
this.pdf.instance.UI.hotkeys.off('esc');
this.pdf.instance.UI.iframeWindow.document.removeEventListener('click', this.handleViewerClick);
super.ngOnDestroy();
this.#wsConnectionSub.unsubscribe();
}
handleEscInsideViewer($event: KeyboardEvent) {
$event.preventDefault();
if (this._annotationManager.selected[0]) {
if (!!this._annotationManager.selected[0]) {
const doesHaveWrapper = this._fileDataService.find(this._annotationManager.selected[0]?.Id);
if (!doesHaveWrapper) {
this._annotationManager.delete(this._annotationManager.selected[0]?.Id);
@ -324,17 +317,24 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
@Bind()
handleViewerClick(event: MouseEvent) {
this._ngZone.run(() => {
if (!event.isTrusted) return;
const clickedElement = event.target as HTMLElement;
const actionIconClicked = ANNOTATION_ACTION_ICONS.some(action => (clickedElement as HTMLImageElement).src?.includes(action));
const actionClicked = ANNOTATION_ACTIONS.some(action => clickedElement.getAttribute('aria-label')?.includes(action));
if (!this._multiSelectService.active() || actionIconClicked || actionClicked) return;
if (clickedElement.querySelector('#selectionrect') || clickedElement.id === `pageWidgetContainer${this.pdf.currentPage()}`) {
if (!this._annotationManager.selected.length) {
this._multiSelectService.deactivate();
if (event.isTrusted) {
const clickedElement = event.target as HTMLElement;
const actionIconClicked = ANNOTATION_ACTION_ICONS.some(action =>
(clickedElement as HTMLImageElement).src?.includes(action),
);
const actionClicked = ANNOTATION_ACTIONS.some(action => clickedElement.getAttribute('aria-label')?.includes(action));
if (this._multiSelectService.active() && !actionIconClicked && !actionClicked) {
if (
clickedElement.querySelector('#selectionrect') ||
clickedElement.id === `pageWidgetContainer${this.pdf.currentPage()}`
) {
if (!this._annotationManager.selected.length) {
this._multiSelectService.deactivate();
}
} else {
this._multiSelectService.deactivate();
}
}
} else {
this._multiSelectService.deactivate();
}
});
}
@ -354,7 +354,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async ngOnInit(): Promise<void> {
this.#wsConnectionSub = this.#wsConnection$.subscribe();
this.#updateViewerPosition();
const file = this.state.file();
@ -456,7 +455,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
#getPixelsInPercentage(pixels: number) {
return (pixels / document.body.getBoundingClientRect().width) * 100;
return (pixels / window.screen.width) * 100;
}
async #updateViewMode(): Promise<void> {
@ -887,30 +886,32 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const components = this._componentLogService.all;
const filteredComponentIds = untracked(this.state.componentReferenceIds);
if (!filteredComponentIds || !annotationFilters) return annotationFilters;
if (filteredComponentIds && annotationFilters) {
const filteredComponentIdsSet = new Set(filteredComponentIds);
const filteredComponentIdsSet = new Set(filteredComponentIds);
const references = new Set<string>();
for (const component of components) {
for (const componentValue of component.componentValues) {
for (const reference of componentValue.entityReferences) {
if (filteredComponentIdsSet.has(reference.id)) {
references.add(reference.type);
const references = new Set<string>();
for (const component of components) {
for (const componentValue of component.componentValues) {
for (const reference of componentValue.entityReferences) {
if (filteredComponentIdsSet.has(reference.id)) {
references.add(reference.type);
}
}
}
}
return annotationFilters
.map(filter => {
const filteredChildren = filter.children.filter(c => references.has(c.label.replace(/ /g, '_').toLowerCase()));
if (filteredChildren.length) {
return { ...filter, children: filteredChildren };
}
return null;
})
.filter(f => f !== null);
}
return annotationFilters
.map(filter => {
const filteredChildren = filter.children.filter(c => references.has(c.label.replace(/ /g, '_').toLowerCase()));
if (filteredChildren.length) {
return { ...filter, children: filteredChildren };
}
return null;
})
.filter(f => f !== null);
return annotationFilters;
}
#updateViewerPosition() {
@ -925,27 +926,4 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
document.getElementById('viewer')?.classList?.add('redaction-viewer');
}
#createDragOverlay() {
if (this.#overlayElement || document.body.contains(this.#overlayElement)) {
return;
}
this.#overlayElement = document.createElement('div');
this.#overlayElement.style.position = 'fixed';
this.#overlayElement.style.top = '0';
this.#overlayElement.style.left = '0';
this.#overlayElement.style.width = '100%';
this.#overlayElement.style.height = '100%';
this.#overlayElement.style.zIndex = '9999';
this.#overlayElement.style.background = 'transparent';
this.#overlayElement.style.pointerEvents = 'all';
document.body.appendChild(this.#overlayElement);
}
#removeDragOverlay() {
if (!this.#overlayElement || !document.body.contains(this.#overlayElement)) return;
document.body.removeChild(this.#overlayElement);
this.#overlayElement = null;
}
}

View File

@ -1,13 +1,11 @@
import { inject, provideEnvironmentInitializer } from '@angular/core';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { IqserRoutes } from '@iqser/common-ui';
import { WebSocketService } from '@services/web-socket.service';
import { FileAssignService } from '../shared-dossiers/services/file-assign.service';
import { FilePreviewScreenComponent } from './file-preview-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { DocumentUnloadedGuard } from './services/document-unloaded.guard';
import { FilePreviewDialogService } from './services/file-preview-dialog.service';
import { ManualRedactionService } from './services/manual-redaction.service';
import { TablesService } from './services/tables.service';
import { FileAssignService } from '../shared-dossiers/services/file-assign.service';
export default [
{
@ -15,15 +13,6 @@ export default [
component: FilePreviewScreenComponent,
pathMatch: 'full',
canDeactivate: [PendingChangesGuard, DocumentUnloadedGuard],
providers: [
FilePreviewDialogService,
ManualRedactionService,
DocumentUnloadedGuard,
TablesService,
FileAssignService,
provideEnvironmentInitializer(async () => {
return inject(WebSocketService).connectAsync('/redaction-gateway-v1/websocket');
}),
],
providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, TablesService, FileAssignService],
},
] satisfies IqserRoutes;

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { IqserDialog } from '@common-ui/dialog/iqser-dialog.service';
import { getConfig } from '@iqser/common-ui';
import { List, log } from '@iqser/common-ui/lib/utils';
import { List } from '@iqser/common-ui/lib/utils';
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Core } from '@pdftron/webviewer';
@ -27,7 +27,6 @@ import { EditAnnotationDialogComponent } from '../dialogs/docu-mine/edit-annotat
import { RemoveAnnotationDialogComponent } from '../dialogs/docu-mine/remove-annotation-dialog/remove-annotation-dialog.component';
import { ResizeAnnotationDialogComponent } from '../dialogs/docu-mine/resize-annotation-dialog/resize-annotation-dialog.component';
import { EditRedactionDialogComponent } from '../dialogs/edit-redaction-dialog/edit-redaction-dialog.component';
import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component';
import { RedactRecommendationDialogComponent } from '../dialogs/redact-recommendation-dialog/redact-recommendation-dialog.component';
import { RemoveRedactionDialogComponent } from '../dialogs/remove-redaction-dialog/remove-redaction-dialog.component';
import { ResizeRedactionDialogComponent } from '../dialogs/resize-redaction-dialog/resize-redaction-dialog.component';
@ -43,7 +42,6 @@ import {
RemoveRedactionPermissions,
RemoveRedactionResult,
ResizeRedactionData,
RevertManualChangesData,
} from '../utils/dialog-types';
import { toPosition } from '../utils/pdf-calculation.utils';
import { FileDataService } from './file-data.service';
@ -51,7 +49,7 @@ import { FilePreviewDialogService } from './file-preview-dialog.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { ManualRedactionService } from './manual-redaction.service';
import { SkippedService } from './skipped.service';
import { RevertManualChangesDialogComponent } from '../dialogs/revert-manual-changes-dialog/revert-manual-changes-dialog.component';
import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component';
@Injectable()
export class AnnotationActionsService {
@ -70,7 +68,6 @@ export class AnnotationActionsService {
private readonly _skippedService: SkippedService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _permissionsService: PermissionsService,
private readonly _iqserDialogService: IqserDialog,
) {}
removeHighlights(highlights: AnnotationWrapper[]): void {
@ -137,11 +134,7 @@ export class AnnotationActionsService {
let recategorizeBody: List<IRecategorizationRequest> | IBulkRecategorizationRequest;
if (
result.option === RedactOrHintOptions.ONLY_HERE ||
result.option === RectangleRedactOptions.ONLY_THIS_PAGE ||
this.#isDocumine
) {
if (result.option === RedactOrHintOptions.ONLY_HERE || result.option === RectangleRedactOptions.ONLY_THIS_PAGE) {
recategorizeBody = annotations.map(annotation => {
const body: IRecategorizationRequest = {
annotationId: annotation.id,
@ -171,7 +164,6 @@ export class AnnotationActionsService {
};
}
result.pageNumbers = result.pageNumbers || [];
await this.#processObsAndEmit(
this._manualRedactionService.recategorizeRedactions(
recategorizeBody,
@ -221,22 +213,6 @@ export class AnnotationActionsService {
}
}
async revertChanges(annotations: AnnotationWrapper[]) {
const dialogData: RevertManualChangesData = {
redactions: annotations,
};
const result = await this._iqserDialogService.openDefault(RevertManualChangesDialogComponent, { data: dialogData }).result();
if (!result) return;
const data = annotations.map(annotation => annotation.id);
const dossierId = this._state.dossierId;
const fileId = this._state.fileId;
this.#processObsAndEmit(
this._manualRedactionService.revertChanges(data, dossierId, fileId, this._state.file().excludedFromAutomaticAnalysis),
).then();
}
undoDirectAction(annotations: AnnotationWrapper[]) {
const { dossierId, fileId } = this._state;
const modifyDictionary = annotations[0].isModifyDictionary;
@ -386,7 +362,7 @@ export class AnnotationActionsService {
}
async #processObsAndEmit(obs: Observable<unknown>) {
await firstValueFrom(obs.pipe(log('==>>[[[CHANGES]]]'))).finally(() => this._fileDataService.annotationsChanged());
await firstValueFrom(obs).finally(() => this._fileDataService.annotationsChanged());
}
#getFalsePositiveText(annotation: AnnotationWrapper) {
@ -611,7 +587,6 @@ export class AnnotationActionsService {
redactions: AnnotationWrapper[],
dialogResult: RemoveRedactionResult,
): List<IRemoveRedactionRequest | IBulkLocalRemoveRequest> {
dialogResult.pageNumbers = dialogResult.pageNumbers || [];
if (dialogResult.bulkLocal || !!dialogResult.pageNumbers.length) {
return dialogResult.positions.map((position, index) => ({
value: redactions[index].value,

View File

@ -1,19 +1,15 @@
import { inject, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { ComponentLogEntry } from '@red/domain';
import { IFilterGroup, INestedFilter, keyChecker, NestedFilter } from '@common-ui/filtering';
import { TranslateService } from '@ngx-translate/core';
import { sortArray } from '@utils/sorters/custom-sort';
import { INestedFilter, NestedFilter } from '@common-ui/filtering';
@Injectable()
export class ComponentLogFilterService {
readonly #translateService = inject(TranslateService);
filterGroups(entities: ComponentLogEntry[]) {
const allDistinctComponentLogs = new Set<string>();
entities?.forEach(entry => allDistinctComponentLogs.add(entry.name));
const componentLogFilters = sortArray([...allDistinctComponentLogs]).map(
const componentLogFilters = [...allDistinctComponentLogs].map(
id =>
new NestedFilter({
id: id,
@ -25,9 +21,7 @@ export class ComponentLogFilterService {
{
slug: 'componentLogFilters',
filters: componentLogFilters,
checker: keyChecker('name'),
filterceptionPlaceholder: this.#translateService.instant('component-management.filter.search-placeholder'),
} as IFilterGroup,
},
];
}

View File

@ -1,8 +1,6 @@
import { effect, inject, Injectable, Signal, signal, WritableSignal } from '@angular/core';
import { effect, inject, Injectable, Signal, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TenantsService } from '@common-ui/tenants';
import { log } from '@common-ui/utils';
import { EntitiesService, getConfig, Toaster } from '@iqser/common-ui';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import {
@ -29,7 +27,6 @@ import { UserPreferenceService } from '@users/user-preference.service';
import dayjs from 'dayjs';
import { NGXLogger } from 'ngx-logger';
import { firstValueFrom, Observable } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { FilePreviewStateService } from './file-preview-state.service';
import { MultiSelectService } from './multi-select.service';
import { ViewModeService } from './view-mode.service';
@ -46,14 +43,13 @@ export function chronologicallyBy<T>(property: (x: T) => string) {
@Injectable()
export class FileDataService extends EntitiesService<AnnotationWrapper, AnnotationWrapper> {
readonly #annotations: WritableSignal<AnnotationWrapper[]>;
readonly #annotations = signal<AnnotationWrapper[]>([]);
readonly #earmarks = signal<Map<number, AnnotationWrapper[]>>(new Map());
#originalViewedPages: ViewedPage[] = [];
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly #logger = inject(NGXLogger);
readonly #toaster = inject(Toaster);
readonly #isIqserDevMode = inject(UserPreferenceService).isIqserDevMode;
readonly #tenantsService = inject(TenantsService);
protected readonly _entityClass = AnnotationWrapper;
missingTypes = new Set<string>();
readonly earmarks: Signal<Map<number, AnnotationWrapper[]>>;
@ -74,24 +70,9 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
private readonly _defaultColorsService: DefaultColorsService,
) {
super();
const localStorageKey = this.#tenantsService.activeTenantId + '-annotations-' + this._state.fileId;
const storedAnnotations = JSON.parse(localStorage.getItem(localStorageKey) || '[]') as [];
this.#annotations = signal<AnnotationWrapper[]>(
storedAnnotations.map(a => {
const newAnn = new AnnotationWrapper();
Object.assign(newAnn, a);
return newAnn;
}),
);
this.annotations$ = toObservable(this.#annotations);
this.annotations = this.#annotations.asReadonly();
this.earmarks = this.#earmarks.asReadonly();
effect(() => {
localStorage.setItem(localStorageKey, JSON.stringify(this.#annotations()));
console.log('FileDataService#annotations', this.#annotations());
});
effect(() => {
const viewMode = this._viewModeService.viewMode();
const earmarks = ([] as AnnotationWrapper[]).concat(...this.#earmarks().values());
@ -138,8 +119,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const file = this._state.file();
const fileReloaded = await this._filesService.reload(file.dossierId, file);
if (!fileReloaded) {
await this.#loadViewedPages(file);
// await this.loadAnnotations(file);
await this.loadAnnotations(file);
}
}
@ -162,31 +142,6 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
this.#annotations.set(annotations);
}
async processEntityLog(entityLog: IEntityLog) {
let annotations = await this.#convertData(entityLog);
this.#checkMissingTypes();
return this.#isIqserDevMode ? annotations : annotations.filter(a => !a.isFalsePositive);
}
updateAnnotations(file: File, analysisNumber: number) {
const delta$ = this._entityLogService.getDelta(file.dossierId, file.fileId, analysisNumber);
return delta$.pipe(
log('[REDACTION_LOG] Delta loaded'),
switchMap(delta => this.processEntityLog(delta)),
tap(annotations => {
const notDeleted = annotations.filter(annotation => !annotation.isRemoved);
this.#annotations.update(old => {
const notUpdated = old.filter(oldAnnotation => {
return !oldAnnotation.pending && !annotations.some(newAnnotation => newAnnotation.id === oldAnnotation.id);
});
return [...notUpdated, ...notDeleted].sort((a, b) => a.positions[0].page - b.positions[0].page);
});
}),
tap(() => this.#logger.info('[REDACTION_LOG] Annotations updated', this.#annotations())),
);
}
#checkMissingTypes() {
if (this.missingTypes.size > 0) {
this.#toaster.error(_('error.missing-types'), {
@ -235,8 +190,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
continue;
}
const isRemoved = entry.state === EntryStates.REMOVED;
const canBeMappedToASuperType = !!SuperTypeMapper[entry.entryType][entry.state](entry) || isRemoved;
const canBeMappedToASuperType = !!SuperTypeMapper[entry.entryType][entry.state](entry);
if (!canBeMappedToASuperType) {
if (this.#isIqserDevMode) {
this.#logger.warn(

View File

@ -1,7 +1,7 @@
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { GenericService, IqserPermissionsService, QueryParam, Toaster } from '@iqser/common-ui';
import { GenericService, IqserPermissionsService, Toaster } from '@iqser/common-ui';
import { List } from '@iqser/common-ui/lib/utils';
import { type AnnotationWrapper } from '@models/file/annotation.wrapper';
import { type ManualRedactionEntryType } from '@models/file/manual-redaction-entry.wrapper';
@ -108,10 +108,6 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
return this.undo(annotationIds, dossierId, fileId).pipe(this.#showToast('undo', modifyDictionary));
}
revertChanges(body: List<string>, dossierId: string, fileId: string, includeOnlyUnprocessed: boolean) {
return this.#revertChanges(body, dossierId, fileId, includeOnlyUnprocessed).pipe(this.#showToast('revert-changes'));
}
removeRedaction(
body: List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest,
dossierId: string,
@ -158,14 +154,6 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
return this._post(body, `${this.#bulkRedaction}/resize/${dossierId}/${fileId}`).pipe(this.#log('Resize', body));
}
#revertChanges(body: List<string>, dossierId: string, fileId: string, includeOnlyUnprocessed: boolean) {
const queryParams: List<QueryParam> = [
{ key: 'includeOnlyUnprocessed', value: includeOnlyUnprocessed },
{ key: 'includeOnlyLocal', value: true },
];
return this.delete(body, `${this._defaultModelPath}/bulk/undo/${dossierId}/${fileId}`, queryParams);
}
#recategorize(
body: List<IRecategorizationRequest> | IBulkRecategorizationRequest,
dossierId: string,

View File

@ -10,7 +10,6 @@ import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manag
import { AnnotationActionsService } from './annotation-actions.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { UI_ROOT_PATH_FN } from '@common-ui/utils';
import { UserPreferenceService } from '@users/user-preference.service';
@Injectable()
export class PdfAnnotationActionsService {
@ -21,10 +20,8 @@ export class PdfAnnotationActionsService {
readonly #annotationActionsService = inject(AnnotationActionsService);
readonly #iqserPermissionsService = inject(IqserPermissionsService);
readonly #annotationManager = inject(REDAnnotationManager);
readonly #userPreferences = inject(UserPreferenceService);
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly #convertPath = inject(UI_ROOT_PATH_FN);
readonly #devMode = this.#userPreferences.isIqserDevMode;
get(annotations: AnnotationWrapper[], annotationChangesAllowed: boolean): IHeaderElement[] {
const availableActions: IHeaderElement[] = [];
@ -100,13 +97,6 @@ export class PdfAnnotationActionsService {
availableActions.push(forceHintButton);
}
if (permissions.canRevertChanges && annotationChangesAllowed && this.#devMode) {
const revertChangesButton = this.#getButton('general/revert-changes', _('annotation-actions.revert-changes.label'), () =>
this.#annotationActionsService.revertChanges(annotations),
);
availableActions.push(revertChangesButton);
}
if (permissions.canRemoveRedaction && sameType && annotationChangesAllowed) {
const removeRedactionButton = this.#getButton('trash', _('annotation-actions.remove-annotation.remove-redaction'), () =>
this.#annotationActionsService.removeRedaction(annotations, permissions),

View File

@ -183,7 +183,3 @@ export interface RectangleDialogData {
export interface RectangleDialogResult {
annotation: IManualRedactionEntry;
}
export interface RevertManualChangesData {
redactions: AnnotationWrapper[];
}

View File

@ -75,7 +75,6 @@ export class IconsModule {
'reference',
'remove-from-dict',
'report',
'revert-changes',
'rotation',
'rss',
'rule',

View File

@ -210,7 +210,6 @@ export class REDAnnotationManager {
}
#getByIds(annotations: List | List<AnnotationWrapper>) {
annotations = annotations ?? [];
return annotations.map((item: string | AnnotationWrapper) => this.#getById(item)).filter(a => !!a);
}

View File

@ -1,9 +1,7 @@
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { Component, computed, HostBinding, inject, Injector, input, Optional, signal, 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';
import { StatusBarComponent } from '@common-ui/shared';
import {
CircleButtonTypes,
getConfig,
@ -22,23 +20,24 @@ import { FileManagementService } from '@services/files/file-management.service';
import { FilesService } from '@services/files/files.service';
import { PermissionsService } from '@services/permissions.service';
import { ReanalysisService, ReanalyzeQueryParams } from '@services/reanalysis.service';
import { ApproveWarningDetailsComponent } from '@shared/components/approve-warning-details/approve-warning-details.component';
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component';
import { LongPressDirective, LongPressEvent } from '@shared/directives/long-press.directive';
import { Roles } from '@users/roles';
import { UserPreferenceService } from '@users/user-preference.service';
import { setLocalStorageDataByFileId } from '@utils/local-storage';
import { firstValueFrom, Observable } from 'rxjs';
import { RulesService } from '../../../admin/services/rules.service';
import { DocumentInfoService } from '../../../file-preview/services/document-info.service';
import { ExcludedPagesService } from '../../../file-preview/services/excluded-pages.service';
import { FileDataService } from '../../../file-preview/services/file-data.service';
import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service';
import { ViewerHeaderService } from '../../../pdf-viewer/services/viewer-header.service';
import { AssignReviewerApproverDialogComponent } from '../../dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
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 { ApproveWarningDetailsComponent } from '@shared/components/approve-warning-details/approve-warning-details.component';
import { RulesService } from '../../../admin/services/rules.service';
@Component({
selector: 'redaction-file-actions',
@ -49,49 +48,8 @@ import { FileAssignService } from '../../services/file-assign.service';
export class FileActionsComponent {
@ViewChild(ExpandableFileActionsComponent)
private readonly _expandableActionsComponent: ExpandableFileActionsComponent;
readonly #fileDataService = inject(FileDataService, { optional: true });
readonly #analysisForced = signal(false);
readonly #ariaExpanded$ = toObservable(this._documentInfoService?.shown);
readonly #areRulesLocked = computed(() => this._rulesService.currentTemplateRules().timeoutDetected);
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly file = input.required<File>();
readonly #assignTooltip? = computed(() =>
this.file().isUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer'),
);
readonly #showReanalyse = computed(
() => (this.#canReanalyse() || this.file().excludedFromAutomaticAnalysis || this.#analysisForced()) && !this.file().dossierArchived,
);
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 #showDownload = computed(
() =>
this._permissionsService.canDownloadRedactedFile() &&
!!this.file().lastProcessed &&
!this.file().isError &&
this.#isDossierMember(),
);
readonly dossier = input.required<Dossier>();
readonly #showImportRedactions = computed(
() => this._permissionsService.canImportRedactions(this.file(), this.dossier()) && !this.file().isError,
);
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 #canToggleAnalysis = computed(() => this._permissionsService.canToggleAnalysis(this.file(), this.dossier()));
readonly #showToggleAnalysis = computed(
() => !!this.file().lastProcessed && this._permissionsService.showToggleAnalysis(this.dossier()),
);
readonly #isDossierMember = computed(() => this._permissionsService.isDossierMember(this.dossier()));
readonly #canDisableAutoAnalysis = computed(
() => !this.#isDocumine && this._permissionsService.canDisableAutoAnalysis([this.file()], this.dossier()) && !this.file().isError,
);
readonly type = input.required<'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow'>();
readonly maxWidth = input<number>();
readonly minWidth = input<number>();
@ -99,21 +57,16 @@ export class FileActionsComponent {
readonly singleEntityAction = input(false);
readonly currentUser = getCurrentUser<User>();
readonly tooltipPosition = IqserTooltipPositions.above;
readonly isDossierOverview = computed(() => this.type().startsWith('dossier-overview'));
readonly #showAssignToSelf = computed(
() => this._permissionsService.canAssignToSelf(this.file(), this.dossier()) && this.isDossierOverview(),
);
readonly #showAssign = computed(
() =>
(this._permissionsService.canAssignUser(this.file(), this.dossier()) ||
this._permissionsService.canUnassignUser(this.file(), this.dossier())) &&
this.isDossierOverview(),
);
readonly #showReanalyseDossierOverview = computed(
() => this.#showReanalyse() && this.isDossierOverview() && !this.file().isApproved && this.#isDossierMember(),
);
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,
@ -124,6 +77,24 @@ export class FileActionsComponent {
!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()) &&
@ -142,12 +113,41 @@ export class FileActionsComponent {
!this.isDossierOverviewWorkflow() &&
!this.file().isError,
);
readonly isFilePreview = computed(() => this.type() === 'file-preview');
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 buttons = computed(() => this.#buttons);
readonly showStatusBar = computed(() => !this.file().isError && !this.file().isUnprocessed && this.isDossierOverviewList());
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()) && !this.file().isError,
);
constructor(
private readonly _injector: Injector,
@ -296,7 +296,7 @@ export class FileActionsComponent {
icon: 'iqser:refresh',
show: this.#showReanalyseFilePreview(),
disabled: this.file().isProcessing || this.#areRulesLocked(),
helpModeKey: this.#isDocumine ? 'analyze_file' : 'stop_analysis',
helpModeKey: 'stop_analysis',
},
{
id: 'btn-toggle_automatic_analysis',
@ -334,7 +334,7 @@ export class FileActionsComponent {
icon: 'iqser:refresh',
show: this.#showReanalyseDossierOverview(),
disabled: this.#areRulesLocked(),
helpModeKey: this.#isDocumine ? 'analyze_file' : 'stop_analysis',
helpModeKey: 'stop_analysis',
},
{
id: 'btn-toggle_analysis',
@ -433,9 +433,6 @@ export class FileActionsComponent {
async #assignToMe() {
await this._fileAssignService.assignToMe([this.file()]);
// TODO: check which one to call
// await firstValueFrom(this.#fileDataService?.updateAnnotations(this.file, this.file.numberOfAnalyses));
await this.#fileDataService?.loadEntityLog();
}
async #reanalyseFile() {

View File

@ -1,6 +1,14 @@
import { Component, HostListener } from '@angular/core';
import { CircleButtonComponent, IconButtonComponent, IqserDialogComponent, LoadingService, Toaster } from '@iqser/common-ui';
import {
CircleButtonComponent,
IconButtonComponent,
IconButtonTypes,
IqserDialogComponent,
LoadingService,
Toaster,
} from '@iqser/common-ui';
import { MatDialogClose } from '@angular/material/dialog';
import { MatFormField } from '@angular/material/form-field';
import { NgIf } from '@angular/common';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
@ -23,7 +31,16 @@ interface ReturnType {
@Component({
selector: 'redaction-edit-dictionary-dialog',
imports: [CircleButtonComponent, IconButtonComponent, MatDialogClose, ReactiveFormsModule, TranslateModule, MatCheckbox, NgIf],
imports: [
CircleButtonComponent,
IconButtonComponent,
MatDialogClose,
MatFormField,
ReactiveFormsModule,
TranslateModule,
MatCheckbox,
NgIf,
],
templateUrl: './edit-dictionary-dialog.component.html',
})
export class EditDictionaryDialogComponent extends IqserDialogComponent<EditDictionaryDialogComponent, DialogData, ReturnType> {

View File

@ -6,7 +6,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogRef } from '@angular/material/dialog';
import { MatFormField, MatSuffix } from '@angular/material/form-field';
import { MatIcon } from '@angular/material/icon';
import { MatOption, MatSelect } from '@angular/material/select';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { MatTooltip } from '@angular/material/tooltip';
import { Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -68,6 +68,7 @@ interface GeneralInfoForm {
MatSuffix,
IconButtonComponent,
NgIf,
MatSelectTrigger,
],
})
export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSectionInterface {

View File

@ -1,10 +1,23 @@
import { Component, computed, input } from '@angular/core';
import { ApproveResponse, File } from '@red/domain';
import { SelectedAnnotationsTableComponent } from '../../../file-preview/components/selected-annotations-table/selected-annotations-table.component';
import { MatExpansionPanel, MatExpansionPanelHeader } from '@angular/material/expansion';
import { KeyValuePipe, NgStyle } from '@angular/common';
import { WarningDetailsPanelComponent } from '@shared/components/warning-details-panel/warning-details-panel.component';
import { CdkFixedSizeVirtualScroll, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
selector: 'redaction-approve-warning-details',
imports: [WarningDetailsPanelComponent],
imports: [
SelectedAnnotationsTableComponent,
MatExpansionPanel,
MatExpansionPanelHeader,
KeyValuePipe,
WarningDetailsPanelComponent,
CdkVirtualScrollViewport,
CdkFixedSizeVirtualScroll,
NgStyle,
],
templateUrl: './approve-warning-details.component.html',
styleUrl: './approve-warning-details.component.scss',
})

View File

@ -23,7 +23,7 @@ import { saveAs } from 'file-saver';
import { List } from '@iqser/common-ui/lib/utils';
import { firstValueFrom } from 'rxjs';
import { MatIcon } from '@angular/material/icon';
import { NgIf } from '@angular/common';
import { NgForOf, NgIf } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { MatTooltip } from '@angular/material/tooltip';
@ -55,6 +55,7 @@ const HELP_MODE_KEYS = {
MatSelect,
MatOption,
MatDivider,
NgForOf,
IconButtonComponent,
EditorComponent,
],

View File

@ -1,22 +1,21 @@
import { HttpErrorResponse, HttpEventType, HttpStatusCode } from '@angular/common/http';
import { ApplicationRef, Injectable, OnDestroy } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TenantsService } from '@common-ui/tenants';
import { ErrorMessageService, GenericService, Toaster } from '@iqser/common-ui';
import { HeadersConfiguration } from '@iqser/common-ui/lib/utils';
import { FileUploadModel } from '../model/file-upload.model';
import { HttpErrorResponse, HttpEventType, HttpStatusCode } from '@angular/common/http';
import { interval, Subject, Subscription } from 'rxjs';
import { ConfigService } from '@services/config.service';
import { TranslateService } from '@ngx-translate/core';
import { IFileUploadResult, OverwriteFileOption, OverwriteFileOptions } from '@red/domain';
import { ConfigService } from '@services/config.service';
import { isAcceptedFileType, isCsv, isDocument, isZip } from '@utils/file-drop-utils';
import { ErrorMessageService, GenericService, Toaster } from '@iqser/common-ui';
import { FilesMapService } from '@services/files/files-map.service';
import { FilesService } from '@services/files/files.service';
import { LicenseService } from '@services/license.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { isAcceptedFileType, isCsv, isZip } from '@utils/file-drop-utils';
import { interval, Subject, Subscription } from 'rxjs';
import { switchMap, tap, throttleTime } from 'rxjs/operators';
import { LicenseFeatures } from '../../admin/screens/license/utils/constants';
import { FileUploadModel } from '../model/file-upload.model';
import { FilesService } from '@services/files/files.service';
import { UploadDownloadDialogService } from './upload-download-dialog.service';
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;
@ -46,7 +45,6 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
private readonly _licenseService: LicenseService,
private readonly _toaster: Toaster,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _tenantsService: TenantsService,
) {
super();
const fileFetch$ = this.#fetchFiles$.pipe(
@ -113,11 +111,6 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
option = res.applyToAllFiles ? currentOption : undefined;
}
const existingFile = dossierFiles.find(pf => pf.filename === file.file.name);
if (OverwriteFileOptions.FULL_OVERWRITE === currentOption || OverwriteFileOptions.PARTIAL_OVERWRITE === currentOption) {
localStorage.removeItem(`${this._tenantsService.activeTenantId}-annotations-${existingFile.id}`);
}
if (currentOption === OverwriteFileOptions.PARTIAL_OVERWRITE) {
file.keepManualRedactions = true;
}

View File

@ -1,18 +0,0 @@
import { Injectable } from '@angular/core';
import { StompService } from '@services/stomp.service';
@Injectable({
providedIn: 'root',
})
export class CopilotService extends StompService {
override get topicPrefix(): string {
return '';
}
override send(message: string) {
this.publish({
destination: '/app/rules-copilot',
body: JSON.stringify({ prompts: [message] }),
});
}
}

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,49 +1,34 @@
import { inject, Injectable } from '@angular/core';
import { GenericService, Toaster } from '@iqser/common-ui';
import { EntryState, EntryStates, IEntityLog, IEntityLogEntry, ISectionGrid } from '@red/domain';
import { EntryStates, IEntityLog, IEntityLogEntry } from '@red/domain';
import { firstValueFrom, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class EntityLogService extends GenericService<unknown> {
readonly #toaster = inject(Toaster);
protected readonly _defaultModelPath = '';
readonly #toaster = inject(Toaster);
async getEntityLog(dossierId: string, fileId: string) {
const queryParams = [{ key: 'includeUnprocessed', value: true }];
const entityLog$ = this._getOne<IEntityLog>([dossierId, fileId], 'entityLog', queryParams);
const entityLog = await firstValueFrom(entityLog$.pipe(catchError(() => of({} as IEntityLog))));
entityLog.entityLogEntry = this.#filterInvalidEntries(entityLog.entityLogEntry, [EntryStates.REMOVED]);
entityLog.entityLogEntry = this.#filterInvalidEntries(entityLog.entityLogEntry);
entityLog.entityLogEntry.sort((a, b) => a.positions[0].pageNumber - b.positions[0].pageNumber);
return entityLog;
}
getDelta(dossierId: string, fileId: string, analysisNumber: number) {
const req$ = this._getOne<IEntityLog>([dossierId, fileId, analysisNumber.toString()], 'entityLog');
return req$.pipe(
map(entityLog => {
entityLog.entityLogEntry = this.#filterInvalidEntries(entityLog.entityLogEntry);
return entityLog;
}),
catchError(() => of({} as IEntityLog)),
);
}
getSectionGrid(dossierId: string, fileId: string) {
return this._getOne<ISectionGrid>([dossierId, fileId], 'sectionGrid');
}
#filterInvalidEntries(entityLogEntry: IEntityLogEntry[], invalidStates: EntryState[] = []) {
#filterInvalidEntries(entityLogEntry: IEntityLogEntry[]) {
return entityLogEntry.filter(entry => {
entry.positions = entry.positions?.filter(p => !!p.rectangle?.length);
const hasPositions = !!entry.positions?.length;
const hasInvalidState = invalidStates.includes(entry.state);
const isRemoved = entry.state === EntryStates.REMOVED;
if (!hasPositions) {
this.#toaster.devInfo(`Entry ${entry.id} was skipped because it has no position`);
}
return hasPositions && !hasInvalidState;
return hasPositions && !isRemoved;
});
}
}

View File

@ -1,73 +0,0 @@
import { inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { KeycloakStatusService } from '@common-ui/tenants';
import { log } from '@common-ui/utils';
import { getConfig } from '@iqser/common-ui';
import { IMessage, IWatchParams, RxStomp } from '@stomp/rx-stomp';
import { StompHeaders } from '@stomp/stompjs';
import { NGXLogger } from 'ngx-logger';
import { firstValueFrom, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
export abstract class StompService extends RxStomp {
readonly #logger = inject(NGXLogger);
readonly #config = getConfig();
readonly #keycloakStatusService = inject(KeycloakStatusService);
constructor() {
super();
this.connectionState$.pipe(log('[WS] Connection state'), takeUntilDestroyed()).subscribe();
this.webSocketErrors$.pipe(log('[WS] Errors'), takeUntilDestroyed()).subscribe();
this.stompErrors$.pipe(takeUntilDestroyed()).subscribe(frame => {
console.error(frame);
console.error('Broker reported error: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
});
this.#keycloakStatusService.token$.pipe(takeUntilDestroyed()).subscribe(token => {
this.#logger.info('[WS] Update connectHeaders');
this.configure({
connectHeaders: { Authorization: 'Bearer ' + token },
});
});
}
get topicPrefix() {
return '';
}
send(value: unknown) {
throw new Error('Not implemented');
}
override watch(opts: IWatchParams): Observable<IMessage>;
override watch(destination: string, headers?: StompHeaders): Observable<IMessage>;
override watch(opts: string | IWatchParams, headers?: StompHeaders): Observable<IMessage> {
if (typeof opts === 'string') {
return super.watch(this.topicPrefix + opts, headers);
}
return super.watch(opts);
}
listen<T>(topic: string): Observable<T> {
return this.watch(topic).pipe(
tap(msg => this.#logger.info('[WS] Response on topic ' + topic, msg.body)),
map(msg => JSON.parse(msg.body)),
);
}
connect(url: string) {
this.configure({
debug: (msg: string) => this.#logger.debug('[WS] ' + msg),
brokerURL: this.#config.API_URL + url,
reconnectDelay: 0,
});
this.activate();
}
async connectAsync(url: string) {
return await firstValueFrom(this.#keycloakStatusService.token$.pipe(map(() => this.connect(url))));
}
}

View File

@ -1,14 +0,0 @@
import { inject, Injectable } from '@angular/core';
import { TenantsService } from '@common-ui/tenants';
import { StompService } from '@services/stomp.service';
@Injectable({
providedIn: 'root',
})
export class WebSocketService extends StompService {
readonly #tenantService = inject(TenantsService);
override get topicPrefix() {
return '/topic/' + this.#tenantService.activeTenantId + '/';
}
}

View File

@ -44,10 +44,6 @@ export const manualRedactionActionsTranslations: Record<ManualRedactionActions,
error: _('annotation-actions.message.manual-redaction.recategorize-annotation.error'),
success: _('annotation-actions.message.manual-redaction.recategorize-annotation.success'),
},
'revert-changes': {
success: _('annotation-actions.message.manual-redaction.revert-changes.success'),
error: _('annotation-actions.message.manual-redaction.revert-changes.error'),
},
undo: {
error: _('annotation-actions.message.manual-redaction.undo.error'),
success: _('annotation-actions.message.manual-redaction.undo.success'),

View File

@ -2,6 +2,7 @@ import { inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { IqserPermissionsService } from '@iqser/common-ui';
import { IqserRoleGuard } from '@iqser/common-ui/lib/users';
import { UserService } from '@users/user.service';
import { NGXLogger } from 'ngx-logger';
@Injectable({
@ -9,9 +10,10 @@ import { NGXLogger } from 'ngx-logger';
})
export class RedRoleGuard extends IqserRoleGuard {
protected readonly _permissionsService = inject(IqserPermissionsService);
protected readonly _userService = inject(UserService);
protected readonly _logger = inject(NGXLogger);
override async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const currentUser = this._userService.currentUser;
if (!currentUser?.hasAnyRole) {

View File

@ -1,10 +1,10 @@
import { inject, Injectable } from '@angular/core';
import { QueryParam } from '@iqser/common-ui';
import { IIqserUser, IqserUserService } from '@iqser/common-ui/lib/users';
import { List } from '@iqser/common-ui/lib/utils';
import { User } from '@red/domain';
import { QueryParam } from '@iqser/common-ui';
import { Roles } from '@users/roles';
import { of } from 'rxjs';
import { IIqserUser, IqserUserService } from '@iqser/common-ui/lib/users';
import { List } from '@iqser/common-ui/lib/utils';
@Injectable({
providedIn: 'root',
@ -13,7 +13,7 @@ export class UserService extends IqserUserService<IIqserUser, User> {
protected readonly _defaultModelPath = 'user';
protected readonly _entityClass = User;
override async loadCurrentUser(): Promise<User | undefined> {
async loadCurrentUser(): Promise<User | undefined> {
const currentUser = await super.loadCurrentUser();
this._permissionsService.add({
@ -23,12 +23,12 @@ export class UserService extends IqserUserService<IIqserUser, User> {
return currentUser;
}
override loadAll() {
loadAll() {
const canReadUsers = this._permissionsService.has(Roles.users.read);
return canReadUsers ? super.loadAll() : of([]);
}
override getAll() {
getAll() {
const canReadAllUsers = this._permissionsService.has(Roles.users.readAll);
const url = canReadAllUsers ? this._defaultModelPath : `${this._defaultModelPath}/red`;
return super.getAll(url);
@ -39,8 +39,8 @@ export class UserService extends IqserUserService<IIqserUser, User> {
return this._post(null, `${this._defaultModelPath}/profile/activate/${user.userId}`, queryParams);
}
protected override readonly _rolesFilter = (role: string) => role.startsWith('RED_');
protected override readonly _permissionsFilter = (role: string) => role.startsWith('red-') || role.startsWith('fforesight-');
protected readonly _rolesFilter = (role: string) => role.startsWith('RED_');
protected readonly _permissionsFilter = (role: string) => role.startsWith('red-') || role.startsWith('fforesight-');
}
export function getCurrentUser() {

View File

@ -51,7 +51,7 @@ export function mainGuard(): AsyncGuard {
userPreferenceService.reload(),
firstValueFrom(updatedDisplayName$),
]);
await tenantsService.storeTenant();
await languageService.setInitialLanguage();
const lastDossierTemplate = userPreferenceService.getLastDossierTemplate();
@ -60,6 +60,8 @@ export function mainGuard(): AsyncGuard {
}
loadingService.stop();
await tenantsService.storeTenant();
configService.updateIsDocumine(tenantsService.activeTenant);
logger.info('[ROUTES] Main guard finished!');

View File

@ -577,11 +577,6 @@
"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",
@ -631,6 +626,7 @@
"elementKey": "workload_page_list",
"documentKey": "workload_page_list"
},
{
"elementKey": "editor_delete_file",
"documentKey": "editor_delete_file"
@ -796,6 +792,10 @@
"elementKey": "components_table",
"documentKey": "components_table"
},
{
"elementKey": "components_table",
"documentKey": "components_table"
},
{
"elementKey": "remove_annotation_DIALOG",
"documentKey": "remove_annotation"

View File

@ -173,6 +173,7 @@
"displayedInFileList": "Wird in der Dokumentenliste angezeigt",
"filterable": "Filterbar",
"filterable-disabled": "Die maximale Anzahl filterbarer Attribute ({maxNumber}) wurde erreicht.",
"includeInCsvExport": "",
"name": "Name des Attributs",
"name-placeholder": "Namen eingeben",
"primary": "Zum Primärattribut machen",
@ -341,10 +342,6 @@
"error": "Entfernen des Hinweises fehlgeschlagen: {error}",
"success": "Hinweis wurde entfernt"
},
"revert-changes": {
"error": "Manuelle Änderungen konnten nicht rückgängig gemacht werden: {error}",
"success": "Manuelle Änderungen wurden erfolgreich rückgängig gemacht."
},
"undo": {
"error": "Die Aktion konnte nicht rückgängig gemacht werden. Fehler: {error}",
"success": "Rücksetzung erfolgreich"
@ -366,9 +363,6 @@
"resize-cancel": {
"label": "Größenänderung abbrechen"
},
"revert-changes": {
"label": "Änderungen rückgängig machen"
},
"see-references": {
"label": "Referenzen anzeigen"
},
@ -560,9 +554,6 @@
"undo": "Zurücksetzen"
},
"components": "Komponenten",
"filter": {
"search-placeholder": "Suche nach Komponente..."
},
"table-header": {
"component": "Komponente",
"value": "Wert"
@ -712,9 +703,6 @@
}
},
"content": "Grund",
"copilot": {
"label": "Copilot"
},
"dashboard": {
"empty-template": {
"description": "Diese Vorlage enthält keine Dossiers. Erstellen Sie ein Dossier, um nach diesen Regeln zu schwärzen.",
@ -1534,6 +1522,7 @@
"csv-column": "CSV-Spalte",
"displayed-in-file-list": "In Dokumentenliste anzeigen",
"filterable": "Filterbar",
"include-in-csv-export": "",
"name": "Name",
"primary": "Primärattribut",
"primary-info-tooltip": "Der Wert des Primärattributs steht in der Dokumentenliste unter dem Dateinamen.",
@ -2378,16 +2367,6 @@
"header": "Größe von {type} ändern"
}
},
"revert-manual-changes-dialog": {
"actions": {
"cancel": "Abbrechen",
"save": "Zu den Originalen zurückkehren"
},
"details": {
"title": "Manuelle Änderungen"
},
"title": "Lokale manuelle Änderungen rückgängig machen"
},
"revert-value-dialog": {
"actions": {
"cancel": "Abbrechen",

View File

@ -173,6 +173,7 @@
"displayedInFileList": "Displayed in file list",
"filterable": "Filterable",
"filterable-disabled": "The maximum number of filterable attributes ({maxNumber}) has been reached.",
"includeInCsvExport": "Include in CSV export",
"name": "Attribute name",
"name-placeholder": "Enter name",
"primary": "Primary attribute",
@ -338,10 +339,6 @@
"error": "Failed to remove redaction: {error}",
"success": "Redaction removed"
},
"revert-changes": {
"error": "Failed to revert manual changes: {error}",
"success": "Reverted manual changes successfully."
},
"undo": {
"error": "Failed to undo: {error}",
"success": "Undo successful"
@ -363,9 +360,6 @@
"resize": {
"label": "Resize"
},
"revert-changes": {
"label": "Revert changes"
},
"see-references": {
"label": "See references"
},
@ -560,9 +554,6 @@
"undo": "Undo"
},
"components": "Components",
"filter": {
"search-placeholder": "Search by component..."
},
"table-header": {
"component": "Component",
"value": "Value"
@ -642,7 +633,7 @@
"confirm-delete-users": {
"cancel": "Keep {usersCount, plural, one{user} other{users}}",
"delete": "Delete {usersCount, plural, one{user} other{users}}",
"impacted-documents": "{documentsCount} {documentsCount, plural, one{document} other{documents}} will be impacted",
"impacted-documents": "All documents pending review from the {usersCount, plural, one{user} other{users}} will be impacted",
"impacted-dossiers": "{dossiersCount} {dossiersCount, plural, one{dossier} other{dossiers}} will be impacted",
"title": "Delete {usersCount, plural, one{user} other{users}} from workspace",
"toast-error": "Please confirm that you understand the consequences of this action.",
@ -712,9 +703,6 @@
}
},
"content": "Reason",
"copilot": {
"label": "Copilot"
},
"dashboard": {
"empty-template": {
"description": "This template does not contain any dossiers. Create a dossier that applies this ruleset.",
@ -1534,6 +1522,7 @@
"csv-column": "CSV column",
"displayed-in-file-list": "Displayed in file list",
"filterable": "Filterable",
"include-in-csv-export": "Include in CSV export",
"name": "Name",
"primary": "Primary",
"primary-info-tooltip": "The value of the attribute set as primary is diplayed below the file name in the document list.",
@ -2378,16 +2367,6 @@
"header": "Resize {type}"
}
},
"revert-manual-changes-dialog": {
"actions": {
"cancel": "Cancel",
"save": "Revert to originals"
},
"details": {
"title": "Manual changes"
},
"title": "Revert local manual changes"
},
"revert-value-dialog": {
"actions": {
"cancel": "Cancel",

View File

@ -173,6 +173,7 @@
"displayedInFileList": "Wird in der Dokumentenliste angezeigt",
"filterable": "Filterbar",
"filterable-disabled": "Die maximale Anzahl filterbarer Attribute ({maxNumber}) wurde erreicht.",
"includeInCsvExport": "",
"name": "Name des Attributs",
"name-placeholder": "Namen eingeben",
"primary": "Zum Primärattribut machen",
@ -225,7 +226,7 @@
"actions": {
"cancel": "Abbrechen",
"save": "Speichern",
"save-and-remember": "Speichern und Auswahl merken"
"save-and-remember": "Save and remember my choice"
},
"content": {
"comment": "Kommentar",
@ -243,7 +244,7 @@
},
"type": "Typ",
"type-placeholder": "Typ auswählen...",
"value": "Wert"
"value": "Wert\n"
},
"title": "Hinweis hinzufügen"
}
@ -275,9 +276,6 @@
"watermarks": "Wasserzeichen"
},
"analysis-disabled": "Analyse deaktiviert",
"annotation": {
"pending": "(Analyse steht aus)"
},
"annotation-actions": {
"accept-recommendation": {
"label": "Empfehlung annehmen"
@ -333,17 +331,13 @@
"error": "Rekategorisierung des Bilds fehlgeschlagen: {error}",
"success": "Bild wurde einer neuen Kategorie zugeordnet."
},
"remove": {
"error": "Entfernen der Annotation fehlgeschlagen: {error}",
"success": "Annotation wurde entfernt"
},
"remove-hint": {
"error": "Entfernen des Hinweises fehlgeschlagen: {error}",
"success": "Hinweis wurde entfernt"
},
"revert-changes": {
"error": "Manuelle Änderungen konnten nicht rückgängig gemacht werden: {error}",
"success": "Manuelle Änderungen wurden erfolgreich rückgängig gemacht."
"remove": {
"error": "Entfernen der Annotation fehlgeschlagen: {error}",
"success": "Annotation wurde entfernt"
},
"undo": {
"error": "Die Aktion konnte nicht rückgängig gemacht werden. Fehler: {error}",
@ -357,17 +351,14 @@
"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"
},
"revert-changes": {
"label": "Änderungen rückgängig machen"
"resize": {
"label": "Größe ändern"
},
"see-references": {
"label": "Referenzen anzeigen"
@ -385,7 +376,7 @@
"removed-manual": "Schwärzung/Hinweis entfernt",
"resized": "Schwärzungsbereich wurde geändert"
},
"annotation-content": "{hasRule, select, true {Rule {matchedRule} trifft zu:{ruleSymbol}} other {}} {hasReason, select, true {{reason}} other {}} {hasLb, select, true {Legal basis: {legalBasis}} other {}} {hasOverride, select, true {Removed by manual override} other {}} {hasSection, select, true {{shouldLower, plural, =0 {I} other {i}}n Abschnitt{sectionSymbol} \\\"{section}\\\"} other {}}",
"annotation-content": "{hasRule, select, true {Rule {matchedRule} trifft zu:{ruleSymbol}} other {}} {hasReason, select, true {{reason}} other {}} {hasLb, select, true {Legal basis: {legalBasis}} other {}} {hasOverride, select, true {Removed by manual override} other {}} {hasSection, select, true {{shouldLower, plural, =0 {I} other {i}}n Abschnitt{sectionSymbol} \"{section}\"} other {}}",
"annotation-engines": {
"dictionary": "{isHint, select, true{Hinweis} other{Schwärzung}} basiert auf Wörterbuch",
"dossier-dictionary": "Annotation basiert auf Dossier-Wörterbuch",
@ -403,6 +394,9 @@
"skipped": "Übersprungen",
"text-highlight": "Markierung"
},
"annotation": {
"pending": "(Analyse steht aus)"
},
"annotations": "Annotationen",
"archived-dossiers-listing": {
"no-data": {
@ -532,11 +526,11 @@
"add-title": "Neue Definition hinzufügen",
"columns": {
"name": "Name",
"position": "pos."
"position": "Pos."
},
"edit-title": "Definition von {displayName} bearbeiten",
"form": {
"autogenerated-label": "Wurde ausgehend vom initialen Anzeigenamen automatisch generiert",
"autogenerated-label": "Wird ausgehend vom ersten Anzeigenamen automatisch generiert.",
"description": "Beschreibung",
"description-placeholder": "Beschreibung",
"display-name": "Anzeigename",
@ -560,9 +554,6 @@
"undo": "Zurücksetzen"
},
"components": "Komponenten",
"filter": {
"search-placeholder": "Suche nach Komponente..."
},
"table-header": {
"component": "Komponente",
"value": "Wert"
@ -581,7 +572,7 @@
},
"search": "Nach Name suchen...",
"table-col-names": {
"column-labels": "Column labels",
"column-labels": "Spaltenbeschriftungen",
"name": "Name",
"number-of-lines": "Zeilenzahl",
"version": "Version"
@ -624,7 +615,7 @@
"impacted-report": "{reportsCount} Berichte nutzen den Platzhalter dieses Attributs. Bitte aktualisieren Sie diese.",
"title": "{count, plural, one{Attribut} other{Attribute}} löschen",
"toast-error": "Bitte bestätigen Sie, dass die Folgen dieser Aktion verstehen.",
"warning": "Warnung: Aktion kann nicht rückgängig gemacht werden."
"warning": "Warnung: Wiederherstellung des Attributs nicht möglich."
},
"confirm-delete-dossier-state": {
"cancel": "Abbrechen",
@ -646,18 +637,18 @@
"impacted-dossiers": "{dossiersCount} {dossiersCount, plural, one{Dossier} other{Dossiers}} sind betroffen",
"title": "{usersCount, plural, one{Benutzer} other{Benutzer}} aus Workspace entfernen",
"toast-error": "Bitte bestätigen Sie, dass Sie die Folgen dieser Aktion verstehen.",
"warning": "Warnung: Aktion kann nicht rückgängig gemacht werden."
"warning": "Warnung: Wiederherstellung des Benutzers nicht möglich."
},
"confirmation-dialog": {
"approve-file": {
"confirmationText": "Dennoch freigeben",
"confirmationText": "Trotzdem genehmigen",
"denyText": "Nein, abbrechen",
"question": "Dieses Dokument enthält ungesehene Änderungen, die sich durch die Reanalyse ergeben haben.<br><br>Möchten Sie es trotzdem freigeben?",
"title": "Warnung!",
"warning-reason": {
"legal-basis-missing": "Legal basis missing",
"pending-changes": "Ausstehende Änderungen",
"unmapped-justification": "Unmapped justification"
"legal-basis-missing": "Rechtsgrundlage fehlt",
"pending-changes": "Änderungen stehen aus",
"unmapped-justification": "Nicht gemappte Begründung"
}
},
"assign-file-to-me": {
@ -712,9 +703,6 @@
}
},
"content": "Grund",
"copilot": {
"label": "Copilot"
},
"dashboard": {
"empty-template": {
"description": "Diese Vorlage enthält keine Dossiers. Erstellen Sie ein Dossier, um nach diesen Regeln zu schwärzen.",
@ -941,7 +929,7 @@
"action": "Ganzes Dossier analysieren"
},
"rules": {
"timeoutError": "Regeln der Dossier-Vorlage gesperrt!"
"timeoutError": "Regeln für Dossier-Vorlagen gesperrt!"
},
"stats": {
"analyzed-pages": "{count, plural, one{Seite} other{Seiten}}",
@ -958,7 +946,7 @@
"table-header": {
"title": "{length} {length, plural, one{aktives Dossier} other{aktive Dossiers}}"
},
"template-inactive": "Die aktuell ausgewählte Dossier-Vorlage ist inaktiv."
"template-inactive": "Aktuell ausgewählte Dossier-Vorlage inaktiv!"
},
"dossier-overview": {
"approve": "Genehmigen",
@ -1036,13 +1024,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": "Bericht herunterladen",
"start-auto-analysis": "Auto-Analyse aktivieren",
"stop-auto-analysis": "Auto-Analyse anhalten",
@ -1118,14 +1106,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",
@ -1160,6 +1140,14 @@
"title": "{length} {length, plural, one{archiviertes Dossier} other{archivierte Dossiers}}"
}
},
"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.",
@ -1327,7 +1315,7 @@
"options": {
"in-document": {
"description": "",
"label": "Im Dokument bearbeiten"
"label": "In Dokument ändern"
},
"only-here": {
"description": "",
@ -1370,15 +1358,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."
@ -1392,19 +1371,28 @@
"title": "Entitätsregeln-Editor",
"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": {
@ -1422,12 +1410,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",
@ -1438,6 +1420,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": {
@ -1534,6 +1522,7 @@
"csv-column": "CSV-Spalte",
"displayed-in-file-list": "In Dokumentenliste anzeigen",
"filterable": "Filterbar",
"include-in-csv-export": "",
"name": "Name",
"primary": "Primärattribut",
"primary-info-tooltip": "Der Wert des Primärattributs steht in der Dokumentenliste unter dem Dateinamen.",
@ -1652,18 +1641,9 @@
"file-upload": {
"type": {
"csv": "Die Datei-Attribute wurden erfolgreich aus der hochgeladenen CSV-Datei importiert.",
"zip": "Die Zip-Datei wurde erfolgreich hochgeladen!"
"zip": ""
}
},
"filter": {
"analysis": "Analyse erforderlich",
"comment": "Kommentare",
"hint": "Nur Hinweise",
"image": "Bilder",
"none": "Keine Annotationen",
"redaction": "Annotationen",
"updated": "Aktualisiert"
},
"filter-menu": {
"filter-options": "Filteroptionen",
"filter-types": "Filter",
@ -1673,6 +1653,15 @@
"unseen-pages": "Nur Anmerkungen auf ungesehenen Seiten",
"with-comments": "Nur Anmerkungen mit Kommentaren"
},
"filter": {
"analysis": "Analyse erforderlich",
"comment": "Kommentare",
"hint": "Nur Hinweise",
"image": "Bilder",
"none": "Keine Annotationen",
"redaction": "Annotationen",
"updated": "Aktualisiert"
},
"filters": {
"assigned-people": "Bearbeiter",
"documents-status": "Dokumentenstatus",
@ -1741,11 +1730,11 @@
"title": "SMTP-Konto konfigurieren"
},
"generic-errors": {
"400": "Die gesendete Anfrage ist ungültig.",
"403": "Der Zugriff auf die angeforderte Ressource ist nicht zulässig.",
"404": "Die angeforderte Ressource konnte nicht gefunden werden.",
"409": "Die Anfrage ist nicht mit dem aktuellen Status kompatibel.",
"500": "Der Server hat einen unerwarteten Fehler festgestellt und konnte die Anfrage nicht bearbeiten."
"400": "",
"403": "",
"404": "",
"409": "",
"500": ""
},
"help-button": {
"disable": "Hilfemodus deaktivieren",
@ -1917,15 +1906,15 @@
"legalBasis": "Rechtsgrundlage",
"options": {
"multiple-pages": {
"description": "Fügen Sie die Annotation auf mehreren Seiten hinzu.",
"extraOptionDescription": "Minus (-) für Bereich und Komma (,) für Aufzählung.",
"extraOptionLabel": "Seiten",
"description": "Annotation auf folgenden Seiten bearbeiten",
"extraOptionDescription": "Minus (-) für Seitenbereich und Komma (,) für Aufzählung.",
"extraOptionLabel": "Seitenbereich",
"extraOptionPlaceholder": "z. B. 1-20,22,32",
"label": "Auf mehreren Seiten anwenden"
},
"only-this-page": {
"description": "Fügen Sie die Annotation nur an dieser Stelle im Dokument hinzu.",
"label": "Nur auf dieser Seite anwenden"
"description": "Annotation nur an dieser Position im Dokument bearbeiten",
"label": "Auf dieser Seite anwenden"
}
},
"reason": "Grund",
@ -1952,22 +1941,15 @@
"document-approved": "<b>{fileHref, select, null{{fileName}} other{<a href=\"{fileHref}\" target=\"_blank\">{fileName}</a>}}</b> wurde genehmigt.",
"dossier-deleted": "Dossier wurde gelöscht: <b>{dossierName}</b>",
"dossier-owner-deleted": "Der Besitzer des Dossiers wurde gelöscht: <b>{dossierName}</b>",
"dossier-owner-removed": "Sie wurden <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b> ",
"dossier-owner-removed": "Der Dossier-Owner von <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b> wurde entfernt!",
"dossier-owner-set": "Sie sind jetzt Besitzer des Dossiers <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>.",
"download-ready": "Ihr <b><a href=\"{downloadHref}\", target=\"_self\">Download</a></b> steht bereit.",
"no-data": "Sie haben aktuell keine Benachrichtigungen",
"unassigned-from-file": "Sie wurden von einem Dokument entfernt. <br>Dokument: <b>{fileHref, select, null{{fileName}} other{<a href=\"{fileHref}\" target=\"_blank\">{fileName}</a>}}</b> <br>Dossier: <b>{dossierHref, select, null{{dossierName}} other{{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}}}</b>",
"user-becomes-dossier-member": "Sie wurden zu einem Dossier hinzugefügt: <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>!",
"user-becomes-dossier-member": "<b>{user}</b> ist jetzt Mitglied des Dossiers <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>!",
"user-demoted-to-reviewer": "Sie wurden auf die Reviewer-Rolle heruntergestuft: <b>{dossierHref, select, null{{dossierName}}\n other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>",
"user-promoted-to-approver": "Sie wurden in einem Dossier zum Genehmiger ernannt: <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b>",
"user-removed-as-dossier-member": "Sie wurden als Mitglied entfernt: <b>{dossierHref, select, null{{dossierName}} other{<a href=\"{dossierHref}\" target=\"_blank\">{dossierName}</a>}}</b> "
},
"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"
"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-screen": {
"category": {
@ -1975,13 +1957,14 @@
"in-app-notifications": "In-App-Benachrichtigungen"
},
"error": {
"generic": "Aktualisierung der Präferenzen fehlgeschlagen."
"generic": "Fehler: Aktualisierung der Präferenzen fehlgeschlagen."
},
"groups": {
"document": "Benachrichtigungen zu Dokumenten",
"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 bin",
"ASSIGN_REVIEWER": "Wenn ich einem Dokument als Prüfer zugewiesen werde",
@ -1999,7 +1982,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": "Sofortig",
@ -2007,6 +1989,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",
@ -2058,20 +2047,20 @@
"compare-button": "Vergleichen",
"layers-panel-button": "Ebenen",
"left-panel-button": "Panel",
"load-all-annotations": "Alle Annotationen laden",
"load-all-annotations": "Alle Annotationen geladen",
"no-outlines-text": "Keine Gliederung verfügbar",
"no-signatures-text": "Dieses Dokument enthält keine Unterschriftenfelder.",
"outline-multi-select": "Bearbeiten",
"outlines-panel-button": "Gliederung",
"pan-tool-button": "Verschieben",
"rectangle-tool-button": "Bereich annotieren",
"rotate-left-button": "Nach links drehen",
"rotate-right-button": "Nach rechts drehen",
"rectangle-tool-button": "Bereich schwärzen",
"rotate-left-button": "Seite nach links drehen",
"rotate-right-button": "Seite nach rechts drehen",
"select-tool-button": "Auswählen",
"signature-panel-button": "Unterschriften",
"thumbnails-panel-button": "Miniaturansicht",
"toggle-layers": "Layoutraster {active, select, true{deaktivieren} false{aktivieren} other{}}",
"toggle-readable-redactions": "Show redactions {active, select, true{as in final document} false{in preview color} other{}}",
"toggle-layers": "Layout-Raster {active, select, true{deaktivieren} false{aktivieren} other{}}",
"toggle-readable-redactions": "Schwärzungen {active, select, true{wie im finalen Dokument} false{in Preview-Farbe anzeigen} other{}}",
"toggle-tooltips": "Tooltips für Annotationen {active, select, true{deaktivieren} false{aktivieren} other{}}",
"zoom-in-button": "Vergrößern",
"zoom-out-button": "Verkleinern"
@ -2118,10 +2107,6 @@
"warnings-label": "Dialoge und Meldungen",
"warnings-subtitle": "„Nicht mehr anzeigen“-Optionen"
},
"processing": {
"basic": "Verarbeitung läuft",
"ocr": "OCR"
},
"processing-status": {
"ocr": "OCR",
"pending": "Ausstehend",
@ -2129,6 +2114,10 @@
"processed": "Verarbeitet",
"processing": "Verarbeitung läuft"
},
"processing": {
"basic": "Verarbeitung läuft",
"ocr": "OCR"
},
"readonly": "Lesemodus",
"readonly-archived":,
"redact-text": {
@ -2145,8 +2134,8 @@
"legal-basis": "Rechtsgrundlage",
"options": {
"in-document": {
"description": "Add redaction for each occurrence of the term in this document.",
"label": "Redact in document"
"description": "Fügen Sie die Schwärzung an allen Stellen in diesem Dokument hinzu.",
"label": "Im Dokument schwärzen"
},
"in-dossier": {
"description": "Fügen Sie die Schwärzung zu jedem Dokument in {dossierName} hinzu.",
@ -2164,7 +2153,7 @@
"type": "Typ",
"type-placeholder": "Typ auswählen...",
"unchanged": "",
"value": "Value"
"value": "Wert"
},
"title": "Text schwärzen"
}
@ -2378,16 +2367,6 @@
"header": "Größe von {type} ändern"
}
},
"revert-manual-changes-dialog": {
"actions": {
"cancel": "Abbrechen",
"save": "Zu den Originalen zurückkehren"
},
"details": {
"title": "Manuelle Änderungen"
},
"title": "Lokale manuelle Änderungen rückgängig machen"
},
"revert-value-dialog": {
"actions": {
"cancel": "Abbrechen",
@ -2408,12 +2387,6 @@
"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",
@ -2437,6 +2410,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": {
@ -2541,7 +2520,7 @@
"overwrite": "Überschreiben"
},
"question": "Wie möchten Sie vorgehen?",
"title": "Das Wörterbuch hat bereits Einträge."
"title": "Das Wörterbuch hat bereits Einträge!"
},
"upload-file": {
"upload-area-text": "Klicken Sie hier oder ziehen Sie die Datei in diesen Bereich..."

View File

@ -173,6 +173,7 @@
"displayedInFileList": "Displayed in file list",
"filterable": "Filterable",
"filterable-disabled": "The maximum number of filterable attributes ({maxNumber}) has been reached.",
"includeInCsvExport": "Include in CSV export",
"name": "Attribute name",
"name-placeholder": "Enter name",
"primary": "Set as primary",
@ -338,10 +339,6 @@
"error": "Failed to remove annotation: {error}",
"success": "Annotation removed"
},
"revert-changes": {
"error": "Failed to revert manual changes: {error}",
"success": "Reverted manual changes successfully."
},
"undo": {
"error": "Failed to undo: {error}",
"success": "Undo successful"
@ -363,9 +360,6 @@
"resize": {
"label": "Resize"
},
"revert-changes": {
"label": "Revert changes"
},
"see-references": {
"label": "See references"
},
@ -560,9 +554,6 @@
"undo": "Undo"
},
"components": "Components",
"filter": {
"search-placeholder": "Search by component..."
},
"table-header": {
"component": "Component",
"value": "Value"
@ -642,7 +633,7 @@
"confirm-delete-users": {
"cancel": "Keep {usersCount, plural, one{user} other{users}}",
"delete": "Delete {usersCount, plural, one{user} other{users}}",
"impacted-documents": "{documentsCount} {documentsCount, plural, one{document} other{documents}} will be impacted",
"impacted-documents": "All documents pending review from the {usersCount, plural, one{user} other{users}} will be impacted",
"impacted-dossiers": "{dossiersCount} {dossiersCount, plural, one{dossier} other{dossiers}} will be impacted",
"title": "Delete {usersCount, plural, one{user} other{users}} from workspace",
"toast-error": "Please confirm that you understand the consequences of this action.",
@ -712,9 +703,6 @@
}
},
"content": "Reason",
"copilot": {
"label": "Copilot"
},
"dashboard": {
"empty-template": {
"description": "This template does not contain any dossiers. Create a dossier that applies this ruleset.",
@ -1327,7 +1315,7 @@
"options": {
"in-document": {
"description": "",
"label": "Edit in document"
"label": "In Dokument ändern"
},
"only-here": {
"description": "",
@ -1534,6 +1522,7 @@
"csv-column": "CSV column",
"displayed-in-file-list": "Displayed in file list",
"filterable": "Filterable",
"include-in-csv-export": "Include in CSV export",
"name": "Name",
"primary": "Primary",
"primary-info-tooltip": "The value of the attribute set as primary is diplayed below the file name in the document list.",
@ -1917,9 +1906,9 @@
"legalBasis": "Legal Basis",
"options": {
"multiple-pages": {
"description": "Add annotation on a range of pages",
"description": "Edit annotation the range of pages",
"extraOptionDescription": "Minus(-) for range and comma(,) for enumeration",
"extraOptionLabel": "Pages",
"extraOptionLabel": "Range",
"extraOptionPlaceholder": "e.g. 1-20,22,32",
"label": "Apply on multiple pages"
},
@ -1968,7 +1957,7 @@
"in-app-notifications": "In-app notifications"
},
"error": {
"generic": "Preferences update failed."
"generic": "Something went wrong... Preferences update failed."
},
"groups": {
"document": "Document related notifications",
@ -2378,16 +2367,6 @@
"header": "Resize {type}"
}
},
"revert-manual-changes-dialog": {
"actions": {
"cancel": "Cancel",
"save": "Revert to originals"
},
"details": {
"title": "Manual changes"
},
"title": "Revert local manual changes"
},
"revert-value-dialog": {
"actions": {
"cancel": "Cancel",

View File

@ -1,12 +0,0 @@
<svg width="100%" height="100%" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor" stroke="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M10.7389 1.57074C9.61055 1.57074 8.5881 2.02374 7.84324 2.75924L9.09148 2.64397L9.19008 3.71175L5.98676 4.00756L5.69095 0.804226L6.75873 0.705624L6.8882 2.1077C7.84807 1.07713 9.21818 0.431396 10.7389 0.431396C13.6446 0.431396 16 2.78686 16 5.69246C16 8.00114 14.513 9.96249 12.4444 10.6709V9.4459C13.8698 8.79722 14.8607 7.36057 14.8607 5.69246C14.8607 3.4161 13.0153 1.57074 10.7389 1.57074Z"
/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M5.12999 5.91604C5.1117 5.12697 4.46581 4.49243 3.67235 4.49243L3.63795 4.49283L3.60374 4.49402C2.83061 4.52987 2.21432 5.1685 2.21432 5.95044L2.21427 10.5306L2.17777 10.5151C1.55239 10.2584 0.814204 10.4274 0.365254 10.9664L0.340815 10.9966L0.316849 11.0276C-0.153667 11.6531 -0.0961886 12.5351 0.462852 13.0937L1.84534 14.4762L1.84539 14.6236L1.84614 14.652C1.8531 14.7838 1.90846 14.9086 2.00217 15.0023C2.10251 15.1027 2.23862 15.1591 2.38042 15.1591H10.1312L10.1595 15.1583C10.2913 15.1514 10.4162 15.096 10.5099 15.0023C10.6103 14.9019 10.6667 14.7658 10.6667 14.6236V8.53383L10.6661 8.49249C10.6555 8.1211 10.5033 7.76714 10.2397 7.50302C9.96631 7.22968 9.5954 7.07625 9.20863 7.07625L9.1681 7.07682L9.12388 7.07873C8.82966 7.09597 8.54852 7.20237 8.31717 7.38225L8.28601 7.40701L8.25512 7.38227L8.22172 7.35706C7.97441 7.17584 7.674 7.07629 7.36305 7.07629L7.32249 7.07686L7.28024 7.07865C6.98508 7.09547 6.70318 7.20186 6.47141 7.38225L6.44088 7.40668L6.42613 7.3945L6.3935 7.36894C6.04214 7.1019 5.58724 7.01172 5.16238 7.12206L5.13034 7.13076L5.13039 5.95044L5.12999 5.91604ZM3.2854 5.92921C3.29644 5.72504 3.46544 5.56294 3.67237 5.56294C3.88643 5.56294 4.05991 5.73641 4.05991 5.95047V10.1947L4.06043 10.2186C4.07289 10.5033 4.30738 10.7302 4.59493 10.7302C4.89081 10.7302 5.13039 10.4906 5.13039 10.1947V8.53388L5.13096 8.51261C5.142 8.30845 5.311 8.14635 5.51793 8.14635C5.73199 8.14635 5.90547 8.31982 5.90547 8.53388V10.1947L5.90599 10.2186C5.91845 10.5033 6.15294 10.7302 6.44049 10.7302C6.73603 10.7302 6.97552 10.4905 6.97552 10.1947V8.53388L6.97609 8.51261C6.98713 8.30845 7.15613 8.14635 7.36306 8.14635C7.57712 8.14635 7.75059 8.31982 7.75059 8.53388V10.1947L7.75112 10.2186C7.76359 10.5034 7.99817 10.7302 8.28605 10.7302C8.58159 10.7302 8.82108 10.4905 8.82108 10.1947V8.53388L8.82165 8.51261C8.83269 8.30845 9.00169 8.14635 9.20862 8.14635L9.22988 8.14692C9.43405 8.15796 9.59615 8.32696 9.59615 8.53388L9.59611 14.0886H2.88926L2.88027 14.063C2.85363 13.9937 2.81261 13.93 2.75909 13.8765L1.19969 12.3169C1.11077 12.2223 1.06145 12.1002 1.05991 11.9721C1.05863 11.8356 1.11237 11.7041 1.20917 11.6069C1.3059 11.5102 1.43762 11.4565 1.57441 11.4581C1.71139 11.4594 1.8418 11.516 1.93649 11.6149L2.37112 12.0496L2.38985 12.0674C2.54251 12.2061 2.76229 12.2451 2.95431 12.1656C3.15436 12.0829 3.28483 11.8878 3.28483 11.6713V5.95047L3.2854 5.92921Z"
/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,4 +1,3 @@
@use 'sass:color';
@use 'common-mixins';
@use 'variables';
@ -56,8 +55,8 @@
}
.ERROR {
stroke: color.adjust(variables.$primary, $lightness: 25%);
background-color: color.adjust(variables.$primary, $lightness: 25%);
stroke: lighten(variables.$primary, 25%);
background-color: lighten(variables.$primary, 25%);
}
.ACTIVE {

View File

@ -1,4 +1,3 @@
@use 'sass:color';
@use '@angular/material' as mat;
@use 'variables';
@ -7,8 +6,8 @@
$primary-palette: (
default: variables.$primary,
lighter: color.adjust(variables.$primary, $lightness: 30%),
darker: color.adjust(variables.$primary, $lightness: -30%),
lighter: lighten(variables.$primary, 30%),
darker: darken(variables.$primary, 30%),
text: variables.$primary,
contrast: (
default: variables.$light,
@ -19,8 +18,8 @@ $primary-palette: (
$secondary-palette: (
default: variables.$accent,
lighter: color.adjust(variables.$accent, $lightness: 30%),
darker: color.adjust(variables.$accent, $lightness: -30%),
lighter: lighten(variables.$accent, 30%),
darker: darken(variables.$accent, 30%),
text: variables.$accent,
contrast: (
default: variables.$light,
@ -31,8 +30,8 @@ $secondary-palette: (
$red-palette: (
default: variables.$primary,
lighter: color.adjust(variables.$primary, $lightness: 30%),
darker: color.adjust(variables.$primary, $lightness: -30%),
lighter: lighten(variables.$primary, 30%),
darker: darken(variables.$primary, 30%),
text: variables.$primary,
contrast: (
default: variables.$light,

View File

@ -1,4 +1,3 @@
@use 'sass:color';
/* You can add global styles to this file, and also import other style files */
@use 'variables' as vars;
@use 'common-functions';
@ -110,12 +109,12 @@
$iqser-app-name-color: vars.$accent
);
$light-accent-5: color.adjust(vars.$accent, $lightness: 5%);
$light-accent-10: color.adjust(vars.$accent, $lightness: 10%);
$light-accent-5: lighten(vars.$accent, 5%);
$light-accent-10: lighten(vars.$accent, 10%);
$dark-accent-5: color.adjust(vars.$accent, $lightness: -5%);
$dark-accent-8: color.adjust(vars.$accent, $lightness: -8%);
$dark-accent-10: color.adjust(vars.$accent, $lightness: -10%);
$dark-accent-5: darken(vars.$accent, 5%);
$dark-accent-8: darken(vars.$accent, 8%);
$dark-accent-10: darken(vars.$accent, 10%);
@include common-variables.configureDark(
$iqser-primary: vars.$primary,
@ -173,8 +172,7 @@ body {
--documine-viewer-width: calc(
100% - var(--structured-component-management-width) - calc(var(--documine-workload-content-width) - 55px) - var(
--quick-navigation-width
) -
3px
) - 3px
);
--viewer-height: calc(100% - calc(var(--iqser-top-bar-height) + 50px));
}
@ -200,8 +198,7 @@ body {
width: calc(
100% - var(--structured-component-management-width) - calc(var(--documine-workload-content-width) - 55px) - var(
--workload-width
) -
3px
) - 3px
);
right: calc(var(--workload-width) + 1px);
}

View File

@ -46,7 +46,7 @@ echo '{
"AVAILABLE_NOTIFICATIONS_DAYS":"'"$AVAILABLE_NOTIFICATIONS_DAYS"'",
"AVAILABLE_OLD_NOTIFICATIONS_MINUTES":"'"$AVAILABLE_OLD_NOTIFICATIONS_MINUTES"'",
"NOTIFICATIONS_THRESHOLD":"'"$NOTIFICATIONS_THRESHOLD"'",
"WATERMARK_PREVIEW_PAPER_FORMAT":"'"$WATERMARK_PREVIEW_PAPER_FORMAT"'",
"WATERMARK_PREVIEW_PAPER_FORMAT":"'"$WATERMARK_PREVIEW_PAPER_FORMAT"'"
"LANDING_PAGE_THEME":"'"$LANDING_PAGE_THEME"'"
}' >/usr/share/nginx/html/ui/assets/config/config.json

@ -1 +1 @@
Subproject commit 7f13fa62d3d2b346c609bc4978cff75f37f1ee6b
Subproject commit 7bb24c4f919faa2525c728315c4f679b1af45842

View File

@ -22,7 +22,6 @@
"style": "kebab-case"
}
],
"@angular-eslint/no-host-metadata-property": "off",
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/naming-convention": [

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