added dossier dictionary dialog

This commit is contained in:
Timo 2021-05-11 19:52:31 +03:00
parent 8ebbf33a0d
commit 288fad9ff6
16 changed files with 218 additions and 70 deletions

View File

@ -3,16 +3,14 @@ import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationService, NotificationType } from '@services/notification.service';
import { NotificationService } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { saveAs } from 'file-saver';
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
import { FormBuilder } from '@angular/forms';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { DictionaryManagerComponent } from '../../../shared/components/dictionary-manager/dictionary-manager.component';
const MIN_WORD_LENGTH = 2;
import { DictionarySaveService } from '../../../shared/services/dictionary-save.service';
@Component({
selector: 'redaction-dictionary-overview-screen',
@ -30,6 +28,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
readonly permissionsService: PermissionsService,
private readonly _notificationService: NotificationService,
protected readonly _translateService: TranslateService,
private readonly _dictionarySaveService: DictionarySaveService,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _dialogService: AdminDialogService,
private readonly _router: Router,
@ -45,20 +44,6 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
this._loadEntries();
}
private _loadEntries() {
this.processing = true;
this._dictionaryControllerService.getDictionaryForType(this.dictionary.ruleSetId, this.dictionary.type).subscribe(
(data) => {
this.processing = false;
this.entries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' }));
},
() => {
this.processing = false;
this.entries = [];
}
);
}
get dictionary(): TypeValue {
return this._appStateService.activeDictionary;
}
@ -81,52 +66,6 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
});
}
async saveEntries(entries: string[]) {
let entriesToAdd = [];
entries.forEach((currentEntry) => {
entriesToAdd.push(currentEntry);
});
// remove empty lines
entriesToAdd = entriesToAdd.filter((e) => e && e.trim().length > 0).map((e) => e.trim());
const invalidRowsExist = entriesToAdd.filter((e) => e.length < MIN_WORD_LENGTH);
if (invalidRowsExist.length === 0) {
// can add at least 1 - block UI
this.processing = true;
let obs: Observable<any>;
if (entriesToAdd.length > 0) {
obs = this._dictionaryControllerService.addEntry(entriesToAdd, this.dictionary.ruleSetId, this.dictionary.type, null, true);
} else {
obs = this._dictionaryControllerService.deleteEntries(this.entries, this.dictionary.ruleSetId, this.dictionary.type);
}
obs.subscribe(
() => {
// TODO
this._loadEntries();
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.success.generic'),
null,
NotificationType.SUCCESS
);
},
() => {
this.processing = false;
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.error.generic'),
null,
NotificationType.ERROR
);
}
);
} else {
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.error.entries-too-short'),
null,
NotificationType.ERROR
);
}
}
download(): void {
const content = this._dictionaryManager.editorValue;
const blob = new Blob([content], {
@ -147,4 +86,31 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
fileReader.readAsText(file);
}
}
saveEntries(entries: string[]) {
this.processing = true;
this._dictionarySaveService.saveEntries(entries, this.entries, this.dictionary.ruleSetId, this.dictionary.type, null).subscribe(
() => {
this.processing = false;
this._loadEntries();
},
() => {
this.processing = false;
}
);
}
private _loadEntries() {
this.processing = true;
this._dictionaryControllerService.getDictionaryForType(this.dictionary.ruleSetId, this.dictionary.type).subscribe(
(data) => {
this.processing = false;
this.entries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' }));
},
() => {
this.processing = false;
this.entries = [];
}
);
}
}

View File

@ -91,6 +91,10 @@
<mat-icon svgIcon="red:template"></mat-icon>
<span>{{ appStateService.getRuleSetById(appStateService.activeProject.ruleSetId)?.name }} </span>
</div>
<div *ngIf="appStateService.activeProject.type" class="pointer" (click)="openDossierDictionaryDialog.emit()">
<mat-icon svgIcon="red:dictionary"></mat-icon>
<span>{{ 'project-overview.project-details.dictionary' | translate }} </span>
</div>
</div>
<div *ngIf="!!appStateService.activeProject.project.description" class="pb-32">

View File

