Pull request #391: Rss editing

Merge in RED/ui from rss-editing to master

* commit '545303b6d5a85d4b80a6f4ed7216fd921d6c6b4a':
  RED-3800 rss editing
  RED-3800: RSS editing
  RED-3800 rss editing
  RED-3800: RSS editing
  RED-3800: RSS editing
  RED-3800 rss editing
  RED-3800 rss editing
This commit is contained in:
Timo Bejan 2022-12-13 14:29:03 +01:00
commit 5277d4384e
14 changed files with 229 additions and 56 deletions

View File

@ -21,7 +21,7 @@
</strong>
{{ annotation.entity.label }}
</div>
<div *ngIf="annotation.shortContent && !annotation.isHint">
<div *ngIf="annotation.shortContent && !annotation.isHint && !iqserPermissionsService.has(roles.getRss)">
<strong><span translate="content"></span>: </strong>{{ annotation.shortContent }}
</div>
<div *ngIf="annotation.isEarmark">

View File

@ -2,6 +2,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { MultiSelectService } from '../../services/multi-select.service';
import { annotationTypesTranslations } from '@translations/annotation-types-translations';
import { ROLES } from '@users/roles';
import { IqserPermissionsService } from '@iqser/common-ui';
@Component({
selector: 'redaction-annotation-card',
@ -10,9 +12,10 @@ import { annotationTypesTranslations } from '@translations/annotation-types-tran
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnnotationCardComponent {
readonly roles = ROLES;
annotationTypesTranslations = annotationTypesTranslations;
@Input() annotation: AnnotationWrapper;
@Input() isSelected = false;
constructor(readonly multiSelectService: MultiSelectService) {}
constructor(readonly iqserPermissionsService: IqserPermissionsService, readonly multiSelectService: MultiSelectService) {}
}

View File

@ -46,14 +46,12 @@
[value]="dictionary.type"
matTooltipPosition="after"
>
<span>
{{ dictionary.label }}
</span>
<span> {{ dictionary.label }} </span>
</mat-option>
</mat-select>
</div>
<div *ngIf="!isDictionaryRequest" class="iqser-input-group required w-450">
<div *ngIf="!isDictionaryRequest && !iqserPermissionsService.has(roles.getRss)" class="iqser-input-group required w-450">
<label translate="manual-annotation.dialog.content.reason"></label>
<mat-select
[placeholder]="'manual-annotation.dialog.content.reason-placeholder' | translate"
@ -71,7 +69,7 @@
</mat-select>
</div>
<div *ngIf="!isDictionaryRequest" class="iqser-input-group w-450">
<div *ngIf="!isDictionaryRequest && !iqserPermissionsService.has(roles.getRss)" class="iqser-input-group w-450">
<label translate="manual-annotation.dialog.content.legalBasis"></label>
<input [value]="form.get('reason').value?.legalBasis" disabled type="text" />
</div>

View File

@ -6,10 +6,11 @@ import { JustificationsService } from '@services/entity-services/justifications.
import { Dictionary, Dossier, File, IAddRedactionRequest, SuperTypes } from '@red/domain';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { BaseDialogComponent, CircleButtonTypes } from '@iqser/common-ui';
import { BaseDialogComponent, CircleButtonTypes, IqserPermissionsService } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
import { ManualRedactionService } from '../../services/manual-redaction.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ROLES } from '@users/roles';
export interface LegalBasisOption {
label?: string;
@ -22,6 +23,7 @@ export interface LegalBasisOption {
styleUrls: ['./manual-annotation-dialog.component.scss'],
})
export class ManualAnnotationDialogComponent extends BaseDialogComponent implements OnInit {
readonly roles = ROLES;
readonly circleButtonTypes = CircleButtonTypes;
isDictionaryRequest: boolean;
isFalsePositiveRequest: boolean;
@ -35,6 +37,7 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
private readonly _dossier: Dossier;
constructor(
readonly iqserPermissionsService: IqserPermissionsService,
private readonly _justificationsService: JustificationsService,
private readonly _manualRedactionService: ManualRedactionService,
activeDossiersService: ActiveDossiersService,
@ -87,6 +90,9 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
}));
this.legalOptions.sort((a, b) => a.label.localeCompare(b.label));
this._selectReason();
if (!this.isRectangle) {
this._formatSelectedTextValue();
}
@ -165,9 +171,15 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
addRedactionRequest.reason = legalOption.description;
addRedactionRequest.legalBasis = legalOption.legalBasis;
}
// todo fix this in backend
if (this.iqserPermissionsService.has(ROLES.getRss)) {
const selectedType = this.possibleDictionaries.find(d => d.type === addRedactionRequest.type);
console.log(selectedType.hasDictionary);
addRedactionRequest.addToDictionary = selectedType.hasDictionary;
} else {
addRedactionRequest.addToDictionary = this.isDictionaryRequest && addRedactionRequest.type !== 'dossier_redaction';
addRedactionRequest.addToDossierDictionary = this.isDictionaryRequest && addRedactionRequest.type === 'dossier_redaction';
}
if (!addRedactionRequest.reason) {
addRedactionRequest.reason = 'Dictionary Request';
@ -179,4 +191,10 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
? this.form.get('classification').value
: this.form.get('selectedText').value;
}
private _selectReason() {
if (this.legalOptions.length === 1) {
this.form.get('reason').setValue(this.legalOptions[0]);
}
}
}

