#84 - Add login page as part of SPA instead of relying on external page
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Garrett Mills 2021-02-15 11:45:04 -06:00
parent aad0aea79a
commit 0fecb8a4ba
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
18 changed files with 518 additions and 172 deletions

View File

@ -1,5 +1,8 @@
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import {LoginPage} from './pages/login/login.page';
import {AuthService} from './service/auth.service';
import {GuestOnlyGuard} from './service/guard/GuestOnly.guard';
const routes: Routes = [
{
@ -9,15 +12,18 @@ const routes: Routes = [
},
{
path: 'home',
canActivate: [AuthService],
loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
},
{
path: 'list',
loadChildren: () => import('./list/list.module').then(m => m.ListPageModule)
path: 'editor',
canActivate: [AuthService],
loadChildren: () => import('./components/components.module').then( m => m.ComponentsModule)
},
{
path: 'editor',
loadChildren: () => import('./components/components.module').then( m => m.ComponentsModule)
path: 'login',
canActivate: [GuestOnlyGuard],
component: LoginPage,
}
];

View File

@ -24,6 +24,7 @@ import {NavigationService} from './service/navigation.service';
import {DatabaseService} from './service/db/database.service';
import {EditorService} from './service/editor.service';
import {debug} from './utility';
import {AuthService} from './service/auth.service';
@Component({
selector: 'app-root',
@ -96,7 +97,6 @@ export class AppComponent implements OnInit {
protected showedNewVersionAlert = false;
protected showedOfflineAlert = false;
protected backbuttonSubscription: any;
protected initialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
constructor(
private platform: Platform,
@ -114,6 +114,7 @@ export class AppComponent implements OnInit {
protected toasts: ToastController,
protected db: DatabaseService,
protected editor: EditorService,
protected auth: AuthService,
) { }
async checkNewVersion() {
@ -479,9 +480,11 @@ export class AppComponent implements OnInit {
}
async initializeApp() {
const initializedOnce = this.navService.initialized$.getValue();
debug('app', this);
this.loader = await this.loading.create({
message: 'Starting up...',
message: 'Setting things up...',
cssClass: 'noded-loading-mask',
showBackdrop: true,
});
@ -493,37 +496,33 @@ export class AppComponent implements OnInit {
let toast: any;
debug('Subscribing to offline changes...');
this.api.offline$.subscribe(async isOffline => {
if ( isOffline && !this.showedOfflineAlert ) {
debug('Application went offline!');
toast = await this.toasts.create({
cssClass: 'compat-toast-container',
message: 'Uh, oh! It looks like you\'re offline. Some features might not work as expected...',
});
if ( !initializedOnce ) {
debug('Subscribing to offline changes...');
this.api.offline$.subscribe(async isOffline => {
if ( isOffline && !this.showedOfflineAlert ) {
debug('Application went offline!');
toast = await this.toasts.create({
cssClass: 'compat-toast-container',
message: 'Uh, oh! It looks like you\'re offline. Some features might not work as expected...',
});
this.showedOfflineAlert = true;
await toast.present();
} else if ( !isOffline && this.showedOfflineAlert ) {
debug('Appliation went online!');
await toast.dismiss();
this.showedOfflineAlert = false;
await this.api.syncOfflineData();
}
});
this.showedOfflineAlert = true;
await toast.present();
} else if ( !isOffline && this.showedOfflineAlert ) {
debug('Appliation went online!');
await toast.dismiss();
this.showedOfflineAlert = false;
await this.api.syncOfflineData();
}
});
}
debug('Getting initial status...');
let stat: any = await this.session.stat();
debug('Got stat:', stat);
if ( stat.public_user ) {
this.api.isPublicUser = true;
}
if ( stat.authenticated_user ) {
this.api.isAuthenticated = true;
}
this.api.isPublicUser = !!stat.public_user;
this.api.isAuthenticated = !!stat.authenticated_user;
this.api.systemBase = stat.system_base;
if ( !this.api.isAuthenticated || this.api.isPublicUser ) {
@ -558,7 +557,7 @@ export class AppComponent implements OnInit {
debug('Initializing session...');
await this.session.initialize();
if ( this.session.get('user.preferences.dark_mode') ) {
if ( this.session.get('user.preferences.dark_mode') && !this.darkMode ) {
this.toggleDark();
}
@ -603,7 +602,7 @@ export class AppComponent implements OnInit {
}
}
this.initialized$.next(true);
this.navService.initialized$.next(true);
if ( !this.api.isPublicUser && this.session.get('user.preferences.default_page') ) {
debug('Navigating to default page!');
@ -611,24 +610,38 @@ export class AppComponent implements OnInit {
const node = this.findNode(id);
if ( node ) {
this.navigateEditorToNode(node);
} else if ( this.auth.authInProgress ) {
await this.router.navigate(['/home']);
}
} else if ( this.auth.authInProgress ) {
await this.router.navigate(['/home']);
}
debug('Creating menu subscription...');
this.navService.sidebarRefresh$.subscribe(([_, quiet]) => {
this.onMenuRefresh(quiet);
});
if ( !initializedOnce ) {
debug('Creating menu subscription...');
this.navService.sidebarRefresh$.subscribe(([_, quiet]) => {
this.onMenuRefresh(quiet);
});
this.navService.navigationRequest$.subscribe(pageId => {
debug('Page navigation request: ', {pageId});
if ( !pageId ) {
debug('Empty page ID. Will not navigate.');
return;
}
this.navService.navigationRequest$.subscribe(pageId => {
debug('Page navigation request: ', {pageId});
if ( !pageId ) {
debug('Empty page ID. Will not navigate.');
return;
}
this.currentPageId = pageId;
this.router.navigate(['/editor', { id: pageId }]);
});
this.currentPageId = pageId;
this.router.navigate(['/editor', { id: pageId }]);
});
this.navService.initializationRequest$.subscribe((count) => {
if ( count === 0 ) {
return;
}
this.initializeApp();
});
}
debug('Reloading menu items...');
this.reloadMenuItems().subscribe(() => {
@ -647,6 +660,8 @@ export class AppComponent implements OnInit {
}, 1000 * 60 * 5); // Check for new version every 5 mins
}
});
this.auth.authInProgress = false;
}
async doPrefetch() {

View File

@ -34,6 +34,7 @@ import {MarkdownModule} from 'ngx-markdown';
import {VersionModalComponent} from './version-modal/version-modal.component';
import {EditorPageRoutingModule} from '../pages/editor/editor-routing.module';
import {EditorPage} from '../pages/editor/editor.page';
import {LoginPage} from '../pages/login/login.page';
import {WysiwygComponent} from './wysiwyg/wysiwyg.component';
import {WysiwygEditorComponent} from './editor/database/editors/wysiwyg/wysiwyg-editor.component';
import {WysiwygModalComponent} from './editor/database/editors/wysiwyg/wysiwyg-modal.component';
@ -76,6 +77,7 @@ import {FileBoxPageComponent} from './nodes/file-box/file-box-page.component';
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,
@ -130,6 +132,7 @@ import {FileBoxPageComponent} from './nodes/file-box/file-box-page.component';
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,
@ -170,6 +173,7 @@ import {FileBoxPageComponent} from './nodes/file-box/file-box-page.component';
MarkdownEditorComponent,
VersionModalComponent,
EditorPage,
LoginPage,
WysiwygComponent,
WysiwygEditorComponent,
WysiwygModalComponent,

View File

@ -33,7 +33,6 @@ export class BooleanRendererComponent implements ICellRendererAngularComp {
) { }
agInit(params: ICellRendererParams): void {
console.log('bool renderer', this);
this.params = params;
// @ts-ignore

View File

@ -3,6 +3,7 @@ import {Router} from '@angular/router';
import {ApiService} from '../../service/api.service';
import {PopoverController} from '@ionic/angular';
import {DatabaseService} from '../../service/db/database.service';
import {AuthService} from '../../service/auth.service';
@Component({
selector: 'app-option-picker',
@ -22,6 +23,7 @@ export class OptionPickerComponent implements OnInit {
protected router: Router,
protected popover: PopoverController,
protected db: DatabaseService,
protected auth: AuthService,
) { }
ngOnInit() {}
@ -30,8 +32,8 @@ export class OptionPickerComponent implements OnInit {
if ( key === 'html_export' ) {
window.open(this.api._build_url('/data/export/html'), '_blank');
} else if ( key === 'logout' ) {
await this.db.purge();
window.location.href = '/auth/logout';
await this.popover.dismiss();
await this.auth.endSession();
} else if ( key === 'toggle_darkmode' ) {
this.toggleDark();
} else if ( key === 'search_everywhere' ) {

View File

@ -8,7 +8,6 @@ import {isDebug, debug} from '../utility';
styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
constructor(
public readonly api: ApiService,
) {}

View File

@ -1,23 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { RouterModule } from '@angular/router';
import { ListPage } from './list.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild([
{
path: '',
component: ListPage
}
])
],
declarations: [ListPage]
})
export class ListPageModule {}

View File

@ -1,27 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>
List
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngFor="let item of items">
<ion-icon [name]="item.icon" slot="start"></ion-icon>
{{item.title}}
<div class="item-note" slot="end">
{{item.note}}
</div>
</ion-item>
</ion-list>
<!--
<div *ngIf="selectedItem" padding>
You navigated here from <b>{{selectedItem.title }}</b>
</div>
-->
</ion-content>

View File

@ -1 +0,0 @@

View File

@ -1,32 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ListPage } from './list.page';
describe('ListPage', () => {
let component: ListPage;
let fixture: ComponentFixture<ListPage>;
let listPage: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ListPage ],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(ListPage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have a list of 10 elements', () => {
listPage = fixture.nativeElement;
const items = listPage.querySelectorAll('ion-item');
expect(items.length).toEqual(10);
});
});

View File

@ -1,39 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-list',
templateUrl: 'list.page.html',
styleUrls: ['list.page.scss']
})
export class ListPage implements OnInit {
private selectedItem: any;
private icons = [
'flask',
'wifi',
'beer',
'football',
'basketball',
'paper-plane',
'american-football',
'boat',
'bluetooth',
'build'
];
public items: Array<{ title: string; note: string; icon: string }> = [];
constructor() {
for (let i = 1; i < 11; i++) {
this.items.push({
title: 'Item ' + i,
note: 'This is item #' + i,
icon: this.icons[Math.floor(Math.random() * this.icons.length)]
});
}
}
ngOnInit() {
}
// add back when alpha.4 is out
// navigate(item) {
// this.router.navigate(['/list', JSON.stringify(item)]);
// }
}

View File

@ -0,0 +1,94 @@
<div class="login-container">
<ion-grid class="ion-align-items-center">
<ion-row class="ion-align-items-center" style="height: 100%" *ngIf="step === 'greeter'">
<ion-col class="ion-align-items-end" style="text-align: right">
<img src="/assets/icon/logo_lines.svg" alt="Noded" width="200">
</ion-col>
<ion-col style="padding: 40px;">
<div class="hero" style="font-size: 2em;"><b style="font-size: 1.5em;">Noded</b><br>is your information, organized.</div>
<button
(click)="advance()"
title="Click to get started"
style="padding: 20px 0; margin: 0; color: #3171e0"
>Get started <i class="fa fa-chevron-right" style="font-size: 0.8em; margin-left: 10px;"></i></button>
</ion-col>
</ion-row>
<ion-row class="ion-align-items-center" style="height: 100%" *ngIf="step === 'username' || step === 'password' || step === 'create-account'">
<ion-col class="ion-align-items-center" style="text-align: center">
<img src="/assets/icon/logo_lines.svg" alt="Noded" height="100" style="margin-bottom: 20px;">
<p class="message">{{ stepMessages[step] }}</p>
<ion-item>
<ion-input
#usernameInput
[(ngModel)]="username"
[disabled]="step !== 'username'"
placeholder="Username"
></ion-input>
</ion-item>
<ion-item *ngIf="step === 'password'" style="padding-top: 10px;">
<ion-input
#passwordInput
[(ngModel)]="password"
placeholder="Password"
type="password"
></ion-input>
</ion-item>
<ion-item *ngIf="step === 'create-account'" style="padding-top: 10px;">
<ion-input
#nameInput
[(ngModel)]="fullName"
placeholder="Full Name"
type="text"
></ion-input>
</ion-item>
<ion-item *ngIf="step === 'create-account'" style="padding-top: 10px;">
<ion-input
[(ngModel)]="password"
placeholder="Create a Password"
type="password"
></ion-input>
</ion-item>
<ion-item *ngIf="step === 'create-account'" style="padding-top: 10px;">
<ion-input
[(ngModel)]="passwordConfirm"
placeholder="Confirm Password"
type="password"
></ion-input>
</ion-item>
<div class="buttons" style="text-align: right;">
<div class="button" style="display: flex; flex-direction: row;" *ngIf="step !== 'username'">
<button
(click)="back()"
style="text-align: left; margin: 20px 0 0 0;"
><i class="fa fa-chevron-left" style="font-size: 0.8em; margin-left: 10px; color: #3171e0"></i></button>
<button
(click)="advance()"
title="Click to sign-in to the system"
style="flex: 1; padding: 20px 0 0 0; margin: 0; text-align: right;"
[ngStyle]="{'color': ((!password || (step !== 'password' && (password !== passwordConfirm || !fullName))) ? '#666' : '#3171e0')}"
[disabled]="!password || (step !== 'password' && (password !== passwordConfirm || !fullName))"
>{{ step === 'password' ? 'Sign-in' : 'Create account' }} <i class="fa fa-chevron-right" style="font-size: 0.8em; margin-left: 10px;"></i></button>
</div>
<div class="button" *ngIf="step === 'username'">
<button
(click)="advance()"
title="Click to continue"
style="padding: 20px 0 0 0; margin: 0;"
[ngStyle]="{'color': (username ? '#3171e0' : '#666')}"
[disabled]="!username"
>Continue <i class="fa fa-chevron-right" style="font-size: 0.8em; margin-left: 10px;"></i></button>
</div>
<div class="button">
<button
(click)="coreid()"
*ngIf="step === 'username'"
title="Click to sign-in with Starship CoreID instead"
style="padding: 20px 0 0 0; margin: 0; color: #3171e0;"
>Sign-in with CoreID <i class="fa fa-chevron-right" style="font-size: 0.8em; margin-left: 10px;"></i></button>
</div>
</div>
<p class="error-message" *ngIf="errorMessage" style="color: darkred; font-size: 0.9em; margin-top: 30px;">{{ errorMessage }}</p>
</ion-col>
</ion-row>
</ion-grid>
</div>

View File

@ -0,0 +1,5 @@
.login-container {
height: 100%;
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,175 @@
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {AlertController, IonInput, LoadingController, ModalController, PopoverController} from '@ionic/angular';
import {EditorService} from '../../service/editor.service';
import {ApiService} from '../../service/api.service';
import {NavigationService} from '../../service/navigation.service';
import {AuthService} from '../../service/auth.service';
export type LoginPageStep = 'greeter' | 'username' | 'password' | 'create-account';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
public step: LoginPageStep = 'greeter';
@ViewChild('usernameInput') usernameInput: IonInput;
@ViewChild('passwordInput') passwordInput: IonInput;
@ViewChild('nameInput') nameInput: IonInput;
stepMessages = {
username: 'Enter a username to continue',
password: 'Enter your password to sign-in',
'create-account': 'Welcome! Create an account to continue',
};
public username = '';
public password = '';
public fullName = '';
public passwordConfirm = '';
public userExists?: boolean;
public userInfo: any = {};
public errorMessage = '';
public readonly coreidUrl: string;
constructor(
protected api: ApiService,
protected route: ActivatedRoute,
protected router: Router,
protected loader: LoadingController,
protected popover: PopoverController,
protected alerts: AlertController,
protected modals: ModalController,
public readonly editorService: EditorService,
public readonly navService: NavigationService,
public readonly auth: AuthService,
) {
this.coreidUrl = this.api._build_url('/auth/starship_coreid/login', '/');
}
ngOnInit() {}
async ionViewDidEnter() {
}
coreid() {
window.location.assign(this.coreidUrl);
}
async back() {
this.errorMessage = '';
if ( this.step === 'password' ) {
this.password = '';
this.step = 'username';
setTimeout(() => {
if ( this.usernameInput ) {
this.usernameInput.setFocus();
}
}, 150);
} else if ( this.step === 'create-account' ) {
this.fullName = '';
this.password = '';
this.passwordConfirm = '';
this.step = 'username';
setTimeout(() => {
if ( this.usernameInput ) {
this.usernameInput.setFocus();
}
}, 150);
}
}
async advance() {
this.errorMessage = '';
if ( this.step === 'greeter' ) {
this.step = 'username';
setTimeout(() => {
if ( this.usernameInput ) {
this.usernameInput.setFocus();
}
}, 150);
} else if ( this.step === 'username' ) {
if ( !this.username ) {
return;
}
const loader = await this.loader.create({ message: 'Checking username...' });
await loader.present();
this.userInfo = await this.api.getUserInfo(this.username);
this.userExists = !!this.userInfo?.exists;
if ( this.userExists ) {
this.step = 'password';
setTimeout(() => {
if ( this.passwordInput ) {
this.passwordInput.setFocus();
}
}, 150);
} else {
this.step = 'create-account';
setTimeout(() => {
if ( this.nameInput ) {
this.nameInput.setFocus();
}
}, 150);
}
await loader.dismiss();
} else if ( this.step === 'password' ) {
if ( !this.username || !this.password ) {
return;
}
const loader = await this.loader.create({ message: 'Signing you in...' });
await loader.present();
const result = await this.api.attemptLogin(this.username, this.password);
await loader.dismiss();
if ( !result.success ) {
this.errorMessage = result.message || 'Un unknown error has occurred';
} else {
this.auth.authInProgress = true;
this.navService.requestInitialization();
this.reset();
}
} else if ( this.step === 'create-account' ) {
if ( !this.username || !this.password || this.passwordConfirm !== this.password || !this.fullName ) {
return;
}
const loader = await this.loader.create({ message: 'Creating your account...' });
await loader.present();
const result = await this.api.attemptRegistration(this.username, this.password, this.passwordConfirm, this.fullName);
await loader.dismiss();
if ( !result.success ) {
this.errorMessage = result.message || 'Un unknown error has occurred';
} else {
this.auth.authInProgress = true;
this.navService.requestInitialization();
this.reset();
}
}
}
reset() {
this.username = '';
this.password = '';
this.passwordConfirm = '';
this.fullName = '';
this.step = 'greeter';
}
}

View File

@ -1401,4 +1401,66 @@ export class ApiService {
});
});
}
public getUserInfo(uid: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.get('/auth/user-info', { uid }).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public attemptLogin(uid: string, password: string): Promise<{ success: boolean, message?: string }> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post('/auth/attempt', { uid, password }).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public attemptRegistration(uid: string, password: string, passwordConfirmation: string, fullName: string):
Promise<{ success: boolean, message?: string }> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post('/auth/register', { uid, password, passwordConfirmation, fullName }).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public endSession(): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post('/auth/end-session').subscribe({
next: result => {
return res(result.data);
},
error: rej,
});
});
}
}

