Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
646dcb91c5 | |||
ad961f3039 | |||
c16f743b07 | |||
8e13f6ef9b | |||
95bcdea69b | |||
0d6fe4a232 | |||
ced4519412 | |||
ef8b7718b1 | |||
b4762dc463 | |||
9851cce382 | |||
a1460a98fd | |||
1a553a296f |
2
.env
2
.env
@ -5,9 +5,9 @@ REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
# POSTGRES
|
||||
POSTGRES_DB=ghostfolio-db
|
||||
POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=password
|
||||
POSTGRES_DB=ghostfolio-db
|
||||
|
||||
ACCESS_TOKEN_SALT=GHOSTFOLIO
|
||||
ALPHA_VANTAGE_API_KEY=
|
||||
|
35
CHANGELOG.md
35
CHANGELOG.md
@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.8.0 - 24.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a section for _Analysis_, _X-ray_ and upcoming tools
|
||||
|
||||
### Changed
|
||||
|
||||
- Introduced a user service implemented as an observable store (single source of truth for state)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the performance chart by considering the investment
|
||||
- Fixed missing header of public pages (_About_, _Pricing_, _Resources_)
|
||||
|
||||
## 1.7.0 - 22.05.2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Hid footer on mobile (except on landing page)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the internal navigation of the _Zen Mode_ in combination with a query parameter
|
||||
|
||||
## 1.6.0 - 22.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added an index in the user table of the admin control panel
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the alignment in the user table of the admin control panel
|
||||
|
||||
## 1.5.0 - 22.05.2021
|
||||
|
||||
### Added
|
||||
|
@ -108,7 +108,7 @@ export class AdminService {
|
||||
createdAt: true,
|
||||
id: true
|
||||
},
|
||||
take: 20,
|
||||
take: 30,
|
||||
where: {
|
||||
NOT: {
|
||||
Analytics: null
|
||||
|
@ -158,8 +158,8 @@ export class PortfolioService {
|
||||
return {
|
||||
date: format(parseISO(portfolioItem.date), 'yyyy-MM-dd'),
|
||||
grossPerformancePercent: portfolioItem.grossPerformancePercent,
|
||||
marketPrice: portfolioItem.value || null,
|
||||
value: portfolioItem.value || null
|
||||
marketPrice: portfolioItem.value ?? null,
|
||||
value: portfolioItem.value - portfolioItem.investment ?? null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -71,6 +71,11 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./pages/login/login-page.module').then((m) => m.LoginPageModule)
|
||||
},
|
||||
{
|
||||
path: 'tools',
|
||||
loadChildren: () =>
|
||||
import('./pages/tools/tools-page.module').then((m) => m.ToolsPageModule)
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
loadChildren: () =>
|
||||
|
@ -4,6 +4,7 @@
|
||||
[currentRoute]="currentRoute"
|
||||
[info]="info"
|
||||
[user]="user"
|
||||
(signOut)="onSignOut()"
|
||||
></gf-header>
|
||||
</header>
|
||||
|
||||
@ -25,7 +26,10 @@
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<footer class="footer d-flex justify-content-center position-absolute w-100">
|
||||
<footer
|
||||
*ngIf="currentRoute === 'start' || deviceType !== 'mobile'"
|
||||
class="footer d-flex justify-content-center position-absolute w-100"
|
||||
>
|
||||
<div class="container text-center">
|
||||
<div>
|
||||
© {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>
|
||||
|
@ -10,12 +10,14 @@ import { primaryColorHex, secondaryColorHex } from '@ghostfolio/common/config';
|
||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { MaterialCssVarsService } from 'angular-material-css-vars';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { environment } from '../environments/environment';
|
||||
import { DataService } from './services/data.service';
|
||||
import { TokenStorageService } from './services/token-storage.service';
|
||||
import { UserService } from './services/user/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-root',
|
||||
@ -27,25 +29,29 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
public canCreateAccount: boolean;
|
||||
public currentRoute: string;
|
||||
public currentYear = new Date().getFullYear();
|
||||
public deviceType: string;
|
||||
public info: InfoItem;
|
||||
public isLoggedIn = false;
|
||||
public user: User;
|
||||
public version = environment.version;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private materialCssVarsService: MaterialCssVarsService,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private tokenStorageService: TokenStorageService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.initializeTheme();
|
||||
this.user = undefined;
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.dataService.fetchInfo().subscribe((info) => {
|
||||
this.info = info;
|
||||
});
|
||||
@ -59,26 +65,22 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.currentRoute = urlSegments[0].path;
|
||||
});
|
||||
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.isLoggedIn = !!this.tokenStorageService.getToken();
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
if (this.isLoggedIn) {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.canCreateAccount = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createUserAccount
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
} else {
|
||||
this.canCreateAccount = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createUserAccount
|
||||
);
|
||||
} else if (!this.tokenStorageService.getToken()) {
|
||||
// User has not been logged in
|
||||
this.user = null;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -87,6 +89,13 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
public onSignOut() {
|
||||
this.tokenStorageService.signOut();
|
||||
this.userService.remove();
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
|
@ -19,18 +19,15 @@
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'analysis' ? 'primary' : null"
|
||||
[routerLink]="['/analysis']"
|
||||
>Analysis</a
|
||||
>
|
||||
<a
|
||||
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'report' ? 'primary' : null"
|
||||
[routerLink]="['/report']"
|
||||
>X-ray</a
|
||||
[color]="
|
||||
currentRoute === 'analysis' ||
|
||||
currentRoute === 'report' ||
|
||||
currentRoute === 'tools'
|
||||
? 'primary'
|
||||
: null
|
||||
"
|
||||
[routerLink]="['/tools']"
|
||||
>Tools</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
@ -142,17 +139,14 @@
|
||||
class="d-block d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'analysis' }"
|
||||
[routerLink]="['/analysis']"
|
||||
>Analysis</a
|
||||
>
|
||||
<a
|
||||
class="d-block d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'report' }"
|
||||
[routerLink]="['/report']"
|
||||
>X-ray</a
|
||||
[ngClass]="{
|
||||
'font-weight-bold':
|
||||
currentRoute === 'analysis' ||
|
||||
currentRoute === 'report' ||
|
||||
currentRoute === 'tools'
|
||||
}"
|
||||
[routerLink]="['/tools']"
|
||||
>Tools</a
|
||||
>
|
||||
<a
|
||||
class="d-block d-sm-none"
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges
|
||||
OnChanges,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
@ -26,6 +28,8 @@ export class HeaderComponent implements OnChanges {
|
||||
@Input() info: InfoItem;
|
||||
@Input() user: User;
|
||||
|
||||
@Output() signOut = new EventEmitter<void>();
|
||||
|
||||
public hasPermissionForSocialLogin: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToAccessAdminControl: boolean;
|
||||
@ -75,8 +79,7 @@ export class HeaderComponent implements OnChanges {
|
||||
}
|
||||
|
||||
public onSignOut() {
|
||||
this.tokenStorageService.signOut();
|
||||
window.location.reload();
|
||||
this.signOut.next();
|
||||
}
|
||||
|
||||
public openLoginDialog(): void {
|
||||
|
@ -28,7 +28,7 @@ export class PerformanceChartDialog {
|
||||
public title: string;
|
||||
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<PerformanceChartDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams
|
||||
@ -46,7 +46,7 @@ export class PerformanceChartDialog {
|
||||
|
||||
this.historicalDataItems = this.data.historicalDataItems;
|
||||
|
||||
this.historicalDataItems.forEach((historicalDataItem) => {
|
||||
this.historicalDataItems?.forEach((historicalDataItem) => {
|
||||
const benchmarkItem = historicalData.find((item) => {
|
||||
return item.date === historicalDataItem.date;
|
||||
});
|
||||
@ -75,7 +75,7 @@ export class PerformanceChartDialog {
|
||||
}
|
||||
});
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.title = `Performance vs. ${this.benchmarkLabel}`;
|
||||
|
@ -34,7 +34,7 @@ export class PositionDetailDialog {
|
||||
public transactionCount: number;
|
||||
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<PositionDetailDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams
|
||||
@ -127,7 +127,7 @@ export class PositionDetailDialog {
|
||||
this.benchmarkDataItems[0].value = this.averagePrice;
|
||||
}
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -9,15 +9,17 @@ import { ViewMode } from '@prisma/client';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { DataService } from '../services/data.service';
|
||||
import { SettingsStorageService } from '../services/settings-storage.service';
|
||||
import { UserService } from '../services/user/user.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthGuard implements CanActivate {
|
||||
private static PUBLIC_PAGE_ROUTES = ['/about', '/pricing', '/resources'];
|
||||
|
||||
constructor(
|
||||
private dataService: DataService,
|
||||
private router: Router,
|
||||
private settingsStorageService: SettingsStorageService
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
@ -29,11 +31,14 @@ export class AuthGuard implements CanActivate {
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
this.dataService
|
||||
.fetchUser()
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
if (state.url !== '/start') {
|
||||
if (AuthGuard.PUBLIC_PAGE_ROUTES.includes(state.url)) {
|
||||
resolve(true);
|
||||
return EMPTY;
|
||||
} else if (state.url !== '/start') {
|
||||
this.router.navigate(['/start']);
|
||||
resolve(false);
|
||||
return EMPTY;
|
||||
@ -45,12 +50,12 @@ export class AuthGuard implements CanActivate {
|
||||
)
|
||||
.subscribe((user) => {
|
||||
if (
|
||||
state.url === '/home' &&
|
||||
state.url.startsWith('/home') &&
|
||||
user.settings.viewMode === ViewMode.ZEN
|
||||
) {
|
||||
this.router.navigate(['/zen']);
|
||||
resolve(false);
|
||||
} else if (state.url === '/start') {
|
||||
} else if (state.url.startsWith('/start')) {
|
||||
if (user.settings.viewMode === ViewMode.ZEN) {
|
||||
this.router.navigate(['/zen']);
|
||||
} else {
|
||||
@ -59,7 +64,7 @@ export class AuthGuard implements CanActivate {
|
||||
|
||||
resolve(false);
|
||||
} else if (
|
||||
state.url === '/zen' &&
|
||||
state.url.startsWith('/zen') &&
|
||||
user.settings.viewMode === ViewMode.DEFAULT
|
||||
) {
|
||||
this.router.navigate(['/home']);
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { AboutPageComponent } from './about-page.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: AboutPageComponent }];
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AboutPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { baseCurrency } from '@ghostfolio/common/config';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -26,28 +25,23 @@ export class AboutPageComponent implements OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.isLoggedIn = !!this.tokenStorageService.getToken();
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
if (this.isLoggedIn)
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -5,15 +5,14 @@
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-content>
|
||||
<p>
|
||||
<strong>Ghostfolio</strong> ({{ version }}) is open source software
|
||||
which empowers busy folks to have a sharp look of their financial
|
||||
assets and to make solid, data-driven investment decisions by
|
||||
evaluating automated static portfolio analysis rules. The project
|
||||
has been initiated by
|
||||
<strong>Ghostfolio</strong> is open source software which empowers
|
||||
busy folks to have a sharp look of their financial assets and to
|
||||
make solid, data-driven investment decisions by evaluating automated
|
||||
static portfolio analysis rules. The project has been initiated by
|
||||
<a href="https://dotsilver.ch">Thomas Kaul</a>.
|
||||
<ng-container *ngIf="lastPublish">
|
||||
This instance has been last published on {{ lastPublish
|
||||
}}.</ng-container
|
||||
This instance is running Ghostfolio {{ version }} and has been
|
||||
last published on {{ lastPublish }}.</ng-container
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
@ -28,9 +28,9 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private userService: UserService
|
||||
) {
|
||||
this.dataService
|
||||
.fetchInfo()
|
||||
@ -44,20 +44,19 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
);
|
||||
});
|
||||
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.updateUserSettings
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -78,11 +77,16 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
this.userService.remove();
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -98,7 +102,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
.subscribe((response) => {
|
||||
this.accesses = response;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Account as AccountModel, AccountType } from '@prisma/client';
|
||||
@ -35,14 +35,14 @@ export class AccountsPageComponent implements OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private userService: UserService
|
||||
) {
|
||||
this.routeQueryParams = route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -75,23 +75,23 @@ export class AccountsPageComponent implements OnInit {
|
||||
this.hasImpersonationId = !!aId;
|
||||
});
|
||||
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateAccount = hasPermission(
|
||||
user.permissions,
|
||||
this.user.permissions,
|
||||
permissions.createAccount
|
||||
);
|
||||
this.hasPermissionToDeleteAccount = hasPermission(
|
||||
user.permissions,
|
||||
this.user.permissions,
|
||||
permissions.deleteAccount
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchAccounts();
|
||||
@ -105,7 +105,7 @@ export class AccountsPageComponent implements OnInit {
|
||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||
}
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { AdminData, User } from '@ghostfolio/common/interfaces';
|
||||
import { formatDistanceToNow, isValid, parseISO, sub } from 'date-fns';
|
||||
@ -32,9 +32,9 @@ export class AdminPageComponent implements OnInit {
|
||||
public constructor(
|
||||
private adminService: AdminService,
|
||||
private cacheService: CacheService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -43,13 +43,12 @@ export class AdminPageComponent implements OnInit {
|
||||
public ngOnInit() {
|
||||
this.fetchAdminData();
|
||||
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
});
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -140,7 +139,7 @@ export class AdminPageComponent implements OnInit {
|
||||
this.transactionCount = transactionCount;
|
||||
this.userCount = userCount;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -73,26 +73,40 @@
|
||||
<table class="gf-table">
|
||||
<thead>
|
||||
<tr class="mat-header-row">
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>#</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Registration Date</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Accounts</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Transactions</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Engagement</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>
|
||||
Registration Date
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>
|
||||
Accounts
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>
|
||||
Transactions
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>
|
||||
Engagement
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Last Activitiy</th>
|
||||
<th class="mat-header-cell px-1 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let userItem of users" class="mat-row">
|
||||
<tr *ngFor="let userItem of users; let i = index" class="mat-row">
|
||||
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
{{ userItem.alias || userItem.id }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
{{ userItem.createdAt | date: defaultDateFormat }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">{{ userItem._count?.Account }}</td>
|
||||
<td class="mat-cell px-1 py-2">{{ userItem._count?.Order }}</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
{{ userItem._count?.Account }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
{{ userItem._count?.Order }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
{{ userItem.Analytics?.activityCount }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
|
@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import {
|
||||
PortfolioItem,
|
||||
PortfolioPosition,
|
||||
@ -40,11 +40,11 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -66,7 +66,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
.subscribe((response) => {
|
||||
this.portfolioItems = response;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
@ -76,18 +76,17 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.portfolioPositions = response;
|
||||
this.initializeAnalysisData(this.portfolioPositions, this.period);
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
RANGE,
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import {
|
||||
PortfolioOverview,
|
||||
PortfolioPerformance,
|
||||
@ -58,7 +58,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
@ -66,7 +66,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private userService: UserService
|
||||
) {
|
||||
this.routeQueryParams = this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -76,14 +76,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
});
|
||||
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
user.permissions,
|
||||
this.user.permissions,
|
||||
permissions.accessFearAndGreedIndex
|
||||
);
|
||||
|
||||
@ -94,17 +94,17 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
.subscribe(({ marketPrice }) => {
|
||||
this.fearAndGreedIndex = marketPrice;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.hasPermissionToReadForeignPortfolio = hasPermission(
|
||||
user.permissions,
|
||||
this.user.permissions,
|
||||
permissions.readForeignPortfolio
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -169,7 +169,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
};
|
||||
});
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
@ -178,14 +178,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
this.performance = response;
|
||||
this.isLoadingPerformance = false;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService.fetchPortfolioOverview().subscribe((response) => {
|
||||
this.overview = response;
|
||||
this.isLoadingOverview = false;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
@ -195,9 +195,9 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
this.hasPositions =
|
||||
this.positions && Object.keys(this.positions).length > 0;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
@ -42,7 +42,7 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.initializeLineChart();
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ export class ShowAccessTokenDialog {
|
||||
public isAgreeButtonDisabled = true;
|
||||
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {}
|
||||
|
||||
@ -26,7 +26,7 @@ export class ShowAccessTokenDialog {
|
||||
setTimeout(() => {
|
||||
this.isAgreeButtonDisabled = false;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { PricingPageComponent } from './pricing-page.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: PricingPageComponent }];
|
||||
const routes: Routes = [
|
||||
{ path: '', component: PricingPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { baseCurrency } from '@ghostfolio/common/config';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -22,28 +21,23 @@ export class PricingPageComponent implements OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.isLoggedIn = !!this.tokenStorageService.getToken();
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
if (this.isLoggedIn)
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -20,7 +20,7 @@ export class ReportPageComponent implements OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService
|
||||
) {}
|
||||
|
||||
@ -38,7 +38,7 @@ export class ReportPageComponent implements OnInit {
|
||||
portfolioReport.rules['currencyClusterRisk'] || null;
|
||||
this.feeRules = portfolioReport.rules['fees'] || null;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { ResourcesPageComponent } from './resources-page.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: ResourcesPageComponent }];
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ResourcesPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
|
15
apps/client/src/app/pages/tools/tools-page-routing.module.ts
Normal file
15
apps/client/src/app/pages/tools/tools-page-routing.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { ToolsPageComponent } from './tools-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ToolsPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ToolsPageRoutingModule {}
|
21
apps/client/src/app/pages/tools/tools-page.component.ts
Normal file
21
apps/client/src/app/pages/tools/tools-page.component.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-tools-page',
|
||||
templateUrl: './tools-page.html',
|
||||
styleUrls: ['./tools-page.scss']
|
||||
})
|
||||
export class ToolsPageComponent implements OnInit {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {}
|
||||
}
|
33
apps/client/src/app/pages/tools/tools-page.html
Normal file
33
apps/client/src/app/pages/tools/tools-page.html
Normal file
@ -0,0 +1,33 @@
|
||||
<div class="container">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Tools</h3>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<h4 i18n>Analysis</h4>
|
||||
<p class="mb-0">
|
||||
Ghostfolio Analysis shows your positions and visualizes your
|
||||
portfolio.
|
||||
</p>
|
||||
<p class="text-right">
|
||||
<button color="primary" i18n mat-button [routerLink]="['/analysis']">
|
||||
Open Analysis →
|
||||
</button>
|
||||
</p>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<h4 i18n>X-ray</h4>
|
||||
<p class="mb-0">
|
||||
Ghostfolio X-ray uses static analysis to identify potential issues and
|
||||
risks in your portfolio.
|
||||
</p>
|
||||
<p class="text-right">
|
||||
<button color="primary" i18n mat-button [routerLink]="['/report']">
|
||||
Open X-ray →
|
||||
</button>
|
||||
</p>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
23
apps/client/src/app/pages/tools/tools-page.module.ts
Normal file
23
apps/client/src/app/pages/tools/tools-page.module.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { ToolsPageRoutingModule } from './tools-page-routing.module';
|
||||
import { ToolsPageComponent } from './tools-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ToolsPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
RouterModule,
|
||||
ToolsPageRoutingModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class ToolsPageModule {}
|
8
apps/client/src/app/pages/tools/tools-page.scss
Normal file
8
apps/client/src/app/pages/tools/tools-page.scss
Normal file
@ -0,0 +1,8 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
@ -42,7 +42,7 @@ export class CreateOrUpdateTransactionDialog {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
|
||||
@ -73,7 +73,7 @@ export class CreateOrUpdateTransactionDialog {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ marketPrice }) => {
|
||||
this.currentMarketPrice = marketPrice;
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -100,7 +100,7 @@ export class CreateOrUpdateTransactionDialog {
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Order as OrderModel } from '@prisma/client';
|
||||
@ -35,14 +35,14 @@ export class TransactionsPageComponent implements OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private userService: UserService
|
||||
) {
|
||||
this.routeQueryParams = route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -75,23 +75,23 @@ export class TransactionsPageComponent implements OnInit {
|
||||
this.hasImpersonationId = !!aId;
|
||||
});
|
||||
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateOrder = hasPermission(
|
||||
user.permissions,
|
||||
this.user.permissions,
|
||||
permissions.createOrder
|
||||
);
|
||||
this.hasPermissionToDeleteOrder = hasPermission(
|
||||
user.permissions,
|
||||
this.user.permissions,
|
||||
permissions.deleteOrder
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchOrders();
|
||||
@ -105,7 +105,7 @@ export class TransactionsPageComponent implements OnInit {
|
||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||
}
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
@ -31,26 +31,25 @@ export class ZenPageComponent implements OnDestroy, OnInit {
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private userService: UserService
|
||||
) {
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToReadForeignPortfolio = hasPermission(
|
||||
user.permissions,
|
||||
this.user.permissions,
|
||||
permissions.readForeignPortfolio
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -87,7 +86,7 @@ export class ZenPageComponent implements OnDestroy, OnInit {
|
||||
};
|
||||
});
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
@ -96,9 +95,9 @@ export class ZenPageComponent implements OnDestroy, OnInit {
|
||||
this.performance = response;
|
||||
this.isLoadingPerformance = false;
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.cd.markForCheck();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
@ -74,13 +74,6 @@ export class DataService {
|
||||
}
|
||||
|
||||
public fetchInfo() {
|
||||
/*
|
||||
if (this.info) {
|
||||
// TODO: Cache info
|
||||
return of(this.info);
|
||||
}
|
||||
*/
|
||||
|
||||
return this.http.get<InfoItem>('/api/info').pipe(
|
||||
map((data) => {
|
||||
if (
|
||||
@ -154,10 +147,6 @@ export class DataService {
|
||||
);
|
||||
}
|
||||
|
||||
public fetchUser() {
|
||||
return this.http.get<User>('/api/user');
|
||||
}
|
||||
|
||||
public loginAnonymous(accessToken: string) {
|
||||
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
const TOKEN_KEY = 'auth-token';
|
||||
|
||||
@ -7,23 +6,15 @@ const TOKEN_KEY = 'auth-token';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TokenStorageService {
|
||||
private hasTokenChangeSubject = new BehaviorSubject<void>(null);
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public getToken(): string {
|
||||
return window.localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
public onChangeHasToken() {
|
||||
return this.hasTokenChangeSubject.asObservable();
|
||||
}
|
||||
|
||||
public saveToken(token: string): void {
|
||||
window.localStorage.removeItem(TOKEN_KEY);
|
||||
window.localStorage.setItem(TOKEN_KEY, token);
|
||||
|
||||
this.hasTokenChangeSubject.next();
|
||||
}
|
||||
|
||||
public signOut(): void {
|
||||
@ -34,7 +25,5 @@ export class TokenStorageService {
|
||||
if (utmSource) {
|
||||
window.localStorage.setItem('utm_source', utmSource);
|
||||
}
|
||||
|
||||
this.hasTokenChangeSubject.next();
|
||||
}
|
||||
}
|
||||
|
4
apps/client/src/app/services/user/user-store.actions.ts
Normal file
4
apps/client/src/app/services/user/user-store.actions.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum UserStoreActions {
|
||||
GetUser = 'GET_USER',
|
||||
RemoveUser = 'REMOVE_USER'
|
||||
}
|
5
apps/client/src/app/services/user/user-store.state.ts
Normal file
5
apps/client/src/app/services/user/user-store.state.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface UserStoreState {
|
||||
user: User;
|
||||
}
|
56
apps/client/src/app/services/user/user.service.ts
Normal file
56
apps/client/src/app/services/user/user.service.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ObservableStore } from '@codewithdan/observable-store';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { of } from 'rxjs';
|
||||
import { throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
import { UserStoreActions } from './user-store.actions';
|
||||
import { UserStoreState } from './user-store.state';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserService extends ObservableStore<UserStoreState> {
|
||||
public constructor(private http: HttpClient) {
|
||||
super({ trackStateHistory: true });
|
||||
|
||||
this.setState({ user: undefined }, 'INIT_STATE');
|
||||
}
|
||||
|
||||
public get() {
|
||||
const state = this.getState();
|
||||
|
||||
if (state?.user) {
|
||||
// Get from cache
|
||||
return of(state.user);
|
||||
} else {
|
||||
// Get from endpoint
|
||||
return this.fetchUser().pipe(catchError(this.handleError));
|
||||
}
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.setState({ user: null }, UserStoreActions.RemoveUser);
|
||||
}
|
||||
|
||||
private fetchUser() {
|
||||
return this.http.get<User>('/api/user').pipe(
|
||||
map((user) => {
|
||||
this.setState({ user }, UserStoreActions.GetUser);
|
||||
return user;
|
||||
}),
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
private handleError(error: any) {
|
||||
if (error.error instanceof Error) {
|
||||
const errMessage = error.error.message;
|
||||
return throwError(errMessage);
|
||||
}
|
||||
|
||||
return throwError(error || 'Server error');
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.5.0",
|
||||
"version": "1.8.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -55,6 +55,7 @@
|
||||
"@angular/platform-browser": "11.2.4",
|
||||
"@angular/platform-browser-dynamic": "11.2.4",
|
||||
"@angular/router": "11.2.4",
|
||||
"@codewithdan/observable-store": "2.2.11",
|
||||
"@nestjs/common": "7.6.5",
|
||||
"@nestjs/config": "0.6.1",
|
||||
"@nestjs/core": "7.6.5",
|
||||
|
@ -1396,6 +1396,11 @@
|
||||
exec-sh "^0.3.2"
|
||||
minimist "^1.2.0"
|
||||
|
||||
"@codewithdan/observable-store@2.2.11":
|
||||
version "2.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@codewithdan/observable-store/-/observable-store-2.2.11.tgz#f5a168e86a2fa185a50ca40a1e838aa5e5fb007d"
|
||||
integrity sha512-6CfqLJUqV0SwS4yE+9vciqxHUJ6CqIptSXXzFw80MonCDoVJvCJ/xhKfs7VZqJ4jDtEu/7ILvovFtZdLg9fiAg==
|
||||
|
||||
"@ctrl/tinycolor@^2.6.0":
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-2.6.1.tgz#0e78cc836a1fd997a9a22fa1c26c555411882160"
|
||||
|
Reference in New Issue
Block a user