diff --git a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts index 13e9a81a1..0517b0fd1 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts @@ -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; - 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 = []; + } + ); + } } diff --git a/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html b/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html index 406668a84..93688a94f 100644 --- a/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html +++ b/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html @@ -91,6 +91,10 @@ {{ appStateService.getRuleSetById(appStateService.activeProject.ruleSetId)?.name }} +
+ + {{ 'project-overview.project-details.dictionary' | translate }} +
diff --git a/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.ts b/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.ts index 368cd05aa..04761517b 100644 --- a/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.ts +++ b/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.ts @@ -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( diff --git a/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.html b/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.html new file mode 100644 index 000000000..870b6adfa --- /dev/null +++ b/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.html @@ -0,0 +1,23 @@ +
+
+ +
+
+ +
+ +
+ + +
+
+
+ + +
diff --git a/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.scss b/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.scss new file mode 100644 index 000000000..b02797090 --- /dev/null +++ b/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.scss @@ -0,0 +1,4 @@ +.dialog-content { + height: calc(90vh - 160px); + padding: 12px 12px 0; +} diff --git a/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.ts b/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.ts new file mode 100644 index 000000000..e36e77d7d --- /dev/null +++ b/apps/red-ui/src/app/modules/projects/dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component.ts @@ -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, + @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(); + }); + } +} diff --git a/apps/red-ui/src/app/modules/projects/projects.module.ts b/apps/red-ui/src/app/modules/projects/projects.module.ts index f18e12ba9..af2361d8d 100644 --- a/apps/red-ui/src/app/modules/projects/projects.module.ts +++ b/apps/red-ui/src/app/modules/projects/projects.module.ts @@ -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] }) diff --git a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html index d894bc06f..12814acff 100644 --- a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html +++ b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html @@ -263,6 +263,7 @@ { + this.reloadProjects(); + }); + } + toggleCollapsedDetails() { this.collapsedDetails = !this.collapsedDetails; } diff --git a/apps/red-ui/src/app/modules/projects/services/projects-dialog.service.ts b/apps/red-ui/src/app/modules/projects/services/projects-dialog.service.ts index 1f977cc7f..bc869d051 100644 --- a/apps/red-ui/src/app/modules/projects/services/projects-dialog.service.ts +++ b/apps/red-ui/src/app/modules/projects/services/projects-dialog.service.ts @@ -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 { + $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 { const ref = this._dialog.open(AssignOwnerDialogComponent, { ...dialogConfig, diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html index 7a2d7513b..829b735e8 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html @@ -73,7 +73,7 @@
-
+
diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts index 70a1aadaf..ff9f5e2cd 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts @@ -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, diff --git a/apps/red-ui/src/app/modules/shared/services/dictionary-save.service.ts b/apps/red-ui/src/app/modules/shared/services/dictionary-save.service.ts new file mode 100644 index 000000000..dc72fa7f0 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/services/dictionary-save.service.ts @@ -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 { + 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; + 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'); + } + } +} diff --git a/apps/red-ui/src/app/state/app-state.service.ts b/apps/red-ui/src/app/state/app-state.service.ts index 651296d80..713af7993 100644 --- a/apps/red-ui/src/app/state/app-state.service.ts +++ b/apps/red-ui/src/app/state/app-state.service.ts @@ -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); diff --git a/apps/red-ui/src/app/state/model/project.wrapper.ts b/apps/red-ui/src/app/state/model/project.wrapper.ts index d1a7913ca..aecbcd1af 100644 --- a/apps/red-ui/src/app/state/model/project.wrapper.ts +++ b/apps/red-ui/src/app/state/model/project.wrapper.ts @@ -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 : []; diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 5906aa7ca..3dac708dc 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -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",