diff --git a/apps/red-ui/src/app/app-routing.module.ts b/apps/red-ui/src/app/app-routing.module.ts
index f8063def5..5fe4abfad 100644
--- a/apps/red-ui/src/app/app-routing.module.ts
+++ b/apps/red-ui/src/app/app-routing.module.ts
@@ -3,12 +3,12 @@ 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';
-const routes = [
+const routes: Routes = [
{
path: '',
redirectTo: 'main/dossiers',
diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts
index 32387e151..824f27453 100644
--- a/apps/red-ui/src/app/app.module.ts
+++ b/apps/red-ui/src/app/app.module.ts
@@ -35,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');
@@ -55,7 +56,7 @@ const screens = [BaseScreenComponent, DownloadsListScreenComponent];
const components = [AppComponent, AuthErrorComponent, NotificationsComponent, SpotlightSearchComponent, ...screens];
@NgModule({
- declarations: [...components],
+ declarations: [...components, BreadcrumbsComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
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 cd67b2fef..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 @@
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 5816397e5..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
@@ -11,6 +11,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
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;
@@ -21,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({
@@ -62,8 +62,8 @@ export class BaseScreenComponent {
{
text: this._translateService.instant('search.this-dossier'),
icon: 'red:enter',
- hide: (): boolean => true, // TODO
- action: (query): void => this._search(query, 'todo'), // TODO
+ hide: (): boolean => this._hideSearchThisDossier,
+ action: (query): void => this._searchThisDossier(query),
},
{
text: this._translateService.instant('search.entire-platform'),
@@ -77,7 +77,6 @@ export class BaseScreenComponent {
startWith(this._router.url),
shareDistinctLast(),
);
- readonly isDossiersView$ = this._navigationStart$.pipe(map(isDossiersView));
readonly isSearchScreen$ = this._navigationStart$.pipe(map(isSearchScreen));
constructor(
@@ -89,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;
}
@@ -99,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/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/modules/dossier/dossiers-routing.module.ts b/apps/red-ui/src/app/modules/dossier/dossiers-routing.module.ts
index 6136a7069..011792c21 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,24 +1,29 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
-import { CompositeRouteGuard } from '@iqser/common-ui';
import { SearchScreenComponent } from './screens/search-screen/search-screen.component';
import { FilePreviewScreenComponent } from './screens/file-preview-screen/file-preview-screen.component';
-import { FilesGuard } from './utils/file.guard';
+import { FilesGuard } from './guards/file.guard';
+import { DossiersGuard } from './guards/dossiers.guard';
+import { GoBackGuard } from '@guards/go-back-guard.service';
const routes: Routes = [
{
path: 'search',
component: SearchScreenComponent,
+ canActivate: [GoBackGuard],
+ canDeactivate: [GoBackGuard],
},
{
path: ':dossierId',
- canActivate: [CompositeRouteGuard],
+ canActivate: [DossiersGuard],
+ canDeactivate: [DossiersGuard],
loadChildren: () => import('./screens/dossier-overview/dossier-overview.module').then(m => m.DossierOverviewModule),
},
{
path: ':dossierId/file/:fileId',
component: FilePreviewScreenComponent,
- canActivate: [FilesGuard],
+ canActivate: [DossiersGuard, FilesGuard],
+ canDeactivate: [FilesGuard],
data: {
reuse: true,
},
@@ -27,6 +32,8 @@ const routes: Routes = [
path: '',
pathMatch: 'full',
loadChildren: () => import('./screens/dossiers-listing/dossiers-listing.module').then(m => m.DossiersListingModule),
+ canActivate: [DossiersGuard],
+ canDeactivate: [DossiersGuard],
},
];
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 1100ecb71..babf35e58 100644
--- a/apps/red-ui/src/app/modules/dossier/dossiers.module.ts
+++ b/apps/red-ui/src/app/modules/dossier/dossiers.module.ts
@@ -40,7 +40,11 @@ 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 { FilesGuard } from './utils/file.guard';
+import { BreadcrumbsService } from '@services/breadcrumbs.service';
+import { of } from 'rxjs';
+import { FilesGuard } from './guards/file.guard';
+import { DossiersGuard } from './guards/dossiers.guard';
+import { TranslateService } from '@ngx-translate/core';
const screens = [FilePreviewScreenComponent, SearchScreenComponent];
@@ -91,7 +95,15 @@ const services = [
@NgModule({
declarations: [...components],
- providers: [...services, FilesGuard],
+ providers: [...services, FilesGuard, DossiersGuard],
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/guards/dossiers.guard.ts b/apps/red-ui/src/app/modules/dossier/guards/dossiers.guard.ts
new file mode 100644
index 000000000..e7fb484dc
--- /dev/null
+++ b/apps/red-ui/src/app/modules/dossier/guards/dossiers.guard.ts
@@ -0,0 +1,61 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, CanDeactivate, Router, RouterStateSnapshot } from '@angular/router';
+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';
+import { LoadingService } from '@iqser/common-ui';
+
+@Injectable()
+export class DossiersGuard implements CanActivate, CanDeactivate {
+ constructor(
+ private readonly _dossiersService: DossiersService,
+ private readonly _appStateService: AppStateService,
+ private readonly _loadingService: LoadingService,
+ private readonly _breadcrumbsService: BreadcrumbsService,
+ private readonly _router: Router,
+ ) {}
+
+ async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise {
+ if (state.url === '/main/dossiers') {
+ this._breadcrumbsService.hideGoBack();
+ return true;
+ }
+
+ const dossierId = route.paramMap.get('dossierId');
+
+ if (!this._dossiersService.find(dossierId)) {
+ this._loadingService.start();
+ await this._appStateService.loadAllDossiers();
+ this._loadingService.stop();
+
+ if (!this._dossiersService.find(dossierId)) {
+ await this._router.navigate(['/main', 'dossiers']);
+ return false;
+ }
+ }
+
+ 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/modules/dossier/guards/file.guard.ts b/apps/red-ui/src/app/modules/dossier/guards/file.guard.ts
new file mode 100644
index 000000000..791da361e
--- /dev/null
+++ b/apps/red-ui/src/app/modules/dossier/guards/file.guard.ts
@@ -0,0 +1,65 @@
+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';
+import { LoadingService } from '@iqser/common-ui';
+
+@Injectable()
+export class FilesGuard implements CanActivate, CanDeactivate {
+ constructor(
+ private readonly _filesMapService: FilesMapService,
+ private readonly _dossiersService: DossiersService,
+ private readonly _appStateService: AppStateService,
+ private readonly _breadcrumbsService: BreadcrumbsService,
+ private readonly _loadingService: LoadingService,
+ private readonly _router: Router,
+ ) {}
+
+ async canActivate(route: ActivatedRouteSnapshot): Promise {
+ const dossierId = route.paramMap.get('dossierId');
+ const fileId = route.paramMap.get('fileId');
+
+ if (!this._filesMapService.get(dossierId, fileId)) {
+ this._loadingService.start();
+ const dossier = this._dossiersService.find(dossierId);
+ await this._appStateService.getFiles(dossier);
+ this._loadingService.stop();
+
+ 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.root.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/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts
index 6bb429dca..97941b9ce 100644
--- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts
+++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts
@@ -122,11 +122,12 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _userPreferenceService: UserPreferenceService,
) {
super();
- this._loadingService.start();
+ _loadingService.start();
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
- this.dossier$ = this._dossiersService.getEntityChanged$(this.dossierId);
+ this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId);
this.fileId = _activatedRoute.snapshot.paramMap.get('fileId');
this.file$ = _filesMapService.watch$(this.dossierId, this.fileId);
+
document.documentElement.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.fullScreen = false;
@@ -259,10 +260,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async ngOnInit(): Promise {
+ this._loadingService.start();
await this._userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId);
-
await this._loadFileData();
- this.displayPDFViewer = true;
this._updateCanPerformActions();
this._subscribeToFileUpdates();
@@ -270,6 +270,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
if (file?.analysisRequired) {
this.fileActions.reanalyseFile();
}
+
+ this._loadingService.stop();
+ this.displayPDFViewer = true;
}
rebuildFilters(deletePreviousAnnotations = false): void {
diff --git a/apps/red-ui/src/app/modules/dossier/utils/file.guard.ts b/apps/red-ui/src/app/modules/dossier/utils/file.guard.ts
deleted file mode 100644
index 566eddbfc..000000000
--- a/apps/red-ui/src/app/modules/dossier/utils/file.guard.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, CanActivate, Router } 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';
-
-@Injectable()
-export class FilesGuard implements CanActivate {
- constructor(
- private readonly _filesMapService: FilesMapService,
- private readonly _dossiersService: DossiersService,
- private readonly _appStateService: AppStateService,
- private readonly _router: Router,
- ) {}
-
- async canActivate(route: ActivatedRouteSnapshot): Promise {
- const dossierId = route.paramMap.get('dossierId');
- const fileId = route.paramMap.get('fileId');
-
- if (!this._filesMapService.get(dossierId, fileId)) {
- const dossier = this._dossiersService.find(dossierId);
- await this._appStateService.getFiles(dossier);
-
- if (!this._filesMapService.get(dossierId, fileId)) {
- await this._router.navigate([dossier.routerLink]);
- return false;
- }
- }
-
- return true;
- }
-}
diff --git a/apps/red-ui/src/app/services/breadcrumbs.service.ts b/apps/red-ui/src/app/services/breadcrumbs.service.ts
new file mode 100644
index 000000000..4d6b7d478
--- /dev/null
+++ b/apps/red-ui/src/app/services/breadcrumbs.service.ts
@@ -0,0 +1,57 @@
+import { Injectable } from '@angular/core';
+import { List, shareDistinctLast } from '@iqser/common-ui';
+import { IsActiveMatchOptions } from '@angular/router';
+import { BehaviorSubject, Observable } from 'rxjs';
+
+export type RouterLinkActiveOptions = { exact: boolean } | IsActiveMatchOptions;
+
+export interface Breadcrumb {
+ readonly name$: Observable;
+ readonly routerLink?: string[];
+ readonly routerLinkActiveOptions?: RouterLinkActiveOptions;
+}
+
+export type Breadcrumbs = List;
+
+@Injectable({
+ providedIn: 'root',
+})
+export class BreadcrumbsService {
+ readonly breadcrumbs$: Observable;
+ readonly showGoBack$: Observable;
+ private readonly _showGoBack$ = new BehaviorSubject(false);
+ private readonly _store$ = new BehaviorSubject([]);
+
+ constructor() {
+ this.breadcrumbs$ = this._store$.asObservable();
+ this.showGoBack$ = this._showGoBack$.asObservable().pipe(shareDistinctLast());
+ }
+
+ get breadcrumbs() {
+ return this._store$.value;
+ }
+
+ append(breadcrumb: Breadcrumb) {
+ const existing = this._store$.value.find(item => item.routerLink.toString() === breadcrumb.routerLink.toString());
+ if (existing) {
+ this.remove(existing.routerLink);
+ }
+ this._store$.next([...this._store$.value, breadcrumb]);
+ }
+
+ clear() {
+ this._store$.next([]);
+ }
+
+ remove(routerLink: string[]) {
+ this._store$.next(this._store$.value.filter(item => item.routerLink.toString() !== routerLink.toString()));
+ }
+
+ showGoBack() {
+ this._showGoBack$.next(true);
+ }
+
+ hideGoBack() {
+ this._showGoBack$.next(false);
+ }
+}