Browse Source

Task/improve type safety in home watchlist and create watchlist item dialog (#6724)

* feat(client): implement CreateWatchlistItemForm

* feat(client): implement inject function

* feat(client): remove formBuilder injection

* fix(client): use AssetProfileIdentifier instead of SymbolProfile

* feat(client): implement defaultLocale to resolve type error

* fix(client): replace deprecated getDeviceInfo with deviceInfo signal

* feat(client): tighten up encapsulation and immutability
pull/6735/head
Kenrick Tandrian 4 days ago
committed by GitHub
parent
commit
34dd8be2d5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 44
      apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.component.ts
  2. 8
      apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/interfaces/interfaces.ts
  3. 54
      apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
  4. 2
      apps/client/src/app/components/home-watchlist/home-watchlist.html

44
apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.component.ts

@ -1,9 +1,8 @@
import type { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete'; import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { import {
AbstractControl,
FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
@ -15,6 +14,8 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { CreateWatchlistItemForm } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'h-100' }, host: { class: 'h-100' },
@ -30,44 +31,41 @@ import { MatFormFieldModule } from '@angular/material/form-field';
styleUrls: ['./create-watchlist-item-dialog.component.scss'], styleUrls: ['./create-watchlist-item-dialog.component.scss'],
templateUrl: 'create-watchlist-item-dialog.html' templateUrl: 'create-watchlist-item-dialog.html'
}) })
export class GfCreateWatchlistItemDialogComponent implements OnInit { export class GfCreateWatchlistItemDialogComponent {
public createWatchlistItemForm: FormGroup; protected readonly createWatchlistItemForm: CreateWatchlistItemForm =
new FormGroup(
public constructor(
public readonly dialogRef: MatDialogRef<GfCreateWatchlistItemDialogComponent>,
public readonly formBuilder: FormBuilder
) {}
public ngOnInit() {
this.createWatchlistItemForm = this.formBuilder.group(
{ {
searchSymbol: new FormControl(null, [Validators.required]) searchSymbol: new FormControl<AssetProfileIdentifier | null>(null, [
Validators.required
])
}, },
{ {
validators: this.validator validators: this.validator
} }
); );
}
public onCancel() { private readonly dialogRef =
inject<MatDialogRef<GfCreateWatchlistItemDialogComponent>>(MatDialogRef);
protected onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }
public onSubmit() { protected onSubmit() {
this.dialogRef.close({ this.dialogRef.close({
dataSource: dataSource:
this.createWatchlistItemForm.get('searchSymbol').value.dataSource, this.createWatchlistItemForm.controls.searchSymbol.value?.dataSource,
symbol: this.createWatchlistItemForm.get('searchSymbol').value.symbol symbol: this.createWatchlistItemForm.controls.searchSymbol.value?.symbol
}); });
} }
private validator(control: AbstractControl): ValidationErrors { private validator(control: CreateWatchlistItemForm): ValidationErrors {
const searchSymbolControl = control.get('searchSymbol'); const searchSymbolControl = control.controls.searchSymbol;
if ( if (
searchSymbolControl.valid && searchSymbolControl.valid &&
searchSymbolControl.value.dataSource && searchSymbolControl.value?.dataSource &&
searchSymbolControl.value.symbol searchSymbolControl.value?.symbol
) { ) {
return { incomplete: false }; return { incomplete: false };
} }

8
apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/interfaces/interfaces.ts

@ -1,4 +1,12 @@
import type { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import type { FormControl, FormGroup } from '@angular/forms';
export interface CreateWatchlistItemDialogParams { export interface CreateWatchlistItemDialogParams {
deviceType: string; deviceType: string;
locale: string; locale: string;
} }
export type CreateWatchlistItemForm = FormGroup<{
searchSymbol: FormControl<AssetProfileIdentifier | null>;
}>;

54
apps/client/src/app/components/home-watchlist/home-watchlist.component.ts

@ -1,5 +1,6 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { locale as defaultLocale } from '@ghostfolio/common/config';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
Benchmark, Benchmark,
@ -14,8 +15,10 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
DestroyRef, DestroyRef,
inject,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -45,26 +48,29 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
templateUrl: './home-watchlist.html' templateUrl: './home-watchlist.html'
}) })
export class GfHomeWatchlistComponent implements OnInit { export class GfHomeWatchlistComponent implements OnInit {
public deviceType: string; protected hasImpersonationId: boolean;
public hasImpersonationId: boolean; protected hasPermissionToCreateWatchlistItem: boolean;
public hasPermissionToCreateWatchlistItem: boolean; protected hasPermissionToDeleteWatchlistItem: boolean;
public hasPermissionToDeleteWatchlistItem: boolean; protected user: User;
public user: User; protected watchlist: Benchmark[];
public watchlist: Benchmark[];
protected readonly deviceType = computed(
public constructor( () => this.deviceDetectorService.deviceInfo().deviceType
private changeDetectorRef: ChangeDetectorRef, );
private dataService: DataService,
private destroyRef: DestroyRef, private readonly changeDetectorRef = inject(ChangeDetectorRef);
private deviceService: DeviceDetectorService, private readonly dataService = inject(DataService);
private dialog: MatDialog, private readonly destroyRef = inject(DestroyRef);
private impersonationStorageService: ImpersonationStorageService, private readonly deviceDetectorService = inject(DeviceDetectorService);
private route: ActivatedRoute, private readonly dialog = inject(MatDialog);
private router: Router, private readonly impersonationStorageService = inject(
private userService: UserService ImpersonationStorageService
) { );
this.deviceType = this.deviceService.getDeviceInfo().deviceType; private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly userService = inject(UserService);
public constructor() {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -110,7 +116,7 @@ export class GfHomeWatchlistComponent implements OnInit {
this.loadWatchlistData(); this.loadWatchlistData();
} }
public onWatchlistItemDeleted({ protected onWatchlistItemDeleted({
dataSource, dataSource,
symbol symbol
}: AssetProfileIdentifier) { }: AssetProfileIdentifier) {
@ -148,10 +154,10 @@ export class GfHomeWatchlistComponent implements OnInit {
>(GfCreateWatchlistItemDialogComponent, { >(GfCreateWatchlistItemDialogComponent, {
autoFocus: false, autoFocus: false,
data: { data: {
deviceType: this.deviceType, deviceType: this.deviceType(),
locale: this.user?.settings?.locale locale: this.user?.settings?.locale ?? defaultLocale
}, },
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef dialogRef

2
apps/client/src/app/components/home-watchlist/home-watchlist.html

@ -11,7 +11,7 @@
<div class="col-xs-12 col-md-10 offset-md-1"> <div class="col-xs-12 col-md-10 offset-md-1">
<gf-benchmark <gf-benchmark
[benchmarks]="watchlist" [benchmarks]="watchlist"
[deviceType]="deviceType" [deviceType]="deviceType()"
[hasPermissionToDeleteItem]="hasPermissionToDeleteWatchlistItem" [hasPermissionToDeleteItem]="hasPermissionToDeleteWatchlistItem"
[locale]="user?.settings?.locale || undefined" [locale]="user?.settings?.locale || undefined"
[user]="user" [user]="user"

Loading…
Cancel
Save