RED-8032: backported new way of managing comments.

This commit is contained in:
Nicoleta Panaghiu 2023-12-06 16:48:46 +02:00
parent d073980d41
commit 53f64d0279
11 changed files with 135 additions and 118 deletions

View File

@ -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;

View File

@ -10,14 +10,14 @@
<div *ngIf="!annotation.item.isEarmark" class="actions-wrapper">
<div
(click)="comments.toggleExpandComments()"
[matTooltip]="'comments.comments' | translate : { count: annotation.item.comments?.length }"
(click)="showComments = !showComments"
[matTooltip]="'comments.comments' | translate: { count: annotation.item.numberOfComments }"
class="comments-counter"
iqserStopPropagation
matTooltipPosition="above"
>
<mat-icon svgIcon="red:comment"></mat-icon>
{{ annotation.item.comments.length }}
{{ annotation.item.numberOfComments }}
</div>
<div *ngIf="_multiSelectService.inactive()" class="actions">
@ -29,7 +29,16 @@
</div>
</div>
<redaction-comments #comments [annotation]="annotation.item"></redaction-comments>
<ng-container *ngIf="showComments">
<redaction-comments [annotation]="annotation.item"></redaction-comments>
<div
(click)="showComments = false"
class="all-caps-label pointer hide-comments"
iqserStopPropagation
translate="comments.hide-comments"
></div>
</ng-container>
</div>
<redaction-annotation-details [annotation]="annotation"></redaction-annotation-details>

View File

@ -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<AnnotationWrapper>;
@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();
}

View File

@ -1,39 +1,30 @@
<ng-container *ngIf="componentContext$ | async as ctx">
<div *ngFor="let comment of annotation.comments; trackBy: trackBy" class="comment">
<div class="comment-details-wrapper">
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date : 'exactDate'" class="small-label">
<strong> {{ comment.user | name }} </strong>
{{ comment.date | date : 'sophisticatedDate' }}
</div>
<div class="comment-actions">
<iqser-circle-button
(action)="deleteComment(comment)"
*ngIf="permissionsService.canDeleteComment(comment, _state.file(), _state.dossier())"
[iconSize]="10"
[size]="20"
class="pointer"
icon="iqser:trash"
></iqser-circle-button>
</div>
<div *ngFor="let comment of comments(); trackBy: trackBy" class="comment">
<div class="comment-details-wrapper">
<div [matTooltipPosition]="'above'" [matTooltip]="comment.date | date: 'exactDate'" class="small-label">
<strong> {{ comment.userId | name }} </strong>
{{ comment.date | date: 'sophisticatedDate' }}
</div>
<div>{{ comment.text }}</div>
<div class="comment-actions">
<iqser-circle-button
(action)="remove(comment)"
*ngIf="permissionsService.canDeleteComment(comment, state.file(), state.dossier())"
[iconSize]="10"
[size]="20"
class="pointer"
icon="iqser:trash"
></iqser-circle-button>
</div>
</div>
<iqser-input-with-action
(action)="addComment($event)"
*ngIf="permissionsService.canAddComment(_state.file(), _state.dossier())"
[placeholder]="'comments.add-comment' | translate"
autocomplete="off"
icon="iqser:collapse"
width="full"
></iqser-input-with-action>
<div>{{ comment.text }}</div>
</div>
<div
(click)="toggleExpandComments()"
class="all-caps-label pointer hide-comments"
iqserStopPropagation
translate="comments.hide-comments"
></div>
</ng-container>
<iqser-input-with-action
(action)="add($event)"
*ngIf="permissionsService.canAddComment(state.file(), state.dossier())"
[placeholder]="'comments.add-comment' | translate"
autocomplete="off"
icon="iqser:collapse"
width="full"
></iqser-input-with-action>

View File

@ -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<CommentsContext> implements OnInit {
@HostBinding('class.hidden') private _hidden = true;
@ViewChild(InputWithActionComponent) private readonly _input: InputWithActionComponent;
@Input() annotation: AnnotationWrapper;
readonly trackBy = trackByFactory();
readonly currentUser = getCurrentUser<User>();
hiddenComments$: Observable<boolean>;
export class CommentsComponent implements OnChanges {
readonly #commentsApiService = inject(CommentsApiService);
readonly #logger = inject(NGXLogger);
protected readonly trackBy = trackByFactory();
protected readonly currentUser = getCurrentUser<User>();
readonly comments = signal<IComment[]>([]);
@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<void> {
async add(value: string): Promise<void> {
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<void> {
async remove(comment: IComment): Promise<void> {
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();
}
}

View File

@ -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);

View File

@ -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',

View File

@ -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<IManualAddResponse> {
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<IAddRedactionRequest> = annotations.map(annotation => ({
addToDictionary: redaction.addToDictionary,

View File

@ -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<IComment> {
protected readonly _defaultModelPath = 'manualRedaction';
readonly comments = signal<Record<string, IComment[]>>({});
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<IComment[]> {
const url = `${this._defaultModelPath}/comments/${dossierId}/${fileId}/${annotationId}`;
return firstValueFrom(super.getAll<{ comments: IComment[] }>(url).pipe(map(res => res.comments)));
}
}

View File

@ -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
);
}

View File

@ -1,6 +1,6 @@
export interface IComment {
id: string;
user: string;
userId: string;
date?: string;
text: string;
annotationId?: string;