@ -19,6 +19,7 @@ export class ProjectDetailsComponent implements OnInit {
@Input() filters: { needsWorkFilters: FilterModel[]; statusFilters: FilterModel[] };
@Output() filtersChanged = new EventEmitter();
@Output() openAssignProjectMembersDialog = new EventEmitter();
@Output() openDossierDictionaryDialog = new EventEmitter();
@Output() toggleCollapse = new EventEmitter();
constructor(

View File

@ -0,0 +1,23 @@
<section class="dialog">
<div [translate]="'dossier-dictionary-dialog.title'" class="dialog-header heading-l"></div>
<form (submit)="saveDossierDictionary()">
<div class="dialog-content">
<redaction-dictionary-manager
#dictionaryManager
[withFloatingActions]="false"
[initialDictionaryEntries]="project.type?.entries"
></redaction-dictionary-manager>
</div>
<div class="dialog-actions">
<button [disabled]="!dictionaryManager.hasChanges" color="primary" mat-flat-button type="submit">
{{ 'dossier-dictionary-dialog.save-changes' | translate }}
</button>
<div [translate]="'dossier-dictionary-dialog.cancel'" class="all-caps-label pointer cancel" mat-dialog-close></div>
</div>
</form>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button>
</section>

View File

@ -0,0 +1,4 @@
.dialog-content {
height: calc(90vh - 160px);
padding: 12px 12px 0;
}

View File

@ -0,0 +1,37 @@
import { Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ProjectWrapper } from '../../../../state/model/project.wrapper';
import { DictionaryManagerComponent } from '../../../shared/components/dictionary-manager/dictionary-manager.component';
import { DictionarySaveService } from '../../../shared/services/dictionary-save.service';
import { AppStateService } from '../../../../state/app-state.service';
@Component({
selector: 'redaction-dossier-dictionary-dialog',
templateUrl: './dossier-dictionary-dialog.component.html',
styleUrls: ['./dossier-dictionary-dialog.component.scss']
})
export class DossierDictionaryDialogComponent {
@ViewChild('dictionaryManager', { static: false }) private _dictionaryManager: DictionaryManagerComponent;
constructor(
public dialogRef: MatDialogRef<DossierDictionaryDialogComponent>,
@Inject(MAT_DIALOG_DATA) public project: ProjectWrapper,
private readonly _appStateService: AppStateService,
private readonly _dictionarySaveService: DictionarySaveService
) {}
saveDossierDictionary() {
this._dictionarySaveService
.saveEntries(
this._dictionaryManager.currentDictionaryEntries,
this._dictionaryManager.initialDictionaryEntries,
this.project.ruleSetId,
'dossier_redaction',
this.project.projectId
)
.subscribe(() => {
this._appStateService.updateProjectDictionary(this.project.ruleSetId, this.project.projectId);
this.dialogRef.close();
});
}
}

View File

@ -35,6 +35,7 @@ import { ManualAnnotationService } from './services/manual-annotation.service';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from './services/annotation-processing.service';
import { AnnotationRemoveActionsComponent } from './components/annotation-remove-actions/annotation-remove-actions.component';
import { DossierDictionaryDialogComponent } from './dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component';
const screens = [ProjectListingScreenComponent, ProjectOverviewScreenComponent, FilePreviewScreenComponent];
@ -80,7 +81,7 @@ const services = [
];
@NgModule({
declarations: [...components],
declarations: [...components, DossierDictionaryDialogComponent],
providers: [...services],
imports: [CommonModule, SharedModule, FileUploadDownloadModule, ProjectsRoutingModule]
})

View File

@ -263,6 +263,7 @@
<redaction-project-details
#projectDetailsComponent
(filtersChanged)="filtersChanged($event)"
(openDossierDictionaryDialog)="openDossierDictionaryDialog()"
(openAssignProjectMembersDialog)="openAssignProjectMembersDialog()"
(toggleCollapse)="toggleCollapsedDetails()"
[filters]="detailsContainerFilters"

View File

@ -232,6 +232,12 @@ export class ProjectOverviewScreenComponent extends BaseListingComponent<FileSta
});
}
openDossierDictionaryDialog() {
this._dialogService.openDossierDictionaryDialog(null, this.activeProject, () => {
this.reloadProjects();
});
}
toggleCollapsedDetails() {
this.collapsedDetails = !this.collapsedDetails;
}

View File

