src/app/core/ui/ui/ui.component.ts

Description

The main user interface component as root element for the app structure which also ties different components together into the overall app layout.

Example

Metadata

Relationships

Index

Properties
Methods

Constructor

constructor()

Methods

closeSidenavOnMobile
closeSidenavOnMobile()
Returns : void
Async logout
logout()

Trigger logout of user.

Returns : any
navigateToProfile
navigateToProfile()
Returns : void

Properties

configReady$
Type : unknown
Default value : new BehaviorSubject<boolean>(false)
isAdminUser
Type : unknown
Default value : toSignal( this.sessionSubject.pipe( map((session) => session?.roles?.includes(ADMIN_APP_ROLE) ?? false), ), { initialValue: false }, )
isDesktop
Type : unknown
Default value : false
isLoggedIn
Type : Signal<boolean>
Default value : toSignal( this.loginState.pipe( map((loginState) => loginState === LoginState.LOGGED_IN), ), { initialValue: false }, )
showPrimaryAction
Type : unknown
Default value : signal(false)
sideNav
Type : unknown
Decorators :
@ViewChild('sideNav')

reference to sideNav component in template, required for toggling the menu on user actions

sideNavMode
Type : unknown
Default value : signal<MatDrawerMode>("side")

display mode for the menu to make it responsive and usable on smaller screens

siteSettings
Type : unknown
Default value : signal<SiteSettings>(new SiteSettings())

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;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""