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 eeaa7c420..26fa7ca9c 100644 --- a/apps/red-ui/src/app/models/file/annotation.wrapper.ts +++ b/apps/red-ui/src/app/models/file/annotation.wrapper.ts @@ -9,7 +9,6 @@ import { EntityTypes, EntryStates, FalsePositiveSuperTypes, - IComment, IEntityLogEntry, ILegalBasis, IPoint, @@ -31,7 +30,7 @@ export class AnnotationWrapper implements IListable { type: string; typeLabel?: string; color: string; - comments: IComment[] = []; + numberOfComments = 0; firstTopLeftPoint: IPoint; shortContent: string; content: string; @@ -233,9 +232,9 @@ export class AnnotationWrapper implements IListable { annotationWrapper.isIgnored = logEntry.state === EntryStates.IGNORED; + annotationWrapper.numberOfComments = logEntry.numberOfComments; annotationWrapper.imported = logEntry.imported; annotationWrapper.legalBasisValue = logEntry.legalBasis; - annotationWrapper.comments = []; //logEntry.comments || []; annotationWrapper.manual = logEntry.manualChanges?.length > 0; annotationWrapper.engines = logEntry.engines ?? []; annotationWrapper.section = logEntry.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 7800d1036..498cb36c9 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,26 +10,35 @@
- {{ annotation.item.comments.length }} + {{ annotation.item.numberOfComments }}
-
+
- + + + +
+
diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.scss b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.scss index 6817bb6ea..36304a969 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.scss +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.scss @@ -67,3 +67,9 @@ } } } + +.hide-comments { + margin-top: 5px; + margin-bottom: 8px; + padding-left: 26px; +} 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 3307c7b6d..3b2c9775c 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 @@ -13,12 +13,13 @@ import { ActionsHelpModeKeys } from '../../utils/constants'; }) export class AnnotationWrapperComponent implements OnChanges { readonly #isDocumine = getConfig().IS_DOCUMINE; - protected readonly _pdfProxyService = inject(PdfProxyService); - protected readonly _multiSelectService = inject(MultiSelectService); + protected readonly pdfProxyService = inject(PdfProxyService); + protected readonly multiSelectService = inject(MultiSelectService); @Input({ required: true }) annotation!: ListItem; @HostBinding('attr.annotation-id') annotationId: string; @HostBinding('class.active') active = false; actionsHelpModeKey?: string; + showComments = false; ngOnChanges() { this.annotationId = this.annotation.item.id; 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..833094349 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.user | name }} + {{ comment.date | date: 'sophisticatedDate' }}
-
{{ comment.text }}
+
+ +
- +
{{ comment.text }}
+
-
- + diff --git a/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.scss b/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.scss index 6d438a45f..f9c8c4a3b 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.scss +++ b/apps/red-ui/src/app/modules/file-preview/components/comments/comments.component.scss @@ -1,7 +1,7 @@ :host { display: flex; flex-direction: column; - padding: 8px 0 8px 16px; + padding: 8px 0 0 16px; .comment { margin-bottom: 10px; @@ -36,12 +36,7 @@ margin: 5px 0 10px 0; } - .hide-comments { - margin-top: 5px; - } - - .comment, - .hide-comments { + .comment { padding-left: 12px; } } 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..62e5ea6b8 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,73 @@ -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); + @ViewChild(InputWithActionComponent) protected readonly input: InputWithActionComponent; + protected readonly trackBy = trackByFactory(); + protected readonly comments = signal([]); + protected readonly currentUser = getCurrentUser(); + @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, + user: 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/file-preview-providers.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts index 32326378d..e809b6ab5 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 @@ -1,22 +1,21 @@ -import { ExcludedPagesService } from './services/excluded-pages.service'; -import { ViewModeService } from './services/view-mode.service'; -import { MultiSelectService } from './services/multi-select.service'; -import { DocumentInfoService } from './services/document-info.service'; -import { CommentingService } from './services/commenting.service'; -import { SkippedService } from './services/skipped.service'; -import { AnnotationActionsService } from './services/annotation-actions.service'; -import { FilePreviewStateService } from './services/file-preview-state.service'; -import { AnnotationReferencesService } from './services/annotation-references.service'; import { EntitiesService, ListingService, SearchService } from '@iqser/common-ui'; -import { AnnotationProcessingService } from './services/annotation-processing.service'; -import { dossiersServiceProvider } from '@services/entity-services/dossiers.service.provider'; -import { FileDataService } from './services/file-data.service'; -import { AnnotationsListingService } from './services/annotations-listing.service'; -import { StampService } from './services/stamp.service'; -import { PdfProxyService } from './services/pdf-proxy.service'; -import { PdfAnnotationActionsService } from './services/pdf-annotation-actions.service'; import { FilterService } from '@iqser/common-ui/lib/filtering'; import { SortingService } from '@iqser/common-ui/lib/sorting'; +import { dossiersServiceProvider } from '@services/entity-services/dossiers.service.provider'; +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 { DocumentInfoService } from './services/document-info.service'; +import { ExcludedPagesService } from './services/excluded-pages.service'; +import { FileDataService } from './services/file-data.service'; +import { FilePreviewStateService } from './services/file-preview-state.service'; +import { MultiSelectService } from './services/multi-select.service'; +import { PdfAnnotationActionsService } from './services/pdf-annotation-actions.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'; export const filePreviewScreenProviders = [ FilterService, @@ -24,7 +23,6 @@ export const filePreviewScreenProviders = [ ViewModeService, MultiSelectService, DocumentInfoService, - CommentingService, SkippedService, AnnotationActionsService, PdfAnnotationActionsService, 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 328f9ae47..9f77c276c 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 0c7f3215f..06f0e5db4 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/commenting.service.ts b/apps/red-ui/src/app/modules/file-preview/services/commenting.service.ts deleted file mode 100644 index 494cfb796..000000000 --- a/apps/red-ui/src/app/modules/file-preview/services/commenting.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; -import { shareDistinctLast } from '@iqser/common-ui/lib/utils'; - -@Injectable() -export class CommentingService { - private _activeAnnotations = new BehaviorSubject>(new Set()); - - /** Annotations with active comments section */ - isActive$(annotationId: string): Observable { - return this._activeAnnotations.pipe( - map(annotations => annotations.has(annotationId)), - startWith(false), - shareDistinctLast(), - ); - } - - toggle(annotationId: string): void { - if (this._activeAnnotations.value.has(annotationId)) { - this._deactivate(annotationId); - } else { - this._activate(annotationId); - } - } - - private _activate(annotationId: string): void { - const currentValue = this._activeAnnotations.value; - const newSet = new Set(currentValue).add(annotationId); - this._activeAnnotations.next(newSet); - } - - private _deactivate(annotationId: string): void { - const currentValue = this._activeAnnotations.value; - const newSet = new Set(currentValue); - newSet.delete(annotationId); - this._activeAnnotations.next(newSet); - } -} 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 38eadfafe..1f08b8b01 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 @@ -212,6 +212,9 @@ export class FileDataService extends EntitiesService { 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/libs/red-domain/src/lib/redaction-log/entity-log-entry.ts b/libs/red-domain/src/lib/redaction-log/entity-log-entry.ts index 9a7db5e90..21d94933f 100644 --- a/libs/red-domain/src/lib/redaction-log/entity-log-entry.ts +++ b/libs/red-domain/src/lib/redaction-log/entity-log-entry.ts @@ -37,4 +37,5 @@ export interface IEntityLogEntry extends ITrackable { engines: LogEntryEngine[]; reference: string[]; importedRedactionIntersections: string[]; + numberOfComments: number; }