View File

@ -1,28 +1,76 @@
<section class="dialog">
<div translate="rss-dialog.title" class="dialog-header heading-l"></div>
<div class="dialog-header heading-l" translate="rss-dialog.title"></div>
<hr />
<div class="dialog-content">
<div *ngIf="rssData$ | async as rssEntry">
<div *ngFor="let entry of rssEntry.result | keyvalue: originalOrder" class="rss-row">
<div class="rss-key">{{ entry.key }}:</div>
<div class="rss-value">{{ entry.value }}</div>
<div *ngIf="rssData$ | async as rssEntry" class="table output-data">
<div class="table-header">Component</div>
<div class="table-header">Value</div>
<div class="table-header">Transformation</div>
<div class="table-header">Annotations</div>
<ng-container *ngFor="let entry of rssEntry.result | keyvalue: originalOrder">
<div class="bold">{{ entry.key }}</div>
<div>
<iqser-editable-input
(save)="saveEdit($event, entry)"
[buttonsType]="iconButtonTypes.dark"
[cancelTooltip]="'rss-dialog.actions.cancel-edit' | translate"
[editTooltip]="'rss-dialog.actions.edit' | translate"
[saveTooltip]="'rss-dialog.actions.save' | translate"
[value]="entry.value.value ?? entry.value.originalValue"
>
<ng-container slot="editing">
<iqser-circle-button
(action)="undo(entry)"
*ngIf="entry.value.value"
[showDot]="true"
[tooltip]="'rss-dialog.actions.undo' | translate: { value: entry.value.originalValue }"
[type]="iconButtonTypes.dark"
class="ml-2"
icon="red:undo"
></iqser-circle-button>
</ng-container>
</iqser-editable-input>
</div>
<div>{{ entry.value.transformation }}</div>
<!-- <div>{{ entry.value.scmAnnotations || '-' }}</div>-->
<div>
<ul *ngIf="entry.value.scmAnnotations; else noAnnotations" class="pl-0">
<li
*ngFor="let annotation of entry.value.scmAnnotations"
class="mb-8"
[innerHTML]="
'rss-dialog.annotations'
| translate
: {
ruleNumber: annotation.ruleNumber,
pageCount: annotation.pages.length,
pages: annotation.pages.join(','),
type: annotation.type
}
"
></li>
</ul>
<ng-template #noAnnotations>-</ng-template>
</div>
</ng-container>
</div>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button type="submit" (click)="exportJSON()">
<button (click)="exportJSON()" color="primary" mat-flat-button type="submit">
{{ 'rss-dialog.actions.export-json' | translate }}
</button>
<button color="primary" mat-flat-button type="button" (click)="exportXML()">
<button (click)="exportXML()" color="primary" mat-flat-button type="button">
{{ 'rss-dialog.actions.export-xml' | translate }}
</button>
<button color="primary" mat-flat-button type="button" (click)="exportAllInDossier()" *ngIf="userPreferences.areDevFeaturesEnabled">
<button (click)="exportAllInDossier()" *ngIf="userPreferences.areDevFeaturesEnabled" color="primary" mat-flat-button type="button">
{{ 'Export All' }}
</button>
<div class="all-caps-label cancel" mat-dialog-close translate="rss-dialog.actions.close"></div>
</div>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -19,3 +19,47 @@
.dialog-content {
overflow: auto;
}
.table {
display: grid;
grid-template-columns: repeat(4, 1fr);
> div {
padding: 8px 10px;
}
.bold {
font-weight: 600;
}
.value-content {
.value {
}
.actions {
}
}
.table-header {
margin: 10px 0;
border-bottom: 1px solid var(--iqser-separator);
background-color: var(--iqser-grey-2);
font-weight: 600;
}
}
.annotation-grid {
display: grid;
grid-template-columns: 3fr 1fr 1fr 5fr;
}
ul {
margin: 0;
}
.output-data > div:nth-child(8n + 9),
.output-data > div:nth-child(8n + 10),
.output-data > div:nth-child(8n + 11),
.output-data > div:nth-child(8n + 12) {
background: var(--iqser-grey-8);
}