View File

@ -0,0 +1,59 @@
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
import {Injectable} from '@angular/core';
import {ApiService} from './api.service';
import {NavigationService} from './navigation.service';
import {DatabaseService} from './db/database.service';
import {LoadingController} from '@ionic/angular';
@Injectable({
providedIn: 'root',
})
export class AuthService implements CanActivate {
public authInProgress = false;
constructor(
protected readonly loader: LoadingController,
protected readonly api: ApiService,
protected readonly router: Router,
protected readonly nav: NavigationService,
protected readonly db: DatabaseService,
) { }
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const checkCanActivate = async () => {
const isAuthenticated = (this.api.isAuthenticated && !this.api.isPublicUser);
if ( !isAuthenticated ) {
await this.router.navigate(['/login']);
return false;
} else {
return true;
}
};
return new Promise(async res => {
if ( !this.nav.initialized$.getValue() ) {
const sub = this.nav.initialized$.subscribe(async initialized => {
if ( initialized ) {
sub.unsubscribe();
return res(await checkCanActivate());
}
});
} else {
return res(await checkCanActivate());
}
});
}
async endSession() {
const loader = await this.loader.create({ message: 'Logging you out...' });
await loader.present();
await this.db.purge();
await this.api.endSession();
await this.router.navigate(['/login']);
await loader.dismiss();
this.nav.requestInitialization();
}
}

