diff --git a/.editorconfig b/.editorconfig index 49601a81f..347f00f8a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,6 +17,7 @@ trim_trailing_whitespace = false [*.ts] ij_typescript_use_double_quotes = false ij_typescript_enforce_trailing_comma = keep +ij_typescript_spaces_within_imports = true [{*.json, .prettierrc, .eslintrc}] indent_size = 2 diff --git a/.gitignore b/.gitignore index a920879bb..7bfea35e2 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ version.properties paligo-styles/style.css* migrations.json +*.iml diff --git a/angular.json b/angular.json index 990116c5b..6706ca3d2 100644 --- a/angular.json +++ b/angular.json @@ -1,23 +1,8 @@ { - "version": 1, "cli": { - "defaultCollection": "@nrwl/angular", - "analytics": false, - "packageManager": "yarn" - }, - "defaultProject": "red-ui", - "schematics": { - "@nrwl/angular:application": { - "linter": "eslint", - "unitTestRunner": "jest", - "e2eTestRunner": "cypress" - }, - "@nrwl/angular:library": { - "linter": "eslint", - "unitTestRunner": "jest" - }, - "@nrwl/angular:component": {} + "analytics": "d22ff5ae-c863-4253-83e3-0a969e4bb5fe" }, + "version": 1, "projects": { "common-ui": { "projectType": "library", @@ -40,30 +25,8 @@ }, "outputs": ["{options.outputFile}"] } - } - }, - "red-domain": { - "projectType": "library", - "root": "libs/red-domain", - "sourceRoot": "libs/red-domain/src", - "prefix": "red", - "architect": { - "test": { - "builder": "@nrwl/jest:jest", - "outputs": ["coverage/libs/red-domain"], - "options": { - "jestConfig": "libs/red-domain/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "builder": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": ["libs/red-domain/src/**/*.ts", "libs/red-domain/src/**/*.html"] - }, - "outputs": ["{options.outputFile}"] - } - } + }, + "tags": [] }, "red-cache": { "projectType": "library", @@ -91,6 +54,30 @@ "@schematics/angular:component": { "style": "scss" } + }, + "tags": [] + }, + "red-domain": { + "projectType": "library", + "root": "libs/red-domain", + "sourceRoot": "libs/red-domain/src", + "prefix": "red", + "architect": { + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/red-domain"], + "options": { + "jestConfig": "libs/red-domain/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["libs/red-domain/src/**/*.ts", "libs/red-domain/src/**/*.html"] + }, + "outputs": ["{options.outputFile}"] + } } }, "red-ui": { @@ -152,7 +139,14 @@ "with": "apps/red-ui/src/environments/environment.prod.ts" } ], - "optimization": true, + "optimization": { + "scripts": true, + "styles": { + "minify": true, + "inlineCritical": false + }, + "fonts": true + }, "outputHashing": "all", "sourceMap": false, "namedChunks": false, @@ -209,7 +203,8 @@ }, "outputs": ["coverage/apps/red-ui"] } - } + }, + "tags": [] } } } diff --git a/apps/red-ui/src/app/app-routing.module.ts b/apps/red-ui/src/app/app-routing.module.ts index 92d9eae7a..24a5d3557 100644 --- a/apps/red-ui/src/app/app-routing.module.ts +++ b/apps/red-ui/src/app/app-routing.module.ts @@ -3,13 +3,13 @@ import { AuthGuard } from './modules/auth/auth.guard'; import { CompositeRouteGuard, CustomRouteReuseStrategy } from '@iqser/common-ui'; import { RedRoleGuard } from './modules/auth/red-role.guard'; import { BaseScreenComponent } from '@components/base-screen/base-screen.component'; -import { RouteReuseStrategy, RouterModule } from '@angular/router'; +import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'; import { NgModule } from '@angular/core'; import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component'; import { AppStateGuard } from '@state/app-state.guard'; -import { UserProfileScreenComponent } from '@components/user-profile/user-profile-screen.component'; +import { DossiersGuard } from '@guards/dossiers.guard'; -const routes = [ +const routes: Routes = [ { path: '', redirectTo: 'main/dossiers', @@ -21,18 +21,9 @@ const routes = [ canActivate: [AuthGuard], }, { - path: 'main/my-profile', + path: 'main/account', component: BaseScreenComponent, - children: [ - { - path: '', - component: UserProfileScreenComponent, - }, - ], - canActivate: [CompositeRouteGuard], - data: { - routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], - }, + loadChildren: () => import('./modules/account/account.module').then(m => m.AccountModule), }, { path: 'main/admin', @@ -43,6 +34,12 @@ const routes = [ path: 'main/dossiers', component: BaseScreenComponent, loadChildren: () => import('./modules/dossier/dossiers.module').then(m => m.DossiersModule), + canActivate: [CompositeRouteGuard], + canDeactivate: [DossiersGuard], + data: { + routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard, DossiersGuard], + requiredRoles: ['RED_USER', 'RED_MANAGER'], + }, }, { path: 'main/downloads', diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 67dfac44c..241a83554 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -20,7 +20,6 @@ import { DownloadsListScreenComponent } from '@components/downloads-list-screen/ import { AppRoutingModule } from './app-routing.module'; import { SharedModule } from '@shared/shared.module'; import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module'; -import { UserProfileScreenComponent } from '@components/user-profile/user-profile-screen.component'; import { PlatformLocation } from '@angular/common'; import { BASE_HREF } from './tokens'; import { MONACO_PATH, MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; @@ -36,6 +35,7 @@ import * as links from '../assets/help-mode/links.json'; import { HELP_DOCS, IqserHelpModeModule, MAX_RETRIES_ON_SERVER_ERROR, ServerErrorInterceptor, ToastComponent } from '@iqser/common-ui'; import { KeycloakService } from 'keycloak-angular'; import { GeneralSettingsService } from '@services/general-settings.service'; +import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component'; export function httpLoaderFactory(httpClient: HttpClient): PruningTranslationLoader { return new PruningTranslationLoader(httpClient, '/assets/i18n/', '.json'); @@ -51,9 +51,9 @@ function cleanupBaseUrl(baseUrl: string) { } } -const screens = [BaseScreenComponent, DownloadsListScreenComponent, UserProfileScreenComponent]; +const screens = [BaseScreenComponent, DownloadsListScreenComponent]; -const components = [AppComponent, AuthErrorComponent, NotificationsComponent, SpotlightSearchComponent, ...screens]; +const components = [AppComponent, AuthErrorComponent, NotificationsComponent, SpotlightSearchComponent, BreadcrumbsComponent, ...screens]; @NgModule({ declarations: [...components], diff --git a/apps/red-ui/src/app/components/base-screen/base-screen.component.html b/apps/red-ui/src/app/components/base-screen/base-screen.component.html index 2c927f27c..6a7584830 100644 --- a/apps/red-ui/src/app/components/base-screen/base-screen.component.html +++ b/apps/red-ui/src/app/components/base-screen/base-screen.component.html @@ -4,41 +4,7 @@
@@ -54,16 +20,16 @@ *ngIf="(isSearchScreen$ | async) === false && (currentUser.isUser || currentUser.isManager)" [actions]="searchActions" [placeholder]="'search.placeholder' | translate" - iqserHelpMode="search" + iqserHelpMode="search-in-entire-application" > - +
diff --git a/apps/red-ui/src/app/components/base-screen/base-screen.component.ts b/apps/red-ui/src/app/components/base-screen/base-screen.component.ts index e51e82711..3bfa76673 100644 --- a/apps/red-ui/src/app/components/base-screen/base-screen.component.ts +++ b/apps/red-ui/src/app/components/base-screen/base-screen.component.ts @@ -8,8 +8,10 @@ import { FileDownloadService } from '@upload-download/services/file-download.ser import { TranslateService } from '@ngx-translate/core'; import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-search-action'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { distinctUntilChanged, filter, map, startWith } from 'rxjs/operators'; +import { filter, map, startWith } from 'rxjs/operators'; import { DossiersService } from '@services/entity-services/dossiers.service'; +import { shareDistinctLast } from '@iqser/common-ui'; +import { BreadcrumbsService } from '@services/breadcrumbs.service'; interface MenuItem { readonly name: string; @@ -20,7 +22,6 @@ interface MenuItem { } const isNavigationStart = event => event instanceof NavigationStart; -const isDossiersView = url => url.includes('/main/dossiers') && !url.includes('/search'); const isSearchScreen = url => url.includes('/main/dossiers') && url.includes('/search'); @Component({ @@ -31,9 +32,10 @@ export class BaseScreenComponent { readonly currentUser = this.userService.currentUser; readonly userMenuItems: readonly MenuItem[] = [ { - name: _('top-bar.navigation-items.my-account.children.my-profile'), - routerLink: '/main/my-profile', - show: true, + name: _('top-bar.navigation-items.my-account.children.account'), + routerLink: '/main/account', + show: this.currentUser.isUser, + action: this.appStateService.reset, showDot: () => false, }, { @@ -60,8 +62,8 @@ export class BaseScreenComponent { { text: this._translateService.instant('search.this-dossier'), icon: 'red:enter', - hide: (): boolean => !this.dossiersService.activeDossier, - action: (query): void => this._search(query, this.dossiersService.activeDossierId), + hide: (): boolean => this._hideSearchThisDossier, + action: (query): void => this._searchThisDossier(query), }, { text: this._translateService.instant('search.entire-platform'), @@ -73,9 +75,8 @@ export class BaseScreenComponent { filter(isNavigationStart), map((event: NavigationStart) => event.url), startWith(this._router.url), - distinctUntilChanged(), + shareDistinctLast(), ); - readonly isDossiersView$ = this._navigationStart$.pipe(map(isDossiersView)); readonly isSearchScreen$ = this._navigationStart$.pipe(map(isSearchScreen)); constructor( @@ -87,8 +88,19 @@ export class BaseScreenComponent { readonly fileDownloadService: FileDownloadService, private readonly _router: Router, private readonly _translateService: TranslateService, + readonly breadcrumbsService: BreadcrumbsService, ) {} + private get _hideSearchThisDossier() { + const routerLink = this.breadcrumbsService.breadcrumbs[1]?.routerLink; + if (!routerLink) { + return true; + } + + const isDossierOverview = routerLink.includes('dossiers') && routerLink.length === 3; + return !isDossierOverview; + } + trackByName(index: number, item: MenuItem) { return item.name; } @@ -97,4 +109,13 @@ export class BaseScreenComponent { const queryParams = { query, dossierId }; this._router.navigate(['main/dossiers/search'], { queryParams }).then(); } + + private _searchThisDossier(query: string) { + const routerLink = this.breadcrumbsService.breadcrumbs[1]?.routerLink; + if (!routerLink) { + return this._search(query); + } + const dossierId = routerLink[2]; + return this._search(query, dossierId); + } } diff --git a/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.html b/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.html new file mode 100644 index 000000000..e1993a09b --- /dev/null +++ b/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.html @@ -0,0 +1,25 @@ + + + + {{ 'top-bar.navigation-items.back' | translate }} + + + + + + + + {{ breadcrumb.name$ | async }} + + + + diff --git a/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.scss b/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.scss new file mode 100644 index 000000000..e04aa5995 --- /dev/null +++ b/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.scss @@ -0,0 +1,10 @@ +:host { + display: flex; + align-items: center; + overflow: hidden; + height: 100%; + + > *:not(:last-child) { + margin-right: 6px; + } +} diff --git a/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.ts b/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.ts new file mode 100644 index 000000000..9b2256831 --- /dev/null +++ b/apps/red-ui/src/app/components/breadcrumbs/breadcrumbs.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { BreadcrumbsService } from '@services/breadcrumbs.service'; + +@Component({ + selector: 'redaction-breadcrumbs', + templateUrl: './breadcrumbs.component.html', + styleUrls: ['./breadcrumbs.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BreadcrumbsComponent { + constructor(readonly breadcrumbsService: BreadcrumbsService) {} +} diff --git a/apps/red-ui/src/app/components/notifications/notifications.component.html b/apps/red-ui/src/app/components/notifications/notifications.component.html index a7ef00a67..c1d447aab 100644 --- a/apps/red-ui/src/app/components/notifications/notifications.component.html +++ b/apps/red-ui/src/app/components/notifications/notifications.component.html @@ -6,7 +6,7 @@ diff --git a/apps/red-ui/src/app/components/notifications/notifications.component.ts b/apps/red-ui/src/app/components/notifications/notifications.component.ts index 01a4289f4..1e4d7fbd1 100644 --- a/apps/red-ui/src/app/components/notifications/notifications.component.ts +++ b/apps/red-ui/src/app/components/notifications/notifications.component.ts @@ -6,9 +6,11 @@ import { UserService } from '@services/user.service'; import { DossiersService } from '@services/entity-services/dossiers.service'; import { NotificationsService } from '@services/notifications.service'; import { Notification } from '@red/domain'; -import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { List } from '@iqser/common-ui'; +import { distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, timer } from 'rxjs'; +import { AutoUnsubscribe, CHANGED_CHECK_INTERVAL, List, shareLast } from '@iqser/common-ui'; + +const INCLUDE_SEEN = false; interface NotificationsGroup { date: string; @@ -21,11 +23,11 @@ interface NotificationsGroup { styleUrls: ['./notifications.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class NotificationsComponent implements OnInit { +export class NotificationsComponent extends AutoUnsubscribe implements OnInit { notifications$: Observable; hasUnreadNotifications$: Observable; groupedNotifications$: Observable; - private _notifications$ = new BehaviorSubject([]); + private _notifications$ = new BehaviorSubject([]); constructor( private readonly _translateService: TranslateService, @@ -35,17 +37,25 @@ export class NotificationsComponent implements OnInit { private readonly _dossiersService: DossiersService, private readonly _datePipe: DatePipe, ) { - this.notifications$ = this._notifications$.asObservable(); + super(); + this.notifications$ = this._notifications$.asObservable().pipe(shareLast()); this.groupedNotifications$ = this.notifications$.pipe(map(notifications => this._groupNotifications(notifications))); this.hasUnreadNotifications$ = this.notifications$.pipe( map(notifications => notifications.filter(n => !n.readDate).length > 0), distinctUntilChanged(), - shareReplay(1), + shareLast(), ); } async ngOnInit(): Promise { await this._loadData(); + + this.addSubscription = timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL) + .pipe( + switchMap(() => this._notificationsService.getNotificationsIfChanged(INCLUDE_SEEN)), + tap(notifications => this._notifications$.next(notifications)), + ) + .subscribe(); } async markRead($event, notifications: List = this._notifications$.getValue().map(n => n.id), isRead = true): Promise { @@ -55,7 +65,7 @@ export class NotificationsComponent implements OnInit { } private async _loadData(): Promise { - const notifications = await this._notificationsService.getNotifications(false).toPromise(); + const notifications = await this._notificationsService.getNotifications(INCLUDE_SEEN).toPromise(); this._notifications$.next(notifications); } diff --git a/apps/red-ui/src/app/components/user-profile/user-profile-screen.component.html b/apps/red-ui/src/app/components/user-profile/user-profile-screen.component.html deleted file mode 100644 index c643dc6b2..000000000 --- a/apps/red-ui/src/app/components/user-profile/user-profile-screen.component.html +++ /dev/null @@ -1,50 +0,0 @@ -
-
-
-
-
-
-
-
-
-
-
- - -
- -
- - -
- -
- - -
-
- - - - {{ translations[language] | translate }} - - -
-
-
- -
- - {{ 'user-profile.actions.change-password' | translate }} -
-
-
-
-
diff --git a/apps/red-ui/src/app/guards/dossier-files-guard.ts b/apps/red-ui/src/app/guards/dossier-files-guard.ts new file mode 100644 index 000000000..fc849eb96 --- /dev/null +++ b/apps/red-ui/src/app/guards/dossier-files-guard.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, CanDeactivate, Router, RouterStateSnapshot } from '@angular/router'; +import { DossiersService } from '../services/entity-services/dossiers.service'; +import { BreadcrumbsService } from '../services/breadcrumbs.service'; +import { pluck } from 'rxjs/operators'; +import { AppStateService } from '../state/app-state.service'; +import { FilesMapService } from '../services/entity-services/files-map.service'; + +@Injectable({ providedIn: 'root' }) +export class DossierFilesGuard implements CanActivate, CanDeactivate { + constructor( + private readonly _dossiersService: DossiersService, + private readonly _appStateService: AppStateService, + private readonly _breadcrumbsService: BreadcrumbsService, + private readonly _filesMapService: FilesMapService, + private readonly _router: Router, + ) {} + + async canActivate(route: ActivatedRouteSnapshot): Promise { + const dossierId = route.paramMap.get('dossierId'); + const dossier = this._dossiersService.find(dossierId); + + if (!dossier) { + await this._router.navigate(['/main', 'dossiers']); + return false; + } + + if (!this._filesMapService.has(dossierId)) { + await this._appStateService.getFiles(dossier); + } + + this._breadcrumbsService.append({ + name$: this._dossiersService.getEntityChanged$(dossierId).pipe(pluck('dossierName')), + routerLink: ['/main', 'dossiers', dossierId], + routerLinkActiveOptions: { exact: true }, + }); + + this._breadcrumbsService.hideGoBack(); + return true; + } + + canDeactivate( + component: unknown, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState?: RouterStateSnapshot, + ) { + const dossierId = currentRoute.paramMap.get('dossierId'); + this._breadcrumbsService.remove(['/main', 'dossiers', dossierId]); + if (!nextState.url.startsWith('/main/dossiers')) { + this._breadcrumbsService.showGoBack(); + } + return true; + } +} diff --git a/apps/red-ui/src/app/guards/dossiers.guard.ts b/apps/red-ui/src/app/guards/dossiers.guard.ts new file mode 100644 index 000000000..d9c5c9938 --- /dev/null +++ b/apps/red-ui/src/app/guards/dossiers.guard.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, CanDeactivate, Router, RouterStateSnapshot } from '@angular/router'; +import { DossiersService } from '@services/entity-services/dossiers.service'; +import { BreadcrumbsService } from '@services/breadcrumbs.service'; +import { AppStateService } from '@state/app-state.service'; + +@Injectable({ providedIn: 'root' }) +export class DossiersGuard implements CanActivate, CanDeactivate { + constructor( + private readonly _dossiersService: DossiersService, + private readonly _appStateService: AppStateService, + private readonly _breadcrumbsService: BreadcrumbsService, + private readonly _router: Router, + ) {} + + async canActivate(): Promise { + await this._dossiersService.loadAll().toPromise(); + this._breadcrumbsService.hideGoBack(); + return true; + } + + canDeactivate( + component: unknown, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState?: RouterStateSnapshot, + ) { + if (!nextState.url.startsWith('/main/dossiers')) { + this._breadcrumbsService.showGoBack(); + } + return true; + } +} diff --git a/apps/red-ui/src/app/guards/file-preview.guard.ts b/apps/red-ui/src/app/guards/file-preview.guard.ts new file mode 100644 index 000000000..e27768d70 --- /dev/null +++ b/apps/red-ui/src/app/guards/file-preview.guard.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, CanDeactivate, Router, RouterStateSnapshot } from '@angular/router'; +import { FilesMapService } from '../services/entity-services/files-map.service'; +import { AppStateService } from '../state/app-state.service'; +import { DossiersService } from '../services/entity-services/dossiers.service'; +import { BreadcrumbsService } from '../services/breadcrumbs.service'; +import { pluck } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class FilePreviewGuard implements CanActivate, CanDeactivate { + constructor( + private readonly _filesMapService: FilesMapService, + private readonly _dossiersService: DossiersService, + private readonly _appStateService: AppStateService, + private readonly _breadcrumbsService: BreadcrumbsService, + private readonly _router: Router, + ) {} + + async canActivate(route: ActivatedRouteSnapshot): Promise { + const dossierId = route.paramMap.get('dossierId'); + const fileId = route.paramMap.get('fileId'); + + const dossier = this._dossiersService.find(dossierId); + + if (!this._filesMapService.get(dossierId, fileId)) { + await this._router.navigate([dossier.routerLink]); + return false; + } + + this._breadcrumbsService.append({ + name$: this._filesMapService.watch$(dossierId, fileId).pipe(pluck('filename')), + routerLink: ['/main', 'dossiers', dossierId, 'file', fileId], + }); + + return true; + } + + canDeactivate( + component: unknown, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState?: RouterStateSnapshot, + ) { + const dossierId = currentRoute.paramMap.get('dossierId'); + const fileId = currentRoute.paramMap.get('fileId'); + this._breadcrumbsService.remove(['/main', 'dossiers', dossierId, 'file', fileId]); + + if (!nextState.url.startsWith('/main/dossiers')) { + this._breadcrumbsService.showGoBack(); + } + + return true; + } +} diff --git a/apps/red-ui/src/app/guards/go-back-guard.service.ts b/apps/red-ui/src/app/guards/go-back-guard.service.ts new file mode 100644 index 000000000..893f470f4 --- /dev/null +++ b/apps/red-ui/src/app/guards/go-back-guard.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, CanDeactivate } from '@angular/router'; +import { BreadcrumbsService } from '@services/breadcrumbs.service'; + +@Injectable({ providedIn: 'root' }) +export class GoBackGuard implements CanActivate, CanDeactivate { + constructor(private readonly _breadcrumbsService: BreadcrumbsService) {} + + canActivate(): boolean { + this._breadcrumbsService.showGoBack(); + return true; + } + + canDeactivate() { + this._breadcrumbsService.hideGoBack(); + return true; + } +} diff --git a/apps/red-ui/src/app/models/file/annotation.permissions.ts b/apps/red-ui/src/app/models/file/annotation.permissions.ts index 62b5645c5..ca2f1f911 100644 --- a/apps/red-ui/src/app/models/file/annotation.permissions.ts +++ b/apps/red-ui/src/app/models/file/annotation.permissions.ts @@ -12,6 +12,7 @@ export class AnnotationPermissions { canRejectSuggestion = true; canForceRedaction = true; canChangeLegalBasis = true; + canResizeAnnotation = true; canRecategorizeImage = true; static forUser(isApprover: boolean, user: User, annotations: AnnotationWrapper | AnnotationWrapper[]) { @@ -23,7 +24,10 @@ export class AnnotationPermissions { for (const annotation of annotations) { const permissions: AnnotationPermissions = new AnnotationPermissions(); + permissions.canUndo = !isApprover && annotation.isSuggestion; + permissions.canAcceptSuggestion = isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion); + permissions.canRejectSuggestion = isApprover && annotation.isSuggestion; permissions.canForceRedaction = annotation.isSkipped && !annotation.isFalsePositive; permissions.canAcceptRecommendation = annotation.isRecommendation; @@ -34,12 +38,11 @@ export class AnnotationPermissions { permissions.canRemoveOrSuggestToRemoveFromDictionary = annotation.isModifyDictionary && (annotation.isRedacted || annotation.isSkipped || annotation.isHint); - permissions.canAcceptSuggestion = isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion); - permissions.canRejectSuggestion = isApprover && annotation.isSuggestion; - permissions.canChangeLegalBasis = annotation.isRedacted; - permissions.canRecategorizeImage = annotation.isImage; + permissions.canRecategorizeImage = (annotation.isImage && !annotation.isSuggestion) || annotation.isSuggestionRecategorizeImage; + permissions.canResizeAnnotation = + ((annotation.isRedacted || annotation.isImage) && !annotation.isSuggestion) || annotation.isSuggestionResize; summedPermissions._merge(permissions); } diff --git a/apps/red-ui/src/app/models/file/annotation.wrapper.ts b/apps/red-ui/src/app/models/file/annotation.wrapper.ts index ff1c95d17..b7994d106 100644 --- a/apps/red-ui/src/app/models/file/annotation.wrapper.ts +++ b/apps/red-ui/src/app/models/file/annotation.wrapper.ts @@ -4,14 +4,11 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { IComment, IPoint, IRectangle } from '@red/domain'; export type AnnotationSuperType = - | 'add-dictionary' - | 'remove-dictionary' - | 'remove-only-here' - | 'change-legal-basis' | 'suggestion-change-legal-basis' | 'suggestion-recategorize-image' | 'suggestion-add-dictionary' | 'suggestion-force-redaction' + | 'suggestion-resize' | 'suggestion-remove-dictionary' | 'suggestion-add' | 'suggestion-remove' @@ -20,7 +17,6 @@ export type AnnotationSuperType = | 'manual-redaction' | 'recommendation' | 'hint' - | 'pending-analysis' | 'declined-suggestion'; export class AnnotationWrapper { @@ -45,6 +41,7 @@ export class AnnotationWrapper { recommendationType: string; legalBasisValue: string; legalBasisChangeValue?: string; + resizing?: boolean; manual?: boolean; @@ -82,7 +79,7 @@ export class AnnotationWrapper { } get isSuperTypeBasedColor() { - return this.isSkipped || this.isSuggestion || this.isReadyForAnalysis || this.isDeclinedSuggestion; + return this.isSkipped || this.isSuggestion || this.isDeclinedSuggestion; } get isSkipped() { @@ -120,16 +117,6 @@ export class AnnotationWrapper { return this.superType === 'declined-suggestion'; } - get isReadyForAnalysis() { - return ( - this.superType === 'add-dictionary' || - this.superType === 'remove-dictionary' || - this.superType === 'remove-only-here' || - this.superType === 'pending-analysis' || - this.superType === 'change-legal-basis' - ); - } - get isApproved() { return this.status === 'APPROVED'; } @@ -143,7 +130,17 @@ export class AnnotationWrapper { } get isSuggestion() { - return this.isSuggestionAdd || this.isSuggestionRemove || this.isSuggestionChangeLegalBasis || this.isSuggestionRecategorizeImage; + return ( + this.isSuggestionAdd || + this.isSuggestionRemove || + this.isSuggestionChangeLegalBasis || + this.isSuggestionRecategorizeImage || + this.isSuggestionResize + ); + } + + get isSuggestionResize() { + return this.superType === 'suggestion-resize'; } get isSuggestionRecategorizeImage() { @@ -171,7 +168,7 @@ export class AnnotationWrapper { } get isConvertedRecommendation() { - return this.isRecommendation && (this.superType === 'suggestion-add-dictionary' || this.superType === 'add-dictionary'); + return this.isRecommendation && this.superType === 'suggestion-add-dictionary'; } get isRecommendation() { @@ -245,6 +242,13 @@ export class AnnotationWrapper { return; } + if (redactionLogEntryWrapper.manualRedactionType === 'RESIZE') { + if (redactionLogEntryWrapper.status === 'REQUESTED') { + annotationWrapper.superType = 'suggestion-resize'; + return; + } + } + if (redactionLogEntryWrapper.manualRedactionType === 'FORCE_REDACT') { annotationWrapper.force = true; @@ -263,10 +267,6 @@ export class AnnotationWrapper { annotationWrapper.superType = 'suggestion-add-dictionary'; return; } - if (redactionLogEntryWrapper.status === 'APPROVED') { - annotationWrapper.superType = 'add-dictionary'; - return; - } if (redactionLogEntryWrapper.status === 'DECLINED') { annotationWrapper.superType = 'declined-suggestion'; return; @@ -318,10 +318,6 @@ export class AnnotationWrapper { annotationWrapper.superType = 'suggestion-add-dictionary'; return; } - if (redactionLogEntryWrapper.status === 'APPROVED') { - annotationWrapper.superType = 'add-dictionary'; - return; - } if (redactionLogEntryWrapper.status === 'DECLINED') { annotationWrapper.superType = 'declined-suggestion'; return; @@ -369,11 +365,16 @@ export class AnnotationWrapper { if (redactionLogEntryWrapper.status === 'APPROVED') { if (redactionLogEntryWrapper.dictionaryEntry) { - annotationWrapper.superType = - redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'add-dictionary' : 'remove-dictionary'; + annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'skipped'; } else { annotationWrapper.superType = - redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'manual-redaction' : 'remove-only-here'; + redactionLogEntryWrapper.manualRedactionType === 'ADD' + ? 'manual-redaction' + : annotationWrapper.redaction + ? 'redaction' + : annotationWrapper.hint + ? 'hint' + : 'skipped'; } return; } diff --git a/apps/red-ui/src/app/models/file/file-data.model.ts b/apps/red-ui/src/app/models/file/file-data.model.ts index 4a82bd0a4..6407c15fd 100644 --- a/apps/red-ui/src/app/models/file/file-data.model.ts +++ b/apps/red-ui/src/app/models/file/file-data.model.ts @@ -24,7 +24,7 @@ export class FileDataModel { const entries: RedactionLogEntryWrapper[] = this._convertData(); let allAnnotations = entries .map(entry => AnnotationWrapper.fromData(entry)) - .filter(ann => !this.file.excludedPages.includes(ann.pageNumber)); + .filter(ann => ann.manual || !this.file.excludedPages.includes(ann.pageNumber)); if (!areDevFeaturesEnabled) { allAnnotations = allAnnotations.filter(annotation => !annotation.isFalsePositive); diff --git a/apps/red-ui/src/app/models/file/manual-redaction-entry.wrapper.ts b/apps/red-ui/src/app/models/file/manual-redaction-entry.wrapper.ts index 39714983c..79504dea3 100644 --- a/apps/red-ui/src/app/models/file/manual-redaction-entry.wrapper.ts +++ b/apps/red-ui/src/app/models/file/manual-redaction-entry.wrapper.ts @@ -1,10 +1,18 @@ import { IManualRedactionEntry } from '@red/domain'; +export const ManualRedactionEntryTypes = { + DICTIONARY: 'DICTIONARY', + REDACTION: 'REDACTION', + FALSE_POSITIVE: 'FALSE_POSITIVE', +} as const; + +export type ManualRedactionEntryType = keyof typeof ManualRedactionEntryTypes; + export class ManualRedactionEntryWrapper { constructor( readonly quads: any, readonly manualRedactionEntry: IManualRedactionEntry, - readonly type: 'DICTIONARY' | 'REDACTION' | 'FALSE_POSITIVE', + readonly type: ManualRedactionEntryType, readonly annotationType: 'TEXT' | 'RECTANGLE' = 'TEXT', readonly rectId?: string, ) {} diff --git a/apps/red-ui/src/app/models/file/redaction-log-entry.wrapper.ts b/apps/red-ui/src/app/models/file/redaction-log-entry.wrapper.ts index 251b5bd77..72a70f608 100644 --- a/apps/red-ui/src/app/models/file/redaction-log-entry.wrapper.ts +++ b/apps/red-ui/src/app/models/file/redaction-log-entry.wrapper.ts @@ -17,7 +17,7 @@ export interface RedactionLogEntryWrapper { id?: string; legalBasis?: string; manual?: boolean; - manualRedactionType?: 'ADD' | 'REMOVE' | 'UNDO' | 'LEGAL_BASIS_CHANGE' | 'FORCE_REDACT' | 'RECATEGORIZE'; + manualRedactionType?: 'ADD' | 'REMOVE' | 'LEGAL_BASIS_CHANGE' | 'FORCE_REDACT' | 'RECATEGORIZE' | 'RESIZE'; matchedRule?: number; positions?: Array; reason?: string; diff --git a/apps/red-ui/src/app/modules/account/account-routing.module.ts b/apps/red-ui/src/app/modules/account/account-routing.module.ts new file mode 100644 index 000000000..777b82c0c --- /dev/null +++ b/apps/red-ui/src/app/modules/account/account-routing.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CompositeRouteGuard } from '@iqser/common-ui'; +import { AuthGuard } from '../auth/auth.guard'; +import { RedRoleGuard } from '../auth/red-role.guard'; +import { AppStateGuard } from '../../state/app-state.guard'; +import { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component'; + +const routes = [ + { path: '', redirectTo: 'user-profile', pathMatch: 'full' }, + { + path: 'user-profile', + component: BaseAccountScreenComponent, + canActivate: [CompositeRouteGuard], + data: { + routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], + requiredRoles: ['RED_USER'], + }, + loadChildren: () => import('./screens/user-profile/user-profile.module').then(m => m.UserProfileModule), + }, + { + path: 'notifications', + component: BaseAccountScreenComponent, + canActivate: [CompositeRouteGuard], + data: { + routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], + }, + loadChildren: () => import('./screens/notifications/notifications.module').then(m => m.NotificationsModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AccountRoutingModule {} diff --git a/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.html b/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.html new file mode 100644 index 000000000..8ebc033dc --- /dev/null +++ b/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.html @@ -0,0 +1,7 @@ + + +
+ {{ item.label | translate }} +
+
+
diff --git a/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.scss b/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.scss new file mode 100644 index 000000000..15c54cccf --- /dev/null +++ b/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.scss @@ -0,0 +1,7 @@ +:host { + height: calc(100vh - 61px); + + &.dossier-templates { + height: calc(100vh - 111px); + } +} diff --git a/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.ts b/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.ts new file mode 100644 index 000000000..f4cf007ae --- /dev/null +++ b/apps/red-ui/src/app/modules/account/account-side-nav/account-side-nav.component.ts @@ -0,0 +1,26 @@ +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +interface NavItem { + readonly label: string; + readonly screen: string; +} + +@Component({ + selector: 'redaction-account-side-nav', + templateUrl: './account-side-nav.component.html', + styleUrls: ['./account-side-nav.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AccountSideNavComponent { + readonly items: NavItem[] = [ + { + screen: 'user-profile', + label: _('user-profile'), + }, + { + screen: 'notifications', + label: _('notifications'), + }, + ]; +} diff --git a/apps/red-ui/src/app/modules/account/account.module.ts b/apps/red-ui/src/app/modules/account/account.module.ts new file mode 100644 index 000000000..28b2ee431 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/account.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { AccountRoutingModule } from './account-routing.module'; +import { AccountSideNavComponent } from './account-side-nav/account-side-nav.component'; +import { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component'; +import { NotificationPreferencesService } from './services/notification-preferences.service'; + +@NgModule({ + declarations: [AccountSideNavComponent, BaseAccountScreenComponent], + imports: [CommonModule, SharedModule, AccountRoutingModule], + providers: [NotificationPreferencesService], +}) +export class AccountModule {} diff --git a/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.html b/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.html new file mode 100644 index 000000000..1597a1874 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.html @@ -0,0 +1,20 @@ +
+
+ + + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
diff --git a/apps/red-ui/src/app/components/user-profile/user-profile-screen.component.scss b/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.scss similarity index 64% rename from apps/red-ui/src/app/components/user-profile/user-profile-screen.component.scss rename to apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.scss index 677b2c3d2..fd508d3c9 100644 --- a/apps/red-ui/src/app/components/user-profile/user-profile-screen.component.scss +++ b/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.scss @@ -1,11 +1,16 @@ -@use 'variables'; -@use 'common-mixins'; +@use 'apps/red-ui/src/assets/styles/variables'; +@use 'libs/common-ui/src/assets/styles/common-mixins'; .content-container { background-color: variables.$grey-2; justify-content: center; @include common-mixins.scroll-bar; overflow: auto; + + .dialog { + width: var(--width); + min-height: unset; + } } .full-height { @@ -17,11 +22,3 @@ height: calc(100% + 50px); z-index: 1; } - -iframe { - background: white; - width: 500px; - height: 500px; - position: absolute; - z-index: 100; -} diff --git a/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.ts b/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.ts new file mode 100644 index 000000000..c87363a82 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/base-account-screen/base-account-screen-component.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component, OnInit, ViewContainerRef } from '@angular/core'; +import { Router } from '@angular/router'; +import { notificationsTranslations } from '../translations/notifications-translations'; + +@Component({ + selector: 'redaction-base-account-screen', + templateUrl: './base-account-screen-component.html', + styleUrls: ['./base-account-screen-component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BaseAccountScreenComponent implements OnInit { + readonly translations = notificationsTranslations; + path: string; + + constructor(private readonly _router: Router, private readonly _hostRef: ViewContainerRef) { + this.path = this._router.url.split('/').pop(); + } + + ngOnInit(): void { + this._setDialogWidth(); + } + + private _setDialogWidth() { + const element = this._hostRef.element.nativeElement as HTMLElement; + element.style.setProperty('--width', this.path === 'user-profile' ? 'unset' : '100%'); + } +} diff --git a/apps/red-ui/src/app/modules/account/screens/notifications/constants.ts b/apps/red-ui/src/app/modules/account/screens/notifications/constants.ts new file mode 100644 index 000000000..3c98f33b1 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/screens/notifications/constants.ts @@ -0,0 +1,38 @@ +export const NotificationCategories = { + inAppNotifications: 'inAppNotifications', + emailNotifications: 'emailNotifications', +} as const; + +export const NotificationCategoriesValues = Object.values(NotificationCategories); + +export const OwnDossiersNotificationsTypes = { + dossierStatusChanges: 'dossierStatusChanges', + requestToJoinTheDossier: 'requestToJoinTheDossier', + documentStatusChanges: 'documentStatusChanges', + documentIsSentForApproval: 'documentIsSentForApproval', +} as const; + +export const OwnDossiersNotificationsTypesValues = Object.values(OwnDossiersNotificationsTypes); + +export const ReviewerOnDossiersNotificationsTypes = { + whenIAmAssignedOnADocument: 'whenIAmAssignedOnADocument', + whenIAmUnassignedFromADocument: 'whenIAmUnassignedFromADocument', + whenADocumentIsApproved: 'whenADocumentIsApproved', +} as const; + +export const ReviewerOnDossiersNotificationsTypesValues = Object.values(ReviewerOnDossiersNotificationsTypes); + +export const ApproverOnDossiersNotificationsTypes = { + whenADocumentIsSentForApproval: 'whenADocumentIsSentForApproval', + whenADocumentIsAssignedToAReviewer: 'whenADocumentIsAssignedToAReviewer', + whenAReviewerIsUnassignedFromADocument: 'whenAReviewerIsUnassignedFromADocument', +} as const; + +export const ApproverOnDossiersNotificationsTypesValues = Object.values(ApproverOnDossiersNotificationsTypes); + +export const NotificationGroupsKeys = ['own', 'reviewer', 'approver'] as const; +export const NotificationGroupsValues = [ + OwnDossiersNotificationsTypesValues, + ReviewerOnDossiersNotificationsTypesValues, + ApproverOnDossiersNotificationsTypesValues, +] as const; diff --git a/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.html b/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.html new file mode 100644 index 000000000..8c76795dc --- /dev/null +++ b/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.html @@ -0,0 +1,43 @@ +
+
+
+
+ {{ + translations[category] | translate + }} +
+ +
+
+
+ + + {{ translations[type.toLocaleLowerCase()] | translate }} +
+
+ +
+ +
+
+
+ + {{ translations[preference] | translate }} + +
+
+
+
+
+ +
+ +
+
diff --git a/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.scss b/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.scss new file mode 100644 index 000000000..82f2afa68 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.scss @@ -0,0 +1,51 @@ +@use 'variables'; +@use 'libs/common-ui/src/assets/styles/common-mixins'; + +.dialog-content { + flex-direction: column; + + .header { + grid-column: span 2; + padding: 10px 10px; + margin-bottom: -1px; + border-top: 1px solid variables.$separator; + border-bottom: 1px solid variables.$separator; + } + + .options-content { + padding: 10px 48px; + + .statement { + opacity: 0.7; + color: variables.$grey-1; + font-weight: 500; + padding: 10px 0; + } + + .radio-container { + display: flex; + padding: 10px 0 10px; + .radio-button { + display: flex; + align-items: center; + padding-right: 30px; + iqser-round-checkbox { + margin-right: 8px; + } + } + } + + .group { + padding: 10px 0; + + .group-title { + color: variables.$grey-1; + font-weight: 600; + } + + .iqser-input-group { + margin-top: 5px; + } + } + } +} diff --git a/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.ts b/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.ts new file mode 100644 index 000000000..e6b8f4a2a --- /dev/null +++ b/apps/red-ui/src/app/modules/account/screens/notifications/notifications-screen/notifications-screen.component.ts @@ -0,0 +1,89 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { notificationsTranslations } from '../../../translations/notifications-translations'; +import { NotificationPreferencesService } from '../../../services/notification-preferences.service'; +import { LoadingService, Toaster } from '@iqser/common-ui'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { NotificationCategoriesValues, NotificationGroupsKeys, NotificationGroupsValues } from '../constants'; +import { EmailNotificationScheduleTypesValues } from '@red/domain'; + +@Component({ + selector: 'redaction-notifications-screen', + templateUrl: './notifications-screen.component.html', + styleUrls: ['./notifications-screen.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NotificationsScreenComponent implements OnInit { + readonly emailNotificationScheduleTypes = EmailNotificationScheduleTypesValues; + readonly notificationCategories = NotificationCategoriesValues; + readonly notificationGroupsKeys = NotificationGroupsKeys; + readonly notificationGroupsValues = NotificationGroupsValues; + readonly translations = notificationsTranslations; + + formGroup: FormGroup; + + constructor( + private readonly _toaster: Toaster, + private readonly _formBuilder: FormBuilder, + private readonly _loadingService: LoadingService, + private readonly _notificationPreferencesService: NotificationPreferencesService, + ) { + this.formGroup = this._formBuilder.group({ + inAppNotificationsEnabled: [undefined], + emailNotificationsEnabled: [undefined], + emailNotificationType: [undefined], + emailNotifications: [undefined], + inAppNotifications: [undefined], + }); + } + + async ngOnInit(): Promise { + await this._initializeForm(); + } + + isCategoryActive(category: string) { + return this.formGroup.get(`${category}Enabled`).value; + } + + setEmailNotificationType(type: string) { + this.formGroup.get('emailNotificationType').setValue(type); + } + + getEmailNotificationType() { + return this.formGroup.get('emailNotificationType').value; + } + + isPreferenceChecked(category: string, preference: string) { + return this.formGroup.get(category).value.includes(preference); + } + + addRemovePreference(checked: boolean, category: string, preference: string) { + const preferences = this.formGroup.get(category).value; + if (checked) { + preferences.push(preference); + } else { + const indexOfPreference = preferences.indexOf(preference); + preferences.splice(indexOfPreference, 1); + } + this.formGroup.get(category).setValue(preferences); + } + + async save() { + this._loadingService.start(); + try { + await this._notificationPreferencesService.update(this.formGroup.value).toPromise(); + } catch (e) { + this._toaster.error(_('notifications-screen.error.generic')); + } + this._loadingService.stop(); + } + + private async _initializeForm() { + this._loadingService.start(); + + const notificationPreferences = await this._notificationPreferencesService.get().toPromise(); + this.formGroup.patchValue(notificationPreferences); + + this._loadingService.stop(); + } +} diff --git a/apps/red-ui/src/app/modules/account/screens/notifications/notifications.module.ts b/apps/red-ui/src/app/modules/account/screens/notifications/notifications.module.ts new file mode 100644 index 000000000..bc6d3f30a --- /dev/null +++ b/apps/red-ui/src/app/modules/account/screens/notifications/notifications.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../../shared/shared.module'; +import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component'; + +const routes = [{ path: '', component: NotificationsScreenComponent }]; + +@NgModule({ + declarations: [NotificationsScreenComponent], + imports: [RouterModule.forChild(routes), CommonModule, SharedModule], +}) +export class NotificationsModule {} diff --git a/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.html b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.html new file mode 100644 index 000000000..25d5a5a44 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.html @@ -0,0 +1,37 @@ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + + {{ translations[language] | translate }} + + +
+ +
+
+ +
+ +
+
diff --git a/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.scss b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.scss new file mode 100644 index 000000000..e1573c569 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.scss @@ -0,0 +1,3 @@ +a { + color: black; +} diff --git a/apps/red-ui/src/app/components/user-profile/user-profile-screen.component.ts b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.ts similarity index 87% rename from apps/red-ui/src/app/components/user-profile/user-profile-screen.component.ts rename to apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.ts index 1601afc46..f1a23936a 100644 --- a/apps/red-ui/src/app/components/user-profile/user-profile-screen.component.ts +++ b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile-screen/user-profile-screen.component.ts @@ -1,19 +1,20 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { UserService } from '@services/user.service'; -import { PermissionsService } from '@services/permissions.service'; -import { LanguageService } from '@i18n/language.service'; -import { TranslateService } from '@ngx-translate/core'; -import { ConfigService } from '@services/config.service'; import { DomSanitizer } from '@angular/platform-browser'; -import { languagesTranslations } from '../../translations/languages-translations'; +import { TranslateService } from '@ngx-translate/core'; import { LoadingService } from '@iqser/common-ui'; import { IProfile } from '@red/domain'; +import { languagesTranslations } from '../../../translations/languages-translations'; +import { PermissionsService } from '../../../../../services/permissions.service'; +import { UserService } from '../../../../../services/user.service'; +import { ConfigService } from '../../../../../services/config.service'; +import { LanguageService } from '../../../../../i18n/language.service'; @Component({ selector: 'redaction-user-profile-screen', templateUrl: './user-profile-screen.component.html', styleUrls: ['./user-profile-screen.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class UserProfileScreenComponent implements OnInit { formGroup: FormGroup; diff --git a/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile.module.ts b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile.module.ts new file mode 100644 index 000000000..928581695 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/screens/user-profile/user-profile.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../../shared/shared.module'; +import { UserProfileScreenComponent } from './user-profile-screen/user-profile-screen.component'; + +const routes = [{ path: '', component: UserProfileScreenComponent }]; + +@NgModule({ + declarations: [UserProfileScreenComponent], + imports: [RouterModule.forChild(routes), CommonModule, SharedModule], +}) +export class UserProfileModule {} diff --git a/apps/red-ui/src/app/modules/account/services/notification-preferences.service.ts b/apps/red-ui/src/app/modules/account/services/notification-preferences.service.ts new file mode 100644 index 000000000..acd23a718 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/services/notification-preferences.service.ts @@ -0,0 +1,31 @@ +import { Injectable, Injector } from '@angular/core'; +import { GenericService } from '@iqser/common-ui'; +import { Observable, of } from 'rxjs'; +import { UserService } from '../../../services/user.service'; +import { EmailNotificationScheduleTypes, INotificationPreferences } from '@red/domain'; +import { catchError } from 'rxjs/operators'; + +@Injectable() +export class NotificationPreferencesService extends GenericService { + constructor(protected readonly _injector: Injector, private readonly _userService: UserService) { + super(_injector, 'notification-preferences'); + } + + get(): Observable { + return super.get().pipe(catchError(() => of(this._defaultPreferences))); + } + + update(notificationPreferences: INotificationPreferences): Observable { + return super._post(notificationPreferences); + } + + private get _defaultPreferences(): INotificationPreferences { + return { + emailNotificationType: EmailNotificationScheduleTypes.INSTANT, + emailNotifications: [], + emailNotificationsEnabled: false, + inAppNotifications: [], + inAppNotificationsEnabled: true, + }; + } +} diff --git a/apps/red-ui/src/app/translations/languages-translations.ts b/apps/red-ui/src/app/modules/account/translations/languages-translations.ts similarity index 100% rename from apps/red-ui/src/app/translations/languages-translations.ts rename to apps/red-ui/src/app/modules/account/translations/languages-translations.ts diff --git a/apps/red-ui/src/app/modules/account/translations/notifications-translations.ts b/apps/red-ui/src/app/modules/account/translations/notifications-translations.ts new file mode 100644 index 000000000..c85d03104 --- /dev/null +++ b/apps/red-ui/src/app/modules/account/translations/notifications-translations.ts @@ -0,0 +1,24 @@ +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; + +export const notificationsTranslations: { [key: string]: string } = { + daily: _('notifications-screen.schedule.instant'), + daily_summary: _('notifications-screen.schedule.daily'), + weekly_summary: _('notifications-screen.schedule.weekly'), + inAppNotifications: _('notifications-screen.category.in-app-notifications'), + emailNotifications: _('notifications-screen.category.email-notifications'), + documentIsSentForApproval: _('notifications-screen.options.document-is-sent-for-approval'), + documentStatusChanges: _('notifications-screen.options.document-status-changes'), + dossierStatusChanges: _('notifications-screen.options.dossier-status-changes'), + requestToJoinTheDossier: _('notifications-screen.options.request-to-join-the-dossier'), + whenADocumentIsApproved: _('notifications-screen.options.when-a-document-is-approved'), + whenADocumentIsAssignedToAReviewer: _('notifications-screen.options.when-a-document-is-assigned-to-a-reviewer'), + whenADocumentIsSentForApproval: _('notifications-screen.options.when-a-document-is-sent-for-approval'), + whenAReviewerIsUnassignedFromADocument: _('notifications-screen.options.when-a-reviewer-is-unassigned-from-a-document'), + whenIAmAssignedOnADocument: _('notifications-screen.options.when-i-am-assigned-on-a-document'), + whenIAmUnassignedFromADocument: _('notifications-screen.options.when-i-am-unassigned-from-a-document'), + approver: _('notifications-screen.groups.approver'), + own: _('notifications-screen.groups.own'), + reviewer: _('notifications-screen.groups.reviewer'), + notifications: _('notifications-screen.title'), + 'user-profile': _('user-profile-screen.title'), +} as const; diff --git a/apps/red-ui/src/app/modules/admin/admin-routing.module.ts b/apps/red-ui/src/app/modules/admin/admin-routing.module.ts index 127bf919b..276e1b59d 100644 --- a/apps/red-ui/src/app/modules/admin/admin-routing.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin-routing.module.ts @@ -15,14 +15,14 @@ import { UserListingScreenComponent } from './screens/user-listing/user-listing- import { LicenseInformationScreenComponent } from './screens/license-information/license-information-screen.component'; import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component'; import { AuditScreenComponent } from './screens/audit/audit-screen.component'; -import { RouterModule } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; import { ReportsScreenComponent } from './screens/reports/reports-screen.component'; import { DossierAttributesListingScreenComponent } from './screens/dossier-attributes-listing/dossier-attributes-listing-screen.component'; import { TrashScreenComponent } from './screens/trash/trash-screen.component'; import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component'; import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component'; -const routes = [ +const routes: Routes = [ { path: '', redirectTo: 'dossier-templates', pathMatch: 'full' }, { path: 'dossier-templates', diff --git a/apps/red-ui/src/app/modules/admin/admin.module.ts b/apps/red-ui/src/app/modules/admin/admin.module.ts index 932ecd4fa..e31f6c9a9 100644 --- a/apps/red-ui/src/app/modules/admin/admin.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin.module.ts @@ -45,7 +45,7 @@ import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen. import { LicenseReportService } from './services/licence-report.service'; import { RulesService } from './services/rules.service'; import { SmtpConfigService } from './services/smtp-config.service'; -import { WatermarkService } from './services/watermark.service'; +import { UploadDictionaryDialogComponent } from './dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component'; const dialogs = [ AddEditDossierTemplateDialogComponent, @@ -58,6 +58,7 @@ const dialogs = [ ConfirmDeleteUsersDialogComponent, FileAttributesCsvImportDialogComponent, AddEditDossierAttributeDialogComponent, + UploadDictionaryDialogComponent, ]; const screens = [ @@ -96,15 +97,7 @@ const components = [ @NgModule({ declarations: [...components], - providers: [ - AdminDialogService, - AuditService, - DigitalSignatureService, - LicenseReportService, - RulesService, - SmtpConfigService, - WatermarkService, - ], + providers: [AdminDialogService, AuditService, DigitalSignatureService, LicenseReportService, RulesService, SmtpConfigService], imports: [CommonModule, SharedModule, AdminRoutingModule, NgxChartsModule, ColorPickerModule, MonacoEditorModule], }) export class AdminModule {} diff --git a/apps/red-ui/src/app/modules/admin/components/dossier-template-actions/dossier-template-actions.component.ts b/apps/red-ui/src/app/modules/admin/components/dossier-template-actions/dossier-template-actions.component.ts index bd7cc22a0..b435bca35 100644 --- a/apps/red-ui/src/app/modules/admin/components/dossier-template-actions/dossier-template-actions.component.ts +++ b/apps/red-ui/src/app/modules/admin/components/dossier-template-actions/dossier-template-actions.component.ts @@ -6,6 +6,7 @@ import { CircleButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { UserService } from '@services/user.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; +import { HttpStatusCode } from '@angular/common/http'; @Component({ selector: 'redaction-dossier-template-actions', @@ -57,7 +58,7 @@ export class DossierTemplateActionsComponent implements OnInit { await this._router.navigate(['main', 'admin']); }) .catch(error => { - if (error.status === 409) { + if (error.status === HttpStatusCode.Conflict) { this._toaster.error(_('dossier-templates-listing.error.conflict')); } else { this._toaster.error(_('dossier-templates-listing.error.generic')); diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.html index a056d7d3c..ea0944da4 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.html @@ -7,16 +7,16 @@
-
{{ dictionary?.type || technicalName || '-' }}
+
{{ dictionary?.type || (technicalName$ | async) || '-' }}
-
+
{{ dictionary.label }}
-
+
- +
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts index 6fe2d7c2e..4c8d470b9 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component.ts @@ -1,39 +1,48 @@ -import { Component, Inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Observable } from 'rxjs'; -import { BaseDialogComponent, Toaster } from '@iqser/common-ui'; +import { BaseDialogComponent, shareDistinctLast, Toaster } from '@iqser/common-ui'; import { TranslateService } from '@ngx-translate/core'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { AppStateService } from '@state/app-state.service'; import { toKebabCase } from '@utils/functions'; import { DictionaryService } from '@shared/services/dictionary.service'; import { Dictionary, IDictionary } from '@red/domain'; +import { UserService } from '@services/user.service'; +import { map } from 'rxjs/operators'; +import { HttpStatusCode } from '@angular/common/http'; @Component({ selector: 'redaction-add-edit-dictionary-dialog', templateUrl: './add-edit-dictionary-dialog.component.html', styleUrls: ['./add-edit-dictionary-dialog.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class AddEditDictionaryDialogComponent extends BaseDialogComponent { - form: FormGroup; - readonly dictionary: Dictionary; - technicalName = ''; - private readonly _dossierTemplateId: string; + readonly form: FormGroup; + readonly dictionary = this._data.dictionary; + readonly canEditLabel$ = this._canEditLabel$; + readonly technicalName$: Observable; + readonly dialogHeader = this._translateService.instant('add-edit-dictionary.title', { + type: this._data.dictionary ? 'edit' : 'create', + name: this._data.dictionary?.label, + }); + readonly hasColor$: Observable; + private readonly _dossierTemplateId = this._data.dossierTemplateId; constructor( - private readonly _dictionaryService: DictionaryService, - private readonly _appStateService: AppStateService, - private readonly _formBuilder: FormBuilder, + readonly userService: UserService, private readonly _toaster: Toaster, + private readonly _formBuilder: FormBuilder, + private readonly _appStateService: AppStateService, private readonly _translateService: TranslateService, + private readonly _dictionaryService: DictionaryService, private readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) - private readonly _data: { dictionary: Dictionary; dossierTemplateId: string }, + private readonly _data: { readonly dictionary: Dictionary; readonly dossierTemplateId: string }, ) { super(); - this.dictionary = _data.dictionary; - this._dossierTemplateId = _data.dossierTemplateId; this.form = _formBuilder.group({ label: [this.dictionary?.label, [Validators.required, Validators.minLength(3)]], description: [this.dictionary?.description], @@ -43,21 +52,8 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent { addToDictionaryAction: [!!this.dictionary?.addToDictionaryAction], caseSensitive: [this.dictCaseSensitive], }); - this.form.get('label').valueChanges.subscribe(() => { - this._updateTechnicalName(); - }); - } - - get dialogHeader(): string { - return this._translateService.instant('add-edit-dictionary.title', { - type: this.dictionary ? 'edit' : 'create', - name: this.dictionary?.label, - }); - } - - get hasColor(): boolean { - const hexColorValue = this.form.get('hexColor').value; - return !hexColorValue || hexColorValue?.length === 0; + this.hasColor$ = this._colorEmpty$; + this.technicalName$ = this.form.get('label').valueChanges.pipe(map(value => this._toTechnicalName(value))); } get dictCaseSensitive(): boolean { @@ -82,47 +78,58 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent { return false; } - save(): void { + private get _canEditLabel$() { + return this.userService.currentUser$.pipe( + map(user => user.isAdmin || !this._data.dictionary), + shareDistinctLast(), + ); + } + + private get _colorEmpty$() { + return this.form.get('hexColor').valueChanges.pipe(map((value: string) => !value || value?.length === 0)); + } + + async save(): Promise { const dictionary = this._formToObject(); let observable: Observable; + const dossierTemplateId = this._data.dossierTemplateId; if (this.dictionary) { // edit mode - observable = this._dictionaryService.updateDictionary(dictionary, this._dossierTemplateId, dictionary.type); + observable = this._dictionaryService.updateDictionary(dictionary, dossierTemplateId, dictionary.type); } else { // create mode - observable = this._dictionaryService.addDictionary({ ...dictionary, dossierTemplateId: this._dossierTemplateId }); + observable = this._dictionaryService.addDictionary({ ...dictionary, dossierTemplateId }); } - observable.subscribe( - () => this._dialogRef.close(true), - error => { - if (error.status === 409) { + return observable + .toPromise() + .then(() => this._dialogRef.close(true)) + .catch(error => { + if (error.status === HttpStatusCode.Conflict) { this._toaster.error(_('add-edit-dictionary.error.dictionary-already-exists')); - } else if (error.status === 400) { + } else if (error.status === HttpStatusCode.BadRequest) { this._toaster.error(_('add-edit-dictionary.error.invalid-color-or-rank')); } else { this._toaster.error(_('add-edit-dictionary.error.generic')); } - }, - ); + }); } - private _updateTechnicalName() { - const displayName = this.form.get('label').value.trim(); + private _toTechnicalName(value: string) { const existingTechnicalNames = Object.keys(this._appStateService.dictionaryData[this._dossierTemplateId]); - const baseTechnicalName: string = toKebabCase(displayName); + const baseTechnicalName = toKebabCase(value.trim()); let technicalName = baseTechnicalName; let suffix = 1; while (existingTechnicalNames.includes(technicalName)) { technicalName = [baseTechnicalName, suffix++].join('-'); } - this.technicalName = technicalName; + return technicalName; } private _formToObject(): IDictionary { return { - type: this.dictionary?.type || this.technicalName, + type: this.dictionary?.type || this._toTechnicalName(this.form.get('label').value), label: this.form.get('label').value, caseInsensitive: !this.form.get('caseSensitive').value, description: this.form.get('description').value, @@ -130,7 +137,7 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent { hint: this.form.get('hint').value, rank: this.form.get('rank').value, addToDictionaryAction: this.form.get('addToDictionaryAction').value, - dossierTemplateId: this._dossierTemplateId, + dossierTemplateId: this._data.dossierTemplateId, }; } } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts index d39d36831..507204c97 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component.ts @@ -7,9 +7,10 @@ import { Moment } from 'moment'; import { applyIntervalConstraints } from '@utils/date-inputs-utils'; import { downloadTypesTranslations } from '../../../../translations/download-types-translations'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; -import { BaseDialogComponent, CONFLICT_ERROR_CODE, Toaster } from '@iqser/common-ui'; +import { BaseDialogComponent, Toaster } from '@iqser/common-ui'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { DownloadFileType, IDossierTemplate } from '@red/domain'; +import { HttpStatusCode } from '@angular/common/http'; @Component({ templateUrl: './add-edit-dossier-template-dialog.component.html', @@ -108,7 +109,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent { this.dialogRef.close(true); } catch (error: any) { const message = - error.status === CONFLICT_ERROR_CODE + error.status === HttpStatusCode.Conflict ? _('add-edit-dossier-template.error.conflict') : _('add-edit-dossier-template.error.generic'); this._toaster.error(message, { error }); diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/user-details/user-details.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/user-details/user-details.component.ts index 515a3f642..a4941ee7d 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/user-details/user-details.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/user-details/user-details.component.ts @@ -6,6 +6,7 @@ import { rolesTranslations } from '../../../../../translations/roles-translation import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { User } from '@red/domain'; import { UserService } from '@services/user.service'; +import { HttpStatusCode } from '@angular/common/http'; @Component({ selector: 'redaction-user-details', @@ -119,7 +120,7 @@ export class UserDetailsComponent implements OnInit { this.closeDialog.emit(true); }) .catch(error => { - if (error.status === 409) { + if (error.status === HttpStatusCode.Conflict) { this._toaster.error(_('add-edit-user.error.email-already-used')); } else { this._toaster.error(_('add-edit-user.error.generic')); diff --git a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.ts index 1a0df33c3..f9b5f38ad 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component.ts @@ -1,9 +1,8 @@ import { Component, EventEmitter, forwardRef, Injector, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; -import { Field } from '../file-attributes-csv-import-dialog.component'; import { CircleButtonTypes, DefaultListingServices, ListingComponent, TableColumnConfig } from '@iqser/common-ui'; import { fileAttributeTypesTranslations } from '../../../translations/file-attribute-types-translations'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { FileAttributeConfigTypes } from '@red/domain'; +import { FileAttributeConfigTypes, IField } from '@red/domain'; @Component({ selector: 'redaction-active-fields-listing', @@ -11,11 +10,11 @@ import { FileAttributeConfigTypes } from '@red/domain'; styleUrls: ['./active-fields-listing.component.scss'], providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => ActiveFieldsListingComponent) }], }) -export class ActiveFieldsListingComponent extends ListingComponent implements OnChanges { +export class ActiveFieldsListingComponent extends ListingComponent implements OnChanges { readonly circleButtonTypes = CircleButtonTypes; readonly translations = fileAttributeTypesTranslations; readonly tableHeaderLabel = _('file-attributes-csv-import.table-header.title'); - readonly tableColumnConfigs: TableColumnConfig[] = [ + readonly tableColumnConfigs: TableColumnConfig[] = [ { label: _('file-attributes-csv-import.table-col-names.name'), class: 'name', @@ -40,10 +39,10 @@ export class ActiveFieldsListingComponent extends ListingComponent implem }, ]; readonly typeOptions = Object.keys(FileAttributeConfigTypes); - @Input() entities: Field[]; - @Output() readonly entitiesChange = new EventEmitter(); + @Input() entities: IField[]; + @Output() readonly entitiesChange = new EventEmitter(); @Output() readonly setHoveredColumn = new EventEmitter(); - @Output() readonly toggleFieldActive = new EventEmitter(); + @Output() readonly toggleFieldActive = new EventEmitter(); constructor(protected readonly _injector: Injector) { super(_injector); @@ -56,8 +55,8 @@ export class ActiveFieldsListingComponent extends ListingComponent implem } deactivateSelection() { - this.allEntities.filter(field => this.isSelected(field)).forEach(field => (field.primaryAttribute = false)); - this.entitiesService.setEntities(this.allEntities.filter(field => !this.isSelected(field))); + this.allEntities.filter(field => this.listingService.isSelected(field)).forEach(field => (field.primaryAttribute = false)); + this.entitiesService.setEntities(this.allEntities.filter(field => !this.listingService.isSelected(field))); this.entitiesChange.emit(this.allEntities); this.listingService.setSelected([]); } @@ -68,7 +67,7 @@ export class ActiveFieldsListingComponent extends ListingComponent implem } } - togglePrimary(field: Field) { + togglePrimary(field: IField) { if (field.primaryAttribute) { field.primaryAttribute = false; return; @@ -80,6 +79,6 @@ export class ActiveFieldsListingComponent extends ListingComponent implem field.primaryAttribute = true; } - itemMouseEnterFn = (field: Field) => this.setHoveredColumn.emit(field.csvColumn); + itemMouseEnterFn = (field: IField) => this.setHoveredColumn.emit(field.csvColumn); itemMouseLeaveFn = () => this.setHoveredColumn.emit(); } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.html index ed7b2cde6..05fe95637 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.html @@ -102,7 +102,7 @@ (click)="toggleFieldActive(field)" (mouseenter)="setHoveredColumn(field.csvColumn)" (mouseleave)="setHoveredColumn()" - *ngFor="let field of fields" + *ngFor="let field of fields; trackBy: trackBy" class="csv-header-pill-wrapper" >
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.ts index e0d094671..63e81cb84 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component.ts @@ -1,33 +1,25 @@ -import { Component, Inject, Injector } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core'; import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import * as Papa from 'papaparse'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; -import { DefaultListingServices, IListable, ListingComponent, TableColumnConfig, Toaster } from '@iqser/common-ui'; +import { DefaultListingServices, ListingComponent, TableColumnConfig, Toaster, trackBy } from '@iqser/common-ui'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { FileAttributeConfig, FileAttributeConfigType, FileAttributeConfigTypes, IFileAttributesConfig } from '@red/domain'; +import { FileAttributeConfig, FileAttributeConfigTypes, IField, IFileAttributesConfig } from '@red/domain'; import { FileAttributesService } from '@services/entity-services/file-attributes.service'; -export interface Field extends IListable { - id: string; - csvColumn: string; - name: string; - type: FileAttributeConfigType; - readonly: boolean; - primaryAttribute: boolean; -} - @Component({ templateUrl: './file-attributes-csv-import-dialog.component.html', styleUrls: ['./file-attributes-csv-import-dialog.component.scss'], providers: [...DefaultListingServices], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FileAttributesCsvImportDialogComponent extends ListingComponent { - readonly tableColumnConfigs: TableColumnConfig[] = []; - parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] }; +export class FileAttributesCsvImportDialogComponent extends ListingComponent { + readonly tableColumnConfigs: TableColumnConfig[] = []; + parseResult: { data: any[]; errors: any[]; meta: any; fields: IField[] }; hoveredColumn: string; - activeFields: Field[] = []; + activeFields: IField[] = []; readonly baseConfigForm: FormGroup; isSearchOpen = false; previewExpanded = true; @@ -36,6 +28,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent a.csvColumnHeader === entity.csvColumn); + const existing = this.data.existingConfiguration.fileAttributeConfigs?.find(a => a.csvColumnHeader === entity.csvColumn); if (existing) { entity.id = existing.id; entity.name = existing.label; @@ -142,21 +135,20 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent f.id === field.id); } - toggleFieldActive(field: Field) { + toggleFieldActive(field: IField) { if (!this.isActive(field)) { this.activeFields = [...this.activeFields, { ...field, searchKey: field.csvColumn }]; } else { - this.activeFields.splice(this.activeFields.indexOf(field), 1); - this.activeFields = [...this.activeFields]; + this.activeFields = this.activeFields.filter(f => f.id !== field.id); } } activateAll() { - this.activeFields = [...this.allEntities.map(item => ({ ...item, searchKey: item.csvColumn }))]; + this.activeFields = this.allEntities.map(item => ({ ...item, searchKey: item.csvColumn })); } deactivateAll() { @@ -165,12 +157,12 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent attr.primaryAttribute); - let fileAttributeConfigs = this.data.existingConfiguration.fileAttributeConfigs; + let fileAttributeConfigs = this.data.existingConfiguration.fileAttributeConfigs ?? []; if (newPrimary) { fileAttributeConfigs = fileAttributeConfigs.map(attr => new FileAttributeConfig({ ...attr, primaryAttribute: false })); } - const fileAttributes = { + const fileAttributes: IFileAttributesConfig = { ...this.baseConfigForm.getRawValue(), fileAttributeConfigs: [ ...fileAttributeConfigs.filter(a => !this.allEntities.find(entity => entity.csvColumn === a.csvColumnHeader)), @@ -181,12 +173,14 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent +
+ +
+

+
+ +
+ + +
+
+ diff --git a/apps/red-ui/src/app/modules/admin/dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component.scss b/apps/red-ui/src/app/modules/admin/dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component.scss new file mode 100644 index 000000000..ffe8bc1fb --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component.scss @@ -0,0 +1,11 @@ +.dialog-header { + color: var(--iqser-primary); +} + +.dialog-actions > *:not(:last-child) { + margin-right: 16px; +} + +.cancel { + margin-left: 8px; +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component.ts new file mode 100644 index 000000000..2117b1040 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { IconButtonTypes } from '@iqser/common-ui'; + +@Component({ + templateUrl: './upload-dictionary-dialog.component.html', + styleUrls: ['./upload-dictionary-dialog.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UploadDictionaryDialogComponent { + readonly iconButtonTypes = IconButtonTypes; + + constructor(private readonly _translateService: TranslateService, public dialogRef: MatDialogRef) {} + + cancel() { + this.dialogRef.close(); + } + + selectOption(option: 'overwrite' | 'merge') { + this.dialogRef.close({ option }); + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts index b6630892b..729330eb0 100644 --- a/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/default-colors/default-colors-screen.component.ts @@ -1,7 +1,6 @@ import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit } from '@angular/core'; import { AppStateService } from '@state/app-state.service'; import { DefaultColorType, IColors } from '@red/domain'; -import { ActivatedRoute } from '@angular/router'; import { AdminDialogService } from '../../services/admin-dialog.service'; import { CircleButtonTypes, @@ -43,7 +42,6 @@ export class DefaultColorsScreenComponent extends ListingComponent imp protected readonly _injector: Injector, private readonly _userService: UserService, private readonly _loadingService: LoadingService, - private readonly _activatedRoute: ActivatedRoute, private readonly _appStateService: AppStateService, private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dialogService: AdminDialogService, diff --git a/apps/red-ui/src/app/modules/admin/screens/dictionary-listing/dictionary-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dictionary-listing/dictionary-listing-screen.component.ts index b5667eed1..07c195482 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dictionary-listing/dictionary-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dictionary-listing/dictionary-listing-screen.component.ts @@ -3,7 +3,6 @@ import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/si import { AppStateService } from '@state/app-state.service'; import { catchError, defaultIfEmpty, tap } from 'rxjs/operators'; import { forkJoin, of } from 'rxjs'; -import { ActivatedRoute } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { CircleButtonTypes, @@ -47,7 +46,6 @@ export class DictionaryListingScreenComponent extends ListingComponent - this._dictionaryService.getFor(this._dossierTemplatesService.activeDossierTemplateId, dict.type).pipe( + this._dictionaryService.getForType(this._dossierTemplatesService.activeDossierTemplateId, dict.type).pipe( tap(values => (dict.entries = [...values.entries] ?? [])), catchError(() => { dict.entries = []; diff --git a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.html index 4600f2688..d7c10b8b8 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.html @@ -58,7 +58,7 @@ (saveDictionary)="saveEntries($event)" [canEdit]="currentUser.isAdmin" [filterByDossierTemplate]="true" - [initialEntries]="entries" + [initialEntries]="initialEntries" >
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 a11c347d3..6f32d193a 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 @@ -20,7 +20,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple readonly circleButtonTypes = CircleButtonTypes; readonly currentUser = this._userService.currentUser; - entries: string[] = []; + initialEntries: string[] = []; dictionary: Dictionary; @ViewChild('dictionaryManager', { static: false }) @@ -98,7 +98,18 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple if (file) { fileReader.onload = () => { - this._dictionaryManager.editor.value = fileReader.result as string; + const fileContent = fileReader.result as string; + if (this._dictionaryManager.editor.value) { + this._dialogService.openDialog('uploadDictionary', null, null, ({ option }) => { + if (option === 'overwrite') { + this._overwrite(fileContent); + } else if (option === 'merge') { + this._merge(fileContent); + } + }); + } else { + this._overwrite(fileContent); + } this._fileInput.nativeElement.value = null; }; fileReader.readAsText(file); @@ -107,38 +118,55 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple saveEntries(entries: string[]) { this._loadingService.start(); - this._dictionaryService.saveEntries(entries, this.entries, this.dictionary.dossierTemplateId, this.dictionary.type, null).subscribe( - async () => { - await this._loadEntries(); - }, - () => { - this._loadingService.stop(); - }, - ); + this._dictionaryService + .saveEntries(entries, this.initialEntries, this.dictionary.dossierTemplateId, this.dictionary.type, null) + .subscribe( + async () => { + await this._loadEntries(); + }, + () => { + this._loadingService.stop(); + }, + ); } ngOnDestroy(): void { this._appStateService.reset(); } + private _overwrite(fileContent: string): void { + this._dictionaryManager.editor.value = fileContent; + } + + private _merge(fileContent: string): void { + const currentEntries = this._dictionaryManager.editor.value.split('\n'); + fileContent + .split('\n') + .filter(entry => !currentEntries.includes(entry)) + .forEach(entry => currentEntries.push(entry)); + this._dictionaryManager.editor.value = currentEntries.join('\n'); + } + private async _loadEntries() { this._loadingService.start(); await this._dictionaryService - .getFor(this.dictionary.dossierTemplateId, this.dictionary.type) + .getForType(this.dictionary.dossierTemplateId, this.dictionary.type) .toPromise() .then( data => { this._loadingService.stop(); - this.entries = [...data.entries].sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })); + this.initialEntries = [...data.entries].sort((str1, str2) => + str1.localeCompare(str2, undefined, { sensitivity: 'accent' }), + ); }, () => { this._loadingService.stop(); - this.entries = []; + this.initialEntries = []; }, ) .catch(() => { this._loadingService.stop(); - this.entries = []; + this.initialEntries = []; }); } } diff --git a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.scss index d9f77b434..4526cdf81 100644 --- a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.scss @@ -17,6 +17,6 @@ form { /* target the input field inexplicably to throw Chrome's AI off. * feel free to use a more complicated selector */ -input[name='keySecret'] { +input[name='keySecret']:not(:placeholder-shown) { font-family: 'secret'; } diff --git a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts index 7dff45c3b..54dd0a374 100644 --- a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts @@ -7,6 +7,7 @@ import { UserService } from '@services/user.service'; import { RouterHistoryService } from '@services/router-history.service'; import { DigitalSignatureService } from '../../services/digital-signature.service'; import { IDigitalSignature } from '@red/domain'; +import { HttpStatusCode } from '@angular/common/http'; @Component({ selector: 'redaction-digital-signature-screen', @@ -55,7 +56,7 @@ export class DigitalSignatureScreenComponent extends AutoUnsubscribe implements this._toaster.success(_('digital-signature-screen.action.save-success')); }, error => { - if (error.status === 400) { + if (error.status === HttpStatusCode.BadRequest) { this._toaster.error(_('digital-signature-screen.action.certificate-not-valid-error')); } else { this._toaster.error(_('digital-signature-screen.action.save-error')); diff --git a/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.ts index c9d02247d..5ad935999 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dossier-attributes-listing/dossier-attributes-listing-screen.component.ts @@ -9,7 +9,6 @@ import { TableColumnConfig, } from '@iqser/common-ui'; import { AppStateService } from '@state/app-state.service'; -import { ActivatedRoute } from '@angular/router'; import { AdminDialogService } from '../../services/admin-dialog.service'; import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service'; import { dossierAttributeTypesTranslations } from '../../translations/dossier-attribute-types-translations'; @@ -43,7 +42,6 @@ export class DossierAttributesListingScreenComponent extends ListingComponent
-
- {{ dossierTemplate.dateAdded | date: 'd MMM. yyyy' }} +
+
+ {{ dossierTemplate.dateAdded | date: 'd MMM. yyyy' }} +
diff --git a/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.ts index e905389d8..94dbda669 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dossier-template-listing/dossier-templates-listing-screen.component.ts @@ -17,6 +17,7 @@ import { UserService } from '@services/user.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { RouterHistoryService } from '@services/router-history.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; +import { HttpStatusCode } from '@angular/common/http'; @Component({ templateUrl: './dossier-templates-listing-screen.component.html', @@ -89,7 +90,7 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent { - if (error.status === 409) { + if (error.status === HttpStatusCode.Conflict) { this._toaster.error(_('dossier-templates-listing.error.conflict')); } else { this._toaster.error(_('dossier-templates-listing.error.generic')); diff --git a/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts index 7aac12a90..63c984f73 100644 --- a/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/file-attributes-listing/file-attributes-listing-screen.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, ElementRef, forwardRef, Injector, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AppStateService } from '@state/app-state.service'; -import { ActivatedRoute } from '@angular/router'; import { AdminDialogService } from '../../services/admin-dialog.service'; import { CircleButtonTypes, @@ -9,6 +8,7 @@ import { ListingComponent, LoadingService, TableColumnConfig, + Toaster, } from '@iqser/common-ui'; import { fileAttributeTypesTranslations } from '../../translations/file-attribute-types-translations'; import { UserService } from '@services/user.service'; @@ -16,6 +16,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { FileAttributeConfig, IFileAttributeConfig, IFileAttributesConfig } from '@red/domain'; import { FileAttributesService } from '@services/entity-services/file-attributes.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; +import { HttpStatusCode } from '@angular/common/http'; @Component({ templateUrl: './file-attributes-listing-screen.component.html', @@ -51,8 +52,8 @@ export class FileAttributesListingScreenComponent extends ListingComponent { - this._loadingService.start(); - await this._fileAttributesService - .setFileAttributesConfig(newValue, this._dossierTemplatesService.activeDossierTemplateId) - .toPromise(); - await this._appStateService.refreshDossierTemplate(this._dossierTemplatesService.activeDossierTemplateId); - await this._loadData(); + (newValue: IFileAttributeConfig) => { + this._loadingService.loadWhile(this._createNewFileAttributeAndRefreshView(newValue)); }, ); } @@ -117,6 +113,23 @@ export class FileAttributesListingScreenComponent extends ListingComponent { + await this._fileAttributesService + .setFileAttributesConfig(newValue, this._dossierTemplatesService.activeDossierTemplateId) + .toPromise() + .catch(error => { + console.log('error'); + if (error.status === HttpStatusCode.Conflict) { + this._toaster.error(_('file-attributes-listing.error.conflict')); + } else { + this._toaster.error(_('file-attributes-listing.error.generic')); + } + this._loadingService.stop(); + }); + await this._appStateService.refreshDossierTemplate(this._dossierTemplatesService.activeDossierTemplateId); + await this._loadData(); + } + private async _loadData() { this._loadingService.start(); diff --git a/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-dialog.service.ts b/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-dialog.service.ts index 523bbee1d..a4ee07326 100644 --- a/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-dialog.service.ts +++ b/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-dialog.service.ts @@ -5,7 +5,6 @@ import { ConfirmationDialogInput, DialogConfig, DialogService, - ListingService, LoadingService, TitleColors, } from '@iqser/common-ui'; @@ -38,7 +37,7 @@ export class JustificationsDialogService extends DialogService { super(_dialog); } - confirmDelete(justifications: Justification[], listingService: ListingService) { + confirmDelete(justifications: Justification[]) { const data = new ConfirmationDialogInput({ title: _('confirmation-dialog.delete-justification.title'), titleColor: TitleColors.WARN, diff --git a/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-screen/justifications-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-screen/justifications-screen.component.ts index d7aa71663..7a2008ccb 100644 --- a/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-screen/justifications-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/justifications/justifications-screen/justifications-screen.component.ts @@ -59,6 +59,6 @@ export class JustificationsScreenComponent extends ListingComponent { @@ -76,6 +78,9 @@ export class AdminDialogService extends DialogService { component: AddEditJustificationDialogComponent, dialogConfig: { autoFocus: true }, }, + uploadDictionary: { + component: UploadDictionaryDialogComponent, + }, }; constructor(protected readonly _dialog: MatDialog) { diff --git a/apps/red-ui/src/app/modules/auth/auth.module.ts b/apps/red-ui/src/app/modules/auth/auth.module.ts index b4bbe3a24..1246d36fd 100644 --- a/apps/red-ui/src/app/modules/auth/auth.module.ts +++ b/apps/red-ui/src/app/modules/auth/auth.module.ts @@ -5,7 +5,6 @@ import { HttpClientModule } from '@angular/common/http'; import { KeycloakAngularModule, KeycloakOptions, KeycloakService } from 'keycloak-angular'; import { ConfigService } from '@services/config.service'; import { BASE_HREF } from '../../tokens'; -import { environment } from '@environments/environment'; function getKeycloakOptions(configService: ConfigService, baseUrl: string) { let url: string = configService.values.OAUTH_URL; @@ -21,9 +20,7 @@ function getKeycloakOptions(configService: ConfigService, baseUrl: string) { initOptions: { checkLoginIframe: false, onLoad: 'check-sso', - silentCheckSsoRedirectUri: environment.production - ? window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html' - : null, + silentCheckSsoRedirectUri: window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html', flow: 'standard', }, enableBearerInterceptor: true, diff --git a/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.html b/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.html index e7ca4524f..661024ca0 100644 --- a/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.html @@ -1,109 +1,142 @@
- + + + - + + - + + + - + - + - + - + - + - + - + - + - + + + + + + + +
diff --git a/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.ts index 2b86746fd..10bcfa8b4 100644 --- a/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.ts @@ -1,11 +1,12 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; -import { AppStateService } from '@state/app-state.service'; import { PermissionsService } from '@services/permissions.service'; import { AnnotationPermissions } from '@models/file/annotation.permissions'; import { AnnotationActionsService } from '../../services/annotation-actions.service'; import { WebViewerInstance } from '@pdftron/webviewer'; import { UserService } from '@services/user.service'; +import { Dossier } from '@red/domain'; +import { Required } from '@iqser/common-ui'; export const AnnotationButtonTypes = { dark: 'dark', @@ -19,17 +20,17 @@ export type AnnotationButtonType = keyof typeof AnnotationButtonTypes; templateUrl: './annotation-actions.component.html', styleUrls: ['./annotation-actions.component.scss'], }) -export class AnnotationActionsComponent implements OnInit { +export class AnnotationActionsComponent implements OnChanges { @Input() buttonType: AnnotationButtonType = AnnotationButtonTypes.dark; @Input() tooltipPosition: 'before' | 'above' = 'before'; @Input() canPerformAnnotationActions: boolean; @Input() viewer: WebViewerInstance; @Input() alwaysVisible: boolean; + @Input() @Required() dossier!: Dossier; @Output() annotationsChanged = new EventEmitter(); annotationPermissions: AnnotationPermissions; constructor( - readonly appStateService: AppStateService, readonly annotationActionsService: AnnotationActionsService, private readonly _permissionsService: PermissionsService, private readonly _userService: UserService, @@ -44,7 +45,6 @@ export class AnnotationActionsComponent implements OnInit { @Input() set annotations(value: AnnotationWrapper[]) { this._annotations = value.filter(a => a !== undefined); - this._setPermissions(); } get viewerAnnotations() { @@ -63,17 +63,27 @@ export class AnnotationActionsComponent implements OnInit { return this.annotations?.reduce((accumulator, annotation) => annotation.isImage && accumulator, true); } - ngOnInit(): void { + get resizing() { + return this.annotations?.length === 1 && this.annotations?.[0].resizing; + } + + ngOnChanges(): void { this._setPermissions(); } suggestRemoveAnnotations($event, removeFromDict: boolean) { $event.stopPropagation(); - this.annotationActionsService.suggestRemoveAnnotation($event, this.annotations, removeFromDict, this.annotationsChanged); + this.annotationActionsService.suggestRemoveAnnotation( + $event, + this.annotations, + this.dossier, + removeFromDict, + this.annotationsChanged, + ); } markAsFalsePositive($event) { - this.annotationActionsService.markAsFalsePositive($event, this.annotations, this.annotationsChanged); + this.annotationActionsService.markAsFalsePositive($event, this.annotations, this.dossier, this.annotationsChanged); } hideAnnotation($event: MouseEvent) { @@ -90,9 +100,21 @@ export class AnnotationActionsComponent implements OnInit { this.annotationActionsService.updateHiddenAnnotation(this.annotations, this.viewerAnnotations, false); } + resize($event: MouseEvent) { + this.annotationActionsService.resize($event, this.viewer, this.annotations[0]); + } + + acceptResize($event: MouseEvent) { + this.annotationActionsService.acceptResize($event, this.viewer, this.dossier, this.annotations[0], this.annotationsChanged); + } + + cancelResize($event: MouseEvent) { + this.annotationActionsService.cancelResize($event, this.viewer, this.annotations[0], this.annotationsChanged); + } + private _setPermissions() { this.annotationPermissions = AnnotationPermissions.forUser( - this._permissionsService.isApprover(), + this._permissionsService.isApprover(this.dossier), this._userService.currentUser, this.annotations, ); diff --git a/apps/red-ui/src/app/modules/dossier/components/comments/comments.component.html b/apps/red-ui/src/app/modules/dossier/components/comments/comments.component.html index 190e5a2c7..8686a4b2e 100644 --- a/apps/red-ui/src/app/modules/dossier/components/comments/comments.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/comments/comments.component.html @@ -1,32 +1,34 @@ -
-
-
- {{ comment.user | name }} - {{ comment.date | date: 'sophisticatedDate' }} + +
+
+
+ {{ comment.user | name }} + {{ comment.date | date: 'sophisticatedDate' }} +
+ +
+ +
-
- -
+
{{ comment.text }}
-
{{ comment.text }}
-
- - + +
diff --git a/apps/red-ui/src/app/modules/dossier/components/comments/comments.component.ts b/apps/red-ui/src/app/modules/dossier/components/comments/comments.component.ts index 0412ddced..454d090e4 100644 --- a/apps/red-ui/src/app/modules/dossier/components/comments/comments.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/comments/comments.component.ts @@ -1,10 +1,13 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, ViewChild } from '@angular/core'; -import { IComment } from '@red/domain'; +import { File, IComment } from '@red/domain'; import { ManualAnnotationService } from '../../services/manual-annotation.service'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { UserService } from '@services/user.service'; import { PermissionsService } from '@services/permissions.service'; import { InputWithActionComponent, trackBy } from '@iqser/common-ui'; +import { FilesMapService } from '@services/entity-services/files-map.service'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; @Component({ selector: 'redaction-comments', @@ -15,27 +18,37 @@ import { InputWithActionComponent, trackBy } from '@iqser/common-ui'; export class CommentsComponent { @Input() annotation: AnnotationWrapper; readonly trackBy = trackBy(); + readonly file$: Observable; @HostBinding('class.hidden') private _hidden = true; @ViewChild(InputWithActionComponent) private readonly _input: InputWithActionComponent; + private readonly _fileId: string; + private readonly _dossierId: string; constructor( readonly permissionsService: PermissionsService, private readonly _userService: UserService, private readonly _manualAnnotationService: ManualAnnotationService, private readonly _changeDetectorRef: ChangeDetectorRef, - ) {} + readonly filesMapService: FilesMapService, + activatedRoute: ActivatedRoute, + ) { + this._fileId = activatedRoute.snapshot.paramMap.get('fileId'); + this._dossierId = activatedRoute.snapshot.paramMap.get('dossierId'); + this.file$ = filesMapService.watch$(this._dossierId, this._fileId); + } addComment(value: string): void { if (!value) { return; } this._manualAnnotationService - .addComment(value, this.annotation.id) + .addComment(value, this.annotation.id, this._dossierId, this._fileId) .toPromise() .then(commentId => { this.annotation.comments.push({ text: value, id: commentId, + annotationId: this.annotation.id, user: this._userService.currentUser.id, }); this._input.reset(); @@ -50,7 +63,7 @@ export class CommentsComponent { deleteComment(comment: IComment): void { this._manualAnnotationService - .deleteComment(comment.id, this.annotation.id) + .deleteComment(comment.id, this.annotation.id, this._dossierId, this._fileId) .toPromise() .then(() => { this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1); diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/components/annotations-list/annotations-list.component.html b/apps/red-ui/src/app/modules/dossier/components/file-workload/components/annotations-list/annotations-list.component.html index 74539ea38..9527825e8 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/components/annotations-list/annotations-list.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/components/annotations-list/annotations-list.component.html @@ -11,7 +11,7 @@
- +
diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/components/annotations-list/annotations-list.component.ts b/apps/red-ui/src/app/modules/dossier/components/file-workload/components/annotations-list/annotations-list.component.ts index a09b7aba9..152023efa 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/components/annotations-list/annotations-list.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/components/annotations-list/annotations-list.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { IqserEventTarget } from '@iqser/common-ui'; +import { File } from '@red/domain'; @Component({ selector: 'redaction-annotations-list', @@ -9,6 +10,7 @@ import { IqserEventTarget } from '@iqser/common-ui'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AnnotationsListComponent { + @Input() file: File; @Input() annotations: AnnotationWrapper[]; @Input() selectedAnnotations: AnnotationWrapper[]; @Input() annotationActionsTemplate: TemplateRef; diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html index ad93dcfe9..79d5cc468 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html @@ -5,11 +5,13 @@ *ngIf="!multiSelectActive && !isReadOnly" class="all-caps-label primary pointer" translate="file-preview.tabs.annotations.select" + iqserHelpMode="bulk-select-annotations" >
@@ -26,17 +28,20 @@
-
-
- - -
+
+
+
+ + +
+
+
-
- - {{ activeViewerPage }} - - {{ activeAnnotations?.length || 0 }} - +
+ + + + + {{ activeViewerPage }} - + {{ activeAnnotations?.length || 0 }} + +
@@ -141,13 +159,9 @@ [verticalPadding]="40" icon="iqser:document" > - + {{ 'file-preview.tabs.annotations.page-is' | translate }} - . @@ -173,13 +187,15 @@
@@ -187,7 +203,7 @@
diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.scss b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.scss index 7636f6bec..46fc452ab 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.scss +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.scss @@ -167,3 +167,11 @@ } } } + +.padding-left-0 { + padding-left: 0 !important; +} + +::ng-deep .page-separator iqser-circle-button mat-icon { + color: var(--iqser-primary); +} diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.ts index abeb97c36..ea23c01ed 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.ts @@ -1,14 +1,25 @@ -import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output, TemplateRef, ViewChild } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Input, + Output, + TemplateRef, + ViewChild, +} from '@angular/core'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationProcessingService } from '../../services/annotation-processing.service'; import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; import scrollIntoView from 'scroll-into-view-if-needed'; -import { CircleButtonTypes, Debounce, FilterService, IconButtonTypes, INestedFilter, IqserEventTarget } from '@iqser/common-ui'; -import { FileDataModel } from '@models/file/file-data.model'; +import { CircleButtonTypes, Debounce, FilterService, IconButtonTypes, INestedFilter, IqserEventTarget, Required } from '@iqser/common-ui'; import { PermissionsService } from '@services/permissions.service'; import { WebViewerInstance } from '@pdftron/webviewer'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +import { Dossier, File, IViewedPage } from '@red/domain'; const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape']; const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; @@ -17,6 +28,7 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; selector: 'redaction-file-workload', templateUrl: './file-workload.component.html', styleUrls: ['./file-workload.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FileWorkloadComponent { readonly iconButtonTypes = IconButtonTypes; @@ -27,7 +39,9 @@ export class FileWorkloadComponent { @Input() activeViewerPage: number; @Input() shouldDeselectAnnotationsOnPageChange: boolean; @Input() dialogRef: MatDialogRef; - @Input() fileData: FileDataModel; + @Input() viewedPages: IViewedPage[]; + @Input() @Required() file!: File; + @Input() @Required() dossier!: Dossier; @Input() hideSkipped: boolean; @Input() excludePages: boolean; @Input() annotationActionsTemplate: TemplateRef; @@ -78,16 +92,16 @@ export class FileWorkloadComponent { } } - get isProcessing(): boolean { - return this.fileData?.file?.isProcessing; - } - get activeAnnotations(): AnnotationWrapper[] | undefined { return this.displayedAnnotations.get(this.activeViewerPage); } get isReadOnly(): boolean { - return !this._permissionsService.canPerformAnnotationActions(); + return !this._permissionsService.canPerformAnnotationActions(this.file); + } + + get currentPageIsExcluded(): boolean { + return this.file?.excludedPages?.includes(this.activeViewerPage); } private get _firstSelectedAnnotation() { @@ -114,6 +128,11 @@ export class FileWorkloadComponent { } } + hasOnlyManualRedactionsAndNotExcluded(pageNumber: number): boolean { + const hasOnlyManualRedactions = this.displayedAnnotations.get(pageNumber).every(annotation => annotation.manual); + return hasOnlyManualRedactions && this.file.excludedPages.includes(pageNumber); + } + pageHasSelection(page: number) { return this.multiSelectActive && !!this.selectedAnnotations?.find(a => a.pageNumber === page); } @@ -162,7 +181,7 @@ export class FileWorkloadComponent { this._navigatePages($event); } - this._changeDetectorRef.detectChanges(); + this._changeDetectorRef.markForCheck(); } scrollAnnotations(): void { @@ -203,7 +222,7 @@ export class FileWorkloadComponent { } scrollQuickNavLast(): void { - this.selectPage.emit(this.fileData.file.numberOfPages); + this.selectPage.emit(this.file.numberOfPages); } pageSelectedByClick($event: number): void { @@ -225,6 +244,10 @@ export class FileWorkloadComponent { this.selectPage.emit(this._nextPageWithAnnotations()); } + viewExcludePages(): void { + this.actionPerformed.emit('view-exclude-pages'); + } + private _filterAnnotations( annotations: AnnotationWrapper[], primary: INestedFilter[], diff --git a/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.html b/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.html index e1923ce1f..623b48ca9 100644 --- a/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.html @@ -1,4 +1,4 @@ -
+
{{ range.startPage }} -{{ range.endPage }} @@ -29,7 +29,7 @@
diff --git a/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.ts b/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.ts index 14c4a721f..122980e03 100644 --- a/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/page-exclusion/page-exclusion.component.ts @@ -40,8 +40,9 @@ export class PageExclusionComponent implements OnChanges { }, []); } - async excludePagesRange(value: string): Promise { + async excludePagesRange(inputValue: string): Promise { this._loadingService.start(); + const value = inputValue.replace(/[^0-9-,]/g, ''); try { const pageRanges = value.split(',').map(range => { const splitted = range.split('-'); diff --git a/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.html b/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.html index c7db169f4..420ce5f46 100644 --- a/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.html @@ -6,7 +6,7 @@ [id]="'quick-nav-page-' + number" class="page-wrapper" > - +
{{ number }}
diff --git a/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.ts b/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.ts index 29ea3b674..cc63a24e3 100644 --- a/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.ts @@ -1,19 +1,26 @@ -import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; -import { AppStateService } from '@state/app-state.service'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { ConfigService } from '@services/config.service'; -import { Subscription } from 'rxjs'; import { DossiersService } from '@services/entity-services/dossiers.service'; import { ViewedPagesService } from '../../shared/services/viewed-pages.service'; -import { IViewedPage } from '@red/domain'; +import { File, IViewedPage } from '@red/domain'; +import { AutoUnsubscribe } from '@iqser/common-ui'; +import { FilesMapService } from '@services/entity-services/files-map.service'; @Component({ selector: 'redaction-page-indicator', templateUrl: './page-indicator.component.html', styleUrls: ['./page-indicator.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { - @Input() active: boolean; +export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy, OnChanges { + @Input() + file: File; + + @Input() + active = false; + + @Input() showDottedIcon = false; @Input() number: number; @Input() viewedPages: IViewedPage[]; @Input() activeSelection = false; @@ -21,16 +28,16 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { @Output() readonly pageSelected = new EventEmitter(); pageReadTimeout: number = null; - canMarkPagesAsViewed: boolean; - private _subscription: Subscription; constructor( private readonly _viewedPagesService: ViewedPagesService, - private readonly _appStateService: AppStateService, + private readonly _filesMapService: FilesMapService, private readonly _dossiersService: DossiersService, private readonly _configService: ConfigService, private readonly _permissionService: PermissionsService, - ) {} + ) { + super(); + } get activePage() { return this.viewedPages?.find(p => p.page === this.number); @@ -45,49 +52,35 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { } } - ngOnInit(): void { - this._subscription = this._appStateService.fileChanged$.subscribe(() => { - if (this.canMarkPagesAsViewed !== this._permissionService.canMarkPagesAsViewed()) { - this.canMarkPagesAsViewed = this._permissionService.canMarkPagesAsViewed(); - this._handlePageRead(); - } - }); + ngOnChanges() { + this.handlePageRead(); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.active) { - this._handlePageRead(); - } - } - - toggleReadState() { - if (this.canMarkPagesAsViewed) { + async toggleReadState() { + if (this._permissionService.canMarkPagesAsViewed(this.file)) { if (this.read) { - this._markPageUnread(); + await this._markPageUnread(); } else { - this._markPageRead(); + await this._markPageRead(); } } } - ngOnDestroy(): void { - if (this._subscription) { - this._subscription.unsubscribe(); + handlePageRead() { + if (!this._permissionService.canMarkPagesAsViewed(this.file)) { + return; } - } - private _handlePageRead() { - if (this.canMarkPagesAsViewed) { - if (this.pageReadTimeout) { - clearTimeout(this.pageReadTimeout); - } - if (this.active && !this.read) { - this.pageReadTimeout = window.setTimeout(() => { - if (this.active && !this.read) { - this._markPageRead(); - } - }, this._configService.values.AUTO_READ_TIME * 1000); - } + if (this.pageReadTimeout) { + clearTimeout(this.pageReadTimeout); + } + + if (this.active && !this.read) { + this.pageReadTimeout = window.setTimeout(async () => { + if (this.active && !this.read) { + await this._markPageRead(); + } + }, this._configService.values.AUTO_READ_TIME * 1000); } } @@ -108,26 +101,20 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { // } // } - private _markPageRead() { - this._viewedPagesService - .addPage({ page: this.number }, this._dossiersService.activeDossierId, this._appStateService.activeFileId) - .subscribe(() => { - if (this.activePage) { - this.activePage.hasChanges = false; - } else { - this.viewedPages?.push({ page: this.number, fileId: this._appStateService.activeFileId }); - } - }); + private async _markPageRead() { + await this._viewedPagesService.addPage({ page: this.number }, this.file.dossierId, this.file.fileId).toPromise(); + if (this.activePage) { + this.activePage.hasChanges = false; + } else { + this.viewedPages?.push({ page: this.number, fileId: this.file.fileId }); + } } - private _markPageUnread() { - this._viewedPagesService - .removePage(this._dossiersService.activeDossierId, this._appStateService.activeFileId, this.number) - .subscribe(() => { - this.viewedPages?.splice( - this.viewedPages?.findIndex(p => p.page === this.number), - 1, - ); - }); + private async _markPageUnread() { + await this._viewedPagesService.removePage(this.file.dossierId, this.file.fileId, this.number).toPromise(); + this.viewedPages?.splice( + this.viewedPages?.findIndex(p => p.page === this.number), + 1, + ); } } diff --git a/apps/red-ui/src/app/modules/dossier/components/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/modules/dossier/components/pdf-viewer/pdf-viewer.component.ts index 4cd21ba90..2b98ab406 100644 --- a/apps/red-ui/src/app/modules/dossier/components/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/pdf-viewer/pdf-viewer.component.ts @@ -11,10 +11,14 @@ import { SimpleChanges, ViewChild, } from '@angular/core'; -import { File, IManualRedactionEntry, ViewMode } from '@red/domain'; +import { Dossier, File, IManualRedactionEntry, ViewMode } from '@red/domain'; import WebViewer, { Core, WebViewerInstance } from '@pdftron/webviewer'; import { TranslateService } from '@ngx-translate/core'; -import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; +import { + ManualRedactionEntryType, + ManualRedactionEntryTypes, + ManualRedactionEntryWrapper, +} from '@models/file/manual-redaction-entry.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { ManualAnnotationService } from '../../services/manual-annotation.service'; import { environment } from '@environments/environment'; @@ -29,10 +33,26 @@ import { loadCompareDocumentWrapper } from '../../utils/compare-mode.utils'; import { PdfViewerUtils } from '../../utils/pdf-viewer.utils'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { ActivatedRoute } from '@angular/router'; +import { toPosition } from '../../utils/pdf-calculation.utils'; +import { DossiersService } from '@services/entity-services/dossiers.service'; import Tools = Core.Tools; import TextTool = Tools.TextTool; import Annotation = Core.Annotations.Annotation; +const ALLOWED_KEYBOARD_SHORTCUTS = ['+', '-', 'p', 'r', 'Escape'] as const; +const dataElements = { + ADD_REDACTION: 'add-redaction', + ADD_DICTIONARY: 'add-dictionary', + ADD_RECTANGLE: 'add-rectangle', + ADD_FALSE_POSITIVE: 'add-false-positive', + SHAPE_TOOL_GROUP_BUTTON: 'shapeToolGroupButton', + RECTANGLE_TOOL_DIVIDER: 'rectangleToolDivider', + ANNOTATION_POPUP: 'annotationPopup', + COMPARE_BUTTON: 'compareButton', + CLOSE_COMPARE_BUTTON: 'closeCompareButton', + COMPARE_TOOL_DIVIDER: 'compareToolDivider', +} as const; + @Component({ selector: 'redaction-pdf-viewer', templateUrl: './pdf-viewer.component.html', @@ -55,10 +75,11 @@ export class PdfViewerComponent implements OnInit, OnChanges { @ViewChild('viewer', { static: true }) viewer: ElementRef; @ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef; instance: WebViewerInstance; + documentViewer: Core.DocumentViewer; + annotationManager: Core.AnnotationManager; utils: PdfViewerUtils; private _selectedText = ''; private _firstPageChange = true; - private readonly _allowedKeyboardShortcuts = ['+', '-', 'p', 'r', 'Escape']; constructor( @Inject(BASE_HREF) private readonly _baseHref: string, @@ -72,6 +93,7 @@ export class PdfViewerComponent implements OnInit, OnChanges { private readonly _annotationActionsService: AnnotationActionsService, private readonly _configService: ConfigService, private readonly _loadingService: LoadingService, + private readonly _dossiersService: DossiersService, ) {} private _viewMode: ViewMode = 'STANDARD'; @@ -85,97 +107,95 @@ export class PdfViewerComponent implements OnInit, OnChanges { this.utils.viewMode = value; } + private get _dossier(): Dossier { + return this._dossiersService.find(this.file.dossierId); + } + async ngOnInit() { - this._documentLoaded = this._documentLoaded.bind(this); + this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this); await this._loadViewer(); } ngOnChanges(changes: SimpleChanges): void { - if (this.instance) { - if (changes.fileData) { - this._loadDocument(); - } - if (changes.canPerformActions) { - this._handleCustomActions(); - } - if (changes.multiSelectActive) { - this.utils.multiSelectActive = this.multiSelectActive; - } + if (!this.instance) { + return; } - } - setInitialViewerState() { - // viewer init - this.instance.UI.setFitMode('FitPage'); + if (changes.fileData) { + this._loadDocument(); + } - const instanceDisplayMode = this.instance.Core.documentViewer.getDisplayModeManager().getDisplayMode(); - instanceDisplayMode.mode = this.viewMode === 'STANDARD' ? 'Single' : 'Facing'; - this.instance.Core.documentViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode); + if (changes.canPerformActions) { + this._handleCustomActions(); + } + + if (changes.multiSelectActive) { + this.utils.multiSelectActive = this.multiSelectActive; + } } uploadFile(files: any) { const fileToCompare = files[0]; this.compareFileInput.nativeElement.value = null; - const fileReader = new FileReader(); - - if (fileToCompare) { - fileReader.onload = async () => { - const pdfData = fileReader.result; - const pdfNet = this.instance.Core.PDFNet; - - await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); - - const mergedDocument = await pdfNet.PDFDoc.create(); - const compareDocument = await pdfNet.PDFDoc.createFromBuffer(pdfData); - const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer()); - - const currentDocumentPageCount = await currentDocument.getPageCount(); - const compareDocumentPageCount = await compareDocument.getPageCount(); - - const loadCompareDocument = async () => { - this._loadingService.start(); - this.utils.ready = false; - await loadCompareDocumentWrapper( - currentDocumentPageCount, - compareDocumentPageCount, - currentDocument, - compareDocument, - mergedDocument, - this.instance, - this.file, - () => { - this.viewMode = 'COMPARE'; - }, - () => { - this.utils.navigateToPage(1); - }, - this.instance.Core.PDFNet, - ); - this._loadingService.stop(); - }; - - if (currentDocumentPageCount !== compareDocumentPageCount) { - this._dialogService.openDialog( - 'confirm', - null, - new ConfirmationDialogInput({ - title: _('confirmation-dialog.compare-file.title'), - question: _('confirmation-dialog.compare-file.question'), - translateParams: { - fileName: fileToCompare.name, - currentDocumentPageCount, - compareDocumentPageCount, - }, - }), - loadCompareDocument, - ); - } else { - await loadCompareDocument(); - } - }; + if (!fileToCompare) { + console.error('No file to compare!'); + return; } + const fileReader = new FileReader(); + fileReader.onload = async () => { + const pdfNet = this.instance.Core.PDFNet; + + await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); + + const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer); + const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer()); + + const loadCompareDocument = async () => { + this._loadingService.start(); + this.utils.ready = false; + const mergedDocument = await pdfNet.PDFDoc.create(); + await loadCompareDocumentWrapper( + currentDocument, + compareDocument, + mergedDocument, + this.instance, + this.file, + () => { + this.viewMode = 'COMPARE'; + }, + () => { + this.utils.navigateToPage(1); + }, + this.instance.Core.PDFNet, + ); + this._loadingService.stop(); + }; + + const currentDocumentPageCount = await currentDocument.getPageCount(); + const compareDocumentPageCount = await compareDocument.getPageCount(); + + if (currentDocumentPageCount !== compareDocumentPageCount) { + this._dialogService.openDialog( + 'confirm', + null, + new ConfirmationDialogInput({ + title: _('confirmation-dialog.compare-file.title'), + question: _('confirmation-dialog.compare-file.question'), + translateParams: { + fileName: fileToCompare.name, + currentDocumentPageCount, + compareDocumentPageCount, + }, + }), + loadCompareDocument, + ); + } else { + await loadCompareDocument(); + } + }; + fileReader.readAsArrayBuffer(fileToCompare); } @@ -187,11 +207,18 @@ export class PdfViewerComponent implements OnInit, OnChanges { this.instance.UI.loadDocument(currentDocument, { filename: this.file ? this.file.filename : 'document.pdf', }); - this.instance.UI.disableElements(['closeCompareButton']); - this.instance.UI.enableElements(['compareButton']); + this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]); + this.instance.UI.enableElements([dataElements.COMPARE_BUTTON]); this.utils.navigateToPage(1); } + private _setInitialDisplayMode() { + this.instance.UI.setFitMode('FitPage'); + const instanceDisplayMode = this.documentViewer.getDisplayModeManager().getDisplayMode(); + instanceDisplayMode.mode = this.viewMode === 'STANDARD' ? 'Single' : 'Facing'; + this.documentViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode); + } + private _convertPath(path: string): string { return this._baseHref + path; } @@ -208,6 +235,8 @@ export class PdfViewerComponent implements OnInit, OnChanges { this.viewer.nativeElement, ); + this.documentViewer = this.instance.Core.documentViewer; + this.annotationManager = this.instance.Core.annotationManager; this.utils = new PdfViewerUtils(this.instance, this.viewMode, this.multiSelectActive); this._setSelectionMode(); @@ -215,8 +244,8 @@ export class PdfViewerComponent implements OnInit, OnChanges { this.utils.disableHotkeys(); this._configureTextPopup(); - this.instance.Core.annotationManager.on('annotationSelected', (annotations, action) => { - this.annotationSelected.emit(this.instance.Core.annotationManager.getSelectedAnnotations().map(ann => ann.Id)); + this.annotationManager.on('annotationSelected', (annotations, action) => { + this.annotationSelected.emit(this.annotationManager.getSelectedAnnotations().map(ann => ann.Id)); if (action === 'deselected') { this._toggleRectangleAnnotationAction(true); } else { @@ -225,16 +254,17 @@ export class PdfViewerComponent implements OnInit, OnChanges { } }); - this.instance.Core.annotationManager.on('annotationChanged', annotations => { + this.annotationManager.on('annotationChanged', annotations => { // when a rectangle is drawn, // it returns one annotation with tool name 'AnnotationCreateRectangle; // this will auto select rectangle after drawing if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') { - this.instance.Core.annotationManager.selectAnnotations(annotations); + this.annotationManager.selectAnnotations(annotations); + annotations[0].setRotationControlEnabled(false); } }); - this.instance.Core.documentViewer.on('pageNumberUpdated', pageNumber => { + this.documentViewer.on('pageNumberUpdated', pageNumber => { if (this.shouldDeselectAnnotationsOnPageChange) { this.utils.deselectAllAnnotations(); } @@ -250,9 +280,9 @@ export class PdfViewerComponent implements OnInit, OnChanges { this._handleCustomActions(); }); - this.instance.Core.documentViewer.on('documentLoaded', this._documentLoaded); + this.documentViewer.on('documentLoaded', this._setReadyAndInitialState); - this.instance.Core.documentViewer.on('keyUp', $event => { + this.documentViewer.on('keyUp', $event => { // arrows and full-screen if ($event.target?.tagName?.toLowerCase() !== 'input') { if ($event.key.startsWith('Arrow') || $event.key === 'f') { @@ -264,18 +294,20 @@ export class PdfViewerComponent implements OnInit, OnChanges { } } - if (this._allowedKeyboardShortcuts.indexOf($event.key) < 0) { + if (ALLOWED_KEYBOARD_SHORTCUTS.indexOf($event.key) < 0) { $event.preventDefault(); $event.stopPropagation(); } }); - this.instance.Core.documentViewer.on('textSelected', (quads, selectedText) => { + this.documentViewer.on('textSelected', (quads, selectedText) => { this._selectedText = selectedText; - if (selectedText.length > 2 && this.canPerformActions) { - this.instance.UI.enableElements(['add-dictionary', 'add-false-positive']); + const textActions = [dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE]; + + if (selectedText.length > 2 && this.canPerformActions && !this.utils.isCurrentPageExcluded) { + this.instance.UI.enableElements(textActions); } else { - this.instance.UI.disableElements(['add-dictionary', 'add-false-positive']); + this.instance.UI.disableElements(textActions); } }); @@ -286,7 +318,7 @@ export class PdfViewerComponent implements OnInit, OnChanges { inputElement.value = ''; }, 0); if (!event.detail.isVisible) { - this.instance.Core.documentViewer.clearSearchResults(); + this.documentViewer.clearSearchResults(); } } }); @@ -295,15 +327,15 @@ export class PdfViewerComponent implements OnInit, OnChanges { } private _setSelectionMode(): void { - const textTool = ( this.instance.Core.Tools.TextTool) as TextTool; + const textTool = this.instance.Core.Tools.TextTool as unknown as TextTool; textTool.SELECTION_MODE = this._configService.values.SELECTION_MODE; } - private _toggleRectangleAnnotationAction(readonly: boolean) { + private _toggleRectangleAnnotationAction(readonly = false) { if (!readonly) { - this.instance.UI.enableElements(['add-rectangle']); + this.instance.UI.enableElements([dataElements.ADD_RECTANGLE]); } else { - this.instance.UI.disableElements(['add-rectangle']); + this.instance.UI.disableElements([dataElements.ADD_RECTANGLE]); } } @@ -331,53 +363,64 @@ export class PdfViewerComponent implements OnInit, OnChanges { 'annotationGroupButton', ]); - this.instance.UI.setHeaderItems(header => { - const originalHeaderItems = header.getItems(); - originalHeaderItems.splice(8, 0, { + const headerItems = [ + { type: 'divider', - dataElement: 'rectangleToolDivider', - }); - originalHeaderItems.splice(9, 0, { + dataElement: dataElements.RECTANGLE_TOOL_DIVIDER, + }, + { type: 'toolGroupButton', toolGroup: 'rectangleTools', - dataElement: 'shapeToolGroupButton', + dataElement: dataElements.SHAPE_TOOL_GROUP_BUTTON, img: this._convertPath('/assets/icons/general/rectangle.svg'), title: 'annotation.rectangle', - }); + }, + ]; + + this.instance.UI.setHeaderItems(header => { + const originalHeaderItems = header.getItems(); + originalHeaderItems.splice(8, 0, ...headerItems); + if (this._userPreferenceService.areDevFeaturesEnabled) { - originalHeaderItems.splice(11, 0, { - type: 'actionButton', - element: 'compare', - dataElement: 'compareButton', - img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'), - title: 'Compare', - onClick: () => { - this.compareFileInput.nativeElement.click(); + const devHeaderItems = [ + { + type: 'actionButton', + element: 'compare', + dataElement: dataElements.COMPARE_BUTTON, + img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'), + title: 'Compare', + onClick: () => { + this.compareFileInput.nativeElement.click(); + }, }, - }); - originalHeaderItems.splice(11, 0, { - type: 'actionButton', - element: 'closeCompare', - dataElement: 'closeCompareButton', - img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'), - title: 'Leave Compare Mode', - onClick: () => { - this.closeCompareMode(); + { + type: 'actionButton', + element: 'closeCompare', + dataElement: dataElements.CLOSE_COMPARE_BUTTON, + img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'), + title: 'Leave Compare Mode', + onClick: async () => { + await this.closeCompareMode(); + }, }, - }); - originalHeaderItems.splice(13, 0, { - type: 'divider', - dataElement: 'compareToolDivider', - }); + { + type: 'divider', + dataElement: dataElements.COMPARE_TOOL_DIVIDER, + }, + ]; + + originalHeaderItems.splice(11, 0, ...devHeaderItems); } }); - this.instance.UI.disableElements(['closeCompareButton']); + this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]); - this.instance.Core.documentViewer.getTool('AnnotationCreateRectangle').setStyles(() => ({ + const dossierTemplateId = this._dossier.dossierTemplateId; + + this.documentViewer.getTool('AnnotationCreateRectangle').setStyles(() => ({ StrokeThickness: 2, - StrokeColor: this._annotationDrawService.getColor(this.instance, 'manual'), - FillColor: this._annotationDrawService.getColor(this.instance, 'manual'), + StrokeColor: this._annotationDrawService.getColor(this.instance, dossierTemplateId, 'manual'), + FillColor: this._annotationDrawService.getColor(this.instance, dossierTemplateId, 'manual'), Opacity: 0.6, })); } @@ -410,11 +453,11 @@ export class PdfViewerComponent implements OnInit, OnChanges { onClick: () => { this._ngZone.run(() => { if (allAreVisible) { - this.instance.Core.annotationManager.hideAnnotations(viewerAnnotations); + this.annotationManager.hideAnnotations(viewerAnnotations); } else { - this.instance.Core.annotationManager.showAnnotations(viewerAnnotations); + this.annotationManager.showAnnotations(viewerAnnotations); } - this.instance.Core.annotationManager.deselectAllAnnotations(); + this.annotationManager.deselectAllAnnotations(); this._annotationActionsService.updateHiddenAnnotation(this.annotations, viewerAnnotations, allAreVisible); }); }, @@ -423,43 +466,53 @@ export class PdfViewerComponent implements OnInit, OnChanges { } this.instance.UI.annotationPopup.add( - this._annotationActionsService.getViewerAvailableActions(annotationWrappers, this.annotationsChanged), + this._annotationActionsService.getViewerAvailableActions( + this.instance, + this._dossier, + annotationWrappers, + this.annotationsChanged, + ), ); } private _configureRectangleAnnotationPopup() { - this.instance.UI.annotationPopup.add({ - type: 'actionButton', - dataElement: 'add-rectangle', - img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), - title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')), - onClick: () => { - const selectedAnnotations = this.instance.Core.annotationManager.getSelectedAnnotations(); - const activeAnnotation = selectedAnnotations[0]; - const activePage = selectedAnnotations[0].getPageNumber(); - const quad = this._annotationDrawService.annotationToQuads(activeAnnotation, this.instance); - const quadsObject = {}; - quadsObject[activePage] = [quad]; - const mre = this._getManualRedactionEntry(quadsObject, 'Rectangle'); - // cleanup selection and button state - this.utils.deselectAllAnnotations(); - this.instance.UI.disableElements(['shapeToolGroupButton', 'rectangleToolDivider']); - this.instance.UI.enableElements(['shapeToolGroupButton', 'rectangleToolDivider']); - // dispatch event - this.manualAnnotationRequested.emit( - new ManualRedactionEntryWrapper([quad], mre, 'REDACTION', 'RECTANGLE', activeAnnotation.Id), - ); + this.instance.UI.annotationPopup.add([ + { + type: 'actionButton', + dataElement: dataElements.ADD_RECTANGLE, + img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), + title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION', this._dossier)), + onClick: () => this._addRectangleManualRedaction(), }, - }); + ]); + } + + private _addRectangleManualRedaction() { + const activeAnnotation = this.annotationManager.getSelectedAnnotations()[0]; + const activePage = activeAnnotation.getPageNumber(); + const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation, this.instance)]; + const manualRedaction = this._getManualRedaction({ [activePage]: quads }, 'Rectangle'); + this._cleanUpSelectionAndButtonState(); + + this.manualAnnotationRequested.emit( + new ManualRedactionEntryWrapper(quads, manualRedaction, 'REDACTION', 'RECTANGLE', activeAnnotation.Id), + ); + } + + private _cleanUpSelectionAndButtonState() { + const rectangleElements = [dataElements.SHAPE_TOOL_GROUP_BUTTON, dataElements.RECTANGLE_TOOL_DIVIDER]; + this.utils.deselectAllAnnotations(); + this.instance.UI.disableElements(rectangleElements); + this.instance.UI.enableElements(rectangleElements); } private _configureTextPopup() { - this.instance.UI.textPopup.add({ + const searchButton = { type: 'actionButton', img: this._convertPath('/assets/icons/general/pdftron-action-search.svg'), title: this._translateService.instant('pdf-viewer.text-popup.actions.search'), onClick: () => { - const text = this.instance.Core.documentViewer.getSelectedText(); + const text = this.documentViewer.getSelectedText(); const searchOptions = { caseSensitive: true, // match case wholeWord: true, // match whole words only @@ -469,117 +522,128 @@ export class PdfViewerComponent implements OnInit, OnChanges { ambientString: true, // return ambient string as part of the result }; this.instance.UI.openElements(['searchPanel']); - setTimeout(() => { - this.instance.UI.searchTextFull(text, searchOptions); - }, 250); + setTimeout(() => this.instance.UI.searchTextFull(text, searchOptions), 250); }, - }); + }; + + this.instance.UI.textPopup.add([searchButton]); // Adding directly to the false-positive dict is only available in dev-mode if (this._userPreferenceService.areDevFeaturesEnabled) { - this.instance.UI.textPopup.add({ - type: 'actionButton', - dataElement: 'add-false-positive', - img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'), - title: this._translateService.instant(this._manualAnnotationService.getTitle('FALSE_POSITIVE')), - onClick: () => { - const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads(); - const text = this.instance.Core.documentViewer.getSelectedText(); - const mre = this._getManualRedactionEntry(selectedQuads, text, true); - this.manualAnnotationRequested.emit( - new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'FALSE_POSITIVE'), - ); + this.instance.UI.textPopup.add([ + { + type: 'actionButton', + dataElement: 'add-false-positive', + img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'), + title: this._translateService.instant( + this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE, this._dossier), + ), + onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE), }, - }); + ]); } - this.instance.UI.textPopup.add({ - type: 'actionButton', - dataElement: 'add-dictionary', - img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'), - title: this._translateService.instant(this._manualAnnotationService.getTitle('DICTIONARY')), - onClick: () => { - const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads(); - const text = this.instance.Core.documentViewer.getSelectedText(); - const mre = this._getManualRedactionEntry(selectedQuads, text, true); - this.manualAnnotationRequested.emit( - new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'DICTIONARY'), - ); + this.instance.UI.textPopup.add([ + { + type: 'actionButton', + dataElement: dataElements.ADD_REDACTION, + img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), + title: this._translateService.instant( + this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.REDACTION, this._dossier), + ), + onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION), }, - }); + { + type: 'actionButton', + dataElement: dataElements.ADD_DICTIONARY, + img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'), + title: this._translateService.instant( + this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.DICTIONARY, this._dossier), + ), + onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY), + }, + ]); - this.instance.UI.textPopup.add({ - type: 'actionButton', - dataElement: 'add-redaction', - img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), - title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')), - onClick: () => { - const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads(); - const text = this.instance.Core.documentViewer.getSelectedText(); - const mre = this._getManualRedactionEntry(selectedQuads, text, true); - this.manualAnnotationRequested.emit( - new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'REDACTION'), - ); - }, - }); this._handleCustomActions(); } + private _addManualRedactionOfType(type: ManualRedactionEntryType) { + const selectedQuads = this.documentViewer.getSelectedTextQuads(); + const text = this.documentViewer.getSelectedText(); + const manualRedaction = this._getManualRedaction(selectedQuads, text, true); + this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type)); + } + private _handleCustomActions() { this.instance.UI.setToolMode('AnnotationEdit'); + const { ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON } = dataElements; + const elements = [ + ADD_REDACTION, + ADD_RECTANGLE, + 'add-false-positive', + SHAPE_TOOL_GROUP_BUTTON, + 'rectangleToolDivider', + ANNOTATION_POPUP, + ]; + if (this.canPerformActions && !this.utils.isCurrentPageExcluded) { this.instance.UI.enableTools(['AnnotationCreateRectangle']); - this.instance.UI.enableElements([ - 'add-redaction', - 'add-rectangle', - 'add-false-positive', - 'shapeToolGroupButton', - 'rectangleToolDivider', - 'annotationPopup', - ]); + this.instance.UI.enableElements(elements); + if (this._selectedText.length > 2) { - this.instance.UI.enableElements(['add-dictionary', 'add-false-positive']); + this.instance.UI.enableElements([dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE]); } + + return; + } + + let elementsToDisable = [...elements, ADD_RECTANGLE]; + + if (this.utils.isCurrentPageExcluded) { + const allowedActionsWhenPageExcluded: string[] = [ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON]; + elementsToDisable = elementsToDisable.filter(element => !allowedActionsWhenPageExcluded.includes(element)); } else { this.instance.UI.disableTools(['AnnotationCreateRectangle']); - this.instance.UI.disableElements([ - 'add-redaction', - 'add-dictionary', - 'add-false-positive', - 'add-rectangle', - 'shapeToolGroupButton', - 'rectangleToolDivider', - 'annotationPopup', - ]); } + + this.instance.UI.disableElements(elementsToDisable); } - private _getManualRedactionEntry(quads: any, text: string, convertQuads: boolean = false): IManualRedactionEntry { + private _getManualRedaction( + quads: Readonly>, + text: string, + convertQuads = false, + ): IManualRedactionEntry { const entry: IManualRedactionEntry = { positions: [] }; + for (const key of Object.keys(quads)) { for (const quad of quads[key]) { const page = parseInt(key, 10); - entry.positions.push(this.utils.toPosition(page, convertQuads ? this.utils.translateQuads(page, quad) : quad)); + const pageHeight = this.documentViewer.getPageHeight(page); + entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuads(page, quad) : quad)); } } + entry.value = text; return entry; } private _loadDocument() { - if (this.fileData) { - this.instance.UI.loadDocument(this.fileData, { - filename: this.file ? this.file.filename : 'document.pdf', - }); + if (!this.fileData) { + return; } + + this.instance.UI.loadDocument(this.fileData, { + filename: this.file ? this.file.filename : 'document.pdf', + }); } - private _documentLoaded(): void { + private _setReadyAndInitialState(): void { this._ngZone.run(() => { this.utils.ready = true; this._firstPageChange = true; this.viewerReady.emit(this.instance); - this.setInitialViewerState(); + this._setInitialDisplayMode(); }); } } diff --git a/apps/red-ui/src/app/modules/dossier/components/team-members-manager/team-members-manager.component.html b/apps/red-ui/src/app/modules/dossier/components/team-members-manager/team-members-manager.component.html index c2fa279d7..e48a1fc4a 100644 --- a/apps/red-ui/src/app/modules/dossier/components/team-members-manager/team-members-manager.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/team-members-manager/team-members-manager.component.html @@ -1,4 +1,4 @@ -
+
{{ 'assign-dossier-owner.dialog.single-user' | translate }} @@ -28,15 +28,19 @@ [canAdd]="false" [canRemove]="true" [largeSpacing]="true" - [memberIds]="selectedReviewersList" + [memberIds]="selectedReviewers$ | async" [perLine]="13" [unremovableMembers]="[selectedOwnerId]" > -

+    

 
     ();
+    @Output() readonly updateDossier = new EventEmitter();
 
     readonly ownersSelectOptions = this.userService.managerUsers.map(m => m.id);
-    selectedReviewersList: string[] = [];
     membersSelectOptions: string[] = [];
-    changed = false;
+    readonly selectedReviewers$ = new BehaviorSubject([]);
 
     constructor(
         readonly userService: UserService,
-        private readonly _toaster: Toaster,
         private readonly _formBuilder: FormBuilder,
         private readonly _dossiersService: DossiersService,
-    ) {}
+    ) {
+        super();
+    }
 
     get selectedOwnerId(): string {
-        return this.teamForm.get('owner').value;
+        return this.form.get('owner').value;
     }
 
     get selectedApproversList(): string[] {
-        return this.teamForm.get('approvers').value;
+        return this.form.get('approvers').value;
     }
 
     get selectedMembersList(): string[] {
-        return this.teamForm.get('members').value;
+        return this.form.get('members').value;
     }
 
     get valid(): boolean {
-        return this.teamForm.valid;
+        return this.form.valid;
+    }
+
+    get disabled() {
+        return !this.userService.currentUser.isManager;
+    }
+
+    get changed() {
+        if (this.dossier.ownerId !== this.selectedOwnerId) {
+            return true;
+        }
+
+        const initialMembers = [...this.dossier.memberIds].sort();
+        const currentMembers = this.selectedMembersList.sort();
+
+        const initialApprovers = [...this.dossier.approverIds].sort();
+        const currentApprovers = this.selectedApproversList.sort();
+        return this._compareLists(initialMembers, currentMembers) || this._compareLists(initialApprovers, currentApprovers);
     }
 
     isOwner(userId: string): boolean {
         return userId === this.selectedOwnerId;
     }
 
-    async saveMembers() {
+    async save() {
         const dossier = {
             ...this.dossier,
             memberIds: this.selectedMembersList,
@@ -59,8 +79,7 @@ export class TeamMembersManagerComponent implements OnInit {
 
         const result = await this._dossiersService.createOrUpdate(dossier).toPromise();
         if (result) {
-            this.save.emit(result);
-            this._updateChanged();
+            this.updateDossier.emit();
         }
     }
 
@@ -112,39 +131,25 @@ export class TeamMembersManagerComponent implements OnInit {
         this._loadData();
     }
 
-    setMembersSelectOptions(): void {
+    setMembersSelectOptions(value = this.searchQuery): void {
         this.membersSelectOptions = this.userService.eligibleUsers
-            .filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(this.searchQuery.toLowerCase()))
+            .filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(value.toLowerCase()))
             .filter(user => this.selectedOwnerId !== user.id)
             .map(user => user.id);
     }
 
-    private _updateChanged() {
-        if (this.dossier.ownerId !== this.selectedOwnerId) {
-            this.changed = true;
-            return;
-        }
-
-        const initialMembers = [...this.dossier.memberIds].sort();
-        const currentMembers = this.selectedMembersList.sort();
-
-        const initialApprovers = [...this.dossier.approverIds].sort();
-        const currentApprovers = this.selectedApproversList.sort();
-
-        this.changed = this._compareLists(initialMembers, currentMembers) || this._compareLists(initialApprovers, currentApprovers);
-    }
-
     private _setSelectedReviewersList() {
-        this.selectedReviewersList = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
+        const selectedReviewers = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
+        this.selectedReviewers$.next(selectedReviewers);
     }
 
     private _loadData() {
-        this.teamForm = this._formBuilder.group({
+        this.form = this._formBuilder.group({
             owner: [this.dossier?.ownerId, Validators.required],
             approvers: [[...this.dossier?.approverIds]],
             members: [[...this.dossier?.memberIds]],
         });
-        this.teamForm.get('owner').valueChanges.subscribe(owner => {
+        this.addSubscription = this.form.get('owner').valueChanges.subscribe(owner => {
             if (!this.isApprover(owner)) {
                 this.toggleApprover(owner);
             }
@@ -157,7 +162,6 @@ export class TeamMembersManagerComponent implements OnInit {
     private _updateLists() {
         this._setSelectedReviewersList();
         this.setMembersSelectOptions();
-        this._updateChanged();
     }
 
     private _compareLists(l1: string[], l2: string[]) {
diff --git a/apps/red-ui/src/app/modules/dossier/components/type-annotation-icon/type-annotation-icon.component.ts b/apps/red-ui/src/app/modules/dossier/components/type-annotation-icon/type-annotation-icon.component.ts
index d018c12b1..7476a9027 100644
--- a/apps/red-ui/src/app/modules/dossier/components/type-annotation-icon/type-annotation-icon.component.ts
+++ b/apps/red-ui/src/app/modules/dossier/components/type-annotation-icon/type-annotation-icon.component.ts
@@ -1,6 +1,8 @@
 import { Component, Input, OnChanges } from '@angular/core';
 import { AnnotationWrapper } from '@models/file/annotation.wrapper';
 import { AppStateService } from '@state/app-state.service';
+import { DossiersService } from '@services/entity-services/dossiers.service';
+import { File } from '@red/domain';
 
 @Component({
     selector: 'redaction-type-annotation-icon',
@@ -9,19 +11,24 @@ import { AppStateService } from '@state/app-state.service';
 })
 export class TypeAnnotationIconComponent implements OnChanges {
     @Input() annotation: AnnotationWrapper;
+    @Input() file: File;
 
     label: string;
     color: string;
     type: 'square' | 'rhombus' | 'circle' | 'hexagon';
 
-    constructor(private _appStateService: AppStateService) {}
+    constructor(private _appStateService: AppStateService, private readonly _dossiersService: DossiersService) {}
+
+    private get _dossierTemplateId(): string {
+        return this._dossiersService.find(this.file.dossierId).dossierTemplateId;
+    }
 
     ngOnChanges(): void {
         if (this.annotation) {
             if (this.annotation.isSuperTypeBasedColor) {
-                this.color = this._appStateService.getDictionaryColor(this.annotation.superType);
+                this.color = this._appStateService.getDictionaryColor(this.annotation.superType, this._dossierTemplateId);
             } else {
-                this.color = this._appStateService.getDictionaryColor(this.annotation.type);
+                this.color = this._appStateService.getDictionaryColor(this.annotation.type, this._dossierTemplateId);
             }
             this.type =
                 this.annotation.isSuggestion || this.annotation.isDeclinedSuggestion
@@ -36,8 +43,6 @@ export class TypeAnnotationIconComponent implements OnChanges {
                     ? 'S'
                     : this.annotation.isSkipped
                     ? 'S'
-                    : this.annotation.isReadyForAnalysis
-                    ? 'A'
                     : this.annotation.type[0].toUpperCase();
         }
     }
diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.html
index 88a3666fd..bba61223a 100644
--- a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.html
+++ b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.html
@@ -41,9 +41,17 @@
                 >
             
- - {{ 'add-dossier-dialog.form.watermark' | translate }} - +
+ + {{ 'add-dossier-dialog.form.watermark' | translate }} + +
+ +
+ + {{ 'add-dossier-dialog.form.watermark-preview' | translate }} + +
diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.scss b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.scss index 6b5cbee1d..077d4cb22 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.scss +++ b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.scss @@ -2,6 +2,10 @@ margin-top: 24px; } +.watermark-preview { + margin-top: 8px; +} + .due-date { margin-top: 8px; min-height: 34px; diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts index 65acb031a..753241a6d 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/add-dossier-dialog/add-dossier-dialog.component.ts @@ -42,6 +42,7 @@ export class AddDossierDialogComponent { description: [null], dueDate: [null], watermarkEnabled: [true], + watermarkPreviewEnabled: [false], }, { validators: control => @@ -123,6 +124,7 @@ export class AddDossierDialogComponent { downloadFileTypes: this.dossierForm.get('downloadFileTypes').value, reportTemplateIds: this.dossierForm.get('reportTemplateIds').value, watermarkEnabled: this.dossierForm.get('watermarkEnabled').value, + watermarkPreviewEnabled: this.dossierForm.get('watermarkPreviewEnabled').value, }; } } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.html index 8c7d61eb1..1e851ab46 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.html @@ -7,13 +7,13 @@ class="dialog-header heading-l" >
- +
{{ 'assign-owner.dialog.label' | translate: { type: data.mode } }} - - + + {{ userId | name: { defaultValue: 'initials-avatar.unassigned' | translate } }} @@ -22,7 +22,7 @@
- diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts index 277be4ad2..b8029ff73 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component.ts @@ -12,8 +12,7 @@ import { PermissionsService } from '@services/permissions.service'; class DialogData { mode: 'approver' | 'reviewer'; - dossier?: Dossier; - files?: File[]; + files: File[]; ignoreChanged?: boolean; withCurrentUserAsDefault?: boolean; } @@ -23,8 +22,8 @@ class DialogData { styleUrls: ['./assign-reviewer-approver-dialog.component.scss'], }) export class AssignReviewerApproverDialogComponent { - usersForm: FormGroup; - searchForm: FormGroup; + form: FormGroup; + dossier: Dossier; constructor( readonly userService: UserService, @@ -37,18 +36,19 @@ export class AssignReviewerApproverDialogComponent { private readonly _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) readonly data: DialogData, ) { + this.dossier = this._dossiersService.find(this.data.files[0].dossierId); this._loadData(); } - get selectedSingleUser(): string { - return this.usersForm.get('singleUser').value; + get selectedUser(): string { + return this.form.get('user').value; } - get singleUsersSelectOptions() { + get userOptions() { const unassignUser = this._canUnassignFiles ? [undefined] : []; return this.data.mode === 'approver' - ? [...this._dossiersService.activeDossier.approverIds, ...unassignUser] - : [...this._dossiersService.activeDossier.memberIds, ...unassignUser]; + ? [...this.dossier.approverIds, ...unassignUser] + : [...this.dossier.memberIds, ...unassignUser]; } get changed(): boolean { @@ -57,7 +57,7 @@ export class AssignReviewerApproverDialogComponent { } for (const file of this.data.files) { - if (file.currentReviewer !== this.selectedSingleUser) { + if (file.currentReviewer !== this.selectedUser) { return true; } } @@ -70,27 +70,25 @@ export class AssignReviewerApproverDialogComponent { } isOwner(userId: string): boolean { - return userId === this.selectedSingleUser; + return userId === this.selectedUser; } async save() { try { - const selectedUser = this.selectedSingleUser; - if (this.data.mode === 'reviewer') { await this._filesService .setReviewerFor( this.data.files.map(f => f.fileId), - this._dossiersService.activeDossierId, - selectedUser, + this.dossier.id, + this.selectedUser, ) .toPromise(); } else { await this._filesService .setUnderApprovalFor( this.data.files.map(f => f.fileId), - this._dossiersService.activeDossierId, - selectedUser, + this.dossier.id, + this.selectedUser, ) .toPromise(); } @@ -112,17 +110,16 @@ export class AssignReviewerApproverDialogComponent { uniqueReviewers.add(file.currentReviewer); } } + let user: string = uniqueReviewers.size === 1 ? uniqueReviewers.values().next().value : this.userService.currentUser.id; + user = this.userOptions.indexOf(user) >= 0 ? user : this.userOptions[0]; - let singleUser: string = uniqueReviewers.size === 1 ? uniqueReviewers.values().next().value : this.userService.currentUser.id; - singleUser = this.singleUsersSelectOptions.indexOf(singleUser) >= 0 ? singleUser : this.singleUsersSelectOptions[0]; - - if (this.data.withCurrentUserAsDefault && this.singleUsersSelectOptions.includes(this.userService.currentUser.id)) { - singleUser = this.userService.currentUser.id; + if (this.data.withCurrentUserAsDefault && this.userOptions.includes(this.userService.currentUser.id)) { + user = this.userService.currentUser.id; } - this.usersForm = this._formBuilder.group({ + this.form = this._formBuilder.group({ // Allow a null reviewer if a previous reviewer exists (= it's not the first assignment) & current user is allowed to unassign - singleUser: [singleUser, this._canUnassignFiles && !singleUser ? Validators.required : null], + user: [user, this._canUnassignFiles && !user ? Validators.required : null], }); } } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component.ts index e9fc4bce4..08cd4eda4 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component.ts @@ -1,12 +1,11 @@ import { Component, Inject, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { AppStateService } from '@state/app-state.service'; import { PermissionsService } from '@services/permissions.service'; import { DossiersService } from '@services/entity-services/dossiers.service'; import { JustificationsService } from '@services/entity-services/justifications.service'; +import { Dossier } from '@red/domain'; export interface LegalBasisOption { label?: string; @@ -15,9 +14,7 @@ export interface LegalBasisOption { } @Component({ - selector: 'redaction-change-legal-basis-dialog', templateUrl: './change-legal-basis-dialog.component.html', - styleUrls: ['./change-legal-basis-dialog.component.scss'], }) export class ChangeLegalBasisDialogComponent implements OnInit { legalBasisForm: FormGroup; @@ -25,34 +22,30 @@ export class ChangeLegalBasisDialogComponent implements OnInit { legalOptions: LegalBasisOption[] = []; constructor( - private readonly _translateService: TranslateService, private readonly _justificationsService: JustificationsService, - private readonly _appStateService: AppStateService, private readonly _dossiersService: DossiersService, private readonly _permissionsService: PermissionsService, private readonly _formBuilder: FormBuilder, - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public annotations: AnnotationWrapper[], + readonly dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) private readonly _data: { annotations: AnnotationWrapper[]; dossier: Dossier }, ) {} get changed(): boolean { - return this.legalBasisForm.get('reason').value.legalBasis !== this.annotations[0].legalBasis; + return this.legalBasisForm.get('reason').value.legalBasis !== this._data.annotations[0].legalBasis; } async ngOnInit() { - this.isDocumentAdmin = this._permissionsService.isApprover(); + this.isDocumentAdmin = this._permissionsService.isApprover(this._data.dossier); this.legalBasisForm = this._formBuilder.group({ reason: [null, Validators.required], comment: this.isDocumentAdmin ? [null] : [null, Validators.required], }); - const data = await this._justificationsService - .getForDossierTemplate(this._dossiersService.activeDossier.dossierTemplateId) - .toPromise(); + const data = await this._justificationsService.getForDossierTemplate(this._data.dossier.dossierTemplateId).toPromise(); this.legalOptions = data - .map(lbm => ({ + .map(lbm => ({ legalBasis: lbm.reason, description: lbm.description, label: lbm.name, @@ -60,7 +53,7 @@ export class ChangeLegalBasisDialogComponent implements OnInit { .sort((a, b) => a.label.localeCompare(b.label)); this.legalBasisForm.patchValue({ - reason: this.legalOptions.find(option => option.legalBasis === this.annotations[0].legalBasis), + reason: this.legalOptions.find(option => option.legalBasis === this._data.annotations[0].legalBasis), }); } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component.html index 9d823f2dc..e79710824 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component.html @@ -65,7 +65,7 @@ (change)="uploadImage($event, attr)" [id]="attr.id" [name]="attr.id" - accept="image/*" + accept="image/jpg, image/jpeg, image/png" class="file-upload-input" hidden type="file" diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component.ts index e46cb671f..e190bbe6d 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component.ts @@ -62,7 +62,6 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa } async save() { - this._loadingService.start(); const dossierAttributeList = this.attributes.map(attr => ({ dossierAttributeConfigId: attr.id, value: this.currentAttrValue(attr), @@ -70,7 +69,6 @@ export class EditDossierAttributesComponent implements EditDossierSectionInterfa await this._dossierAttributesService.setAttributes(this.dossier, dossierAttributeList).toPromise(); await this._loadAttributes(); this.updateDossier.emit(); - this._loadingService.stop(); } fileInputClick(attr: DossierAttributeWithValue) { diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/deleted-documents/edit-dossier-deleted-documents.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/deleted-documents/edit-dossier-deleted-documents.component.ts index 19ec288a5..1ef89bd72 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/deleted-documents/edit-dossier-deleted-documents.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/deleted-documents/edit-dossier-deleted-documents.component.ts @@ -115,7 +115,7 @@ export class EditDossierDeletedDocumentsComponent extends ListingComponent f.fileId); await this._fileManagementService.restore(fileIds, this.dossier.id).toPromise(); this._removeFromList(fileIds); - await this._appStateService.reloadActiveDossierFiles(); + await this._appStateService.reloadDossierFiles(files[0].dossierId); this.updateDossier.emit(); } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.html index 06dee2be4..6cd5f9d5f 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.html @@ -1,10 +1,10 @@
-
{{ dossier.type?.label }}
+
{{ dossierDictionary?.label }}
- {{ 'edit-dossier-dialog.dictionary.entries' | translate: { length: (dossier.type?.entries || []).length } }} + {{ 'edit-dossier-dialog.dictionary.entries' | translate: { length: (dossierDictionary?.entries || []).length } }}
@@ -19,13 +19,13 @@ [placeholder]="'edit-dossier-dialog.dictionary.display-name.placeholder' | translate" [saveTooltip]="'edit-dossier-dialog.dictionary.display-name.save' | translate" [showPreview]="false" - [value]="dossier.type?.label" + [value]="dossierDictionary?.label" >
diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.ts index 956ab598e..e59bca316 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component.ts @@ -18,6 +18,7 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa @Output() readonly updateDossier = new EventEmitter(); canEdit = false; readonly circleButtonTypes = CircleButtonTypes; + dossierDictionary: IDictionary; @ViewChild(DictionaryManagerComponent, { static: false }) private readonly _dictionaryManager: DictionaryManagerComponent; @@ -40,16 +41,16 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa async ngOnInit() { this._loadingService.start(); this.canEdit = this._permissionsService.isDossierMember(this.dossier); - await this._dossiersService.updateDossierDictionary(this.dossier.dossierTemplateId, this.dossier.id); + await this._updateDossierDictionary(); this._loadingService.stop(); } async updateDisplayName(label: string) { - const dictionary: IDictionary = { ...this.dossier.type, label }; + const dictionary: IDictionary = { ...this.dossierDictionary, label }; await this._dictionaryService .updateDictionary(dictionary, this.dossier.dossierTemplateId, 'dossier_redaction', this.dossier.id) .toPromise(); - await this._dossiersService.updateDossierDictionary(this.dossier.dossierTemplateId, this.dossier.id); + await this._updateDossierDictionary(); this.updateDossier.emit(); } @@ -64,11 +65,16 @@ export class EditDossierDictionaryComponent implements EditDossierSectionInterfa false, ) .toPromise(); - await this._dossiersService.updateDossierDictionary(this.dossier.dossierTemplateId, this.dossier.id); + await this._updateDossierDictionary(); this.updateDossier.emit(); } revert() { this._dictionaryManager.revert(); } + + private async _updateDossierDictionary() { + const { dossierId, dossierTemplateId } = this.dossier; + this.dossierDictionary = await this._dictionaryService.getForType(dossierTemplateId, 'dossier_redaction', dossierId).toPromise(); + } } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts index dc9931e08..9766ac1e5 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts @@ -58,7 +58,7 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS } get disabled() { - return this.dossierForm?.invalid; + return this.dossierForm?.invalid || !this.dossierForm?.value?.downloadFileTypes?.length; } reportTemplateValueMapper = (reportTemplate: IReportTemplate) => reportTemplate.templateId; diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.html index 915d29570..7e5df0c58 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.html @@ -1,4 +1,4 @@ -
+
{{ 'edit-dossier-dialog.header' | translate: { dossierName: dossier.dossierName } }}
@@ -13,6 +13,7 @@ class="item" >
+
@@ -37,11 +38,11 @@ [dossier]="dossier" > - + > {{ 'edit-dossier-dialog.actions.save' | translate }} + +
diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts index daef546a9..75851073a 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/edit-dossier-dialog.component.ts @@ -1,18 +1,20 @@ import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core'; -import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Dossier } from '@red/domain'; import { EditDossierGeneralInfoComponent } from './general-info/edit-dossier-general-info.component'; import { EditDossierDownloadPackageComponent } from './download-package/edit-dossier-download-package.component'; import { EditDossierSectionInterface } from './edit-dossier-section.interface'; -import { Toaster } from '@iqser/common-ui'; +import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { EditDossierDictionaryComponent } from './dictionary/edit-dossier-dictionary.component'; -import { EditDossierTeamMembersComponent } from './team-members/edit-dossier-team-members.component'; import { EditDossierAttributesComponent } from './attributes/edit-dossier-attributes.component'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { EditDossierDeletedDocumentsComponent } from './deleted-documents/edit-dossier-deleted-documents.component'; import { AppStateService } from '@state/app-state.service'; import { DossiersService } from '@services/entity-services/dossiers.service'; +import { TeamMembersManagerComponent } from '../../components/team-members-manager/team-members-manager.component'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; type Section = 'dossierInfo' | 'downloadPackage' | 'dossierDictionary' | 'members' | 'dossierAttributes' | 'deletedDocuments'; @@ -22,24 +24,28 @@ type Section = 'dossierInfo' | 'downloadPackage' | 'dossierDictionary' | 'member }) export class EditDossierDialogComponent { readonly navItems: { key: Section; title?: string; sideNavTitle?: string }[]; + readonly iconButtonTypes = IconButtonTypes; activeNav: Section; - dossier: Dossier; + readonly dossier$: Observable; @ViewChild(EditDossierGeneralInfoComponent) generalInfoComponent: EditDossierGeneralInfoComponent; @ViewChild(EditDossierDownloadPackageComponent) downloadPackageComponent: EditDossierDownloadPackageComponent; @ViewChild(EditDossierDictionaryComponent) dictionaryComponent: EditDossierDictionaryComponent; - @ViewChild(EditDossierTeamMembersComponent) membersComponent: EditDossierTeamMembersComponent; + @ViewChild(TeamMembersManagerComponent) membersComponent: TeamMembersManagerComponent; @ViewChild(EditDossierAttributesComponent) attributesComponent: EditDossierAttributesComponent; @ViewChild(EditDossierDeletedDocumentsComponent) deletedDocumentsComponent: EditDossierDeletedDocumentsComponent; + private _dossierName: string; constructor( private readonly _toaster: Toaster, private readonly _appStateService: AppStateService, private readonly _dossiersService: DossiersService, private readonly _changeRef: ChangeDetectorRef, + private readonly _dialogRef: MatDialogRef, + private readonly _loadingService: LoadingService, @Inject(MAT_DIALOG_DATA) private readonly _data: { - dossier: Dossier; + dossierId: string; afterSave: Function; section?: Section; }, @@ -75,7 +81,9 @@ export class EditDossierDialogComponent { }, ]; - this.dossier = _data.dossier; + this.dossier$ = this._dossiersService + .getEntityChanged$(_data.dossierId) + .pipe(tap(dossier => (this._dossierName = dossier.dossierName))); this.activeNav = _data.section || 'dossierInfo'; } @@ -107,9 +115,7 @@ export class EditDossierDialogComponent { } updatedDossier() { - this._toaster.success(_('edit-dossier-dialog.change-successful'), { params: { dossierName: this.dossier.dossierName } }); - this.dossier = this._dossiersService.find(this.dossier.id); - this._changeRef.detectChanges(); + this._toaster.success(_('edit-dossier-dialog.change-successful'), { params: { dossierName: this._dossierName } }); this.afterSave(); } @@ -119,8 +125,14 @@ export class EditDossierDialogComponent { } } - async save() { + async save(closeAfterSave: boolean = false) { + this._loadingService.start(); await this.activeComponent.save(); + this._loadingService.stop(); + + if (closeAfterSave) { + this._dialogRef.close(); + } } revert() { diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.html index e42b607f1..f27c24b26 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.html @@ -37,9 +37,17 @@ >
- - {{ 'edit-dossier-dialog.general-info.form.watermark' | translate }} - +
+ + {{ 'edit-dossier-dialog.general-info.form.watermark' | translate }} + +
+ +
+ + {{ 'edit-dossier-dialog.general-info.form.watermark-preview' | translate }} + +
diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.scss b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.scss index aae48e38a..695782375 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.scss +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.scss @@ -2,6 +2,10 @@ margin-top: 24px; } +.watermark-preview { + margin-top: 8px; +} + .due-date { margin-top: 8px; min-height: 34px; diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts index fb680d5fe..df28a625e 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts @@ -12,6 +12,7 @@ import { ConfirmationDialogInput, IconButtonTypes, TitleColors, Toaster } from ' import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { DossiersService } from '@services/entity-services/dossiers.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; +import { DossierStatsService } from '@services/entity-services/dossier-stats.service'; @Component({ selector: 'redaction-edit-dossier-general-info', @@ -32,6 +33,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti readonly permissionsService: PermissionsService, private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossiersService: DossiersService, + private readonly _dossierStatsService: DossierStatsService, private readonly _formBuilder: FormBuilder, private readonly _dialogService: DossiersDialogService, private readonly _router: Router, @@ -71,13 +73,14 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti dossierTemplateId: [ { value: this.dossier.dossierTemplateId, - disabled: this.dossier.hasFiles, + disabled: this._dossierStatsService.get(this.dossier.dossierId).hasFiles, }, Validators.required, ], description: [this.dossier.description], dueDate: [this.dossier.dueDate], watermarkEnabled: [this.dossier.watermarkEnabled], + watermarkPreviewEnabled: [this.dossier.watermarkPreviewEnabled], }); this.hasDueDate = !!this.dossier.dueDate; } @@ -88,6 +91,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti dossierTemplateId: this.dossier.dossierTemplateId, description: this.dossier.description, watermarkEnabled: this.dossier.watermarkEnabled, + watermarkPreviewEnabled: this.dossier.watermarkPreviewEnabled, dueDate: this.dossier.dueDate, }); } @@ -98,6 +102,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti dossierName: this.dossierForm.get('dossierName').value, description: this.dossierForm.get('description').value, watermarkEnabled: this.dossierForm.get('watermarkEnabled').value, + watermarkPreviewEnabled: this.dossierForm.get('watermarkPreviewEnabled').value, dueDate: this.hasDueDate ? this.dossierForm.get('dueDate').value : undefined, dossierTemplateId: this.dossierForm.get('dossierTemplateId').value, } as IDossierRequest; diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component.html deleted file mode 100644 index cb8de7e94..000000000 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component.ts deleted file mode 100644 index cf0521c84..000000000 --- a/apps/red-ui/src/app/modules/dossier/dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { Dossier } from '@red/domain'; -import { EditDossierSectionInterface } from '../edit-dossier-section.interface'; -import { TeamMembersManagerComponent } from '../../../components/team-members-manager/team-members-manager.component'; -import { UserService } from '@services/user.service'; - -@Component({ - selector: 'redaction-edit-dossier-team-members', - templateUrl: './edit-dossier-team-members.component.html', - styleUrls: ['./edit-dossier-team-members.component.scss'], -}) -export class EditDossierTeamMembersComponent implements EditDossierSectionInterface { - readonly currentUser = this._userService.currentUser; - - @Input() dossier: Dossier; - @Output() readonly updateDossier = new EventEmitter(); - - @ViewChild(TeamMembersManagerComponent) managerComponent: TeamMembersManagerComponent; - - constructor(private readonly _userService: UserService) {} - - get changed() { - return this.managerComponent.changed; - } - - get disabled() { - return !this.currentUser.isManager; - } - - async save() { - await this.managerComponent.saveMembers(); - } - - revert() { - this.managerComponent.revert(); - } -} diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/force-redaction-dialog/force-redaction-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/force-redaction-dialog/force-redaction-dialog.component.ts index 934cb3dcd..ab74193d5 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/force-redaction-dialog/force-redaction-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/force-redaction-dialog/force-redaction-dialog.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Toaster } from '@iqser/common-ui'; import { TranslateService } from '@ngx-translate/core'; import { UserService } from '@services/user.service'; @@ -8,7 +8,7 @@ import { ManualAnnotationService } from '../../services/manual-annotation.servic import { PermissionsService } from '@services/permissions.service'; import { DossiersService } from '@services/entity-services/dossiers.service'; import { JustificationsService } from '@services/entity-services/justifications.service'; -import { ILegalBasisChangeRequest } from '@red/domain'; +import { Dossier, ILegalBasisChangeRequest } from '@red/domain'; export interface LegalBasisOption { label?: string; @@ -36,8 +36,9 @@ export class ForceRedactionDialogComponent implements OnInit { private readonly _manualAnnotationService: ManualAnnotationService, private readonly _permissionsService: PermissionsService, public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) private readonly _data: { readonly dossier: Dossier }, ) { - this.isDocumentAdmin = this._permissionsService.isApprover(); + this.isDocumentAdmin = this._permissionsService.isApprover(this._data.dossier); this.redactionForm = this._formBuilder.group({ reason: [null, Validators.required], @@ -46,9 +47,7 @@ export class ForceRedactionDialogComponent implements OnInit { } async ngOnInit() { - const data = await this._justificationsService - .getForDossierTemplate(this._dossiersService.activeDossier.dossierTemplateId) - .toPromise(); + const data = await this._justificationsService.getForDossierTemplate(this._data.dossier.dossierTemplateId).toPromise(); this.legalOptions = data.map(lbm => ({ legalBasis: lbm.reason, diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.html index 8696056f2..1788ae51a 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.html @@ -3,14 +3,14 @@
- +
- {{ format(manualRedactionEntryWrapper.manualRedactionEntry.value) }} + {{ format(data.manualRedactionEntryWrapper.manualRedactionEntry.value) }}
- +
diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts index afe1b932d..51a87aeb4 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/manual-redaction-dialog/manual-annotation-dialog.component.ts @@ -2,16 +2,12 @@ import { Component, Inject, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AppStateService } from '@state/app-state.service'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Toaster } from '@iqser/common-ui'; -import { TranslateService } from '@ngx-translate/core'; -import { UserService } from '@services/user.service'; import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; import { ManualAnnotationService } from '../../services/manual-annotation.service'; import { ManualAnnotationResponse } from '@models/file/manual-annotation-response'; import { PermissionsService } from '@services/permissions.service'; -import { DossiersService } from '@services/entity-services/dossiers.service'; import { JustificationsService } from '@services/entity-services/justifications.service'; -import { Dictionary, IAddRedactionRequest } from '@red/domain'; +import { Dictionary, Dossier, IAddRedactionRequest } from '@red/domain'; export interface LegalBasisOption { label?: string; @@ -36,21 +32,17 @@ export class ManualAnnotationDialogComponent implements OnInit { constructor( private readonly _appStateService: AppStateService, - private readonly _dossiersService: DossiersService, - private readonly _userService: UserService, private readonly _formBuilder: FormBuilder, - private readonly _notificationService: Toaster, - private readonly _translateService: TranslateService, private readonly _justificationsService: JustificationsService, private readonly _manualAnnotationService: ManualAnnotationService, private readonly _permissionsService: PermissionsService, public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public manualRedactionEntryWrapper: ManualRedactionEntryWrapper, + @Inject(MAT_DIALOG_DATA) public data: { manualRedactionEntryWrapper: ManualRedactionEntryWrapper; dossier: Dossier }, ) { - this.isDocumentAdmin = this._permissionsService.isApprover(); + this.isDocumentAdmin = this._permissionsService.isApprover(this.data.dossier); - this.isFalsePositiveRequest = this.manualRedactionEntryWrapper.type === 'FALSE_POSITIVE'; - this.isDictionaryRequest = this.manualRedactionEntryWrapper.type === 'DICTIONARY' || this.isFalsePositiveRequest; + this.isFalsePositiveRequest = this.data.manualRedactionEntryWrapper.type === 'FALSE_POSITIVE'; + this.isDictionaryRequest = this.data.manualRedactionEntryWrapper.type === 'DICTIONARY' || this.isFalsePositiveRequest; this.redactionForm = this._formBuilder.group({ reason: this.isDictionaryRequest ? [null] : [null, Validators.required], @@ -60,8 +52,8 @@ export class ManualAnnotationDialogComponent implements OnInit { comment: this.isDocumentAdmin ? [null] : [null, Validators.required], }); - for (const key of Object.keys(this._appStateService.dictionaryData[this._dossiersService.activeDossier.dossierTemplateId])) { - const dictionaryData = this._appStateService.getDictionary(key); + for (const key of Object.keys(this._appStateService.dictionaryData[data.dossier.dossierTemplateId])) { + const dictionaryData = this._appStateService.getDictionary(key, data.dossier.dossierTemplateId); if (!dictionaryData.virtual && dictionaryData.addToDictionaryAction) { this.redactionDictionaries.push(dictionaryData); } @@ -70,7 +62,7 @@ export class ManualAnnotationDialogComponent implements OnInit { } get title() { - return this._manualAnnotationService.getTitle(this.manualRedactionEntryWrapper.type); + return this._manualAnnotationService.getTitle(this.data.manualRedactionEntryWrapper.type, this.data.dossier); } get displayedDictionaryLabel() { @@ -82,9 +74,7 @@ export class ManualAnnotationDialogComponent implements OnInit { } async ngOnInit() { - const data = await this._justificationsService - .getForDossierTemplate(this._dossiersService.activeDossier.dossierTemplateId) - .toPromise(); + const data = await this._justificationsService.getForDossierTemplate(this.data.dossier.dossierTemplateId).toPromise(); this.legalOptions = data.map(lbm => ({ legalBasis: lbm.reason, @@ -96,11 +86,13 @@ export class ManualAnnotationDialogComponent implements OnInit { } handleAddRedaction() { - this._enhanceManualRedaction(this.manualRedactionEntryWrapper.manualRedactionEntry); - this._manualAnnotationService.addAnnotation(this.manualRedactionEntryWrapper.manualRedactionEntry).subscribe( - response => this.dialogRef.close(new ManualAnnotationResponse(this.manualRedactionEntryWrapper, response)), - () => this.dialogRef.close(), - ); + this._enhanceManualRedaction(this.data.manualRedactionEntryWrapper.manualRedactionEntry); + this._manualAnnotationService + .addAnnotation(this.data.manualRedactionEntryWrapper.manualRedactionEntry, this.data.dossier) + .subscribe( + response => this.dialogRef.close(new ManualAnnotationResponse(this.data.manualRedactionEntryWrapper, response)), + () => this.dialogRef.close(), + ); } format(value: string) { diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.ts index 807d1134a..3e6771b28 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.ts @@ -5,11 +5,10 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { imageCategoriesTranslations } from '../../translations/image-categories-translations'; import { ImageCategory } from '../../models/image-category.model'; +import { Dossier } from '@red/domain'; @Component({ - selector: 'redaction-recategorize-image-dialog', templateUrl: './recategorize-image-dialog.component.html', - styleUrls: ['./recategorize-image-dialog.component.scss'], }) export class RecategorizeImageDialogComponent implements OnInit { recategorizeImageForm: FormGroup; @@ -21,18 +20,18 @@ export class RecategorizeImageDialogComponent implements OnInit { private readonly _permissionsService: PermissionsService, private readonly _formBuilder: FormBuilder, public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public annotations: AnnotationWrapper[], + @Inject(MAT_DIALOG_DATA) public data: { annotations: AnnotationWrapper[]; dossier: Dossier }, ) {} get changed(): boolean { - return this.recategorizeImageForm.get('type').value !== this.annotations[0].type; + return this.recategorizeImageForm.get('type').value !== this.data.annotations[0].type; } ngOnInit() { - this.isDocumentAdmin = this._permissionsService.isApprover(); + this.isDocumentAdmin = this._permissionsService.isApprover(this.data.dossier); this.recategorizeImageForm = this._formBuilder.group({ - type: [this.annotations[0].type, Validators.required], + type: [this.data.annotations[0].type, Validators.required], comment: this.isDocumentAdmin ? [null] : [null, Validators.required], }); } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.html index be51531b1..5a7e4f1a5 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.html +++ b/apps/red-ui/src/app/modules/dossier/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.html @@ -39,7 +39,7 @@ -
+
diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.ts index f049be409..8c8f00246 100644 --- a/apps/red-ui/src/app/modules/dossier/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.ts +++ b/apps/red-ui/src/app/modules/dossier/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.ts @@ -5,14 +5,15 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { PermissionsService } from '@services/permissions.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { humanize } from '@iqser/common-ui'; +import { Dossier } from '@red/domain'; export interface RemoveAnnotationsDialogInput { annotationsToRemove: AnnotationWrapper[]; removeFromDictionary: boolean; + dossier: Dossier; } @Component({ - selector: 'redaction-remove-annotations-dialog', templateUrl: './remove-annotations-dialog.component.html', styleUrls: ['./remove-annotations-dialog.component.scss'], }) @@ -27,7 +28,7 @@ export class RemoveAnnotationsDialogComponent { @Inject(MAT_DIALOG_DATA) public data: RemoveAnnotationsDialogInput, ) { this.redactionForm = this._formBuilder.group({ - comment: this.permissionsService.isApprover() ? [null] : [null, Validators.required], + comment: this.permissionsService.isApprover(this.data.dossier) ? [null] : [null, Validators.required], }); } diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/resize-annotation-dialog/resize-annotation-dialog.component.html b/apps/red-ui/src/app/modules/dossier/dialogs/resize-annotation-dialog/resize-annotation-dialog.component.html new file mode 100644 index 000000000..72a505f70 --- /dev/null +++ b/apps/red-ui/src/app/modules/dossier/dialogs/resize-annotation-dialog/resize-annotation-dialog.component.html @@ -0,0 +1,21 @@ +
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + + +
diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/resize-annotation-dialog/resize-annotation-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/resize-annotation-dialog/resize-annotation-dialog.component.ts new file mode 100644 index 000000000..51e55a1ef --- /dev/null +++ b/apps/red-ui/src/app/modules/dossier/dialogs/resize-annotation-dialog/resize-annotation-dialog.component.ts @@ -0,0 +1,34 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { PermissionsService } from '@services/permissions.service'; +import { Dossier } from '@red/domain'; + +@Component({ + templateUrl: './resize-annotation-dialog.component.html', +}) +export class ResizeAnnotationDialogComponent implements OnInit { + resizeForm: FormGroup; + isDocumentAdmin: boolean; + + constructor( + private readonly _permissionsService: PermissionsService, + private readonly _formBuilder: FormBuilder, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) private readonly _data: { dossier: Dossier }, + ) {} + + ngOnInit() { + this.isDocumentAdmin = this._permissionsService.isApprover(this._data.dossier); + + this.resizeForm = this._formBuilder.group({ + comment: this.isDocumentAdmin ? [null] : [null, Validators.required], + }); + } + + save() { + this.dialogRef.close({ + comment: this.resizeForm.get('comment').value, + }); + } +} diff --git a/apps/red-ui/src/app/modules/dossier/dossiers-routing.module.ts b/apps/red-ui/src/app/modules/dossier/dossiers-routing.module.ts index e20b57beb..ed4a66bfc 100644 --- a/apps/red-ui/src/app/modules/dossier/dossiers-routing.module.ts +++ b/apps/red-ui/src/app/modules/dossier/dossiers-routing.module.ts @@ -1,27 +1,25 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CompositeRouteGuard } from '@iqser/common-ui'; -import { AuthGuard } from '../auth/auth.guard'; -import { RedRoleGuard } from '../auth/red-role.guard'; -import { AppStateGuard } from '@state/app-state.guard'; import { SearchScreenComponent } from './screens/search-screen/search-screen.component'; import { FilePreviewScreenComponent } from './screens/file-preview-screen/file-preview-screen.component'; +import { FilePreviewGuard } from '../../guards/file-preview.guard'; +import { DossierFilesGuard } from '../../guards/dossier-files-guard'; +import { GoBackGuard } from '@guards/go-back-guard.service'; +import { CompositeRouteGuard } from '@iqser/common-ui'; const routes: Routes = [ { path: 'search', component: SearchScreenComponent, - canActivate: [CompositeRouteGuard], - data: { - routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], - }, + canActivate: [GoBackGuard], + canDeactivate: [GoBackGuard], }, { path: ':dossierId', canActivate: [CompositeRouteGuard], + canDeactivate: [DossierFilesGuard], data: { - routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], - reuse: true, + routeGuards: [DossierFilesGuard], }, loadChildren: () => import('./screens/dossier-overview/dossier-overview.module').then(m => m.DossierOverviewModule), }, @@ -30,18 +28,14 @@ const routes: Routes = [ component: FilePreviewScreenComponent, canActivate: [CompositeRouteGuard], data: { - routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], + routeGuards: [DossierFilesGuard, FilePreviewGuard], reuse: true, }, + canDeactivate: [FilePreviewGuard], }, { path: '', pathMatch: 'full', - canActivate: [CompositeRouteGuard], - data: { - routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard], - reuse: true, - }, loadChildren: () => import('./screens/dossiers-listing/dossiers-listing.module').then(m => m.DossiersListingModule), }, ]; diff --git a/apps/red-ui/src/app/modules/dossier/dossiers.module.ts b/apps/red-ui/src/app/modules/dossier/dossiers.module.ts index 4a1007320..dd543cd5d 100644 --- a/apps/red-ui/src/app/modules/dossier/dossiers.module.ts +++ b/apps/red-ui/src/app/modules/dossier/dossiers.module.ts @@ -27,7 +27,6 @@ import { EditDossierDialogComponent } from './dialogs/edit-dossier-dialog/edit-d import { EditDossierGeneralInfoComponent } from './dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component'; import { EditDossierDownloadPackageComponent } from './dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component'; import { EditDossierDictionaryComponent } from './dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component'; -import { EditDossierTeamMembersComponent } from './dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component'; import { TeamMembersManagerComponent } from './components/team-members-manager/team-members-manager.component'; import { ChangeLegalBasisDialogComponent } from './dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component'; import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component'; @@ -40,6 +39,10 @@ import { AnnotationSourceComponent } from './components/file-workload/components import { OverlayModule } from '@angular/cdk/overlay'; import { SharedDossiersModule } from './shared/shared-dossiers.module'; import { PlatformSearchService } from './shared/services/platform-search.service'; +import { ResizeAnnotationDialogComponent } from './dialogs/resize-annotation-dialog/resize-annotation-dialog.component'; +import { BreadcrumbsService } from '@services/breadcrumbs.service'; +import { of } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; const screens = [FilePreviewScreenComponent, SearchScreenComponent]; @@ -49,6 +52,7 @@ const dialogs = [ ManualAnnotationDialogComponent, ForceRedactionDialogComponent, RemoveAnnotationsDialogComponent, + ResizeAnnotationDialogComponent, DocumentInfoDialogComponent, AssignReviewerApproverDialogComponent, ChangeLegalBasisDialogComponent, @@ -66,7 +70,6 @@ const components = [ EditDossierGeneralInfoComponent, EditDossierDownloadPackageComponent, EditDossierDictionaryComponent, - EditDossierTeamMembersComponent, EditDossierAttributesComponent, TeamMembersManagerComponent, PageExclusionComponent, @@ -93,4 +96,12 @@ const services = [ providers: [...services], imports: [CommonModule, SharedModule, SharedDossiersModule, FileUploadDownloadModule, DossiersRoutingModule, OverlayModule], }) -export class DossiersModule {} +export class DossiersModule { + constructor(breadcrumbsService: BreadcrumbsService, translateService: TranslateService) { + breadcrumbsService.append({ + name$: of(translateService.instant('top-bar.navigation-items.dossiers')), + routerLink: ['/main', 'dossiers'], + routerLinkActiveOptions: { exact: true }, + }); + } +} diff --git a/apps/red-ui/src/app/modules/dossier/models/annotation-action-mode.model.ts b/apps/red-ui/src/app/modules/dossier/models/annotation-action-mode.model.ts index eabbda0a9..83f3965a0 100644 --- a/apps/red-ui/src/app/modules/dossier/models/annotation-action-mode.model.ts +++ b/apps/red-ui/src/app/modules/dossier/models/annotation-action-mode.model.ts @@ -11,4 +11,6 @@ export type AnnotationActionMode = | 'suggest' | 'undo' | 'force-redaction' - | 'request-force-redaction'; + | 'request-force-redaction' + | 'resize' + | 'request-resize'; diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/bulk-actions/dossier-overview-bulk-actions.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/bulk-actions/dossier-overview-bulk-actions.component.html index b343383f7..0dd90ce37 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/bulk-actions/dossier-overview-bulk-actions.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/bulk-actions/dossier-overview-bulk-actions.component.html @@ -1,4 +1,4 @@ - + - + , + readonly listingService: ListingService, private readonly _userPreferenceService: UserPreferenceService, ) {} get selectedFiles(): File[] { - return this._listingService.selected; - } - - get areAllFilesSelected() { - return this.dossier.files.length !== 0 && this.selectedFiles.length === this.dossier.files.length; - } - - get areSomeFilesSelected() { - return this.selectedFiles.length > 0; + return this.listingService.selected; } get allSelectedFilesCanBeAssignedIntoSameState() { - if (this.areSomeFilesSelected) { - const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce( - (acc, file) => acc && (file.isUnderReview || file.isUnassigned), - true, - ); - const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true); - return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval; - } - return false; + const allFilesAreUnderReviewOrUnassigned = this.selectedFiles.reduce( + (acc, file) => acc && (file.isUnderReview || file.isUnassigned), + true, + ); + const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true); + return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval; } get canAssignToSelf() { @@ -92,10 +82,6 @@ export class DossierOverviewBulkActionsComponent { return this.selectedFiles.reduce((acc, file) => acc && file.canBeOCRed, true); } - get files() { - return this.selectedFiles.map(file => file.status); - } - get canSetToUnderReview() { return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderReview(file), true); } @@ -143,7 +129,6 @@ export class DossierOverviewBulkActionsComponent { this.dossier.dossierId, ) .toPromise(); - await this._appStateService.reloadActiveDossierFiles(); this.reload.emit(); this._loadingService.stop(); }, diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.html index 7ac4f3f4c..d350e144d 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview/components/dossier-details-stats/dossier-details-stats.component.html @@ -1,68 +1,74 @@ - +
- {{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: dossier.files.length } }} + {{ 'dossier-overview.dossier-details.stats.documents' | translate: { count: stats.numberOfFiles } }}
+ +
+ + {{ + 'dossier-overview.dossier-details.stats.processing-documents' | translate: { count: stats.numberOfProcessingFiles } + }} +
+
{{ 'dossier-overview.dossier-details.stats.people' | translate: { count: dossier.memberIds.length } }}
+
- {{ - 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: dossier.totalNumberOfPages | number } - }} + {{ 'dossier-overview.dossier-details.stats.analysed-pages' | translate: { count: stats.numberOfPages | number } }}
-
- - {{ - 'dossier-overview.dossier-details.stats.created-on' - | translate - : { - date: dossier.date | date: 'd MMM. yyyy' - } - }} - -
-
- - {{ - 'dossier-overview.dossier-details.stats.due-date' - | translate - : { - date: dossier.dueDate | date: 'd MMM. yyyy' - } - }} -
-
- - {{ dossierTemplate(dossier).name }} -
-