diff --git a/apps/red-ui/src/app/models/file/annotation.wrapper.ts b/apps/red-ui/src/app/models/file/annotation.wrapper.ts index a5af43e45..a08479fa6 100644 --- a/apps/red-ui/src/app/models/file/annotation.wrapper.ts +++ b/apps/red-ui/src/app/models/file/annotation.wrapper.ts @@ -10,7 +10,6 @@ import { Dictionary, Earmark, FalsePositiveSuperTypes, - IComment, ILegalBasis, IManualChange, IPoint, @@ -38,7 +37,7 @@ export class AnnotationWrapper implements IListable { recategorizationType: string; color: string; entity: Dictionary; - comments: IComment[] = []; + numberOfComments = 0; firstTopLeftPoint: IPoint; id: string; shortContent: string; @@ -327,7 +326,6 @@ export class AnnotationWrapper implements IListable { annotationWrapper.image = redactionLogEntry.image; annotationWrapper.imported = redactionLogEntry.imported; annotationWrapper.legalBasisValue = redactionLogEntry.legalBasis; - annotationWrapper.comments = redactionLogEntry.comments || []; annotationWrapper.manual = redactionLogEntry.manualChanges?.length > 0; annotationWrapper.engines = redactionLogEntry.engines; annotationWrapper.section = redactionLogEntry.section; diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.html b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.html index 7482014ca..beba7304b 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.html @@ -10,14 +10,14 @@
- {{ annotation.item.comments.length }} + {{ annotation.item.numberOfComments }}
@@ -29,7 +29,16 @@
- + + + +
+
diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts index ce271befd..0bd7abc1c 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts @@ -5,6 +5,8 @@ import { ListItem } from '@models/file/list-item'; import { MultiSelectService } from '../../services/multi-select.service'; import { PdfProxyService } from '../../services/pdf-proxy.service'; import { ActionsHelpModeKeys } from '../../utils/constants'; +import { CommentsApiService } from '@services/comments-api.service'; +import { FilePreviewStateService } from '../../services/file-preview-state.service'; @Component({ selector: 'redaction-annotation-wrapper', @@ -13,15 +15,22 @@ import { ActionsHelpModeKeys } from '../../utils/constants'; }) export class AnnotationWrapperComponent implements OnChanges { readonly #isDocumine = getConfig().IS_DOCUMINE; + readonly #commentsApiService = inject(CommentsApiService); protected readonly _pdfProxyService = inject(PdfProxyService); protected readonly _multiSelectService = inject(MultiSelectService); + readonly state = inject(FilePreviewStateService); + actionsHelpModeKey?: string; + showComments = false; @Input({ required: true }) annotation!: ListItem; @HostBinding('attr.annotation-id') annotationId: string; @HostBinding('class.active') active = false; - actionsHelpModeKey?: string; ngOnChanges() { this.annotationId = this.annotation.item.id; + const request = this.#commentsApiService.fetch(this.state.dossierId, this.state.fileId, this.annotationId); + request.then(comments => { + this.annotation.item.numberOfComments = comments.length; + }); this.active = this.annotation.isSelected; this.actionsHelpModeKey = this.#getActionsHelpModeKey(); } diff --git a/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.html b/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.html index 3b7c4a0a2..06601b714 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.html @@ -1,39 +1,30 @@ - -
-
-
- {{ comment.user | name }} - {{ comment.date | date : 'sophisticatedDate' }} -
- -
- -
+
+
+
+ {{ comment.userId | name }} + {{ comment.date | date: 'sophisticatedDate' }}
-
{{ comment.text }}
+
+ +
- +
{{ comment.text }}
+
-
- + diff --git a/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.ts b/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.ts index 578020003..8ffac3ed2 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.ts @@ -1,84 +1,75 @@ -import { ChangeDetectorRef, Component, HostBinding, Input, OnInit, ViewChild } from '@angular/core'; -import type { IComment, User } from '@red/domain'; -import { AnnotationWrapper } from '@models/file/annotation.wrapper'; -import { PermissionsService } from '@services/permissions.service'; +import { Component, inject, Input, OnChanges, signal, SimpleChanges, ViewChild } from '@angular/core'; import { InputWithActionComponent, LoadingService } from '@iqser/common-ui'; -import { Observable } from 'rxjs'; -import { CommentingService } from '../../services/commenting.service'; -import { tap } from 'rxjs/operators'; -import { FilePreviewStateService } from '../../services/file-preview-state.service'; -import { ManualRedactionService } from '../../services/manual-redaction.service'; import { getCurrentUser } from '@iqser/common-ui/lib/users'; -import { ContextComponent, trackByFactory } from '@iqser/common-ui/lib/utils'; - -interface CommentsContext { - hiddenComments: boolean; -} +import { trackByFactory } from '@iqser/common-ui/lib/utils'; +import { AnnotationWrapper } from '@models/file/annotation.wrapper'; +import type { IComment, User } from '@red/domain'; +import { CommentsApiService } from '@services/comments-api.service'; +import { PermissionsService } from '@services/permissions.service'; +import { NGXLogger } from 'ngx-logger'; +import { FilePreviewStateService } from '../../services/file-preview-state.service'; @Component({ selector: 'redaction-comments', templateUrl: './comments.component.html', styleUrls: ['./comments.component.scss'], }) -export class CommentsComponent extends ContextComponent implements OnInit { - @HostBinding('class.hidden') private _hidden = true; - @ViewChild(InputWithActionComponent) private readonly _input: InputWithActionComponent; - @Input() annotation: AnnotationWrapper; - readonly trackBy = trackByFactory(); - readonly currentUser = getCurrentUser(); - hiddenComments$: Observable; +export class CommentsComponent implements OnChanges { + readonly #commentsApiService = inject(CommentsApiService); + readonly #logger = inject(NGXLogger); + protected readonly trackBy = trackByFactory(); + protected readonly currentUser = getCurrentUser(); + readonly comments = signal([]); + @ViewChild(InputWithActionComponent) protected readonly input: InputWithActionComponent; + @Input({ required: true }) annotation: AnnotationWrapper; constructor( readonly permissionsService: PermissionsService, - private readonly _manualRedactionService: ManualRedactionService, - private readonly _commentingService: CommentingService, private readonly _loadingService: LoadingService, - private readonly _changeRef: ChangeDetectorRef, - protected readonly _state: FilePreviewStateService, - ) { - super(); + protected readonly state: FilePreviewStateService, + ) {} + + ngOnChanges(changes: SimpleChanges) { + const currentAnnotation: AnnotationWrapper = changes.annotation?.currentValue; + const previousAnnotation: AnnotationWrapper = changes.annotation?.previousValue; + const annotationChanged = currentAnnotation?.id !== previousAnnotation?.id; + const commentsChanged = currentAnnotation?.numberOfComments !== previousAnnotation?.numberOfComments; + if (annotationChanged || commentsChanged) { + this.#logger.info(`[COMMENTS] State of annotation ${this.annotation.value} changed. Fetch comments.`); + const request = this.#commentsApiService.fetch(this.state.dossierId, this.state.fileId, this.annotation.id); + request.then(comments => { + this.comments.set(comments); + }); + } } - ngOnInit() { - this.hiddenComments$ = this._commentingService.isActive$(this.annotation.id).pipe( - tap(active => { - this._hidden = !active; - }), - ); - - super._initContext({ - hiddenComments: this.hiddenComments$, - }); - } - - async addComment(value: string): Promise { + async add(value: string): Promise { if (!value) { return; } this._loadingService.start(); - const { dossierId, fileId } = this._state; - const commentId = await this._manualRedactionService.addComment(value, this.annotation.id, dossierId, fileId); - this.annotation.comments.push({ - text: value, - id: commentId, - annotationId: this.annotation.id, - user: this.currentUser.id, - }); - this._input.reset(); - this._changeRef.markForCheck(); + const { dossierId, fileId } = this.state; + const commentId = await this.#commentsApiService.add(value, this.annotation.id, dossierId, fileId); + this.annotation.numberOfComments++; + this.comments.update(current => [ + ...current, + { + text: value, + id: commentId, + annotationId: this.annotation.id, + userId: this.currentUser.id, + }, + ]); + this.input.reset(); this._loadingService.stop(); } - toggleExpandComments(): void { - this._commentingService.toggle(this.annotation.id); - } - - async deleteComment(comment: IComment): Promise { + async remove(comment: IComment): Promise { this._loadingService.start(); - const { dossierId, fileId } = this._state; - await this._manualRedactionService.deleteComment(comment.id, this.annotation.id, dossierId, fileId); - this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1); - this._changeRef.markForCheck(); + const { dossierId, fileId } = this.state; + await this.#commentsApiService.remove(comment.id, this.annotation.id, dossierId, fileId); + this.annotation.numberOfComments--; + this.comments.update(current => current.filter(c => c.id !== comment.id)); this._loadingService.stop(); } } 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 ac7a577ea..ec8b741ff 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,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { IqserDialog } from '@common-ui/dialog/iqser-dialog.service'; import { getConfig, Toaster } from '@iqser/common-ui'; import { List, log } from '@iqser/common-ui/lib/utils'; @@ -13,6 +13,7 @@ import { IRectangle, IResizeRequest, } from '@red/domain'; +import { CommentsApiService } from '@services/comments-api.service'; import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service'; import { PermissionsService } from '@services/permissions.service'; import { firstValueFrom, Observable, zip } from 'rxjs'; @@ -46,6 +47,7 @@ import { SkippedService } from './skipped.service'; @Injectable() export class AnnotationActionsService { readonly #isDocumine = getConfig().IS_DOCUMINE; + readonly #commentsApiService = inject(CommentsApiService); constructor( private readonly _manualRedactionService: ManualRedactionService, @@ -129,7 +131,7 @@ export class AnnotationActionsService { if (result.comment) { try { for (const a of annotations) { - await this._manualRedactionService.addComment(result.comment, a.id, dossierId, fileId); + await this.#commentsApiService.add(result.comment, a.id, dossierId, fileId); } } catch (error) { this._toaster.rawError(error.error.message); diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-processing.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-processing.service.ts index 0fb05a0b3..e354d02ea 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-processing.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-processing.service.ts @@ -42,7 +42,7 @@ export class AnnotationProcessingService { label: _('filter-menu.with-comments'), checked: false, topLevelFilter: true, - checker: (annotation: AnnotationWrapper) => annotation?.comments?.length > 0, + checker: (annotation: AnnotationWrapper) => annotation?.numberOfComments > 0, }, { id: 'redaction-changes', diff --git a/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts b/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts index 2329b4354..bb0cc6565 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts @@ -21,7 +21,7 @@ import { PermissionsService } from '@services/permissions.service'; import { dictionaryActionsTranslations, manualRedactionActionsTranslations } from '@translations/annotation-actions-translations'; import { Roles } from '@users/roles'; import { NGXLogger } from 'ngx-logger'; -import { firstValueFrom, of } from 'rxjs'; +import { of } from 'rxjs'; import { tap } from 'rxjs/operators'; function getResponseType(error: boolean, isConflict: boolean) { @@ -54,17 +54,6 @@ export class ManualRedactionService extends GenericService { super(); } - async addComment(comment: string, annotationId: string, dossierId: string, fileId: string) { - const url = `${this._defaultModelPath}/comment/add/${dossierId}/${fileId}/${annotationId}`; - const request = await firstValueFrom(this._post<{ commentId: string }>({ text: comment }, url)); - return request.commentId; - } - - deleteComment(commentId: string, annotationId: string, dossierId: string, fileId: string) { - const url = `${this._defaultModelPath}/comment/undo/${dossierId}/${fileId}/${annotationId}/${commentId}`; - return firstValueFrom(super.delete({}, url)); - } - addRecommendation(annotations: AnnotationWrapper[], redaction: IAddRedactionRequest, dossierId: string, fileId: string) { const recommendations: List = annotations.map(annotation => ({ addToDictionary: redaction.addToDictionary, diff --git a/apps/red-ui/src/app/services/comments-api.service.ts b/apps/red-ui/src/app/services/comments-api.service.ts new file mode 100644 index 000000000..5f78a71e9 --- /dev/null +++ b/apps/red-ui/src/app/services/comments-api.service.ts @@ -0,0 +1,28 @@ +import { Injectable, signal } from '@angular/core'; +import { GenericService } from '@common-ui/services/generic.service'; +import { IComment } from '@red/domain'; +import { firstValueFrom, map } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class CommentsApiService extends GenericService { + protected readonly _defaultModelPath = 'manualRedaction'; + readonly comments = signal>({}); + + async add(comment: string, annotationId: string, dossierId: string, fileId: string) { + const url = `${this._defaultModelPath}/comment/add/${dossierId}/${fileId}/${annotationId}`; + const request = await firstValueFrom(this._post<{ commentId: string }>({ text: comment }, url)); + return request.commentId; + } + + remove(commentId: string, annotationId: string, dossierId: string, fileId: string) { + const url = `${this._defaultModelPath}/comment/undo/${dossierId}/${fileId}/${annotationId}/${commentId}`; + return firstValueFrom(super.delete({}, url)); + } + + fetch(dossierId: string, fileId: string, annotationId: string): Promise { + const url = `${this._defaultModelPath}/comments/${dossierId}/${fileId}/${annotationId}`; + return firstValueFrom(super.getAll<{ comments: IComment[] }>(url).pipe(map(res => res.comments))); + } +} diff --git a/apps/red-ui/src/app/services/permissions.service.ts b/apps/red-ui/src/app/services/permissions.service.ts index 4f3d6ba9b..dffa16845 100644 --- a/apps/red-ui/src/app/services/permissions.service.ts +++ b/apps/red-ui/src/app/services/permissions.service.ts @@ -341,7 +341,7 @@ export class PermissionsService { canDeleteComment(comment: IComment, file: File, dossier: Dossier) { return ( this._iqserPermissionsService.has(Roles.comments.delete) && - (comment.user === this.#userId || this.isApprover(dossier)) && + (comment.userId === this.#userId || this.isApprover(dossier)) && !file.isApproved ); } diff --git a/libs/red-domain/src/lib/redaction-log/comment.ts b/libs/red-domain/src/lib/redaction-log/comment.ts index d2fb3a87e..d14fb54bc 100644 --- a/libs/red-domain/src/lib/redaction-log/comment.ts +++ b/libs/red-domain/src/lib/redaction-log/comment.ts @@ -1,6 +1,6 @@ export interface IComment { id: string; - user: string; + userId: string; date?: string; text: string; annotationId?: string;