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 @@
-
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 @@
+
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 @@
+
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 @@