Merge branch 'VM/RED-8045' into 'master'

RED-8045 - Dossier template selection/filter at dossier dictionary comparison

Closes RED-8045

See merge request redactmanager/red-ui!243
This commit is contained in:
Dan Percic 2024-01-08 10:54:57 +01:00
commit 3d2eec6ef7
5 changed files with 123 additions and 163 deletions

View File

@ -4,8 +4,9 @@
[canDownload]="permissionsService.canDownloadEntityDictionary()" [canDownload]="permissionsService.canDownloadEntityDictionary()"
[canEdit]="currentUser.isAdmin && (iqserPermissionsService.has$(roles.dictionaryEntries.write) | async)" [canEdit]="currentUser.isAdmin && (iqserPermissionsService.has$(roles.dictionaryEntries.write) | async)"
[entityType]="entityType" [entityType]="entityType"
[filterByDossierTemplate]="true"
[initialEntries]="initialEntries$ | async" [initialEntries]="initialEntries$ | async"
[isLeavingPage]="isLeavingPage" [isLeavingPage]="isLeavingPage"
[currentDossierTemplateId]="dossierTemplateId"
[selectedDictionaryType]="entityType"
[type]="type" [type]="type"
></redaction-dictionary-manager> ></redaction-dictionary-manager>

View File

