RED-8032: backported new way of managing comments.
This commit is contained in:
parent
d073980d41
commit
53f64d0279
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
28
apps/red-ui/src/app/services/comments-api.service.ts
Normal file
28
apps/red-ui/src/app/services/comments-api.service.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export interface IComment {
|
||||
id: string;
|
||||
user: string;
|
||||
userId: string;
|
||||
date?: string;
|
||||
text: string;
|
||||
annotationId?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user