View File

@ -0,0 +1,41 @@
import {CanActivate, Router} from '@angular/router';
import {ApiService} from '../api.service';
import {NavigationService} from '../navigation.service';
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class GuestOnlyGuard implements CanActivate {
constructor(
protected readonly api: ApiService,
protected readonly router: Router,
protected readonly nav: NavigationService,
) { }
async canActivate(): Promise<boolean> {
const checkCanActivate = async () => {
const isAuthenticated = (this.api.isAuthenticated && !this.api.isPublicUser);
if ( isAuthenticated ) {
await this.router.navigate(['/home']);
return false;
} else {
return true;
}
};
return new Promise(async res => {
if ( !this.nav.initialized$.getValue() ) {
const sub = this.nav.initialized$.subscribe(async initialized => {
if ( initialized ) {
sub.unsubscribe();
return res(await checkCanActivate());
}
});
} else {
return res(await checkCanActivate());
}
});
}
}

View File

@ -9,6 +9,8 @@ export class NavigationService {
protected refreshCount = 0;
public readonly sidebarRefresh$: BehaviorSubject<[number, boolean]> = new BehaviorSubject<[number, boolean]>([this.refreshCount, true]);
public readonly navigationRequest$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public readonly initializationRequest$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
public readonly initialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
requestSidebarRefresh({ quiet = false }) {
this.refreshCount += 1;
@ -21,4 +23,9 @@ export class NavigationService {
this.navigationRequest$.next(pageId);
}
requestInitialization() {
debug('Requesting application initialization');
this.initializationRequest$.next(this.initializationRequest$.getValue() + 1);
}
}