Browse Source

Merge branch 'main' into feature/support-comment-in-activity-import-csv

pull/2416/head
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
c7b17bb021
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      CHANGELOG.md
  2. 6
      DEVELOPMENT.md
  3. 2
      apps/api/src/app/account/create-account.dto.ts
  4. 2
      apps/api/src/app/account/update-account.dto.ts
  5. 4
      apps/api/src/app/admin/admin.service.ts
  6. 91
      apps/api/src/app/benchmark/benchmark.controller.ts
  7. 37
      apps/api/src/app/benchmark/benchmark.service.ts
  8. 21
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  9. 18
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  10. 2
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts
  11. 4
      apps/client/src/app/components/admin-platform/admin-platform.component.ts
  12. 5
      apps/client/src/app/components/admin-tag/admin-tag.component.ts
  13. 37
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts
  14. 40
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  15. 19
      apps/client/src/app/services/data.service.ts
  16. 1
      libs/common/src/lib/interfaces/admin-data.interface.ts
  17. 2
      package.json

9
CHANGELOG.md

@ -10,17 +10,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support for notes in the activities import - Added support for notes in the activities import
- Added the application version to the endpoint `GET api/v1/admin`
## Unreleased ## 2.8.0 - 2023-10-03
### Added ### Added
- Added the version to the admin control panel - Supported enter key press to submit the form of the create or update account dialog
- Added the application version to the admin control panel
- Added pagination parameters (`skip`, `take`) to the endpoint `GET api/v1/order` - Added pagination parameters (`skip`, `take`) to the endpoint `GET api/v1/order`
### Changed ### Changed
- Harmonized the settings icon of the user account page - Harmonized the settings icon of the user account page
- Improved the usability to set an asset profile as a benchmark
- Reload platforms after making a change in the admin control panel
- Reload tags after making a change in the admin control panel
### Fixed ### Fixed

6
DEVELOPMENT.md

@ -18,6 +18,12 @@
### Prisma ### Prisma
#### Access database via GUI
Run `yarn database:gui`
https://www.prisma.io/studio
#### Synchronize schema with database for prototyping #### Synchronize schema with database for prototyping
Run `yarn database:push` Run `yarn database:push`

2
apps/api/src/app/account/create-account.dto.ts

@ -12,7 +12,7 @@ import { isString } from 'lodash';
export class CreateAccountDto { export class CreateAccountDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
accountType: AccountType; accountType?: AccountType;
@IsNumber() @IsNumber()
balance: number; balance: number;

2
apps/api/src/app/account/update-account.dto.ts

@ -12,7 +12,7 @@ import { isString } from 'lodash';
export class UpdateAccountDto { export class UpdateAccountDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
accountType: AccountType; accountType?: AccountType;
@IsNumber() @IsNumber()
balance: number; balance: number;

4
apps/api/src/app/admin/admin.service.ts

@ -1,4 +1,5 @@
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -97,7 +98,8 @@ export class AdminService {
settings: await this.propertyService.get(), settings: await this.propertyService.get(),
transactionCount: await this.prismaService.order.count(), transactionCount: await this.prismaService.order.count(),
userCount: await this.prismaService.user.count(), userCount: await this.prismaService.user.count(),
users: await this.getUsersWithAnalytics() users: await this.getUsersWithAnalytics(),
version: environment.version
}; };
} }

91
apps/api/src/app/benchmark/benchmark.controller.ts

@ -10,6 +10,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
Delete,
Get, Get,
HttpException, HttpException,
Inject, Inject,
@ -32,35 +33,49 @@ export class BenchmarkController {
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@Get() @Post()
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {
return {
benchmarks: await this.benchmarkService.getBenchmarks()
};
}
@Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor) public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
public async getBenchmarkMarketDataBySymbol( if (
@Param('dataSource') dataSource: DataSource, !hasPermission(
@Param('startDateString') startDateString: string, this.request.user.permissions,
@Param('symbol') symbol: string permissions.accessAdminControl
): Promise<BenchmarkMarketDataDetails> { )
const startDate = new Date(startDateString); ) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.benchmarkService.getMarketDataBySymbol({ try {
dataSource, const benchmark = await this.benchmarkService.addBenchmark({
startDate, dataSource,
symbol symbol
}); });
if (!benchmark) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
return benchmark;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
} }
@Post() @Delete(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { public async deleteBenchmark(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
) {
if ( if (
!hasPermission( !hasPermission(
this.request.user.permissions, this.request.user.permissions,
@ -74,7 +89,7 @@ export class BenchmarkController {
} }
try { try {
const benchmark = await this.benchmarkService.addBenchmark({ const benchmark = await this.benchmarkService.deleteBenchmark({
dataSource, dataSource,
symbol symbol
}); });
@ -94,4 +109,30 @@ export class BenchmarkController {
); );
} }
} }
@Get()
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {
return {
benchmarks: await this.benchmarkService.getBenchmarks()
};
}
@Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string
): Promise<BenchmarkMarketDataDetails> {
const startDate = new Date(startDateString);
return this.benchmarkService.getMarketDataBySymbol({
dataSource,
startDate,
symbol
});
}
} }