View File

@ -1,9 +1,9 @@
import { Component, Inject } from '@angular/core';
import { BaseDialogComponent } from '@iqser/common-ui';
import { Component, Inject, OnInit } from '@angular/core';
import { BaseDialogComponent, IconButtonTypes } from '@iqser/common-ui';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { RssService } from '@services/files/rss.service';
import { IFile, IRssEntry } from '@red/domain';
import { firstValueFrom, Observable } from 'rxjs';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { map } from 'rxjs/operators';
import { FilesMapService } from '@services/files/files-map.service';
import { UserPreferenceService } from '@users/user-preference.service';
@ -17,10 +17,10 @@ interface RssData {
templateUrl: './rss-dialog.component.html',
styleUrls: ['./rss-dialog.component.scss'],
})
export class RssDialogComponent extends BaseDialogComponent {
rssData$: Observable<IRssEntry>;
export class RssDialogComponent extends BaseDialogComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
originalOrder = (a: KeyValue<string, string>, b: KeyValue<string, string>): number => 0;
rssData$ = new BehaviorSubject<IRssEntry>(null);
constructor(
protected readonly _dialogRef: MatDialogRef<RssDialogComponent>,
@ -30,21 +30,14 @@ export class RssDialogComponent extends BaseDialogComponent {
@Inject(MAT_DIALOG_DATA) readonly data: RssData,
) {
super(_dialogRef);
this.rssData$ = this._rssService.getRSSData(this.data.file.dossierId, this.data.file.fileId).pipe(
map(entry => {
const mapped = {};
for (const key of Object.keys(entry.result)) {
const newKey = key.replace(new RegExp('_', 'g'), ' ');
mapped[newKey] = entry.result[key];
}
return {
filaName: entry.filaName,
result: mapped,
};
}),
);
async ngOnInit(): Promise<void> {
await this.#loadData();
}
originalOrder = (a: KeyValue<string, any>, b: KeyValue<string, any>): number => 0;
exportJSON() {
this._rssService.exportJSON(this.data.file.dossierId, this.data.file.fileId, this.data.file.filename).subscribe();
}
@ -64,4 +57,40 @@ export class RssDialogComponent extends BaseDialogComponent {
save(): void {
this.exportJSON();
}
async undo(entry: KeyValue<string, any>) {
this._loadingService.start();
await firstValueFrom(this._rssService.revertOverride(this.data.file.dossierId, this.data.file.fileId, [entry.value.originalKey]));
await this.#loadData();
}
async saveEdit(event: string, entry: KeyValue<string, any>) {
this._loadingService.start();
await firstValueFrom(
this._rssService.override(this.data.file.dossierId, this.data.file.fileId, { [entry.value.originalKey]: event }),
);
await this.#loadData();
}
async #loadData(): Promise<void> {
this._loadingService.start();
const rssData = await firstValueFrom(
this._rssService.getRSSData(this.data.file.dossierId, this.data.file.fileId).pipe(
map(entry => {
const mapped = {};
for (const key of Object.keys(entry.result)) {
const newKey = key.replace(new RegExp('_', 'g'), ' ');
(<any>entry.result[key]).originalKey = key;
mapped[newKey] = entry.result[key];
}
return {
filaName: entry.filaName,
result: mapped,
};
}),
),
);
this.rssData$.next(rssData);
this._loadingService.stop();
}
}

View File

@ -253,6 +253,7 @@ export class PdfProxyService {
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION),
});
if (!this._iqserPermissionsService.has(ROLES.getRss)) {
popups.push({
type: 'actionButton',
dataElement: TextPopups.ADD_DICTIONARY,
@ -261,6 +262,7 @@ export class PdfProxyService {
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY),
});
}
}
this._pdf.configureTextPopups(popups);

