Browse Source

Merge branch 'main' into feature/rename-symbol-profile-overrides-to-asset-profile-overrides

pull/7271/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
b87e8e2b5a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 8
      apps/api/src/app/user/user.service.ts
  3. 2
      apps/client/src/app/components/admin-settings/admin-settings.component.html
  4. 6
      apps/client/src/app/components/admin-users/admin-users.component.ts
  5. 2
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  6. 1
      apps/client/src/app/components/home-overview/home-overview.html
  7. 22
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html
  8. 79
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
  9. 2
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  10. 9
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  11. 36
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  12. 2
      apps/client/src/app/pages/faq/saas/saas-page.html
  13. 4
      apps/client/src/app/pages/pricing/pricing-page.html
  14. 1
      libs/common/src/lib/permissions.ts
  15. 2
      prisma/schema.prisma

2
CHANGELOG.md

@ -13,7 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the user account deletion flow in the user settings of the user account page
- Set the change detection strategy to `OnPush` in the _FIRE_ page - Set the change detection strategy to `OnPush` in the _FIRE_ page
- Set the change detection strategy to `OnPush` in the users section of the admin control panel
- Renamed the `SymbolProfileOverrides` _Prisma_ data model to `AssetProfileOverrides` while keeping the database table name - Renamed the `SymbolProfileOverrides` _Prisma_ data model to `AssetProfileOverrides` while keeping the database table name
- Improved the language localization for Dutch (`nl`) - Improved the language localization for Dutch (`nl`)
- Improved the language localization for French (`fr`) - Improved the language localization for French (`fr`)

8
apps/api/src/app/user/user.service.ts

@ -535,6 +535,14 @@ export class UserService {
user.subscription.offer.label = undefined; user.subscription.offer.label = undefined;
} }
if (
!hasRole(user, Role.DEMO) &&
(user.provider !== 'ANONYMOUS' ||
user.subscription?.type === SubscriptionType.Premium)
) {
currentPermissions.push(permissions.requestOwnUserDeletion);
}
if (hasRole(user, Role.ADMIN)) { if (hasRole(user, Role.ADMIN)) {
currentPermissions.push(permissions.syncDemoUserAccount); currentPermissions.push(permissions.syncDemoUserAccount);
} }

2
apps/client/src/app/components/admin-settings/admin-settings.component.html

@ -21,7 +21,7 @@
<mat-card-actions class="pb-3 pt-0 px-3"> <mat-card-actions class="pb-3 pt-0 px-3">
<a <a
class="special" class="special"
href="mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Data Provider&body=Hello,%0D%0DI am interested in the Ghostfolio Premium data provider. Could you please give me access so I can try it for some time?%0D%0DKind regards" href="mailto:hi@ghostfol.io?subject=Ghostfolio Premium Data Provider&body=Hello,%0D%0DI am interested in the Ghostfolio Premium data provider. Could you please give me access so I can try it for some time?%0D%0DKind regards"
mat-flat-button mat-flat-button
> >
<ng-container i18n>Get Access</ng-container> <ng-container i18n>Get Access</ng-container>

6
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -27,6 +27,7 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed, computed,
@ -67,6 +68,7 @@ import { interval } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators'; import { switchMap, tap } from 'rxjs/operators';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
CommonModule, CommonModule,
GfPremiumIndicatorComponent, GfPremiumIndicatorComponent,
@ -165,6 +167,8 @@ export class GfAdminUsersComponent implements OnInit {
this.user.permissions, this.user.permissions,
permissions.impersonateAllUsers permissions.impersonateAllUsers
); );
this.changeDetectorRef.markForCheck();
} }
}), }),
switchMap(() => this.route.paramMap) switchMap(() => this.route.paramMap)
@ -196,6 +200,8 @@ export class GfAdminUsersComponent implements OnInit {
pageIndex: this.paginator().pageIndex, pageIndex: this.paginator().pageIndex,
showLoading: false showLoading: false
}); });
this.changeDetectorRef.markForCheck();
}); });
} }

