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. 103
      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. 110
      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
- 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 users section of the admin control panel
- 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 French (`fr`)

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

@ -535,6 +535,14 @@ export class UserService {
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)) {
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">
<a
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
>
<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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
@ -67,6 +68,7 @@ import { interval } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfPremiumIndicatorComponent,
@ -165,6 +167,8 @@ export class GfAdminUsersComponent implements OnInit {
this.user.permissions,
permissions.impersonateAllUsers
);
this.changeDetectorRef.markForCheck();
}
}),
switchMap(() => this.route.paramMap)
@ -196,6 +200,8 @@ export class GfAdminUsersComponent implements OnInit {
pageIndex: this.paginator().pageIndex,
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.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) {
this.assetClass = translate(this.assetProfile?.assetClass);

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

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

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

@ -13,10 +13,11 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import {
ChangeDetectionStrategy,
Component,
effect,
ElementRef,
Input,
OnChanges,
ViewChild
inject,
input,
viewChild
} from '@angular/core';
import { IonIcon } from '@ionic/angular/standalone';
import { CountUp } from 'countup.js';
@ -32,58 +33,68 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
styleUrls: ['./portfolio-performance.component.scss'],
templateUrl: './portfolio-performance.component.html'
})
export class GfPortfolioPerformanceComponent implements OnChanges {
@Input() deviceType: string;
@Input() errors: ResponseError['errors'];
@Input() isLoading: boolean;
@Input() locale = getLocale();
@Input() performance: PortfolioPerformance;
@Input() precision: number;
@Input() showDetails: boolean;
@Input() unit: string;
export class GfPortfolioPerformanceComponent {
public readonly errors = input<ResponseError['errors']>();
public readonly isLoading = input<boolean>();
public readonly locale = input<string>(getLocale());
public readonly performance = input.required<PortfolioPerformance>();
public readonly precision = input.required<number, number>({
transform: (value) => {
return value >= 0 ? value : 2;
}
});
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) {
addIcons({ timeOutline });
}
private readonly notificationService = inject(NotificationService);
public ngOnChanges() {
this.precision = this.precision >= 0 ? this.precision : 2;
public constructor() {
addIcons({ timeOutline });
if (this.isLoading) {
if (this.value?.nativeElement) {
this.value.nativeElement.innerHTML = '';
}
} else {
if (isNumber(this.performance?.currentValueInBaseCurrency)) {
new CountUp('value', this.performance?.currentValueInBaseCurrency, {
decimal: getNumberFormatDecimal(this.locale),
decimalPlaces: this.precision,
duration: 1,
separator: getNumberFormatGroup(this.locale)
}).start();
} else if (this.showDetails === false) {
new CountUp(
'value',
this.performance?.netPerformancePercentageWithCurrencyEffect * 100,
{
decimal: getNumberFormatDecimal(this.locale),
decimalPlaces: 2,
duration: 1,
separator: getNumberFormatGroup(this.locale)
}
).start();
effect(() => {
if (this.isLoading()) {
if (this.value().nativeElement) {
this.value().nativeElement.innerHTML = '';
}
} else {
this.value.nativeElement.innerHTML = '*****';
if (isNumber(this.performance().currentValueInBaseCurrency)) {
new CountUp('value', this.performance().currentValueInBaseCurrency, {
decimal: getNumberFormatDecimal(this.locale()),
decimalPlaces: this.precision(),
duration: 1,
separator: getNumberFormatGroup(this.locale())
}).start();
} else if (this.showDetails() === false) {
new CountUp(
'value',
this.performance().netPerformancePercentageWithCurrencyEffect * 100,
{
decimal: getNumberFormatDecimal(this.locale()),
decimalPlaces: 2,
duration: 1,
separator: getNumberFormatGroup(this.locale())
}
).start();
} else {
this.value().nativeElement.innerHTML = '*****';
}
}
}
});
}
public onShowErrors() {
const errorMessageParts = [];
protected onShowErrors() {
const errors = this.errors();
if (!errors?.length) {
return;
}
const errorMessageParts: string[] = [];
for (const error of this.errors) {
for (const error of errors) {
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 routerLinkPricing = publicRoutes.pricing.routerLink;
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 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 {
public appearancePlaceholder = $localize`Auto`;
public baseCurrency: string;
public closeUserAccountMail: string;
public currencies: string[] = [];
public deleteOwnUserForm = this.formBuilder.group({
accessToken: ['', Validators.required]
});
public hasPermissionToDeleteOwnUser: boolean;
public hasPermissionToRequestOwnUserDeletion: boolean;
public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public isAccessTokenHidden = true;
@ -124,11 +126,18 @@ export class GfUserAccountSettingsComponent implements OnInit {
if (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.user.permissions,
permissions.deleteOwnUser
);
this.hasPermissionToRequestOwnUserDeletion = hasPermission(
this.user.permissions,
permissions.requestOwnUserDeletion
);
this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions,
permissions.updateUserSettings

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

@ -280,61 +280,81 @@
<div class="align-items-center d-flex py-1">
<div class="pr-1 w-50"></div>
<div class="pl-1 text-monospace w-50">
<button color="primary" mat-flat-button (click)="onExport()">
<span i18n>Export Data</span>
<button color="primary" i18n mat-flat-button (click)="onExport()">
Export Data
</button>
</div>
</div>
@if (hasPermissionToDeleteOwnUser) {
@if (
hasPermissionToDeleteOwnUser || hasPermissionToRequestOwnUserDeletion
) {
<hr class="mt-5" />
<form
class="w-100"
[formGroup]="deleteOwnUserForm"
(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
appearance="outline"
class="without-hint w-100"
[hideRequiredMarker]="true"
<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
class="w-100"
[formGroup]="deleteOwnUserForm"
(ngSubmit)="onCloseAccount()"
>
<mat-label i18n>Security Token</mat-label>
<input
formControlName="accessToken"
matInput
[type]="isAccessTokenHidden ? 'password' : 'text'"
/>
<button
class="mx-2"
mat-icon-button
matSuffix
type="button"
(click)="isAccessTokenHidden = !isAccessTokenHidden"
<mat-form-field
appearance="outline"
class="without-hint w-100"
[hideRequiredMarker]="true"
>
<ion-icon
class="mat-icon"
[name]="
isAccessTokenHidden ? 'eye-outline' : 'eye-off-outline'
"
<mat-label i18n>Security Token</mat-label>
<input
formControlName="accessToken"
matInput
[type]="isAccessTokenHidden ? 'password' : 'text'"
/>
<button
class="mx-2"
mat-icon-button
matSuffix
type="button"
(click)="isAccessTokenHidden = !isAccessTokenHidden"
>
<ion-icon
class="mat-icon"
[name]="
isAccessTokenHidden ? 'eye-outline' : 'eye-off-outline'
"
/>
</button>
</mat-form-field>
<button
class="mt-2"
color="warn"
i18n
mat-flat-button
type="submit"
[disabled]="
!(deleteOwnUserForm.dirty && deleteOwnUserForm.valid)
"
>
Close Account
</button>
</mat-form-field>
<button
class="mt-2"
color="warn"
mat-flat-button
type="submit"
[disabled]="
!(deleteOwnUserForm.dirty && deleteOwnUserForm.valid)
"
>
<span i18n>Close Account</span>
</form>
} @else if (
hasPermissionToRequestOwnUserDeletion &&
(user?.accounts?.length > 0 || user?.activitiesCount > 0)
) {
<button color="warn" i18n mat-flat-button [disabled]="true">
Close Account
</button>
</div>
<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>
</form>
</div>
}
</div>
</div>

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

@ -96,7 +96,7 @@
</mat-card-header>
<mat-card-content
>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
>
</mat-card>

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

@ -332,7 +332,7 @@
}
<ng-container i18n>please</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
>
<ng-container>&nbsp;</ng-container>
@ -343,7 +343,7 @@
<ng-container>&nbsp;</ng-container>
<ng-container i18n>Request it</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 i18n>with your university e-mail address</ng-container>.
</p>

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

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

2
prisma/schema.prisma

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

Loading…
Cancel
Save