Browse Source

Merge branch 'main' into task/improve-usability-in-admin-control-panel-by-eliminating-page-reload

pull/7518/head
Thomas Kaul 4 weeks ago
committed by GitHub
parent
commit
f1d61c3e92
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 2
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
  3. 2
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
  4. 12
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
  5. 10
      apps/api/src/app/user/user.controller.ts
  6. 4
      apps/api/src/app/user/user.module.ts
  7. 2
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  8. 59
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  9. 2
      apps/client/src/app/components/admin-users/admin-users.html
  10. 7
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  11. 28
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

4
CHANGELOG.md

@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the usability of the admin control panel by eliminating the page reload on flushing the cache - Improved the usability of the admin control panel by eliminating the page reload on flushing the cache
- Improved the usability of the admin control panel by eliminating the page reload on gathering historical market data - Improved the usability of the admin control panel by eliminating the page reload on gathering historical market data
### Fixed
- Fixed the loading state in the user detail dialog of the admin control panel’s users section
## 3.40.0 - 2026-08-02 ## 3.40.0 - 2026-08-02
### Changed ### Changed

2
apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts

@ -142,6 +142,8 @@ export class AssetProfilesController {
}); });
return this.assetProfilesService.createSplit({ return this.assetProfilesService.createSplit({
dataSource,
symbol,
symbolProfileId, symbolProfileId,
date: parseISO(data.date), date: parseISO(data.date),
denominator: data.denominator, denominator: data.denominator,

2
apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts

@ -8,6 +8,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -23,6 +24,7 @@ import { AssetProfilesService } from './asset-profiles.service';
ApiModule, ApiModule,
AssetProfileSplitModule, AssetProfileSplitModule,
BenchmarkModule, BenchmarkModule,
DataGatheringQueueModule,
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,
MarketDataModule, MarketDataModule,

12
apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts

@ -5,6 +5,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
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';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos';
import { import {
@ -33,6 +34,7 @@ export class AssetProfilesService {
private readonly activitiesService: ActivitiesService, private readonly activitiesService: ActivitiesService,
private readonly assetProfileSplitService: AssetProfileSplitService, private readonly assetProfileSplitService: AssetProfileSplitService,
private readonly benchmarkService: BenchmarkService, private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
@ -41,22 +43,28 @@ export class AssetProfilesService {
) {} ) {}
public async createSplit({ public async createSplit({
dataSource,
date, date,
denominator, denominator,
numerator, numerator,
symbol,
symbolProfileId symbolProfileId
}: { }: {
date: Date; date: Date;
denominator: number; denominator: number;
numerator: number; numerator: number;
symbolProfileId: string; symbolProfileId: string;
}) { } & AssetProfileIdentifier) {
return this.assetProfileSplitService.upsert({ const assetProfileSplit = await this.assetProfileSplitService.upsert({
date, date,
denominator, denominator,
numerator, numerator,
symbolProfileId symbolProfileId
}); });
await this.dataGatheringService.gatherSymbol({ dataSource, symbol });
return assetProfileSplit;
} }
public async deleteSplit({ public async deleteSplit({

10
apps/api/src/app/user/user.controller.ts

@ -1,7 +1,9 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { CustomThrottlerGuard } from '@ghostfolio/api/guards/custom-throttler.guard'; import { CustomThrottlerGuard } from '@ghostfolio/api/guards/custom-throttler.guard';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { decodeDataSource } from '@ghostfolio/api/helper/data-source.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -119,6 +121,7 @@ export class UserController {
@Get() @Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getUser( public async getUser(
@Headers('accept-language') acceptLanguage: string, @Headers('accept-language') acceptLanguage: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
@ -165,6 +168,7 @@ export class UserController {
@Put('setting') @Put('setting')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async updateUserSetting(@Body() data: UpdateUserSettingDto) { public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
if ( if (
size(data) === 1 && size(data) === 1 &&
@ -192,6 +196,12 @@ export class UserController {
data data
); );
if (userSettings['filters.dataSource']) {
userSettings['filters.dataSource'] = decodeDataSource(
userSettings['filters.dataSource']
);
}
for (const key in userSettings) { for (const key in userSettings) {
if (userSettings[key] === false || userSettings[key] === null) { if (userSettings[key] === false || userSettings[key] === null) {
delete userSettings[key]; delete userSettings[key];

4
apps/api/src/app/user/user.module.ts

@ -1,6 +1,7 @@
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module'; import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module'; import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
@ -30,7 +31,8 @@ import { UserService } from './user.service';
PropertyModule, PropertyModule,
RedactValuesInResponseModule, RedactValuesInResponseModule,
SubscriptionModule, SubscriptionModule,
TagModule TagModule,
TransformDataSourceInResponseModule
], ],
providers: [UserService] providers: [UserService]
}) })

2
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -78,6 +78,7 @@ export class TransformDataSourceInResponseInterceptor<
valueMap, valueMap,
object: data, object: data,
paths: [ paths: [
'["filters.dataSource"]',
'activities[*].assetProfile.dataSource', 'activities[*].assetProfile.dataSource',
'activities[*].dataSource', 'activities[*].dataSource',
'assetProfile.dataSource', 'assetProfile.dataSource',
@ -88,6 +89,7 @@ export class TransformDataSourceInResponseInterceptor<
'holdings[*].assetProfile.dataSource', 'holdings[*].assetProfile.dataSource',
'holdings[*].dataSource', 'holdings[*].dataSource',
'items[*].dataSource', 'items[*].dataSource',
'settings["filters.dataSource"]',
'watchlist[*].dataSource' 'watchlist[*].dataSource'
] ]
}); });

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

@ -514,10 +514,6 @@
<div class="container mt-3 p-0"> <div class="container mt-3 p-0">
<div class="no-gutters row w-100"> <div class="no-gutters row w-100">
<div class="col-12"> <div class="col-12">
<p class="text-muted" i18n>
Splits are stored for this asset profile, but are not applied
to the portfolio calculation yet.
</p>
@if (splits.length > 0) { @if (splits.length > 0) {
<table class="gf-table mb-3 w-100"> <table class="gf-table mb-3 w-100">
<thead> <thead>
@ -580,38 +576,33 @@
</mat-datepicker-toggle> </mat-datepicker-toggle>
<mat-datepicker #dateOfSplit /> <mat-datepicker #dateOfSplit />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="mb-3 w-100"> <div class="align-items-center d-flex mb-3">
<mat-label i18n>Shares After</mat-label> <mat-form-field
<input appearance="outline"
formControlName="numerator" class="w-100 without-hint"
matInput
step="1"
type="number"
/>
<mat-hint
><ng-container i18n>Numerator</ng-container>,
<ng-container i18n
>e.g. 4 for a 4:1 split or 1 for a 1:10 reverse
split</ng-container
></mat-hint
> >
</mat-form-field> <mat-label i18n>Shares After</mat-label>
<mat-form-field appearance="outline" class="mb-3 w-100"> <input
<mat-label i18n>Shares Before</mat-label> formControlName="numerator"
<input matInput
formControlName="denominator" step="1"
matInput type="number"
step="1" />
type="number" </mat-form-field>
/> <div class="mx-2">:</div>
<mat-hint <mat-form-field
><ng-container i18n>Denominator</ng-container>, appearance="outline"
<ng-container i18n class="w-100 without-hint"
>e.g. 1 for a 4:1 split or 10 for a 1:10 reverse
split</ng-container
></mat-hint
> >
</mat-form-field> <mat-label i18n>Shares Before</mat-label>
<input
formControlName="denominator"
matInput
step="1"
type="number"
/>
</mat-form-field>
</div>
<div class="text-right"> <div class="text-right">
<button <button
color="primary" color="primary"

2
apps/client/src/app/components/admin-users/admin-users.html

@ -251,7 +251,7 @@
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" /> <ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete User</span> <span i18n>Delete</span>
</span> </span>
</button> </button>
</mat-menu> </mat-menu>

7
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts

@ -56,14 +56,18 @@ export class GfUserDetailDialogComponent implements OnInit {
protected readonly baseCurrency: string; protected readonly baseCurrency: string;
protected readonly canDeleteUser = canDeleteUser; protected readonly canDeleteUser = canDeleteUser;
protected readonly getCountryName = getCountryName; protected readonly getCountryName = getCountryName;
protected isLoading = true;
protected readonly subscriptionsDataSource = protected readonly subscriptionsDataSource =
new MatTableDataSource<Subscription>(); new MatTableDataSource<Subscription>();
protected readonly subscriptionsDisplayedColumns = [ protected readonly subscriptionsDisplayedColumns = [
'createdAt', 'createdAt',
'type', 'type',
'price', 'price',
'expiresAt' 'expiresAt'
]; ];
protected user: AdminUserResponse; protected user: AdminUserResponse;
protected readonly data = inject<UserDetailDialogParams>(MAT_DIALOG_DATA); protected readonly data = inject<UserDetailDialogParams>(MAT_DIALOG_DATA);
@ -72,6 +76,7 @@ export class GfUserDetailDialogComponent implements OnInit {
private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly dialogRef = private readonly dialogRef =
inject<MatDialogRef<GfUserDetailDialogComponent, UserDetailDialogResult>>( inject<MatDialogRef<GfUserDetailDialogComponent, UserDetailDialogResult>>(
MatDialogRef MatDialogRef
@ -101,6 +106,8 @@ export class GfUserDetailDialogComponent implements OnInit {
this.subscriptionsDataSource.data = this.user.subscriptions ?? []; this.subscriptionsDataSource.data = this.user.subscriptions ?? [];
this.isLoading = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }

28
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -36,12 +36,14 @@
i18n i18n
size="medium" size="medium"
[enableCopyToClipboardButton]="true" [enableCopyToClipboardButton]="true"
[value]="user?.id" [value]="isLoading ? undefined : user.id"
>User ID</gf-value >User ID</gf-value
> >
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="user?.role">Role</gf-value> <gf-value i18n size="medium" [value]="isLoading ? undefined : user.role"
>Role</gf-value
>
</div> </div>
</div> </div>
@ -52,7 +54,7 @@
size="medium" size="medium"
[isDate]="true" [isDate]="true"
[locale]="data.locale" [locale]="data.locale"
[value]="user?.createdAt" [value]="isLoading ? undefined : user.createdAt"
>Registration Date</gf-value >Registration Date</gf-value
> >
</div> </div>
@ -61,7 +63,7 @@
i18n i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="user?.provider" [value]="isLoading ? undefined : user.provider"
>Authentication</gf-value >Authentication</gf-value
> >
</div> </div>
@ -74,7 +76,9 @@
i18n i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="user?.subscription ? 'Premium' : 'Basic'" [value]="
isLoading ? undefined : user.subscription ? 'Premium' : 'Basic'
"
>Membership</gf-value >Membership</gf-value
> >
</div> </div>
@ -83,7 +87,11 @@
i18n i18n
size="medium" size="medium"
[value]=" [value]="
user?.country ? getCountryName({ code: user.country }) : '-' isLoading
? undefined
: user.country
? getCountryName({ code: user.country })
: '-'
" "
>Country</gf-value >Country</gf-value
> >
@ -97,7 +105,7 @@
i18n i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="user?.accountCount" [value]="isLoading ? undefined : user.accountCount"
>Accounts</gf-value >Accounts</gf-value
> >
</div> </div>
@ -106,7 +114,7 @@
i18n i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="user?.activityCount" [value]="isLoading ? undefined : user.activityCount"
>Activities</gf-value >Activities</gf-value
> >
</div> </div>
@ -120,7 +128,7 @@
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[precision]="0" [precision]="0"
[value]="user?.engagement" [value]="isLoading ? undefined : user.engagement"
>Engagement per Day</gf-value >Engagement per Day</gf-value
> >
</div> </div>
@ -129,7 +137,7 @@
i18n i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="user?.dailyApiRequests" [value]="isLoading ? undefined : user.dailyApiRequests"
>API Requests Today</gf-value >API Requests Today</gf-value
> >
</div> </div>

Loading…
Cancel
Save