Browse Source

Task/improve type safety of header component (#6913)

* feat(common): allow null for filters

* feat(client): implement login with access token dialog result interface

* feat(client): enforce encapsulation

* feat(client): enforce immutability

* feat(client): replace constructor based DI with inject function

* feat(client): implement view child signals

* feat(client): implement output signals

* feat(client): implement input signals

* fix(client): format html file
pull/6872/head^2
Kenrick Tandrian 2 days ago
committed by GitHub
parent
commit
cde519fa7b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 121
      apps/client/src/app/components/header/header.component.html
  2. 211
      apps/client/src/app/components/header/header.component.ts
  3. 4
      apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts
  4. 10
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts
  5. 10
      libs/common/src/lib/dtos/update-user-setting.dto.ts

121
apps/client/src/app/components/header/header.component.html

@ -1,14 +1,14 @@
<mat-toolbar class="px-0"> <mat-toolbar class="px-0">
@if (user) { @if (user()) {
<div class="d-flex h-100 logo-container" [class.filled]="hasTabs"> <div class="d-flex h-100 logo-container" [class.filled]="hasTabs()">
<a <a
class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0" class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0"
mat-button mat-button
[class.w-100]="hasTabs" [class.w-100]="hasTabs()"
[routerLink]="['/']" [routerLink]="['/']"
(click)="onLogoClick()" (click)="onLogoClick()"
> >
<gf-logo [label]="pageTitle" /> <gf-logo [label]="pageTitle()" />
</a> </a>
</div> </div>
<span class="gf-spacer"></span> <span class="gf-spacer"></span>
@ -20,11 +20,11 @@
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': 'font-weight-bold':
currentRoute === internalRoutes.home.path || currentRoute() === internalRoutes.home.path ||
currentRoute === internalRoutes.zen.path, currentRoute() === internalRoutes.zen.path,
'text-decoration-underline': 'text-decoration-underline':
currentRoute === internalRoutes.home.path || currentRoute() === internalRoutes.home.path ||
currentRoute === internalRoutes.zen.path currentRoute() === internalRoutes.zen.path
}" }"
[routerLink]="['/']" [routerLink]="['/']"
>Overview</a >Overview</a
@ -36,9 +36,10 @@
i18n i18n
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === internalRoutes.portfolio.path, 'font-weight-bold':
currentRoute() === internalRoutes.portfolio.path,
'text-decoration-underline': 'text-decoration-underline':
currentRoute === internalRoutes.portfolio.path currentRoute() === internalRoutes.portfolio.path
}" }"
[routerLink]="routerLinkPortfolio" [routerLink]="routerLinkPortfolio"
>Portfolio</a >Portfolio</a
@ -50,9 +51,9 @@
i18n i18n
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === internalRoutes.accounts.path, 'font-weight-bold': currentRoute() === internalRoutes.accounts.path,
'text-decoration-underline': 'text-decoration-underline':
currentRoute === internalRoutes.accounts.path currentRoute() === internalRoutes.accounts.path
}" }"
[routerLink]="routerLinkAccounts" [routerLink]="routerLinkAccounts"
>Accounts</a >Accounts</a
@ -66,9 +67,9 @@
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': 'font-weight-bold':
currentRoute === internalRoutes.adminControl.path, currentRoute() === internalRoutes.adminControl.path,
'text-decoration-underline': 'text-decoration-underline':
currentRoute === internalRoutes.adminControl.path currentRoute() === internalRoutes.adminControl.path
}" }"
[routerLink]="routerLinkAdminControl" [routerLink]="routerLinkAdminControl"
>Admin Control</a >Admin Control</a
@ -81,29 +82,29 @@
i18n i18n
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === routeResources, 'font-weight-bold': currentRoute() === routeResources,
'text-decoration-underline': currentRoute === routeResources 'text-decoration-underline': currentRoute() === routeResources
}" }"
[routerLink]="routerLinkResources" [routerLink]="routerLinkResources"
>Resources</a >Resources</a
> >
</li> </li>
@if ( @if (
hasPermissionForSubscription && user?.subscription?.type === 'Basic' hasPermissionForSubscription && user()?.subscription?.type === 'Basic'
) { ) {
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block rounded" class="d-none d-sm-block rounded"
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === routePricing, 'font-weight-bold': currentRoute() === routePricing,
'text-decoration-underline': currentRoute === routePricing 'text-decoration-underline': currentRoute() === routePricing
}" }"
[routerLink]="routerLinkPricing" [routerLink]="routerLinkPricing"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<span i18n>Pricing</span> <span i18n>Pricing</span>
@if (currentRoute !== routePricing && hasPromotion) { @if (currentRoute() !== routePricing && hasPromotion()) {
<span class="badge badge-warning ml-1">%</span> <span class="badge badge-warning ml-1">%</span>
} }
</span> </span>
@ -116,8 +117,8 @@
i18n i18n
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === routeAbout, 'font-weight-bold': currentRoute() === routeAbout,
'text-decoration-underline': currentRoute === routeAbout 'text-decoration-underline': currentRoute() === routeAbout
}" }"
[routerLink]="routerLinkAbout" [routerLink]="routerLinkAbout"
>About</a >About</a
@ -131,7 +132,7 @@
matBadge="&NoBreak;" matBadge="&NoBreak;"
matBadgeSize="small" matBadgeSize="small"
matButton matButton
[matBadgeHidden]="!hasFilters || !hasPermissionToChangeFilters" [matBadgeHidden]="!hasFilters || !hasPermissionToChangeFilters()"
[matMenuTriggerFor]="assistantMenu" [matMenuTriggerFor]="assistantMenu"
[matMenuTriggerRestoreFocus]="false" [matMenuTriggerRestoreFocus]="false"
(menuOpened)="onOpenAssistant()" (menuOpened)="onOpenAssistant()"
@ -143,17 +144,19 @@
class="no-max-width" class="no-max-width"
xPosition="before" xPosition="before"
[overlapTrigger]="true" [overlapTrigger]="true"
(closed)="assistantElement?.setIsOpen(false)" (closed)="assistantElement()?.setIsOpen(false)"
> >
<gf-assistant <gf-assistant
#assistant #assistant
[deviceType]="deviceType" [deviceType]="deviceType()"
[hasPermissionToAccessAdminControl]=" [hasPermissionToAccessAdminControl]="
hasPermissionToAccessAdminControl hasPermissionToAccessAdminControl
" "
[hasPermissionToChangeDateRange]="hasPermissionToChangeDateRange" [hasPermissionToChangeDateRange]="
[hasPermissionToChangeFilters]="hasPermissionToChangeFilters" hasPermissionToChangeDateRange()
[user]="user" "
[hasPermissionToChangeFilters]="hasPermissionToChangeFilters()"
[user]="user()"
(closed)="closeAssistant()" (closed)="closeAssistant()"
(dateRangeChanged)="onDateRangeChange($event)" (dateRangeChanged)="onDateRangeChange($event)"
(filtersChanged)="onFiltersChanged($event)" (filtersChanged)="onFiltersChanged($event)"
@ -182,12 +185,13 @@
</button> </button>
<mat-menu #accountMenu="matMenu" xPosition="before"> <mat-menu #accountMenu="matMenu" xPosition="before">
@if ( @if (
hasPermissionForSubscription && user?.subscription?.type === 'Basic' hasPermissionForSubscription &&
user()?.subscription?.type === 'Basic'
) { ) {
<a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing" <a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing"
><span class="align-items-center d-flex" ><span class="align-items-center d-flex"
><span> ><span>
@if (user.subscription.offer.isRenewal) { @if (user().subscription.offer.isRenewal) {
<ng-container i18n>Renew Plan</ng-container> <ng-container i18n>Renew Plan</ng-container>
} @else { } @else {
<ng-container i18n>Upgrade Plan</ng-container> <ng-container i18n>Upgrade Plan</ng-container>
@ -199,7 +203,7 @@
></a> ></a>
<hr class="m-0" /> <hr class="m-0" />
} }
@if (user?.access?.length > 0) { @if (user()?.access?.length > 0) {
<button mat-menu-item (click)="impersonateAccount(null)"> <button mat-menu-item (click)="impersonateAccount(null)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon <ion-icon
@ -213,7 +217,7 @@
<span i18n>Me</span> <span i18n>Me</span>
</span> </span>
</button> </button>
@for (accessItem of user?.access; track accessItem) { @for (accessItem of user()?.access; track accessItem) {
<button mat-menu-item (click)="impersonateAccount(accessItem.id)"> <button mat-menu-item (click)="impersonateAccount(accessItem.id)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon <ion-icon
@ -240,8 +244,8 @@
i18n i18n
mat-menu-item mat-menu-item
[class.font-weight-bold]=" [class.font-weight-bold]="
currentRoute === internalRoutes.home.path || currentRoute() === internalRoutes.home.path ||
currentRoute === internalRoutes.zen.path currentRoute() === internalRoutes.zen.path
" "
[routerLink]="['/']" [routerLink]="['/']"
>Overview</a >Overview</a
@ -251,7 +255,7 @@
i18n i18n
mat-menu-item mat-menu-item
[class.font-weight-bold]=" [class.font-weight-bold]="
currentRoute === internalRoutes.portfolio.path currentRoute() === internalRoutes.portfolio.path
" "
[routerLink]="routerLinkPortfolio" [routerLink]="routerLinkPortfolio"
>Portfolio</a >Portfolio</a
@ -261,7 +265,7 @@
i18n i18n
mat-menu-item mat-menu-item
[class.font-weight-bold]=" [class.font-weight-bold]="
currentRoute === internalRoutes.accounts.path currentRoute() === internalRoutes.accounts.path
" "
[routerLink]="routerLinkAccounts" [routerLink]="routerLinkAccounts"
>Accounts</a >Accounts</a
@ -270,7 +274,7 @@
i18n i18n
mat-menu-item mat-menu-item
[class.font-weight-bold]=" [class.font-weight-bold]="
currentRoute === internalRoutes.account.path currentRoute() === internalRoutes.account.path
" "
[routerLink]="routerLinkAccount" [routerLink]="routerLinkAccount"
>My Ghostfolio</a >My Ghostfolio</a
@ -281,7 +285,7 @@
i18n i18n
mat-menu-item mat-menu-item
[class.font-weight-bold]=" [class.font-weight-bold]="
currentRoute === internalRoutes.adminControl.path currentRoute() === internalRoutes.adminControl.path
" "
[routerLink]="routerLinkAdminControl" [routerLink]="routerLinkAdminControl"
>Admin Control</a >Admin Control</a
@ -292,22 +296,23 @@
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
[class.font-weight-bold]="currentRoute === routeResources" [class.font-weight-bold]="currentRoute() === routeResources"
[routerLink]="routerLinkResources" [routerLink]="routerLinkResources"
>Resources</a >Resources</a
> >
@if ( @if (
hasPermissionForSubscription && user?.subscription?.type === 'Basic' hasPermissionForSubscription &&
user()?.subscription?.type === 'Basic'
) { ) {
<a <a
class="d-flex d-sm-none" class="d-flex d-sm-none"
mat-menu-item mat-menu-item
[class.font-weight-bold]="currentRoute === routePricing" [class.font-weight-bold]="currentRoute() === routePricing"
[routerLink]="routerLinkPricing" [routerLink]="routerLinkPricing"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<span i18n>Pricing</span> <span i18n>Pricing</span>
@if (currentRoute !== routePricing && hasPromotion) { @if (currentRoute() !== routePricing && hasPromotion()) {
<span class="badge badge-warning ml-1">%</span> <span class="badge badge-warning ml-1">%</span>
} }
</span> </span>
@ -317,7 +322,7 @@
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
[class.font-weight-bold]="currentRoute === routeAbout" [class.font-weight-bold]="currentRoute() === routeAbout"
[routerLink]="routerLinkAbout" [routerLink]="routerLinkAbout"
>About Ghostfolio</a >About Ghostfolio</a
> >
@ -327,17 +332,17 @@
</li> </li>
</ul> </ul>
} }
@if (user === null) { @if (user() === null) {
<div class="d-flex h-100 logo-container" [class.filled]="hasTabs"> <div class="d-flex h-100 logo-container" [class.filled]="hasTabs()">
<a <a
class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0" class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0"
mat-button mat-button
[class.w-100]="hasTabs" [class.w-100]="hasTabs()"
[routerLink]="['/']" [routerLink]="['/']"
> >
<gf-logo <gf-logo
[label]="pageTitle" [label]="pageTitle()"
[showLabel]="currentRoute !== 'register'" [showLabel]="currentRoute() !== 'register'"
/> />
</a> </a>
</div> </div>
@ -349,8 +354,8 @@
i18n i18n
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === routeFeatures, 'font-weight-bold': currentRoute() === routeFeatures,
'text-decoration-underline': currentRoute === routeFeatures 'text-decoration-underline': currentRoute() === routeFeatures
}" }"
[routerLink]="routerLinkFeatures" [routerLink]="routerLinkFeatures"
>Features</a >Features</a
@ -362,8 +367,8 @@
i18n i18n
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === routeAbout, 'font-weight-bold': currentRoute() === routeAbout,
'text-decoration-underline': currentRoute === routeAbout 'text-decoration-underline': currentRoute() === routeAbout
}" }"
[routerLink]="routerLinkAbout" [routerLink]="routerLinkAbout"
>About</a >About</a
@ -375,14 +380,14 @@
class="d-sm-block rounded" class="d-sm-block rounded"
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === routePricing, 'font-weight-bold': currentRoute() === routePricing,
'text-decoration-underline': currentRoute === routePricing 'text-decoration-underline': currentRoute() === routePricing
}" }"
[routerLink]="routerLinkPricing" [routerLink]="routerLinkPricing"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<span i18n>Pricing</span> <span i18n>Pricing</span>
@if (currentRoute !== routePricing && hasPromotion) { @if (currentRoute() !== routePricing && hasPromotion()) {
<span class="badge badge-warning ml-1">%</span> <span class="badge badge-warning ml-1">%</span>
} }
</span> </span>
@ -396,8 +401,8 @@
i18n i18n
mat-button mat-button
[class]="{ [class]="{
'font-weight-bold': currentRoute === routeMarkets, 'font-weight-bold': currentRoute() === routeMarkets,
'text-decoration-underline': currentRoute === routeMarkets 'text-decoration-underline': currentRoute() === routeMarkets
}" }"
[routerLink]="routerLinkMarkets" [routerLink]="routerLinkMarkets"
>Markets</a >Markets</a
@ -421,7 +426,7 @@
<ng-container i18n>Sign in</ng-container> <ng-container i18n>Sign in</ng-container>
</button> </button>
</li> </li>
@if (currentRoute !== 'register' && hasPermissionToCreateUser) { @if (currentRoute() !== 'register' && hasPermissionToCreateUser) {
<li class="list-inline-item ml-1"> <li class="list-inline-item ml-1">
<a <a
class="d-none d-sm-block px-3 rounded" class="d-none d-sm-block px-3 rounded"

211
apps/client/src/app/components/header/header.component.ts

@ -1,4 +1,7 @@
import { LoginWithAccessTokenDialogParams } from '@ghostfolio/client/components/login-with-access-token-dialog/interfaces/interfaces'; import {
LoginWithAccessTokenDialogParams,
LoginWithAccessTokenDialogResult
} from '@ghostfolio/client/components/login-with-access-token-dialog/interfaces/interfaces';
import { GfLoginWithAccessTokenDialogComponent } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component'; import { GfLoginWithAccessTokenDialogComponent } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { LayoutService } from '@ghostfolio/client/core/layout.service'; import { LayoutService } from '@ghostfolio/client/core/layout.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
@ -24,12 +27,12 @@ import {
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
DestroyRef, DestroyRef,
EventEmitter,
HostListener, HostListener,
Input, inject,
input,
OnChanges, OnChanges,
Output, output,
ViewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatBadgeModule } from '@angular/material/badge'; import { MatBadgeModule } from '@angular/material/badge';
@ -71,78 +74,67 @@ import { catchError } from 'rxjs/operators';
styleUrls: ['./header.component.scss'] styleUrls: ['./header.component.scss']
}) })
export class GfHeaderComponent implements OnChanges { export class GfHeaderComponent implements OnChanges {
@HostListener('window:keydown', ['$event']) public readonly currentRoute = input.required<string>();
openAssistantWithHotKey(event: KeyboardEvent) { public readonly deviceType = input.required<string>();
if ( public readonly hasPermissionToChangeDateRange = input.required<boolean>();
event.key === '/' && public readonly hasPermissionToChangeFilters = input.required<boolean>();
event.target instanceof Element && public readonly hasPromotion = input.required<boolean>();
event.target?.nodeName?.toLowerCase() !== 'input' && public readonly hasTabs = input.required<boolean>();
event.target?.nodeName?.toLowerCase() !== 'textarea' && public readonly info = input.required<InfoItem | undefined>();
this.hasPermissionToAccessAssistant public readonly pageTitle = input.required<string>();
) { public readonly user = input.required<User | undefined>();
this.assistantElement.setIsOpen(true);
this.assistentMenuTriggerElement.openMenu(); public readonly signOut = output<void>();
event.preventDefault(); protected readonly assistantElement =
} viewChild.required<GfAssistantComponent>('assistant');
} protected readonly assistentMenuTriggerElement =
viewChild.required<MatMenuTrigger>('assistantTrigger');
@Input() currentRoute: string;
@Input() deviceType: string; protected hasFilters: boolean;
@Input() hasPermissionToChangeDateRange: boolean; protected hasImpersonationId: boolean;
@Input() hasPermissionToChangeFilters: boolean; protected hasPermissionForAuthGoogle: boolean;
@Input() hasPromotion: boolean; protected hasPermissionForAuthOidc: boolean;
@Input() hasTabs: boolean; protected hasPermissionForAuthToken: boolean;
@Input() info: InfoItem; protected hasPermissionForSubscription: boolean;
@Input() pageTitle: string; protected hasPermissionToAccessAdminControl: boolean;
@Input() user: User; protected hasPermissionToAccessAssistant: boolean;
protected hasPermissionToAccessFearAndGreedIndex: boolean;
@Output() signOut = new EventEmitter<void>(); protected hasPermissionToCreateUser: boolean;
protected impersonationId: string;
@ViewChild('assistant') assistantElement: GfAssistantComponent; protected readonly internalRoutes = internalRoutes;
@ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger; protected isMenuOpen: boolean;
protected readonly routeAbout = publicRoutes.about.path;
public hasFilters: boolean; protected readonly routeFeatures = publicRoutes.features.path;
public hasImpersonationId: boolean; protected readonly routeMarkets = publicRoutes.markets.path;
public hasPermissionForAuthGoogle: boolean; protected readonly routePricing = publicRoutes.pricing.path;
public hasPermissionForAuthOidc: boolean; protected readonly routeResources = publicRoutes.resources.path;
public hasPermissionForAuthToken: boolean; protected readonly routerLinkAbout = publicRoutes.about.routerLink;
public hasPermissionForSubscription: boolean; protected readonly routerLinkAccount = internalRoutes.account.routerLink;
public hasPermissionToAccessAdminControl: boolean; protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink;
public hasPermissionToAccessAssistant: boolean; protected readonly routerLinkAdminControl =
public hasPermissionToAccessFearAndGreedIndex: boolean; internalRoutes.adminControl.routerLink;
public hasPermissionToCreateUser: boolean; protected readonly routerLinkFeatures = publicRoutes.features.routerLink;
public impersonationId: string; protected readonly routerLinkMarkets = publicRoutes.markets.routerLink;
public internalRoutes = internalRoutes; protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink;
public isMenuOpen: boolean; protected readonly routerLinkPricing = publicRoutes.pricing.routerLink;
public routeAbout = publicRoutes.about.path; protected readonly routerLinkRegister = publicRoutes.register.routerLink;
public routeFeatures = publicRoutes.features.path; protected readonly routerLinkResources = publicRoutes.resources.routerLink;
public routeMarkets = publicRoutes.markets.path;
public routePricing = publicRoutes.pricing.path; private readonly dataService = inject(DataService);
public routeResources = publicRoutes.resources.path; private readonly destroyRef = inject(DestroyRef);
public routerLinkAbout = publicRoutes.about.routerLink; private readonly dialog = inject(MatDialog);
public routerLinkAccount = internalRoutes.account.routerLink; private readonly impersonationStorageService = inject(
public routerLinkAccounts = internalRoutes.accounts.routerLink; ImpersonationStorageService
public routerLinkAdminControl = internalRoutes.adminControl.routerLink; );
public routerLinkFeatures = publicRoutes.features.routerLink; private readonly layoutService = inject(LayoutService);
public routerLinkMarkets = publicRoutes.markets.routerLink; private readonly notificationService = inject(NotificationService);
public routerLinkPortfolio = internalRoutes.portfolio.routerLink; private readonly router = inject(Router);
public routerLinkPricing = publicRoutes.pricing.routerLink; private readonly settingsStorageService = inject(SettingsStorageService);
public routerLinkRegister = publicRoutes.register.routerLink; private readonly tokenStorageService = inject(TokenStorageService);
public routerLinkResources = publicRoutes.resources.routerLink; private readonly userService = inject(UserService);
public constructor( public constructor() {
private dataService: DataService,
private destroyRef: DestroyRef,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService,
private notificationService: NotificationService,
private router: Router,
private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -162,55 +154,71 @@ export class GfHeaderComponent implements OnChanges {
}); });
} }
@HostListener('window:keydown', ['$event'])
protected openAssistantWithHotKey(event: KeyboardEvent) {
if (
event.key === '/' &&
event.target instanceof Element &&
event.target?.nodeName?.toLowerCase() !== 'input' &&
event.target?.nodeName?.toLowerCase() !== 'textarea' &&
this.hasPermissionToAccessAssistant
) {
this.assistantElement().setIsOpen(true);
this.assistentMenuTriggerElement().openMenu();
event.preventDefault();
}
}
public ngOnChanges() { public ngOnChanges() {
this.hasFilters = this.userService.hasFilters(); this.hasFilters = this.userService.hasFilters();
this.hasPermissionForAuthGoogle = hasPermission( this.hasPermissionForAuthGoogle = hasPermission(
this.info?.globalPermissions, this.info()?.globalPermissions,
permissions.enableAuthGoogle permissions.enableAuthGoogle
); );
this.hasPermissionForAuthOidc = hasPermission( this.hasPermissionForAuthOidc = hasPermission(
this.info?.globalPermissions, this.info()?.globalPermissions,
permissions.enableAuthOidc permissions.enableAuthOidc
); );
this.hasPermissionForAuthToken = hasPermission( this.hasPermissionForAuthToken = hasPermission(
this.info?.globalPermissions, this.info()?.globalPermissions,
permissions.enableAuthToken permissions.enableAuthToken
); );
this.hasPermissionForSubscription = hasPermission( this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions, this.info()?.globalPermissions,
permissions.enableSubscription permissions.enableSubscription
); );
this.hasPermissionToAccessAdminControl = hasPermission( this.hasPermissionToAccessAdminControl = hasPermission(
this.user?.permissions, this.user()?.permissions,
permissions.accessAdminControl permissions.accessAdminControl
); );
this.hasPermissionToAccessAssistant = hasPermission( this.hasPermissionToAccessAssistant = hasPermission(
this.user?.permissions, this.user()?.permissions,
permissions.accessAssistant permissions.accessAssistant
); );
this.hasPermissionToAccessFearAndGreedIndex = hasPermission( this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions, this.info()?.globalPermissions,
permissions.enableFearAndGreedIndex permissions.enableFearAndGreedIndex
); );
this.hasPermissionToCreateUser = hasPermission( this.hasPermissionToCreateUser = hasPermission(
this.info?.globalPermissions, this.info()?.globalPermissions,
permissions.createUserAccount permissions.createUserAccount
); );
} }
public closeAssistant() { protected closeAssistant() {
this.assistentMenuTriggerElement?.closeMenu(); this.assistentMenuTriggerElement().closeMenu();
} }
public impersonateAccount(aId: string) { protected impersonateAccount(aId: string) {
if (aId) { if (aId) {
this.impersonationStorageService.setId(aId); this.impersonationStorageService.setId(aId);
} else { } else {
@ -220,7 +228,7 @@ export class GfHeaderComponent implements OnChanges {
window.location.reload(); window.location.reload();
} }
public onDateRangeChange(dateRange: DateRange) { protected onDateRangeChange(dateRange: DateRange) {
this.dataService this.dataService
.putUserSetting({ dateRange }) .putUserSetting({ dateRange })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -232,7 +240,7 @@ export class GfHeaderComponent implements OnChanges {
}); });
} }
public onFiltersChanged(filters: Filter[]) { protected onFiltersChanged(filters: Filter[]) {
const userSetting: UpdateUserSettingDto = {}; const userSetting: UpdateUserSettingDto = {};
for (const filter of filters) { for (const filter of filters) {
@ -260,32 +268,33 @@ export class GfHeaderComponent implements OnChanges {
}); });
} }
public onLogoClick() { protected onLogoClick() {
if (['home', 'zen'].includes(this.currentRoute)) { if (['home', 'zen'].includes(this.currentRoute())) {
this.layoutService.getShouldReloadSubject().next(); this.layoutService.getShouldReloadSubject().next();
} }
} }
public onMenuClosed() { protected onMenuClosed() {
this.isMenuOpen = false; this.isMenuOpen = false;
} }
public onMenuOpened() { protected onMenuOpened() {
this.isMenuOpen = true; this.isMenuOpen = true;
} }
public onOpenAssistant() { protected onOpenAssistant() {
this.assistantElement.initialize(); this.assistantElement().initialize();
} }
public onSignOut() { protected onSignOut() {
this.signOut.next(); this.signOut.emit();
} }
public openLoginDialog() { protected openLoginDialog() {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfLoginWithAccessTokenDialogComponent, GfLoginWithAccessTokenDialogComponent,
LoginWithAccessTokenDialogParams LoginWithAccessTokenDialogParams,
LoginWithAccessTokenDialogResult
>(GfLoginWithAccessTokenDialogComponent, { >(GfLoginWithAccessTokenDialogComponent, {
autoFocus: false, autoFocus: false,
data: { data: {
@ -322,7 +331,7 @@ export class GfHeaderComponent implements OnChanges {
}); });
} }
public setToken(aToken: string) { private setToken(aToken: string) {
this.tokenStorageService.saveToken( this.tokenStorageService.saveToken(
aToken, aToken,
this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true' this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true'

4
apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts

@ -5,3 +5,7 @@ export interface LoginWithAccessTokenDialogParams {
hasPermissionToUseAuthToken: boolean; hasPermissionToUseAuthToken: boolean;
title: string; title: string;
} }
export interface LoginWithAccessTokenDialogResult {
accessToken: string | null;
}

10
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts

@ -22,7 +22,10 @@ import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { eyeOffOutline, eyeOutline } from 'ionicons/icons'; import { eyeOffOutline, eyeOutline } from 'ionicons/icons';
import { LoginWithAccessTokenDialogParams } from './interfaces/interfaces'; import {
LoginWithAccessTokenDialogParams,
LoginWithAccessTokenDialogResult
} from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -49,7 +52,10 @@ export class GfLoginWithAccessTokenDialogComponent {
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: LoginWithAccessTokenDialogParams, @Inject(MAT_DIALOG_DATA) public data: LoginWithAccessTokenDialogParams,
public dialogRef: MatDialogRef<GfLoginWithAccessTokenDialogComponent>, public dialogRef: MatDialogRef<
GfLoginWithAccessTokenDialogComponent,
LoginWithAccessTokenDialogResult
>,
private settingsStorageService: SettingsStorageService private settingsStorageService: SettingsStorageService
) { ) {
addIcons({ eyeOffOutline, eyeOutline }); addIcons({ eyeOffOutline, eyeOutline });

10
libs/common/src/lib/dtos/update-user-setting.dto.ts

@ -58,23 +58,23 @@ export class UpdateUserSettingDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
'filters.accounts'?: string[]; 'filters.accounts'?: string[] | null;
@IsArray() @IsArray()
@IsOptional() @IsOptional()
'filters.assetClasses'?: string[]; 'filters.assetClasses'?: string[] | null;
@IsString() @IsString()
@IsOptional() @IsOptional()
'filters.dataSource'?: string; 'filters.dataSource'?: string | null;
@IsString() @IsString()
@IsOptional() @IsOptional()
'filters.symbol'?: string; 'filters.symbol'?: string | null;
@IsArray() @IsArray()
@IsOptional() @IsOptional()
'filters.tags'?: string[]; 'filters.tags'?: string[] | null;
@IsIn(['CHART', 'TABLE'] as HoldingsViewMode[]) @IsIn(['CHART', 'TABLE'] as HoldingsViewMode[])
@IsOptional() @IsOptional()

Loading…
Cancel
Save