@ -21,6 +21,7 @@ import { ManualAnnotationService } from './manual-annotation.service';
import { TranslateService } from '@ngx-translate/core';
import { ManualAnnotationDialogComponent } from '../dialogs/manual-redaction-dialog/manual-annotation-dialog.component';
import { AssignOwnerDialogComponent } from '../dialogs/assign-owner-dialog/assign-owner-dialog.component';
import { DossierDictionaryDialogComponent } from '../dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component';
const dialogConfig = {
width: '662px',
@ -201,6 +202,23 @@ export class ProjectsDialogService {
return ref;
}
openDossierDictionaryDialog($event: MouseEvent, project: ProjectWrapper, cb?: Function): MatDialogRef<DossierDictionaryDialogComponent> {
$event?.stopPropagation();
const ref = this._dialog.open(DossierDictionaryDialogComponent, {
...dialogConfig,
width: '90vw',
height: '90vh',
autoFocus: false,
data: project
});
ref.afterClosed().subscribe((result) => {
if (cb) {
cb(result);
}
});
return ref;
}
openAssignFileReviewerDialog(file: FileStatus, cb?: Function, ignoreDialogChanges = false): MatDialogRef<AssignOwnerDialogComponent> {
const ref = this._dialog.open(AssignOwnerDialogComponent, {
...dialogConfig,

View File

@ -73,7 +73,7 @@
</ace-editor>
</div>
<div *ngIf="hasChanges && permissionsService.isAdmin()" [class.offset]="compareForm.get('active').value" class="changes-box">
<div *ngIf="withFloatingActions && hasChanges && permissionsService.isAdmin()" [class.offset]="compareForm.get('active').value" class="changes-box">
<redaction-icon-button (action)="saveEntries()" icon="red:check" text="dictionary-overview.save-changes" type="primary"></redaction-icon-button>
<div (click)="revert()" class="all-caps-label cancel" translate="dictionary-overview.revert-changes"></div>
</div>

View File

@ -1,11 +1,10 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild } from '@angular/core';
import { DictionaryControllerService, RuleSetModel, TypeValue } from '@redaction/red-ui-http';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AceEditorComponent } from 'ng2-ace-editor';
import { PermissionsService } from '@services/permissions.service';
import { NotificationService } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { AdminDialogService } from '../../../admin/services/admin-dialog.service';
import { ActivatedRoute, Router } from '@angular/router';
import { AppStateService } from '@state/app-state.service';
import { debounce } from '@utils/debounce';
@ -19,6 +18,9 @@ const MIN_WORD_LENGTH = 2;
styleUrls: ['./dictionary-manager.component.scss']
})
export class DictionaryManagerComponent implements OnInit, OnChanges {
@Input()
withFloatingActions = true;
@Input()
initialDictionaryEntries: string[];
@ -49,7 +51,6 @@ export class DictionaryManagerComponent implements OnInit, OnChanges {
private readonly _notificationService: NotificationService,
protected readonly _translateService: TranslateService,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _dialogService: AdminDialogService,
private readonly _router: Router,
private readonly _activatedRoute: ActivatedRoute,
private readonly _appStateService: AppStateService,

View File

@ -0,0 +1,65 @@
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { NotificationService, NotificationType } from '../../../services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { DictionaryControllerService } from '@redaction/red-ui-http';
import { tap } from 'rxjs/operators';
const MIN_WORD_LENGTH = 2;
@Injectable({
providedIn: 'root'
})
export class DictionarySaveService {
constructor(
private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService,
private readonly _dictionaryControllerService: DictionaryControllerService
) {}
saveEntries(entries: string[], initialEntries: string[], ruleSetId: string, type: string, dossierId: string): Observable<any> {
let entriesToAdd = [];
entries.forEach((currentEntry) => {
entriesToAdd.push(currentEntry);
});
// remove empty lines
entriesToAdd = entriesToAdd.filter((e) => e && e.trim().length > 0).map((e) => e.trim());
const invalidRowsExist = entriesToAdd.filter((e) => e.length < MIN_WORD_LENGTH);
if (invalidRowsExist.length === 0) {
// can add at least 1 - block UI
let obs: Observable<any>;
if (entriesToAdd.length > 0) {
obs = this._dictionaryControllerService.addEntry(entriesToAdd, ruleSetId, type, dossierId, true);
} else {
obs = this._dictionaryControllerService.deleteEntries(initialEntries, ruleSetId, type, dossierId);
}
return obs.pipe(
tap(
() => {
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.success.generic'),
null,
NotificationType.SUCCESS
);
},
() => {
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.error.generic'),
null,
NotificationType.ERROR
);
}
)
);
} else {
this._notificationService.showToastNotification(
this._translateService.instant('dictionary-overview.error.entries-too-short'),
null,
NotificationType.ERROR
);
return throwError('Entries to short');
}
}
}

View File

@ -320,9 +320,23 @@ export class AppStateService {
this._appState.activeProjectId = null;
this._router.navigate(['/main/projects']);
return;
} else {
this.updateProjectDictionary(this.activeProject.ruleSetId, projectId);
}
}
updateProjectDictionary(ruleSetId: string, projectId: string) {
// project exists, load it's dictionary
this._dictionaryControllerService.getDictionaryForType(ruleSetId, 'dossier_redaction', projectId).subscribe(
(typeData) => {
this.activeProject.type = typeData;
},
() => {
this.activeProject.type = null;
}
);
}
activateFile(projectId: string, fileId: string) {
if (this._appState.activeProjectId === projectId && this._appState.activeFileId === fileId) return;
this.activateProject(projectId);

View File

@ -1,6 +1,6 @@
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import * as moment from 'moment';
import { Project } from '@redaction/red-ui-http';
import { Dictionary, Project } from '@redaction/red-ui-http';
export class ProjectWrapper {
totalNumberOfPages?: number;
@ -12,6 +12,7 @@ export class ProjectWrapper {
hasPendingOrProcessing?: boolean;
allFilesApproved?: boolean;
type: Dictionary;
constructor(public project: Project, files: FileStatusWrapper[]) {
this._files = files ? files : [];

View File

@ -185,6 +185,11 @@
"expand": "Show Details",
"collapse": "Hide Details"
},
"dossier-dictionary-dialog": {
"title": "Dossier Dictionary",
"save-changes": "Save Changes",
"cancel": "cancel"
},
"project-overview": {
"no-ocr": "No OCR",
"ocr-performed": "OCR was performed for this file.",
@ -272,6 +277,7 @@
"created-on": "Created on {{date}}",
"due-date": "Due {{date}}"
},
"dictionary": "Dossier Dictionary",
"description": "Description"
},
"header": "Dossier Overview",