@ -17,13 +17,13 @@ import { getParam, List } from '@iqser/common-ui/lib/utils';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DictionaryScreenComponent implements OnInit { export class DictionaryScreenComponent implements OnInit {
readonly currentUser = getCurrentUser<User>(); protected readonly currentUser = getCurrentUser<User>();
readonly roles = Roles; protected readonly roles = Roles;
readonly initialEntries$ = new BehaviorSubject<List>([]); protected readonly initialEntries$ = new BehaviorSubject<List>([]);
isLeavingPage = false; protected readonly type: DictionaryType;
readonly type: DictionaryType; protected readonly entityType = getParam(ENTITY_TYPE);
readonly entityType = getParam(ENTITY_TYPE); protected readonly dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID); protected isLeavingPage = false;
@ViewChild('dictionaryManager', { static: false }) @ViewChild('dictionaryManager', { static: false })
private readonly _dictionaryManager: DictionaryManagerComponent; private readonly _dictionaryManager: DictionaryManagerComponent;
@ -55,7 +55,7 @@ export class DictionaryScreenComponent implements OnInit {
await this._dictionaryService.saveEntries( await this._dictionaryService.saveEntries(
entries, entries,
this.initialEntries$.value, this.initialEntries$.value,
this.#dossierTemplateId, this.dossierTemplateId,
this.entityType, this.entityType,
null, null,
true, true,
@ -70,7 +70,7 @@ export class DictionaryScreenComponent implements OnInit {
async #loadEntries() { async #loadEntries() {
this._loadingService.start(); this._loadingService.start();
try { try {
const data = await this._dictionaryService.getForType(this.#dossierTemplateId, this.entityType); const data = await this._dictionaryService.getForType(this.dossierTemplateId, this.entityType);
const entries: List = data[DICTIONARY_TYPE_KEY_MAP[this.type]]; const entries: List = data[DICTIONARY_TYPE_KEY_MAP[this.type]];
this.initialEntries$.next(entries); this.initialEntries$.next(entries);
} catch (e) { } catch (e) {

View File

@ -41,55 +41,45 @@
</mat-checkbox> </mat-checkbox>
</div> </div>
<ng-container *ngIf="filterByDossierTemplate"> <ng-container>
<div *ngIf="dossierTemplatesService.all$ | async as templates" class="iqser-input-group w-200 mt-0 mr-8"> <div *ngIf="dossierTemplates" class="iqser-input-group w-200 mt-0 mr-8">
<mat-form-field> <mat-form-field>
<mat-select [(ngModel)]="dossierTemplate" [disabled]="!compare"> <mat-select
<mat-option [value]="selectDossierTemplate">{{ selectDossierTemplate.name | translate }}</mat-option> [(ngModel)]="selectedDossierTemplate"
<mat-option *ngFor="let dossierTemplate of templates" [value]="dossierTemplate"> [disabled]="!compare"
{{ dossierTemplate.name }} [placeholder]="compare ? selectedDossierTemplate.name : (selectDossierTemplate.name | translate)"
</mat-option> >
<ng-container *ngFor="let dossierTemplate of dossierTemplates">
<mat-option [value]="dossierTemplate" *ngIf="dossierTemplate?.id !== selectedDossierTemplate.id">
{{ dossierTemplate.name }}
</mat-option>
</ng-container>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="iqser-input-group w-200 mt-0"> <div class="iqser-input-group w-200 mt-0">
<mat-form-field> <mat-form-field>
<mat-select [(ngModel)]="compareDictionary" [disabled]="!compare || dossierTemplateIsNotSelected"> <mat-select
<mat-option [value]="selectDictionary">{{ selectDictionary.label | translate }}</mat-option> [(ngModel)]="selectedDossier"
<mat-option *ngFor="let dictionary of dictionaries" [value]="dictionary"> [disabled]="!compare"
{{ dictionary.label }} [placeholder]="selectedDossier.dossierId ? selectedDossier.dossierName : (selectDictionary.label | translate)"
</mat-option> >
<ng-container *ngFor="let dossier of dossiers; let index = index">
<mat-option
*ngIf="dossier.dossierId !== selectedDossier.dossierId && dossier.dossierId !== currentDossierId"
[value]="dossier"
>
{{ dossier.dossierName }}
</mat-option>
<mat-divider
*ngIf="index === dossiers.length - 2 && !selectedDossier.dossierId?.includes('template')"
></mat-divider>
</ng-container>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
</ng-container> </ng-container>
<div
*ngIf="!filterByDossierTemplate && !!dossiers.length"
class="iqser-input-group w-200 mt-0"
[class.not-clickable]="dossiers.length === 2 && !!selectedDossier.id"
>
<mat-form-field>
<mat-select
[(ngModel)]="selectedDossier"
[disabled]="!compare"
[placeholder]="selectedDossier.dossierId ? selectedDossier.dossierName : (selectDictionary.label | translate)"
>
<ng-container *ngFor="let dossier of dossiers; let index = index">
<mat-option
*ngIf="dossier.dossierId !== selectedDossier.dossierId && dossier.dossierId !== currentDossierId"
[value]="dossier"
>
{{ dossier.dossierName }}
</mat-option>
<mat-divider
*ngIf="index === dossiers.length - 2 && !selectedDossier.dossierId?.includes('template')"
></mat-divider>
</ng-container>
</mat-select>
</mat-form-field>
</div>
</div> </div>
</div> </div>

View File

@ -1,15 +1,6 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { IconButtonTypes, LoadingService } from '@iqser/common-ui';
import { import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, DictionaryType, Dossier, DossierTemplate, IDictionary } from '@red/domain';
Dictionary,
DICTIONARY_TYPE_KEY_MAP,
DictionaryEntryType,
DictionaryEntryTypes,
DictionaryType,
Dossier,
DossierTemplate,
IDictionary,
} from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DictionaryService } from '@services/entity-services/dictionary.service'; import { DictionaryService } from '@services/entity-services/dictionary.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
@ -22,6 +13,7 @@ import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
import FindMatch = monaco.editor.FindMatch; import FindMatch = monaco.editor.FindMatch;
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
const COMPARE_ENTRIES_ERROR = 'compare-entries-error';
const SMOOTH_SCROLL = 0; const SMOOTH_SCROLL = 0;
const HELP_MODE_KEYS = { const HELP_MODE_KEYS = {
dictionary: 'dictionary_entity', dictionary: 'dictionary_entity',
@ -40,9 +32,8 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
@Input() type: DictionaryType = 'dictionary'; @Input() type: DictionaryType = 'dictionary';
@Input() entityType?: string; @Input() entityType?: string;
@Input() currentDossierId: string; @Input() currentDossierId: string;
@Input() currentDossierTemplateId: string; @Input({ required: true }) currentDossierTemplateId!: string;
@Input() withFloatingActions = true; @Input() withFloatingActions = true;
@Input() filterByDossierTemplate = false;
@Input() initialEntries: List; @Input() initialEntries: List;
@Input() canEdit = false; @Input() canEdit = false;
@Input() canDownload = false; @Input() canDownload = false;
@ -54,7 +45,8 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
@Output() readonly saveDictionary = new EventEmitter<string[]>(); @Output() readonly saveDictionary = new EventEmitter<string[]>();
@ViewChild(EditorComponent) readonly editor: EditorComponent; @ViewChild(EditorComponent) readonly editor: EditorComponent;
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly dossiers: Dossier[]; dossiers: Dossier[];
dossierTemplates: DossierTemplate[];
currentMatch = 0; currentMatch = 0;
findMatches: FindMatch[] = []; findMatches: FindMatch[] = [];
diffEditorText = ''; diffEditorText = '';
@ -64,110 +56,64 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
selectDictionary = { selectDictionary = {
label: _('dictionary-overview.compare.select-dictionary'), label: _('dictionary-overview.compare.select-dictionary'),
} as Dictionary; } as Dictionary;
#compareDictionary = this.selectDictionary;
selectDossierTemplate = { name: _('dictionary-overview.compare.select-dossier-template') } as DossierTemplate; selectDossierTemplate = { name: _('dictionary-overview.compare.select-dossier-template') } as DossierTemplate;
compare = false; compare = false;
dictionaries: List<Dictionary> = this.#dictionaries; dictionaries: List<Dictionary> = this.#dictionaries;
#dossierTemplate = this.dossierTemplatesService.all[0];
#dossier = this.selectDossier;
#initialDossierTemplateId: string;
constructor( constructor(
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
private readonly _dictionariesMapService: DictionariesMapService, private readonly _dictionariesMapService: DictionariesMapService,
protected readonly _loadingService: LoadingService, protected readonly _loadingService: LoadingService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _activeDossiersService: ActiveDossiersService, private readonly _activeDossiersService: ActiveDossiersService,
private readonly _toaster: Toaster,
readonly dossierTemplatesService: DossierTemplatesService, readonly dossierTemplatesService: DossierTemplatesService,
) { ) {}
this.dossiers = _activeDossiersService.all;
}
ngOnInit() { ngOnInit() {
const templateDictionary = { this.#initialDossierTemplateId = this.currentDossierTemplateId;
id: 'template', this.#updateDropdownsOptions();
dossierId: 'template',
dossierName: 'Template Dictionary',
dossierTemplateId: this.currentDossierTemplateId,
} as Dossier;
this.dossiers.push(templateDictionary);
} }
private _dossierTemplate = this.selectDossierTemplate; get selectedDossierTemplate() {
return this.#dossierTemplate;
get dossierTemplate() {
return this._dossierTemplate;
} }
set dossierTemplate(value: DossierTemplate) { set selectedDossierTemplate(value: DossierTemplate) {
this._dossierTemplate = value; if (!value) {
value = this.dossierTemplates.find(t => t.id === this.#initialDossierTemplateId);
}
this.#dossierTemplate = value;
this.currentDossierTemplateId = value.dossierTemplateId;
this.#dossier = this.selectDossier;
this.dictionaries = this.#dictionaries; this.dictionaries = this.#dictionaries;
this.#compareDictionary = this.selectDictionary; this.#disableDiffEditor();
this.showDiffEditor = false;
}
private _dossier = this.selectDossier; this.#updateDropdownsOptions(false);
}
get selectedDossier() { get selectedDossier() {
return this._dossier; return this.#dossier;
} }
set selectedDossier(dossier: Dossier) { set selectedDossier(dossier: Dossier) {
this._dossier = dossier; this.#dossier = dossier;
if (dossier.dossierName === this.selectDossier.dossierName) { if (dossier.dossierName === this.selectDossier.dossierName) {
this.showDiffEditor = false; this.#disableDiffEditor();
this.diffEditorText = '';
return; return;
} }
this.#onDossierChanged(dossier.dossierTemplateId, dossier.id).then(entries => { this.#onDossierChanged(dossier.dossierTemplateId, dossier.id).then(entries => {
this.diffEditorText = entries; if (!entries.startsWith(COMPARE_ENTRIES_ERROR)) {
this.showDiffEditor = true; this.diffEditorText = entries;
this.showDiffEditor = true;
}
}); });
} }
get compareDictionary() {
return this.#compareDictionary;
}
set compareDictionary(dictionary: Dictionary) {
this._loadingService.start();
this.#compareDictionary = dictionary;
if (dictionary.label === this.selectDictionary.label) {
this.showDiffEditor = false;
this.diffEditorText = '';
return;
}
const entries: List =
this.#compareDictionary.getEntries(this.type) ??
this._dictionariesMapService.get(this.#compareDictionary.dossierTemplateId, this.#compareDictionary.type).getEntries(this.type);
if (entries.length) {
this.diffEditorText = entries.join('\n');
this.showDiffEditor = true;
return;
}
this._dictionaryService
.getForType(this.#compareDictionary.dossierTemplateId, this.#compareDictionary.type)
.then(values => {
this.#compareDictionary.setEntries([...values[DICTIONARY_TYPE_KEY_MAP[this.type]]] ?? [], this.type);
this.diffEditorText = [...(this.#compareDictionary.getEntries(this.type) as string[])].join('\n');
this.showDiffEditor = true;
this._changeRef.markForCheck();
this._loadingService.stop();
})
.catch(() => this.#compareDictionary.setEntries([], this.type));
}
get dossierTemplateIsNotSelected() {
return this.filterByDossierTemplate && this._dossierTemplate.name === this.selectDossierTemplate.name;
}
get optionNotSelected() { get optionNotSelected() {
if (this.filterByDossierTemplate) {
return this.selectDictionary.label === this.#compareDictionary.label;
}
return this.selectedDossier.dossierName === this.selectDossier.dossierName; return this.selectedDossier.dossierName === this.selectDossier.dossierName;
} }
@ -176,10 +122,16 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
} }
get #dictionaries(): Dictionary[] { get #dictionaries(): Dictionary[] {
if (!this._dossierTemplate || this._dossierTemplate.name === this.selectDossierTemplate.name) { if (!this.#dossierTemplate || this.#dossierTemplate.name === this.selectDossierTemplate.name) {
return; return;
} }
return this._dictionariesMapService.get(this.dossierTemplate?.dossierTemplateId).filter(dict => !dict.virtual); return this._dictionariesMapService.get(this.selectedDossierTemplate?.dossierTemplateId).filter(dict => !dict.virtual);
}
get #templatesWithCurrentEntityType() {
return this.dossierTemplatesService.all.filter(t =>
this._dictionaryService.hasType(t.dossierTemplateId, this.selectedDictionaryType),
);
} }
download(): void { download(): void {
@ -198,8 +150,8 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
@Debounce() @Debounce()
searchChanged(text: string) { searchChanged(text: string) {
this.findMatches = this._getMatches(text.toLowerCase()); this.findMatches = this.#getMatches(text.toLowerCase());
this._applySearchDecorations(); this.#applySearchDecorations();
this.currentMatch = 0; this.currentMatch = 0;
this.nextSearchMatch(); this.nextSearchMatch();
@ -211,7 +163,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
} }
this.currentMatch = this.currentMatch < this.findMatches.length ? this.currentMatch + 1 : 1; this.currentMatch = this.currentMatch < this.findMatches.length ? this.currentMatch + 1 : 1;
this._scrollToCurrentMatch(); this.#scrollToCurrentMatch();
} }
previousSearchMatch(): void { previousSearchMatch(): void {
@ -220,7 +172,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
} }
this.currentMatch = this.currentMatch > 1 ? this.currentMatch - 1 : this.findMatches.length; this.currentMatch = this.currentMatch > 1 ? this.currentMatch - 1 : this.findMatches.length;
this._scrollToCurrentMatch(); this.#scrollToCurrentMatch();
} }
toggleCompareMode() { toggleCompareMode() {
@ -231,39 +183,39 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if ( if (changes.activeEntryType && this.#dossier?.dossierTemplateId && this.selectedDossier?.dossierId) {
(changes.selectedDictionaryType || changes.activeEntryType) && this.#onDossierChanged(this.#dossier.dossierTemplateId, this.#dossier.dossierId).then(entries => {
this._dossier?.dossierTemplateId && if (!entries.startsWith(COMPARE_ENTRIES_ERROR)) {
this.selectedDossier?.dossierId this.diffEditorText = entries;
) { this.showDiffEditor = true;
this.#onDossierChanged(this._dossier.dossierTemplateId, this._dossier.dossierId).then(entries => { }
this.diffEditorText = entries;
this.showDiffEditor = true;
}); });
} }
if (changes.selectedDictionaryType) { if (changes.selectedDictionaryType) {
this.searchText = ''; this.#disableDiffEditor();
this.#updateDropdownsOptions();
} }
} }
private _applySearchDecorations() { #applySearchDecorations() {
this._searchDecorations = this.editor.codeEditor?.deltaDecorations(this._searchDecorations, []) || []; this._searchDecorations = this.editor.codeEditor?.deltaDecorations(this._searchDecorations, []) || [];
const decorations = this.findMatches.map(match => this._getSearchDecoration(match)); const decorations = this.findMatches.map(match => this.#getSearchDecoration(match));
this._searchDecorations = this.editor.codeEditor?.deltaDecorations(this._searchDecorations, decorations) || []; this._searchDecorations = this.editor.codeEditor?.deltaDecorations(this._searchDecorations, decorations) || [];
} }
private _getMatches(text: string): FindMatch[] { #getMatches(text: string): FindMatch[] {
const model = this.editor.codeEditor?.getModel(); const model = this.editor.codeEditor?.getModel();
return model?.findMatches(text, false, false, false, null, false) || []; return model?.findMatches(text, false, false, false, null, false) || [];
} }
private _getSearchDecoration(match: FindMatch): IModelDeltaDecoration { #getSearchDecoration(match: FindMatch): IModelDeltaDecoration {
return { range: match.range, options: { inlineClassName: 'search-marker' } }; return { range: match.range, options: { inlineClassName: 'search-marker' } };
} }
private _scrollToCurrentMatch(): void { #scrollToCurrentMatch(): void {
const range = this.findMatches[this.currentMatch - 1].range; const range = this.findMatches[this.currentMatch - 1].range;
this.editor.codeEditor.setSelection(range); this.editor.codeEditor.setSelection(range);
@ -279,14 +231,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
await firstValueFrom( await firstValueFrom(
this._dictionaryService.loadDictionaryEntriesByType([this.selectedDictionaryType], dossierTemplateId, dossierId), this._dictionaryService.loadDictionaryEntriesByType([this.selectedDictionaryType], dossierTemplateId, dossierId),
).catch(() => { ).catch(() => {
const selectedDossierTemplate = this.dossierTemplatesService.find(dossierTemplateId); return [{ entries: [COMPARE_ENTRIES_ERROR], type: '' }];
this._toaster.error(_('dictionary-overview.compare-error'), {
params: {
type: this.selectedDictionaryTypeLabel,
template: selectedDossierTemplate.name,
},
});
return [{ entries: [], type: '' }];
}) })
)[0]; )[0];
} }
@ -294,8 +239,28 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
this.activeEntryType === DictionaryEntryTypes.ENTRY || this.hint this.activeEntryType === DictionaryEntryTypes.ENTRY || this.hint
? [...dictionary.entries] ? [...dictionary.entries]
: this.activeEntryType === DictionaryEntryTypes.FALSE_POSITIVE : this.activeEntryType === DictionaryEntryTypes.FALSE_POSITIVE
? [...dictionary.falsePositiveEntries] ? [...dictionary.falsePositiveEntries]
: [...dictionary.falseRecommendationEntries]; : [...dictionary.falseRecommendationEntries];
return activeEntries.join('\n'); return activeEntries.join('\n');
} }
#updateDropdownsOptions(updateSelectedDossierTemplate = true) {
if (updateSelectedDossierTemplate) {
this.dossierTemplates = this.#templatesWithCurrentEntityType;
this.selectedDossierTemplate = this.dossierTemplates.find(t => t.id === this.currentDossierTemplateId);
}
this.dossiers = this._activeDossiersService.all.filter(d => d.dossierTemplateId === this.currentDossierTemplateId);
const templateDictionary = {
id: 'template',
dossierId: 'template',
dossierName: 'Template Dictionary',
dossierTemplateId: this.currentDossierTemplateId,
} as Dossier;
this.dossiers.push(templateDictionary);
}
#disableDiffEditor() {
this.showDiffEditor = false;
this.diffEditorText = '';
}
} }

View File

@ -133,6 +133,10 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
return !!this._dictionariesMapService.get(dossierTemplateId).find(e => e.type === SuperTypes.ManualRedaction && !e.virtual); return !!this._dictionariesMapService.get(dossierTemplateId).find(e => e.type === SuperTypes.ManualRedaction && !e.virtual);
} }
hasType(dossierTemplateId: string, type: string): boolean {
return !!this._dictionariesMapService.get(dossierTemplateId).find(e => e.type === type && !e.virtual);
}
getRedactTextDictionaries(dossierTemplateId: string, dossierDictionaryOnly: boolean): Dictionary[] { getRedactTextDictionaries(dossierTemplateId: string, dossierDictionaryOnly: boolean): Dictionary[] {
return this._dictionariesMapService return this._dictionariesMapService
.get(dossierTemplateId) .get(dossierTemplateId)