diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8ace6ab80..bb177da4b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,41 +4,40 @@ 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"
+ - 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"
diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts
index 41f9e4bbc..44759e43d 100644
--- a/apps/red-ui/src/app/app.module.ts
+++ b/apps/red-ui/src/app/app.module.ts
@@ -135,20 +135,20 @@ export const appModuleFactory = (config: AppConfig) => {
features: {
ANNOTATIONS: {
color: 'aqua',
- enabled: true,
+ enabled: false,
level: NgxLoggerLevel.DEBUG,
},
FILTERS: {
enabled: false,
},
TENANTS: {
- enabled: true,
+ enabled: false,
},
ROUTES: {
- enabled: true,
+ enabled: false,
},
PDF: {
- enabled: true,
+ enabled: false,
},
FILE: {
enabled: false,
@@ -171,6 +171,9 @@ export const appModuleFactory = (config: AppConfig) => {
DOSSIERS_CHANGES: {
enabled: false,
},
+ GUARDS: {
+ enabled: false,
+ },
},
} as ILoggerConfig,
},
diff --git a/apps/red-ui/src/app/guards/if-logged-in.guard.ts b/apps/red-ui/src/app/guards/if-logged-in.guard.ts
index caf15c448..4d7ed1b64 100644
--- a/apps/red-ui/src/app/guards/if-logged-in.guard.ts
+++ b/apps/red-ui/src/app/guards/if-logged-in.guard.ts
@@ -40,8 +40,7 @@ export function ifLoggedIn(): AsyncGuard {
logger.info('[KEYCLOAK] Keycloak init...');
await keycloakInitializer(tenant);
- logger.info('[KEYCLOAK] Keycloak init done!');
- console.log({ tenant });
+ logger.info('[KEYCLOAK] Keycloak init done for tenant: ', { tenant });
await tenantsService.selectTenant(tenant);
await usersService.initialize();
await licenseService.loadLicenses();
@@ -51,6 +50,7 @@ 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);
}
}
diff --git a/apps/red-ui/src/app/modules/admin/admin.routes.ts b/apps/red-ui/src/app/modules/admin/admin.routes.ts
index fd10a56ab..127879694 100644
--- a/apps/red-ui/src/app/modules/admin/admin.routes.ts
+++ b/apps/red-ui/src/app/modules/admin/admin.routes.ts
@@ -1,22 +1,24 @@
-import { CompositeRouteGuard, IqserPermissionsGuard, IqserRoutes } from '@iqser/common-ui';
-import { RedRoleGuard } from '@users/red-role.guard';
-import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component';
+import { inject, provideEnvironmentInitializer } from '@angular/core';
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 { 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 { 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 { PermissionsGuard } from '@guards/permissions-guard';
-import { Roles } from '@users/roles';
-import { IqserAuthGuard } from '@iqser/common-ui/lib/users';
+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 { AdminDialogService } from './services/admin-dialog.service';
import { AuditService } from './services/audit.service';
import { DigitalSignatureService } from './services/digital-signature.service';
@@ -78,7 +80,12 @@ const dossierTemplateIdRoutes: IqserRoutes = [
},
type: 'ENTITY',
},
- providers: [RulesService],
+ providers: [
+ RulesService,
+ provideEnvironmentInitializer(() => {
+ return inject(CopilotService).connectAsync('/api/llm/llm-websocket');
+ }),
+ ],
},
{
path: 'component-rules',
diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html
index d2ab68930..83e6cc357 100644
--- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html
+++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html
@@ -1,8 +1,57 @@
-
+
+
+
+
+
+
+
+
+
+ @for (comment of conversation(); track comment) {
+
+ }
+
+
+
+
+
+
+
+
+
@@ -12,15 +61,15 @@
@@ -29,11 +78,11 @@
diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss
index 49e950daf..b36407be4 100644
--- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss
+++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss
@@ -81,3 +81,22 @@ 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;
+}
diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts
index 4e2a7c5f8..33ae6d39c 100644
--- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts
+++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts
@@ -1,22 +1,40 @@
-import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, OnInit, signal } from '@angular/core';
-import { PermissionsService } from '@services/permissions.service';
-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 { 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 { Debounce, getParam } from '@iqser/common-ui/lib/utils';
-import { ActivatedRoute } from '@angular/router';
+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 { PermissionsService } from '@services/permissions.service';
+import { DatePipe } from '@shared/pipes/date.pipe';
+import { firstValueFrom } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { RulesService } from '../../../services/rules.service';
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;
@@ -31,18 +49,57 @@ 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],
+ imports: [
+ MonacoEditorModule,
+ MatIcon,
+ FormsModule,
+ IconButtonComponent,
+ NgIf,
+ TranslateModule,
+ CircleButtonComponent,
+ InputWithActionComponent,
+ MatTooltip,
+ NgTemplateOutlet,
+ DatePipe,
+ TitleCasePipe,
+ ],
})
export default class RulesScreenComponent implements OnInit, ComponentCanDeactivate {
+ readonly #errorGlyphs = signal([]);
+ #codeEditor: ICodeEditor;
+ #decorations: string[] = [];
+ readonly #errors = signal([]);
+ #ruleValidationTimeout: number = null;
+ readonly #copilotService = inject(CopilotService);
+ readonly #currentUser = getCurrentUser();
+ readonly #conversation = signal([endingSentence]);
+ protected readonly collapsed = signal(true);
+ protected readonly IqserTooltipPositions = IqserTooltipPositions;
+ readonly dossierTemplateId = input.required();
readonly translations = rulesScreenTranslations;
-
readonly iconButtonTypes = IconButtonTypes;
+ readonly inputWithAction = viewChild(InputWithActionComponent);
readonly editorOptions: IStandaloneEditorConstructionOptions = {
theme: 'vs',
language: 'java',
@@ -54,19 +111,43 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
initialLines: string[] = [];
currentLines: string[] = [];
isLeaving = false;
- readonly type: IRules['ruleFileType'];
- readonly #errorGlyphs = signal([]);
+ readonly type = input.required();
readonly numberOfErrors = computed(() => this.#errors().filter(e => !e.warning).length);
readonly numberOfWarnings = computed(() => this.#errors().filter(e => e.warning).length);
- readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
- #codeEditor: ICodeEditor;
- #decorations: string[] = [];
- #errors = signal([]);
- #ruleValidationTimeout: number = null;
+ 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 }];
+ });
+ });
+ }
set isLeavingPage(isLeaving: boolean) {
this.isLeaving = isLeaving;
- this._changeDetectorRef.detectChanges();
+ this._changeDetectorRef.markForCheck();
}
get changed(): boolean {
@@ -84,16 +165,23 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
this.#closeProblemsView();
}
- 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;
+ 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);
}
async ngOnInit() {
@@ -121,7 +209,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
}
(window as any).monaco.editor.setTheme(this._editorThemeService.getTheme(true));
await this.#configureSyntaxHighlighting();
- this._changeDetectorRef.detectChanges();
+ this._changeDetectorRef.markForCheck();
}
@Debounce()
@@ -141,12 +229,20 @@ 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(
@@ -155,7 +251,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 => {
@@ -172,20 +268,12 @@ 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) {
@@ -295,7 +383,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();
diff --git a/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts b/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts
index 565dd35d1..1325d4ccd 100644
--- a/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts
+++ b/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts
@@ -1,3 +1,4 @@
+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 {
@@ -16,6 +17,7 @@ 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,
@@ -26,6 +28,7 @@ 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';
@@ -33,6 +36,7 @@ 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';
@@ -42,17 +46,13 @@ 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',
diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html
index 3a79d9f8e..3d08ac883 100644
--- a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html
+++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html
@@ -83,14 +83,15 @@
icon="iqser:trash"
>
-
+
+
+
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 annotationChangesAllowed = computed(
+ () => (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.somePending(),
+ );
readonly canRemoveRedaction = computed(
() => this.annotationChangesAllowed() && this.annotationPermissions().canRemoveRedaction && this.sameType(),
);
@@ -75,10 +79,6 @@ 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,
);
diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts
index f36557668..065d0ccd2 100644
--- a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts
+++ b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts
@@ -1,3 +1,5 @@
+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';
@@ -5,25 +7,24 @@ 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 { 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';
+import { AnnotationWrapperComponent } from '../annotation-wrapper/annotation-wrapper.component';
+import { HighlightsSeparatorComponent } from '../highlights-separator/highlights-separator.component';
@Component({
selector: 'redaction-annotations-list',
templateUrl: './annotations-list.component.html',
styleUrls: ['./annotations-list.component.scss'],
- imports: [NgIf, HighlightsSeparatorComponent, AnnotationWrapperComponent, AnnotationReferencesListComponent],
+ imports: [NgIf, AnnotationReferencesListComponent, HighlightsSeparatorComponent, AnnotationWrapperComponent],
})
export class AnnotationsListComponent extends HasScrollbarDirective {
+ protected readonly isDocumine = getConfig().IS_DOCUMINE;
readonly annotations = input.required[]>();
readonly pagesPanelActive = output();
readonly displayedHighlightGroups = computed(() => {
@@ -43,7 +44,6 @@ export class AnnotationsListComponent extends HasScrollbarDirective {
return result;
});
- protected readonly isDocumine = getConfig().IS_DOCUMINE;
constructor(
protected readonly _elementRef: ElementRef,
diff --git a/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.html b/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.html
index cdb189220..4b188f4c3 100644
--- a/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.html
+++ b/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.html
@@ -41,7 +41,7 @@
>
[{ length: 1, color: this.state.file().workflowStatus }]);
readonly assignTooltip = computed(() => {
@@ -71,6 +73,13 @@ 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;
@@ -84,6 +93,11 @@ 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 };
diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts
index e4abcaae4..b4e5f147d 100644
--- a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts
+++ b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts
@@ -6,6 +6,7 @@ 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';
@@ -16,7 +17,6 @@ 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,
diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts
index 6074bc6a1..33a23038f 100644
--- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts
+++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts
@@ -1,10 +1,12 @@
-import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
+import { CdkDrag, CdkDragEnd, CdkDragMove, CdkDragStart } from '@angular/cdk/drag-drop';
+import { NgIf } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
effect,
ElementRef,
+ inject,
NgZone,
OnDestroy,
OnInit,
@@ -13,6 +15,7 @@ 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 {
@@ -29,21 +32,25 @@ import {
Toaster,
} from '@iqser/common-ui';
import { copyLocalStorageFiltersValues, FilterService, INestedFilter, NestedFilter, processFilters } from '@iqser/common-ui/lib/filtering';
-import { AutoUnsubscribe, Bind, bool, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils';
+import { AutoUnsubscribe, Bind, bool, List, log, 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 { File, ViewModes } from '@red/domain';
+import { TranslateModule } from '@ngx-translate/core';
+import { AnalyseStatuses, AnalysisEvent, File, ViewModes, WsTopics } 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 } from 'rxjs';
+import { combineLatest, first, firstValueFrom, Observable, of, pairwise, Subscription } 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';
@@ -54,33 +61,28 @@ 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 { 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';
+import { RedactTextData } from './utils/dialog-types';
@Component({
templateUrl: './file-preview-screen.component.html',
@@ -99,18 +101,27 @@ import { CdkDrag, CdkDragEnd, CdkDragMove, CdkDragStart } from '@angular/cdk/dra
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('resize');
- protected readonly isDocumine = getConfig().IS_DOCUMINE;
@ViewChild('annotationFilterTemplate', {
read: TemplateRef,
static: false,
})
private readonly _filterTemplate: TemplateRef;
#loadAllAnnotationsEnabled = false;
+ readonly #wsConnection$ = inject(WebSocketService)
+ .listen(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('resize');
constructor(
readonly pdf: PdfViewer,
@@ -152,11 +163,6 @@ 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) {
@@ -164,6 +170,13 @@ 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()) {
@@ -286,12 +299,14 @@ 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) {
@@ -354,6 +369,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async ngOnInit(): Promise {
+ this.#wsConnectionSub = this.#wsConnection$.subscribe();
this.#updateViewerPosition();
const file = this.state.file();
diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts
index ca59f7a45..86058303e 100644
--- a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts
+++ b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts
@@ -1,11 +1,13 @@
-import { IqserRoutes } from '@iqser/common-ui';
-import { FilePreviewScreenComponent } from './file-preview-screen.component';
+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 { 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 [
{
@@ -13,6 +15,15 @@ export default [
component: FilePreviewScreenComponent,
pathMatch: 'full',
canDeactivate: [PendingChangesGuard, DocumentUnloadedGuard],
- providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, TablesService, FileAssignService],
+ providers: [
+ FilePreviewDialogService,
+ ManualRedactionService,
+ DocumentUnloadedGuard,
+ TablesService,
+ FileAssignService,
+ provideEnvironmentInitializer(async () => {
+ return inject(WebSocketService).connectAsync('/redaction-gateway-v1/websocket');
+ }),
+ ],
},
] satisfies IqserRoutes;
diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts
index 651fb0949..2ee928182 100644
--- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts
+++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts
@@ -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 } from '@iqser/common-ui/lib/utils';
+import { List, log } 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,6 +27,7 @@ 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';
@@ -49,7 +50,6 @@ 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 { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component';
@Injectable()
export class AnnotationActionsService {
@@ -362,7 +362,7 @@ export class AnnotationActionsService {
}
async #processObsAndEmit(obs: Observable) {
- await firstValueFrom(obs).finally(() => this._fileDataService.annotationsChanged());
+ await firstValueFrom(obs.pipe(log('==>>[[[CHANGES]]]'))).finally(() => this._fileDataService.annotationsChanged());
}
#getFalsePositiveText(annotation: AnnotationWrapper) {
diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts
index 8cb8a5ece..a87e9e814 100644
--- a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts
+++ b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts
@@ -1,6 +1,8 @@
-import { effect, inject, Injectable, Signal, signal } from '@angular/core';
+import { effect, inject, Injectable, Signal, signal, WritableSignal } 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 {
@@ -27,6 +29,7 @@ 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';
@@ -43,13 +46,14 @@ export function chronologicallyBy(property: (x: T) => string) {
@Injectable()
export class FileDataService extends EntitiesService {
- readonly #annotations = signal([]);
+ readonly #annotations: WritableSignal;
readonly #earmarks = signal
{{ comment.text }}+