add breadcrumbs service

This commit is contained in:
Dan Percic 2021-11-17 23:14:40 +02:00
parent 3fd9d12526
commit 0059344083
15 changed files with 309 additions and 85 deletions

View File

@ -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',

View File

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

View File

@ -4,41 +4,7 @@
<div *ngIf="!currentUser.isUser" class="menu-placeholder"></div>
<div *ngIf="currentUser.isUser" class="menu flex-2 visible-lg breadcrumbs-container">
<a *ngIf="(isDossiersView$ | async) === false" class="breadcrumb back" redactionNavigateLastDossiersScreen>
<mat-icon svgIcon="iqser:expand"></mat-icon>
{{ 'top-bar.navigation-items.back' | translate }}
</a>
<ng-container *ngIf="isDossiersView$ | async">
<a
[routerLinkActiveOptions]="{ exact: true }"
class="breadcrumb"
routerLink="/main/dossiers"
routerLinkActive="active"
translate="top-bar.navigation-items.dossiers"
></a>
<!-- <ng-container *ngIf="activeDossier$ | async as dossier">-->
<!-- <mat-icon svgIcon="iqser:arrow-right"></mat-icon>-->
<!-- <a-->
<!-- [routerLinkActiveOptions]="{ exact: true }"-->
<!-- [routerLink]="dossier.routerLink"-->
<!-- class="breadcrumb"-->
<!-- routerLinkActive="active"-->
<!-- >-->
<!-- {{ dossier.dossierName }}-->
<!-- </a>-->
<!-- </ng-container>-->
<!-- <ng-container *ngIf="appStateService.activeFile as activeFile">-->
<!-- <mat-icon svgIcon="iqser:arrow-right"></mat-icon>-->
<!-- <a [routerLink]="activeFile.routerLink" class="breadcrumb" routerLinkActive="active">-->
<!-- {{ activeFile.filename }}-->
<!-- </a>-->
<!-- </ng-container>-->
</ng-container>
<redaction-breadcrumbs></redaction-breadcrumbs>
</div>
<div class="center">

View File

@ -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);
}
}

View File

@ -0,0 +1,25 @@
<ng-container *ngIf="breadcrumbsService.breadcrumbs$ | async as breadcrumbs">
<a
*ngIf="breadcrumbs.length === 0 || (breadcrumbsService.showGoBack$ | async) === true; else items"
class="breadcrumb back"
redactionNavigateLastDossiersScreen
>
<mat-icon svgIcon="iqser:expand"></mat-icon>
{{ 'top-bar.navigation-items.back' | translate }}
</a>
<ng-template #items>
<ng-container *ngFor="let breadcrumb of breadcrumbs; first as first">
<mat-icon *ngIf="!first" svgIcon="iqser:arrow-right"></mat-icon>
<a
[routerLinkActiveOptions]="breadcrumb.routerLinkActiveOptions ?? { exact: false }"
[routerLink]="breadcrumb.routerLink"
class="breadcrumb"
routerLinkActive="active"
>
{{ breadcrumb.name$ | async }}
</a>
</ng-container>
</ng-template>
</ng-container>

View File

@ -0,0 +1,10 @@
:host {
display: flex;
align-items: center;
overflow: hidden;
height: 100%;
> *:not(:last-child) {
margin-right: 6px;
}
}

View File

@ -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) {}
}

View File

@ -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<unknown> {
constructor(private readonly _breadcrumbsService: BreadcrumbsService) {}
canActivate(): boolean {
this._breadcrumbsService.showGoBack();
return true;
}
canDeactivate() {
this._breadcrumbsService.hideGoBack();
return true;
}
}

View File

@ -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],
},
];

View File

@ -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 },
});
}
}

View File

@ -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<unknown> {
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<boolean> {
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;
}
}

View File

@ -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<unknown> {
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<boolean> {
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;
}
}

View File

@ -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<void> {
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 {

View File

@ -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<boolean> {
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;
}
}

View File

@ -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<string>;
readonly routerLink?: string[];
readonly routerLinkActiveOptions?: RouterLinkActiveOptions;
}
export type Breadcrumbs = List<Breadcrumb>;
@Injectable({
providedIn: 'root',
})
export class BreadcrumbsService {
readonly breadcrumbs$: Observable<Breadcrumbs>;
readonly showGoBack$: Observable<boolean>;
private readonly _showGoBack$ = new BehaviorSubject<boolean>(false);
private readonly _store$ = new BehaviorSubject<Breadcrumbs>([]);
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);
}
}