2
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -456,7 +456,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.assetProfile?.symbol ? ` (${this.assetProfile.symbol})` : '' this.assetProfile?.symbol ? ` (${this.assetProfile.symbol})` : ''
}`; }`;
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=${reportDataGlitchSubject}&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${this.assetProfile?.symbol}%0DData Source: ${this.assetProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`; this.reportDataGlitchMail = `mailto:hi@ghostfol.io?subject=${reportDataGlitchSubject}&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${this.assetProfile?.symbol}%0DData Source: ${this.assetProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
if (this.assetProfile?.assetClass) { if (this.assetProfile?.assetClass) {
this.assetClass = translate(this.assetProfile?.assetClass); this.assetClass = translate(this.assetProfile?.assetClass);

1
apps/client/src/app/components/home-overview/home-overview.html

@ -86,7 +86,6 @@
<div class="col"> <div class="col">
<gf-portfolio-performance <gf-portfolio-performance
class="pb-4" class="pb-4"
[deviceType]="deviceType()"
[errors]="errors()" [errors]="errors()"
[isLoading]="isLoadingPerformance()" [isLoading]="isLoadingPerformance()"
[locale]="user()?.settings?.locale" [locale]="user()?.settings?.locale"

22
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html

@ -1,7 +1,7 @@
<div class="container p-0"> <div class="container p-0">
<div class="no-gutters row"> <div class="no-gutters row">
<div class="status-container text-muted text-right"> <div class="status-container text-muted text-right">
@if (errors?.length > 0 && !isLoading) { @if (errors()?.length > 0 && !isLoading()) {
<ion-icon <ion-icon
i18n-title i18n-title
name="time-outline" name="time-outline"
@ -10,7 +10,7 @@
/> />
} }
</div> </div>
@if (isLoading) { @if (isLoading()) {
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
@ -24,25 +24,25 @@
} }
<div <div
class="display-4 font-weight-bold m-0 text-center value-container" class="display-4 font-weight-bold m-0 text-center value-container"
[hidden]="isLoading" [hidden]="isLoading()"
> >
<span #value id="value"></span> <span #value id="value"></span>
</div> </div>
<div class="currency-container flex-grow-1 px-1"> <div class="currency-container flex-grow-1 px-1">
{{ unit }} {{ unit() }}
</div> </div>
</div> </div>
@if (showDetails) { @if (showDetails()) {
<div class="row"> <div class="row">
<div class="d-flex col justify-content-end"> <div class="d-flex col justify-content-end">
<gf-value <gf-value
[colorizeSign]="true" [colorizeSign]="true"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale()"
[value]=" [value]="
isLoading isLoading()
? undefined ? undefined
: performance?.netPerformanceWithCurrencyEffect : performance().netPerformanceWithCurrencyEffect
" "
/> />
</div> </div>
@ -50,11 +50,11 @@
<gf-value <gf-value
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale()"
[value]=" [value]="
isLoading isLoading()
? undefined ? undefined
: performance?.netPerformancePercentageWithCurrencyEffect : performance().netPerformancePercentageWithCurrencyEffect
" "
/> />
</div> </div>

79
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts

@ -13,10 +13,11 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
effect,
ElementRef, ElementRef,
Input, inject,
OnChanges, input,
ViewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { CountUp } from 'countup.js'; import { CountUp } from 'countup.js';
@ -32,58 +33,68 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
styleUrls: ['./portfolio-performance.component.scss'], styleUrls: ['./portfolio-performance.component.scss'],
templateUrl: './portfolio-performance.component.html' templateUrl: './portfolio-performance.component.html'
}) })
export class GfPortfolioPerformanceComponent implements OnChanges { export class GfPortfolioPerformanceComponent {
@Input() deviceType: string; public readonly errors = input<ResponseError['errors']>();
@Input() errors: ResponseError['errors']; public readonly isLoading = input<boolean>();
@Input() isLoading: boolean; public readonly locale = input<string>(getLocale());
@Input() locale = getLocale(); public readonly performance = input.required<PortfolioPerformance>();
@Input() performance: PortfolioPerformance; public readonly precision = input.required<number, number>({
@Input() precision: number; transform: (value) => {
@Input() showDetails: boolean; return value >= 0 ? value : 2;
@Input() unit: string; }
});
public readonly showDetails = input<boolean>(false);
public readonly unit = input.required<string>();
@ViewChild('value') value: ElementRef; private readonly value =
viewChild.required<ElementRef<HTMLSpanElement>>('value');
public constructor(private notificationService: NotificationService) { private readonly notificationService = inject(NotificationService);
addIcons({ timeOutline });
}
public ngOnChanges() { public constructor() {
this.precision = this.precision >= 0 ? this.precision : 2; addIcons({ timeOutline });
if (this.isLoading) { effect(() => {
if (this.value?.nativeElement) { if (this.isLoading()) {
this.value.nativeElement.innerHTML = ''; if (this.value().nativeElement) {
this.value().nativeElement.innerHTML = '';
} }
} else { } else {
if (isNumber(this.performance?.currentValueInBaseCurrency)) { if (isNumber(this.performance().currentValueInBaseCurrency)) {
new CountUp('value', this.performance?.currentValueInBaseCurrency, { new CountUp('value', this.performance().currentValueInBaseCurrency, {
decimal: getNumberFormatDecimal(this.locale), decimal: getNumberFormatDecimal(this.locale()),
decimalPlaces: this.precision, decimalPlaces: this.precision(),
duration: 1, duration: 1,
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale())
}).start(); }).start();
} else if (this.showDetails === false) { } else if (this.showDetails() === false) {
new CountUp( new CountUp(
'value', 'value',
this.performance?.netPerformancePercentageWithCurrencyEffect * 100, this.performance().netPerformancePercentageWithCurrencyEffect * 100,
{ {
decimal: getNumberFormatDecimal(this.locale), decimal: getNumberFormatDecimal(this.locale()),
decimalPlaces: 2, decimalPlaces: 2,
duration: 1, duration: 1,
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale())
} }
).start(); ).start();
} else { } else {
this.value.nativeElement.innerHTML = '*****'; this.value().nativeElement.innerHTML = '*****';
} }
} }
});
}
protected onShowErrors() {
const errors = this.errors();
if (!errors?.length) {
return;
} }
public onShowErrors() { const errorMessageParts: string[] = [];
const errorMessageParts = [];
for (const error of this.errors) { for (const error of errors) {
errorMessageParts.push(`${error.symbol} (${error.dataSource})`); errorMessageParts.push(`${error.symbol} (${error.dataSource})`);
} }

2
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -52,7 +52,7 @@ export class GfUserAccountMembershipComponent {
public priceId: string; public priceId: string;
public routerLinkPricing = publicRoutes.pricing.routerLink; public routerLinkPricing = publicRoutes.pricing.routerLink;
public trySubscriptionMail = public trySubscriptionMail =
'mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards'; 'mailto:hi@ghostfol.io?subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards';
public user: User; public user: User;
public constructor( public constructor(

9
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -71,11 +71,13 @@ import { catchError } from 'rxjs/operators';
export class GfUserAccountSettingsComponent implements OnInit { export class GfUserAccountSettingsComponent implements OnInit {
public appearancePlaceholder = $localize`Auto`; public appearancePlaceholder = $localize`Auto`;
public baseCurrency: string; public baseCurrency: string;
public closeUserAccountMail: string;
public currencies: string[] = []; public currencies: string[] = [];
public deleteOwnUserForm = this.formBuilder.group({ public deleteOwnUserForm = this.formBuilder.group({
accessToken: ['', Validators.required] accessToken: ['', Validators.required]
}); });
public hasPermissionToDeleteOwnUser: boolean; public hasPermissionToDeleteOwnUser: boolean;
public hasPermissionToRequestOwnUserDeletion: boolean;
public hasPermissionToUpdateViewMode: boolean; public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public isAccessTokenHidden = true; public isAccessTokenHidden = true;
@ -124,11 +126,18 @@ export class GfUserAccountSettingsComponent implements OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.closeUserAccountMail = `mailto:hi@ghostfol.io?subject=Delete Account&body=Hello%0D%0DPlease delete my Ghostfolio account.%0D%0DUser ID: ${this.user.id}%0D%0DKind regards`;
this.hasPermissionToDeleteOwnUser = hasPermission( this.hasPermissionToDeleteOwnUser = hasPermission(
this.user.permissions, this.user.permissions,
permissions.deleteOwnUser permissions.deleteOwnUser
); );
this.hasPermissionToRequestOwnUserDeletion = hasPermission(
this.user.permissions,
permissions.requestOwnUserDeletion
);
this.hasPermissionToUpdateUserSettings = hasPermission( this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions, this.user.permissions,
permissions.updateUserSettings permissions.updateUserSettings

36
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -280,21 +280,24 @@
<div class="align-items-center d-flex py-1"> <div class="align-items-center d-flex py-1">
<div class="pr-1 w-50"></div> <div class="pr-1 w-50"></div>
<div class="pl-1 text-monospace w-50"> <div class="pl-1 text-monospace w-50">
<button color="primary" mat-flat-button (click)="onExport()"> <button color="primary" i18n mat-flat-button (click)="onExport()">
<span i18n>Export Data</span> Export Data
</button> </button>
</div> </div>
</div> </div>
@if (hasPermissionToDeleteOwnUser) { @if (
hasPermissionToDeleteOwnUser || hasPermissionToRequestOwnUserDeletion
) {
<hr class="mt-5" /> <hr class="mt-5" />
<div class="d-flex py-1">
<div class="pr-1 text-danger w-50" i18n>Danger Zone</div>
<div class="pl-1 w-50">
@if (hasPermissionToDeleteOwnUser) {
<form <form
class="w-100" class="w-100"
[formGroup]="deleteOwnUserForm" [formGroup]="deleteOwnUserForm"
(ngSubmit)="onCloseAccount()" (ngSubmit)="onCloseAccount()"
> >
<div class="d-flex py-1">
<div class="pr-1 text-danger w-50" i18n>Danger Zone</div>
<div class="pl-1 w-50">
<mat-form-field <mat-form-field
appearance="outline" appearance="outline"
class="without-hint w-100" class="without-hint w-100"
@ -324,17 +327,34 @@
<button <button
class="mt-2" class="mt-2"
color="warn" color="warn"
i18n
mat-flat-button mat-flat-button
type="submit" type="submit"
[disabled]=" [disabled]="
!(deleteOwnUserForm.dirty && deleteOwnUserForm.valid) !(deleteOwnUserForm.dirty && deleteOwnUserForm.valid)
" "
> >
<span i18n>Close Account</span> Close Account
</button>
</form>
} @else if (
hasPermissionToRequestOwnUserDeletion &&
(user?.accounts?.length > 0 || user?.activitiesCount > 0)
) {
<button color="warn" i18n mat-flat-button [disabled]="true">
Close Account
</button> </button>
<div class="hint-text mt-2 text-muted" i18n>
For security reasons, please delete all activities and accounts
first before your Ghostfolio account can be closed.
</div>
} @else if (hasPermissionToRequestOwnUserDeletion) {
<a color="warn" i18n mat-flat-button [href]="closeUserAccountMail"
>Close Account</a
>
}
</div> </div>
</div> </div>
</form>
} }
</div> </div>
</div> </div>

2
apps/client/src/app/pages/faq/saas/saas-page.html

@ -96,7 +96,7 @@
</mat-card-header> </mat-card-header>
<mat-card-content <mat-card-content
>Request your student discount >Request your student discount
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with <a href="mailto:hi@ghostfol.io?subject=Student Discount">here</a> with
your university e-mail address.</mat-card-content your university e-mail address.</mat-card-content
> >
</mat-card> </mat-card>

4
apps/client/src/app/pages/pricing/pricing-page.html

@ -332,7 +332,7 @@
} }
<ng-container i18n>please</ng-container> <ng-container i18n>please</ng-container>
<ng-container>&nbsp;</ng-container> <ng-container>&nbsp;</ng-container>
<a href="mailto:hi@ghostfol.io?Subject=Referral link for..." i18n <a href="mailto:hi@ghostfol.io?subject=Referral link for..." i18n
>contact us</a >contact us</a
> >
<ng-container>&nbsp;</ng-container> <ng-container>&nbsp;</ng-container>
@ -343,7 +343,7 @@
<ng-container>&nbsp;</ng-container> <ng-container>&nbsp;</ng-container>
<ng-container i18n>Request it</ng-container> <ng-container i18n>Request it</ng-container>
<ng-container>&nbsp;</ng-container> <ng-container>&nbsp;</ng-container>
<a href="mailto:hi@ghostfol.io?Subject=Student Discount" i18n>here</a> <a href="mailto:hi@ghostfol.io?subject=Student Discount" i18n>here</a>
<ng-container>&nbsp;</ng-container> <ng-container>&nbsp;</ng-container>
<ng-container i18n>with your university e-mail address</ng-container>. <ng-container i18n>with your university e-mail address</ng-container>.
</p> </p>

1
libs/common/src/lib/permissions.ts

@ -50,6 +50,7 @@ export const permissions = {
readTags: 'readTags', readTags: 'readTags',
readWatchlist: 'readWatchlist', readWatchlist: 'readWatchlist',
reportDataGlitch: 'reportDataGlitch', reportDataGlitch: 'reportDataGlitch',
requestOwnUserDeletion: 'requestOwnUserDeletion',
syncDemoUserAccount: 'syncDemoUserAccount', syncDemoUserAccount: 'syncDemoUserAccount',
toggleReadOnlyMode: 'toggleReadOnlyMode', toggleReadOnlyMode: 'toggleReadOnlyMode',
updateAccount: 'updateAccount', updateAccount: 'updateAccount',

2
prisma/schema.prisma

@ -219,11 +219,11 @@ model SymbolProfile {
isActive Boolean @default(true) isActive Boolean @default(true)
isin String? isin String?
name String? name String?
updatedAt DateTime @updatedAt
scraperConfiguration Json? scraperConfiguration Json?
sectors Json? sectors Json?
symbol String symbol String
symbolMapping Json? symbolMapping Json?
updatedAt DateTime @updatedAt
url String? url String?
user User? @relation(fields: [userId], onDelete: Cascade, references: [id]) user User? @relation(fields: [userId], onDelete: Cascade, references: [id])
userId String? userId String?

Loading…
Cancel
Save