src/app/core/ui/ui/ui.component.ts
The main user interface component as root element for the app structure which also ties different components together into the overall app layout.
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-ui |
| standalone | true |
| imports |
MatToolbarModule
MatButtonModule
FontAwesomeModule
Angulartics2Module
MatSidenavModule
|
| styleUrls | ./ui.component.scss |
| templateUrl | ./ui.component.html |
MatToolbarModule
MatButtonModule
FontAwesomeModule
RouterLink
Angulartics2Module
SearchComponent
SyncStatusComponent
MatSidenavModule
NavigationComponent
PwaInstallComponent
AppVersionComponent
RouterOutlet
PrimaryActionComponent
DisplayImgComponent
SetupWizardButtonComponent
NotificationComponent
GotoThirdPartySystemComponent
AssistantButtonComponent
PoweredByComponent
AsyncPipe
Properties |
Methods |
constructor()
|
|
Defined in src/app/core/ui/ui/ui.component.ts:125
|
| closeSidenavOnMobile |
closeSidenavOnMobile()
|
|
Defined in src/app/core/ui/ui/ui.component.ts:176
|
|
Returns :
void
|
| Async logout |
logout()
|
|
Defined in src/app/core/ui/ui/ui.component.ts:167
|
|
Trigger logout of user.
Returns :
any
|
| navigateToProfile |
navigateToProfile()
|
|
Defined in src/app/core/ui/ui/ui.component.ts:189
|
|
Returns :
void
|
| configReady$ |
Type : unknown
|
Default value : new BehaviorSubject<boolean>(false)
|
|
Defined in src/app/core/ui/ui/ui.component.ts:124
|
| isAdminUser |
Type : unknown
|
Default value : toSignal(
this.sessionSubject.pipe(
map((session) => session?.roles?.includes(ADMIN_APP_ROLE) ?? false),
),
{ initialValue: false },
)
|
|
Defined in src/app/core/ui/ui/ui.component.ts:182
|
| isDesktop |
Type : unknown
|
Default value : false
|
|
Defined in src/app/core/ui/ui/ui.component.ts:116
|
| isLoggedIn |
Type : Signal<boolean>
|
Default value : toSignal(
this.loginState.pipe(
map((loginState) => loginState === LoginState.LOGGED_IN),
),
{ initialValue: false },
)
|
|
Defined in src/app/core/ui/ui/ui.component.ts:117
|
| showPrimaryAction |
Type : unknown
|
Default value : signal(false)
|
|
Defined in src/app/core/ui/ui/ui.component.ts:125
|
| sideNav |
Type : unknown
|
Decorators :
@ViewChild('sideNav')
|
|
Defined in src/app/core/ui/ui/ui.component.ts:112
|
|
reference to sideNav component in template, required for toggling the menu on user actions |
| sideNavMode |
Type : unknown
|
Default value : signal<MatDrawerMode>("side")
|
|
Defined in src/app/core/ui/ui/ui.component.ts:109
|
|
display mode for the menu to make it responsive and usable on smaller screens |
| siteSettings |
Type : unknown
|
Default value : signal<SiteSettings>(new SiteSettings())
|
|
Defined in src/app/core/ui/ui/ui.component.ts:115
|
|
latest version of the site settings |
import {
ChangeDetectionStrategy,
Component,
inject,
Signal,
signal,
ViewChild,
} from "@angular/core";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { MatDrawerMode, MatSidenavModule } from "@angular/material/sidenav";
import { ScreenWidthObserver } from "../../../utils/media/screen-size-observer.service";
import { MatToolbarModule } from "@angular/material/toolbar";
import { AsyncPipe } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import {
NavigationEnd,
Router,
RouterLink,
RouterOutlet,
} from "@angular/router";
import { Angulartics2Module } from "angulartics2";
import { SearchComponent } from "../search/search.component";
import { SyncStatusComponent } from "../sync-status/sync-status/sync-status.component";
import { NavigationComponent } from "../navigation/navigation/navigation.component";
import { PwaInstallComponent } from "../../pwa-install/pwa-install.component";
import { AppVersionComponent } from "../latest-changes/app-version/app-version.component";
import { PrimaryActionComponent } from "../primary-action/primary-action.component";
import { SiteSettingsService } from "../../site-settings/site-settings.service";
import { DisplayImgComponent } from "../../../features/file/display-img/display-img.component";
import { SiteSettings } from "../../site-settings/site-settings";
import { SessionManagerService } from "../../session/session-service/session-manager.service";
import { SetupWizardButtonComponent } from "../../admin/setup-wizard/setup-wizard-button/setup-wizard-button.component";
import { NotificationComponent } from "../../../features/notification/notification.component";
import { GotoThirdPartySystemComponent } from "../../../features/third-party-authentication/goto-third-party-system/goto-third-party-system.component";
import { SetupService } from "app/core/setup/setup.service";
import { AssistantButtonComponent } from "../../setup/assistant-button/assistant-button.component";
import { PoweredByComponent } from "./powered-by/powered-by.component";
import { filter, map } from "rxjs/operators";
import { BehaviorSubject } from "rxjs";
import { LoginStateSubject } from "../../session/session-type";
import { toSignal } from "@angular/core/rxjs-interop";
import { LoginState } from "../../session/session-states/login-state.enum";
import { ConfigService } from "../../config/config.service";
import { SessionSubject } from "../../session/auth/session-info";
import { ADMIN_APP_ROLE } from "../../permissions/permission-types";
/**
* The main user interface component as root element for the app structure
* which also ties different components together into the overall app layout.
*/
@UntilDestroy()
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-ui",
templateUrl: "./ui.component.html",
styleUrls: ["./ui.component.scss"],
imports: [
MatToolbarModule,
MatButtonModule,
FontAwesomeModule,
RouterLink,
Angulartics2Module,
SearchComponent,
SyncStatusComponent,
MatSidenavModule,
NavigationComponent,
PwaInstallComponent,
AppVersionComponent,
RouterOutlet,
PrimaryActionComponent,
DisplayImgComponent,
SetupWizardButtonComponent,
NotificationComponent,
GotoThirdPartySystemComponent,
AssistantButtonComponent,
PoweredByComponent,
AsyncPipe,
],
})
export class UiComponent {
private screenWidthObserver = inject(ScreenWidthObserver);
private siteSettingsService = inject(SiteSettingsService);
private sessionManager = inject(SessionManagerService);
private setupService = inject(SetupService);
private router = inject(Router);
private loginState = inject(LoginStateSubject);
private configService = inject(ConfigService);
private readonly sessionSubject = inject(SessionSubject);
/** display mode for the menu to make it responsive and usable on smaller screens */
sideNavMode = signal<MatDrawerMode>("side");
/** reference to sideNav component in template, required for toggling the menu on user actions */
@ViewChild("sideNav") sideNav;
/** latest version of the site settings*/
siteSettings = signal<SiteSettings>(new SiteSettings());
isDesktop = false;
isLoggedIn: Signal<boolean> = toSignal(
this.loginState.pipe(
map((loginState) => loginState === LoginState.LOGGED_IN),
),
{ initialValue: false },
);
configReady$ = new BehaviorSubject<boolean>(false);
showPrimaryAction = signal(false);
constructor() {
this.screenWidthObserver
.platform()
.pipe(untilDestroyed(this))
.subscribe((isDesktop) => {
this.isDesktop = isDesktop;
this.updateDisplayMode();
});
this.router.events
.pipe(filter((e) => e instanceof NavigationEnd))
.subscribe(() => this.updateDisplayMode());
this.configReady$.subscribe((ready) => this.updateDisplayMode());
this.siteSettingsService.siteSettings.subscribe((s) =>
this.siteSettings.set(s),
);
if (this.configService.hasConfig()) {
this.configReady$.next(true);
} else {
this.setupService
.waitForConfigReady(true)
.then((ready) => this.configReady$.next(ready));
}
}
private updateDisplayMode() {
const currentUrl = this.router.url;
const configFullscreen =
currentUrl.startsWith("/admin/entity/") ||
currentUrl.startsWith("/admin/dashboard") ||
currentUrl.startsWith("/admin/matching");
this.sideNavMode.set(configFullscreen || !this.isDesktop ? "over" : "side");
this.showPrimaryAction.set(this.configReady$.value && !configFullscreen);
}
/**
* Trigger logout of user.
*/
async logout() {
this.sessionManager.logout();
// Re-evaluate config state to update UI layout (e.g., hide toolbar and sidebar after logout)
this.setupService
.waitForConfigReady()
.then((ready) => this.configReady$.next(ready));
}
closeSidenavOnMobile() {
if (this.sideNavMode() === "over") {
this.sideNav.close();
}
}
isAdminUser = toSignal(
this.sessionSubject.pipe(
map((session) => session?.roles?.includes(ADMIN_APP_ROLE) ?? false),
),
{ initialValue: false },
);
navigateToProfile(): void {
this.closeSidenavOnMobile();
this.router.navigate(["user-account"]);
}
}
<!--
~ This file is part of ndb-core.
~
~ ndb-core is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ ndb-core is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
-->
<!-- HEADER TOOLBAR -->
<mat-toolbar color="primary" class="ui-toolbar">
<!-- Left items -->
<div class="flex-row align-center">
@if ((configReady$ | async) && sideNavMode() === "over" && isLoggedIn()) {
<span>
<button mat-icon-button (click)="sideNav.toggle()">
<fa-icon class="header-icon" icon="bars"></fa-icon>
</button>
</span>
}
@if (
(sideNavMode() !== "over" || !isLoggedIn()) &&
!siteSettings().hideSiteNameInToolbar
) {
<a
[routerLink]="['']"
class="header-title"
angulartics2On="click"
angularticsCategory="Navigation"
angularticsAction="navbar_site_title_link"
>
{{ siteSettings().siteName }}
</a>
}
</div>
<!--top right icons and search-->
<div
class="flex-row align-center flex-grow-1-3 justify-content-end"
[class.gap-small]="isDesktop"
>
@if (isLoggedIn()) {
<app-search [class.full-width]="isDesktop"></app-search>
<app-sync-status></app-sync-status>
<app-notification />
}
@if (isLoggedIn()) {
<app-assistant-button></app-assistant-button>
}
</div>
</mat-toolbar>
<!-- MAIN NAVIGATION + CONTENT -->
<mat-sidenav-container (backdropClick)="closeSidenavOnMobile()" autosize>
@if ((configReady$ | async) && isLoggedIn()) {
<mat-sidenav
#sideNav
[autoFocus]="false"
[mode]="sideNavMode()"
[fixedInViewport]="true"
[opened]="sideNavMode() === 'side'"
class="sidenav-menu"
disableClose
>
<div
class="flex-column justify-space-between full-height overflow-y-hidden"
>
<div class="toolbar-spacer"></div>
@if (siteSettings().logo) {
<app-display-img
[entity]="siteSettings()"
imgProperty="logo"
class="site-logo"
></app-display-img>
}
<app-navigation
class="overflow-auto-y"
(click)="closeSidenavOnMobile()"
></app-navigation>
<div class="flex-grow"></div>
<div class="flex-column">
<app-goto-third-party-system></app-goto-third-party-system>
<app-setup-wizard-button></app-setup-wizard-button>
<app-pwa-install></app-pwa-install>
<!-- Admin Overview Button -->
@if (isAdminUser()) {
<button
(click)="closeSidenavOnMobile()"
mat-button
routerLink="/admin"
class="footer-cell admin-overview-button"
>
<fa-icon icon="wrench" class="standard-icon-with-text"></fa-icon>
<span i18n="Navigate to settings page">Settings</span>
</button>
}
<div class="flex-row">
<button
(click)="navigateToProfile()"
mat-button
class="footer-cell width-1-2"
>
<fa-icon icon="user" class="standard-icon-with-text"></fa-icon>
<span i18n="Navigate to user profile page">Profile</span>
</button>
<button mat-button (click)="logout()" class="footer-cell width-1-2">
<fa-icon
icon="sign-out-alt"
class="standard-icon-with-text"
></fa-icon>
<span i18n="Sign out of the app">Sign out</span>
</button>
</div>
<div class="flex-row">
<button
mat-button
class="footer-cell full-width padding-small"
style="height: 100%"
(click)="version.showLatestChanges()"
>
<app-version #version></app-version>
<app-powered-by></app-powered-by>
</button>
</div>
</div>
</div>
</mat-sidenav>
}
<mat-sidenav-content class="sidenav-content">
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>
@if (showPrimaryAction() && isLoggedIn()) {
<app-primary-action></app-primary-action>
}
./ui.component.scss
@use "@angular/material/core/style/elevation" as mat-elevation;
@use "variables/sizes";
@use "variables/colors";
@use "variables/breakpoints";
/*
* outermost elements; make them as tall as possible.
* Consumes all of the available screen
*/
html,
body {
/* height: 100%; */
}
/** Toolbar **/
/*
* Directive for the only toolbar row.
* Both of these directives are required to make the shadow show
*/
.ui-toolbar {
position: sticky;
top: 0;
padding-right: 0px;
z-index: 2;
justify-content: space-between;
@include mat-elevation.elevation(2);
}
.search-field {
/*
* Use a fixed width here because
* the form field does not work with
* relative / computed sizes
*/
width: 450px;
}
.header-icon {
color: white;
font-size: 15pt;
}
/** App content **/
:host {
background-color: colors.$background;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
/*
This only targets iOs devices. Note that (just like the line below), this is
a hacky solution. While it is practically guaranteed that only iOs will have this
property, it is not guaranteed that iOs will keep this property for ever.
*/
@supports (-webkit-touch-callout: none) {
/* mobile viewport bug fix.
The viewport height on iOs is not necessarily the visible screen size.
There might be a bar on the top of the screen. The following line
fixes this, but since this is no official css feature, this line should
be replaced as soon as a nicer solution exists.
*/
height: -webkit-fill-available;
}
}
.sidenav-menu {
background-color: colors.$background-secondary;
}
.toolbar-spacer {
margin-top: var(--mat-toolbar-standard-height, 64px);
}
::ng-deep .cdk-overlay-pane.mat-mdc-dialog-panel {
margin-top: var(--toolbar-height) !important;
}
/** Main content **/
mat-sidenav-container {
background: none;
}
/**
* reset the styles from the anchor element
*/
.header-title {
color: white;
text-decoration: none;
}
/**
* Remove the right border so that the background color
* merges into the main element
*/
.mat-drawer-side {
border-right: 0;
}
.site-logo {
width: 180px;
padding: 8px;
margin: auto;
}
.footer-cell {
border-top: solid 1px rgba(0, 0, 0, 0.12);
border-radius: 0;
display: flex;
place-content: center;
place-items: center;
min-width: fit-content;
box-sizing: border-box;
&:not(:last-child) {
border-right: solid 1px rgba(0, 0, 0, 0.12);
}
}
.admin-overview-button {
width: 100%;
border-right: none !important;
}
.info-icon {
margin: auto;
font-size: 20px;
cursor: pointer;
}
.info-button {
width: 48px;
border-right: solid 1px rgba(0, 0, 0, 0.12);
border-radius: 0;
overflow: hidden;
}
.justify-content-end {
justify-content: end;
}
app-powered-by ::ng-deep .powered-by {
/* make powered-by more dense with version */
padding-top: 0;
}