37
apps/api/src/app/benchmark/benchmark.service.ts

@ -245,6 +245,43 @@ export class BenchmarkService {
}; };
} }
public async deleteBenchmark({
dataSource,
symbol
}: UniqueAsset): Promise<Partial<SymbolProfile>> {
const assetProfile = await this.prismaService.symbolProfile.findFirst({
where: {
dataSource,
symbol
}
});
if (!assetProfile) {
return null;
}
let benchmarks =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? [];
benchmarks = benchmarks.filter(({ symbolProfileId }) => {
return symbolProfileId !== assetProfile.id;
});
await this.propertyService.put({
key: PROPERTY_BENCHMARKS,
value: JSON.stringify(benchmarks)
});
return {
dataSource,
symbol,
id: assetProfile.id,
name: assetProfile.name
};
}
private getMarketCondition(aPerformanceInPercent: number) { private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET'; return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
} }

21
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -146,9 +146,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
.postBenchmark({ dataSource, symbol }) .postBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => { .subscribe(() => {
setTimeout(() => { this.dataService.updateInfo();
window.location.reload();
}, 300); this.isBenchmark = true;
this.changeDetectorRef.markForCheck();
}); });
} }
@ -185,6 +187,19 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}); });
} }
public onUnsetBenchmark({ dataSource, symbol }: UniqueAsset) {
this.dataService
.deleteBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.dataService.updateInfo();
this.isBenchmark = false;
this.changeDetectorRef.markForCheck();
});
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

18
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -37,13 +37,6 @@
> >
<ng-container i18n>Gather Profile Data</ng-container> <ng-container i18n>Gather Profile Data</ng-container>
</button> </button>
<button
mat-menu-item
[disabled]="isBenchmark"
(click)="onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
>
<ng-container i18n>Set as Benchmark</ng-container>
</button>
</mat-menu> </mat-menu>
</div> </div>
@ -151,6 +144,17 @@
</ng-template> </ng-template>
</ng-container> </ng-container>
</div> </div>
<div class="d-flex my-3">
<div class="w-50">
<mat-checkbox
color="primary"
i18n
[checked]="isBenchmark"
(change)="isBenchmark ? onUnsetBenchmark({dataSource: data.dataSource, symbol: data.symbol}) : onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
>Benchmark</mat-checkbox
>
</div>
</div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol Mapping</mat-label> <mat-label i18n>Symbol Mapping</mat-label>

2
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts

@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -21,6 +22,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
GfPortfolioProportionChartModule, GfPortfolioProportionChartModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule,
MatDialogModule, MatDialogModule,
MatInputModule, MatInputModule,
MatMenuModule, MatMenuModule,

4
apps/client/src/app/components/admin-platform/admin-platform.component.ts

@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Platform } from '@prisma/client'; import { Platform } from '@prisma/client';
import { get } from 'lodash'; import { get } from 'lodash';
@ -40,6 +41,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -119,6 +121,8 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get; this.dataSource.sortingDataAccessor = get;
this.dataService.updateInfo();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }

5
apps/client/src/app/components/admin-tag/admin-tag.component.ts