View File

@ -15,7 +15,7 @@ export class RssService extends GenericService<void> {
const queryParams: QueryParam[] = [];
queryParams.push({ key: 'fileId', value: fileId });
const rssData$ = this._getOne<IRssData>([dossierId], 'rss', queryParams);
const rssData$ = this._getOne<IRssData>([dossierId], 'rss/detailed', queryParams);
return rssData$.pipe(
map(data => data.files[0]),
catchError(() => of({} as IRssEntry)),
@ -40,6 +40,20 @@ export class RssService extends GenericService<void> {
});
}
@Validate()
override(
@RequiredParam() dossierId: string,
@RequiredParam() fileId: string,
@RequiredParam() componentOverrides: Record<string, string>,
) {
return this._post({ componentOverrides }, `rss/override/${dossierId}/${fileId}`);
}
@Validate()
revertOverride(@RequiredParam() dossierId: string, @RequiredParam() fileId: string, @RequiredParam() components: string[]) {
return this._post({ components }, `rss/override/revert/${dossierId}/${fileId}`);
}
exportJSON(dossierId: string, fileId: string, name: string) {
return this.getRSSData(dossierId, fileId).pipe(
tap(data => {

View File

@ -1,7 +1,7 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dev-08.iqser.cloud/redaction-gateway-v1",
"API_URL": "https://syngenta-scm.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
@ -11,11 +11,11 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dev-08.iqser.cloud/auth/realms/redaction",
"OAUTH_URL": "https://syngenta-scm.iqser.cloud/auth/realms/redaction",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview",
"ANNOTATIONS_THRESHOLD": 1000,
"THEME": "redact",
"BASE_TRANSLATIONS_DIRECTORY": "/assets/i18n/redact/"
"THEME": "scm",
"BASE_TRANSLATIONS_DIRECTORY": "/assets/i18n/scm/"
}

View File

@ -1953,9 +1953,13 @@
},
"rss-dialog": {
"actions": {
"cancel-edit": "",
"close": "",
"edit": "",
"export-json": "",
"export-xml": ""
"export-xml": "",
"save": "",
"undo": ""
},
"title": ""
},

View File

@ -1953,9 +1953,13 @@
},
"rss-dialog": {
"actions": {
"cancel-edit": "Cancel",
"close": "Close",
"edit": "Edit",
"export-json": "Export JSON",
"export-xml": "Export XML"
"export-xml": "Export XML",
"save": "Save",
"undo": "Undo"
},
"title": "Structured Component Management"
},

View File

@ -1953,9 +1953,13 @@
},
"rss-dialog": {
"actions": {
"cancel-edit": "",
"close": "",
"edit": "",
"export-json": "",
"export-xml": ""
"export-xml": "",
"save": "",
"undo": ""
},
"title": ""
},

View File

@ -1953,10 +1953,15 @@
},
"rss-dialog": {
"actions": {
"cancel-edit": "Cancel",
"close": "Close",
"edit": "Edit",
"export-json": "Export JSON",
"export-xml": "Export XML"
"export-xml": "Export XML",
"save": "Save",
"undo": "Undo to: {value}"
},
"annotations": "<strong>{type}</strong> found on {pageCount, plural, one{page} other{pages}} {pages} by rule #{ruleNumber}",
"title": "Structured Component Management"
},
"rules-screen": {