RED-7619 I think its ready?

This commit is contained in:
Dan Percic 2023-10-30 20:56:49 +02:00
parent 7d141c5b23
commit f2e2a5ba1a
15 changed files with 154 additions and 182 deletions

View File

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

View File

@ -10,26 +10,35 @@
<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">
<div *ngIf="multiSelectService.inactive()" class="actions">
<redaction-annotation-actions
[annotations]="[annotation.item]"
[attr.help-mode-key]="actionsHelpModeKey"
[canPerformAnnotationActions]="_pdfProxyService.canPerformActions()"
[canPerformAnnotationActions]="pdfProxyService.canPerformActions()"
></redaction-annotation-actions>
</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

@ -67,3 +67,9 @@
}
}
}
.hide-comments {
margin-top: 5px;
margin-bottom: 8px;
padding-left: 26px;
}

View File

@ -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<AnnotationWrapper>;
@HostBinding('attr.annotation-id') annotationId: string;
@HostBinding('class.active') active = false;
actionsHelpModeKey?: string;
showComments = false;
ngOnChanges() {
this.annotationId = this.annotation.item.id;

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.user | 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,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;
}
}

View File

@ -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<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);
@ViewChild(InputWithActionComponent) protected readonly input: InputWithActionComponent;
protected readonly trackBy = trackByFactory();
protected readonly comments = signal<IComment[]>([]);
protected readonly currentUser = getCurrentUser<User>();
@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,
user: 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,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,

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

@ -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<Set<string>>(new Set<string>());
/** Annotations with active comments section */
isActive$(annotationId: string): Observable<boolean> {
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<string>(currentValue).add(annotationId);
this._activeAnnotations.next(newSet);
}
private _deactivate(annotationId: string): void {
const currentValue = this._activeAnnotations.value;
const newSet = new Set<string>(currentValue);
newSet.delete(annotationId);
this._activeAnnotations.next(newSet);
}
}

View File

@ -212,6 +212,9 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
easeTime: 500,
},
);
}
if (!canBeMappedToASuperType) {
continue;
}

View File

@ -18,7 +18,7 @@ import type {
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) {
@ -48,17 +48,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

@ -37,4 +37,5 @@ export interface IEntityLogEntry extends ITrackable {
engines: LogEntryEngine[];
reference: string[];
importedRedactionIntersections: string[];
numberOfComments: number;
}