@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
import { get } from 'lodash'; import { get } from 'lodash';
@ -40,6 +41,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -114,10 +116,13 @@ export class AdminTagComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((tags) => { .subscribe((tags) => {
this.tags = tags; this.tags = tags;
this.dataSource = new MatTableDataSource(this.tags); this.dataSource = new MatTableDataSource(this.tags);
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get; this.dataSource.sortingDataAccessor = get;
this.dataService.updateInfo();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }

37
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts

@ -4,7 +4,10 @@ import {
Inject, Inject,
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
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 { DataService } from '@ghostfolio/client/services/data.service';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -18,15 +21,17 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
templateUrl: 'create-or-update-account-dialog.html' templateUrl: 'create-or-update-account-dialog.html'
}) })
export class CreateOrUpdateAccountDialog implements OnDestroy { export class CreateOrUpdateAccountDialog implements OnDestroy {
public accountForm: FormGroup;
public currencies: string[] = []; public currencies: string[] = [];
public platforms: { id: string; name: string }[]; public platforms: { id: string; name: string }[];
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>, public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams private formBuilder: FormBuilder
) {} ) {}
ngOnInit() { ngOnInit() {
@ -34,12 +39,42 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
this.currencies = currencies; this.currencies = currencies;
this.platforms = platforms; this.platforms = platforms;
this.accountForm = this.formBuilder.group({
accountId: [{ disabled: true, value: this.data.account.id }],
balance: [this.data.account.balance, Validators.required],
comment: [this.data.account.comment],
currency: [this.data.account.currency, Validators.required],
isExcluded: [this.data.account.isExcluded],
name: [this.data.account.name, Validators.required],
platformId: [this.data.account.platformId]
});
} }
public onCancel() { public onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }
public onSubmit() {
const account: CreateAccountDto | UpdateAccountDto = {
balance: this.accountForm.controls['balance'].value,
comment: this.accountForm.controls['comment'].value,
currency: this.accountForm.controls['currency'].value,
id: this.accountForm.controls['accountId'].value,
isExcluded: this.accountForm.controls['isExcluded'].value,
name: this.accountForm.controls['name'].value,
platformId: this.accountForm.controls['platformId'].value
};
if (this.data.account.id) {
(account as UpdateAccountDto).id = this.data.account.id;
} else {
delete (account as CreateAccountDto).id;
}
this.dialogRef.close({ account });
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

40
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -1,17 +1,22 @@
<form #addAccountForm="ngForm" class="d-flex flex-column h-100"> <form
class="d-flex flex-column h-100"
[formGroup]="accountForm"
(keyup.enter)="accountForm.valid && onSubmit()"
(ngSubmit)="onSubmit()"
>
<h1 *ngIf="data.account.id" i18n mat-dialog-title>Update account</h1> <h1 *ngIf="data.account.id" i18n mat-dialog-title>Update account</h1>
<h1 *ngIf="!data.account.id" i18n mat-dialog-title>Add account</h1> <h1 *ngIf="!data.account.id" i18n mat-dialog-title>Add account</h1>
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label> <mat-label i18n>Name</mat-label>
<input matInput name="name" required [(ngModel)]="data.account.name" /> <input formControlName="name" matInput />
</mat-form-field> </mat-form-field>
</div> </div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label> <mat-label i18n>Currency</mat-label>
<mat-select name="currency" required [(value)]="data.account.currency"> <mat-select formControlName="currency">
<mat-option *ngFor="let currency of currencies" [value]="currency" <mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option >{{ currency }}</mat-option
> >
@ -21,20 +26,14 @@
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Cash Balance</mat-label> <mat-label i18n>Cash Balance</mat-label>
<input <input formControlName="balance" matInput type="number" />
matInput
name="balance"
required
type="number"
[(ngModel)]="data.account.balance"
/>
<span class="ml-2" matTextSuffix>{{ data.account.currency }}</span> <span class="ml-2" matTextSuffix>{{ data.account.currency }}</span>
</mat-form-field> </mat-form-field>
</div> </div>
<div [ngClass]="{ 'd-none': platforms?.length < 1 }"> <div [ngClass]="{ 'd-none': platforms?.length < 1 }">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Platform</mat-label> <mat-label i18n>Platform</mat-label>
<mat-select name="platformId" [(value)]="data.account.platformId"> <mat-select formControlName="platformId">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
<mat-option *ngFor="let platform of platforms" [value]="platform.id" <mat-option *ngFor="let platform of platforms" [value]="platform.id"
>{{ platform.name }}</mat-option >{{ platform.name }}</mat-option
@ -48,30 +47,21 @@
<textarea <textarea
cdkAutosizeMinRows="2" cdkAutosizeMinRows="2"
cdkTextareaAutosize cdkTextareaAutosize
formControlName="comment"
matInput matInput
name="comment"
[(ngModel)]="data.account.comment"
(keyup.enter)="$event.stopPropagation()" (keyup.enter)="$event.stopPropagation()"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3 px-2"> <div class="mb-3 px-2">
<mat-checkbox <mat-checkbox color="primary" formControlName="isExcluded"
color="primary"
name="isExcluded"
[(ngModel)]="data.account.isExcluded"
>Exclude from Analysis</mat-checkbox >Exclude from Analysis</mat-checkbox
> >
</div> </div>
<div *ngIf="data.account.id"> <div *ngIf="data.account.id">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label> <mat-label i18n>Account ID</mat-label>
<input <input formControlName="accountId" matInput />
disabled
matInput
name="accountId"
[(ngModel)]="data.account.id"
/>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
@ -80,8 +70,8 @@
<button <button
color="primary" color="primary"
mat-flat-button mat-flat-button
[disabled]="!addAccountForm.form.valid" type="submit"
[mat-dialog-close]="data" [disabled]="!accountForm.valid"
> >
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>

19
apps/client/src/app/services/data.service.ts

@ -204,6 +204,10 @@ export class DataService {
return this.http.delete<any>(`/api/v1/order/`); return this.http.delete<any>(`/api/v1/order/`);
} }
public deleteBenchmark({ dataSource, symbol }: UniqueAsset) {
return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
}
public deleteOrder(aId: string) { public deleteOrder(aId: string) {
return this.http.delete<any>(`/api/v1/order/${aId}`); return this.http.delete<any>(`/api/v1/order/${aId}`);
} }
@ -496,4 +500,19 @@ export class DataService {
couponCode couponCode
}); });
} }
public updateInfo() {
this.http.get<InfoItem>('/api/v1/info').subscribe((info) => {
const utmSource = <'ios' | 'trusted-web-activity'>(
window.localStorage.getItem('utm_source')
);
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
(window as any).info = info;
});
}
} }

1
libs/common/src/lib/interfaces/admin-data.interface.ts

@ -12,4 +12,5 @@ export interface AdminData {
lastActivity: Date; lastActivity: Date;
transactionCount: number; transactionCount: number;
}[]; }[];
version: string;
} }

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.7.0", "version": "2.8.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",

Loading…
Cancel
Save