Feature/add frontend for watchlist (#4604)
* Add frontend for watchlist * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>pull/4616/head
parent
456327d199
commit
c671ea4022
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfSymbolAutocompleteComponent,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
selector: 'gf-create-watchlist-item-dialog',
|
||||
styleUrls: ['./create-watchlist-item-dialog.component.scss'],
|
||||
templateUrl: 'create-watchlist-item-dialog.html'
|
||||
})
|
||||
export class CreateWatchlistItemDialogComponent implements OnInit, OnDestroy {
|
||||
public createWatchlistItemForm: FormGroup;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
public readonly dialogRef: MatDialogRef<CreateWatchlistItemDialogComponent>,
|
||||
public readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.createWatchlistItemForm = this.formBuilder.group(
|
||||
{
|
||||
searchSymbol: new FormControl(null, [Validators.required])
|
||||
},
|
||||
{
|
||||
validators: this.validator
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
this.dialogRef.close({
|
||||
dataSource:
|
||||
this.createWatchlistItemForm.get('searchSymbol').value.dataSource,
|
||||
symbol: this.createWatchlistItemForm.get('searchSymbol').value.symbol
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private validator(control: AbstractControl): ValidationErrors {
|
||||
const searchSymbolControl = control.get('searchSymbol');
|
||||
|
||||
if (
|
||||
searchSymbolControl.valid &&
|
||||
searchSymbolControl.value.dataSource &&
|
||||
searchSymbolControl.value.symbol
|
||||
) {
|
||||
return { incomplete: false };
|
||||
}
|
||||
|
||||
return { incomplete: true };
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<form
|
||||
class="d-flex flex-column h-100"
|
||||
[formGroup]="createWatchlistItemForm"
|
||||
(keyup.enter)="createWatchlistItemForm.valid && onSubmit()"
|
||||
(ngSubmit)="onSubmit()"
|
||||
>
|
||||
<h1 i18n mat-dialog-title>Add asset to watchlist</h1>
|
||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Name, symbol or ISIN</mat-label>
|
||||
<gf-symbol-autocomplete formControlName="searchSymbol" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
type="submit"
|
||||
[disabled]="createWatchlistItemForm.hasError('incomplete')"
|
||||
>
|
||||
<ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -0,0 +1,4 @@
|
||||
export interface CreateWatchlistItemDialogParams {
|
||||
deviceType: string;
|
||||
locale: string;
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Benchmark, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { CreateWatchlistItemDialogComponent } from './create-watchlist-item-dialog/create-watchlist-item-dialog.component';
|
||||
import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfBenchmarkComponent,
|
||||
GfPremiumIndicatorComponent,
|
||||
MatButtonModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-home-watchlist',
|
||||
styleUrls: ['./home-watchlist.scss'],
|
||||
templateUrl: './home-watchlist.html'
|
||||
})
|
||||
export class HomeWatchlistComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasPermissionToCreateWatchlistItem: boolean;
|
||||
public user: User;
|
||||
public watchlist: Benchmark[];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['createWatchlistItemDialog']) {
|
||||
this.openCreateWatchlistItemDialog();
|
||||
}
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateWatchlistItem = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createWatchlistItem
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.loadWatchlistData();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private loadWatchlistData() {
|
||||
this.dataService
|
||||
.fetchWatchlist()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ watchlist }) => {
|
||||
this.watchlist = watchlist.map(({ dataSource, symbol }) => ({
|
||||
dataSource,
|
||||
symbol,
|
||||
marketCondition: null,
|
||||
name: symbol,
|
||||
performances: null,
|
||||
trend50d: 'UNKNOWN',
|
||||
trend200d: 'UNKNOWN'
|
||||
}));
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private openCreateWatchlistItemDialog() {
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
const dialogRef = this.dialog.open(CreateWatchlistItemDialogComponent, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale
|
||||
} as CreateWatchlistItemDialogParams,
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ dataSource, symbol } = {}) => {
|
||||
if (dataSource && symbol) {
|
||||
this.dataService
|
||||
.postWatchlistItem({ dataSource, symbol })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe({
|
||||
next: () => this.loadWatchlistData()
|
||||
});
|
||||
}
|
||||
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<div class="container">
|
||||
<h1 class="d-none d-sm-block h3 mb-4">
|
||||
<span class="align-items-center d-flex justify-content-center">
|
||||
<span i18n>Watchlist</span>
|
||||
@if (user?.subscription?.type === 'Basic') {
|
||||
<gf-premium-indicator class="ml-1" />
|
||||
}
|
||||
</span>
|
||||
</h1>
|
||||
<div class="mb-3 row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<gf-benchmark
|
||||
[benchmarks]="watchlist"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale || undefined"
|
||||
[user]="user"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (hasPermissionToCreateWatchlistItem) {
|
||||
<div class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
color="primary"
|
||||
mat-fab
|
||||
[queryParams]="{ createWatchlistItemDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large" />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface WatchlistResponse {
|
||||
watchlist: AssetProfileIdentifier[];
|
||||
}
|
||||
Loading…
Reference in new issue