Browse Source

Merge branch 'main' into bug/import_activities_dialog_extends_off_page

pull/5527/head
Thomas Kaul 3 months ago
committed by GitHub
parent
commit
d587f583d9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .github/FUNDING.yml
  2. 17
      CHANGELOG.md
  3. 29
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  4. 324
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  5. 1
      apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts
  6. 1
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  7. 1
      apps/client/src/app/components/home-market/home-market.html
  8. 1
      apps/client/src/app/components/markets/markets.html
  9. 12
      apps/client/src/app/pages/api/api-page.component.ts
  10. 4
      apps/client/src/app/pages/api/api-page.html
  11. 13
      apps/client/src/app/pages/register/register-page.component.ts
  12. 2
      apps/client/src/app/pages/register/user-account-registration-dialog/interfaces/interfaces.ts
  13. 12
      apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts
  14. 0
      apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html
  15. 0
      apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.scss
  16. 7
      libs/ui/src/lib/activities-table/activities-table.component.ts
  17. 7
      libs/ui/src/lib/benchmark/benchmark.component.html
  18. 1
      libs/ui/src/lib/benchmark/benchmark.component.ts
  19. 12
      package-lock.json
  20. 4
      package.json
  21. 17
      prisma/migrations/20250915163323_added_asset_profile_resolution/migration.sql
  22. 14
      prisma/schema.prisma

2
.github/FUNDING.yml

@ -1 +1 @@
custom: ['https://www.buymeacoffee.com/ghostfolio'] buy_me_a_coffee: ghostfolio

17
CHANGELOG.md

@ -9,7 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added paging of activities to import acitivities dialog - Added the symbol to the benchmark component
- Added pagination to the activities table of the activities import dialog
- Added an option to configure the account column of the activities table component
### Changed
- Hid the account column from the activities table of the account detail dialog to avoid redundant information
- Renamed the show access token dialog component to user account registration dialog component
- Refreshed the cryptocurrencies list
- Upgraded `countup.js` from version `2.8.2` to `2.9.0`
### Fixed
- Fixed an issue with `unitPriceInAssetProfileCurrency` in the value redaction interceptor for the impersonation mode
## 2.200.0 - 2025-09-17
### Changed ### Changed

29
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -56,11 +56,36 @@ export class GhostfolioService {
requestTimeout, requestTimeout,
symbol symbol
}) })
.then((assetProfile) => { .then(async (assetProfile) => {
const dataSourceOrigin = DataSource.GHOSTFOLIO;
if (assetProfile) {
await this.prismaService.assetProfileResolution.upsert({
create: {
dataSourceOrigin,
currency: assetProfile.currency,
dataSourceTarget: assetProfile.dataSource,
symbolOrigin: symbol,
symbolTarget: assetProfile.symbol
},
update: {
requestCount: {
increment: 1
}
},
where: {
dataSourceOrigin_symbolOrigin: {
dataSourceOrigin,
symbolOrigin: symbol
}
}
});
}
result = { result = {
...result, ...result,
...assetProfile, ...assetProfile,
dataSource: DataSource.GHOSTFOLIO dataSource: dataSourceOrigin
}; };
return assetProfile; return assetProfile;

324
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

File diff suppressed because it is too large

1
apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts

@ -61,6 +61,7 @@ export class RedactValuesInResponseInterceptor<T>
'totalInterestInBaseCurrency', 'totalInterestInBaseCurrency',
'totalValueInBaseCurrency', 'totalValueInBaseCurrency',
'unitPrice', 'unitPrice',
'unitPriceInAssetProfileCurrency',
'value', 'value',
'valueInBaseCurrency' 'valueInBaseCurrency'
].map((attribute) => { ].map((attribute) => {

1
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -126,6 +126,7 @@
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showAccountColumn]="false"
[showActions]=" [showActions]="
!data.hasImpersonationId && !data.hasImpersonationId &&
data.hasPermissionToCreateActivity && data.hasPermissionToCreateActivity &&

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

@ -34,6 +34,7 @@
[benchmarks]="benchmarks" [benchmarks]="benchmarks"
[deviceType]="deviceType" [deviceType]="deviceType"
[locale]="user?.settings?.locale || undefined" [locale]="user?.settings?.locale || undefined"
[showSymbol]="false"
[user]="user" [user]="user"
/> />
@if (benchmarks?.length > 0) { @if (benchmarks?.length > 0) {

1
apps/client/src/app/components/markets/markets.html

@ -43,6 +43,7 @@
[benchmarks]="benchmarks" [benchmarks]="benchmarks"
[deviceType]="deviceType" [deviceType]="deviceType"
[locale]="user?.settings?.locale || undefined" [locale]="user?.settings?.locale || undefined"
[showSymbol]="false"
[user]="user" [user]="user"
/> />
@if (benchmarks?.length > 0) { @if (benchmarks?.length > 0) {

12
apps/client/src/app/pages/api/api-page.component.ts

@ -4,6 +4,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
DataProviderGhostfolioAssetProfileResponse,
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
DividendsResponse, DividendsResponse,
HistoricalResponse, HistoricalResponse,
@ -25,6 +26,7 @@ import { map, Observable, Subject, takeUntil } from 'rxjs';
templateUrl: './api-page.html' templateUrl: './api-page.html'
}) })
export class GfApiPageComponent implements OnInit { export class GfApiPageComponent implements OnInit {
public assetProfile$: Observable<DataProviderGhostfolioAssetProfileResponse>;
public dividends$: Observable<DividendsResponse['dividends']>; public dividends$: Observable<DividendsResponse['dividends']>;
public historicalData$: Observable<HistoricalResponse['historicalData']>; public historicalData$: Observable<HistoricalResponse['historicalData']>;
public isinLookupItems$: Observable<LookupResponse['items']>; public isinLookupItems$: Observable<LookupResponse['items']>;
@ -40,6 +42,7 @@ export class GfApiPageComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.apiKey = prompt($localize`Please enter your Ghostfolio API key:`); this.apiKey = prompt($localize`Please enter your Ghostfolio API key:`);
this.assetProfile$ = this.fetchAssetProfile({ symbol: 'AAPL' });
this.dividends$ = this.fetchDividends({ symbol: 'KO' }); this.dividends$ = this.fetchDividends({ symbol: 'KO' });
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' }); this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' });
this.isinLookupItems$ = this.fetchLookupItems({ query: 'US0378331005' }); this.isinLookupItems$ = this.fetchLookupItems({ query: 'US0378331005' });
@ -53,6 +56,15 @@ export class GfApiPageComponent implements OnInit {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private fetchAssetProfile({ symbol }: { symbol: string }) {
return this.http
.get<DataProviderGhostfolioAssetProfileResponse>(
`/api/v1/data-providers/ghostfolio/asset-profile/${symbol}`,
{ headers: this.getHeaders() }
)
.pipe(takeUntil(this.unsubscribeSubject));
}
private fetchDividends({ symbol }: { symbol: string }) { private fetchDividends({ symbol }: { symbol: string }) {
const params = new HttpParams() const params = new HttpParams()
.set('from', format(startOfYear(new Date()), DATE_FORMAT)) .set('from', format(startOfYear(new Date()), DATE_FORMAT))

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

@ -3,6 +3,10 @@
<h2 class="text-center">Status</h2> <h2 class="text-center">Status</h2>
<div>{{ status$ | async | json }}</div> <div>{{ status$ | async | json }}</div>
</div> </div>
<div class="mb-3">
<h2 class="text-center">Asset Profile</h2>
<div>{{ assetProfile$ | async | json }}</div>
</div>
<div> <div>
<h2 class="text-center">Lookup</h2> <h2 class="text-center">Lookup</h2>
@if (lookupItems$) { @if (lookupItems$) {

13
apps/client/src/app/pages/register/register-page.component.ts

@ -18,8 +18,8 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { ShowAccessTokenDialogParams } from './show-access-token-dialog/interfaces/interfaces'; import { UserAccountRegistrationDialogParams } from './user-account-registration-dialog/interfaces/interfaces';
import { GfShowAccessTokenDialogComponent } from './show-access-token-dialog/show-access-token-dialog.component'; import { GfUserAccountRegistrationDialogComponent } from './user-account-registration-dialog/user-account-registration-dialog.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -84,15 +84,18 @@ export class GfRegisterPageComponent implements OnDestroy, OnInit {
} }
public openShowAccessTokenDialog() { public openShowAccessTokenDialog() {
const dialogRef = this.dialog.open(GfShowAccessTokenDialogComponent, { const dialogRef = this.dialog.open(
GfUserAccountRegistrationDialogComponent,
{
data: { data: {
deviceType: this.deviceType, deviceType: this.deviceType,
needsToAcceptTermsOfService: this.hasPermissionForSubscription needsToAcceptTermsOfService: this.hasPermissionForSubscription
} as ShowAccessTokenDialogParams, } as UserAccountRegistrationDialogParams,
disableClose: true, disableClose: true,
height: this.deviceType === 'mobile' ? '98vh' : undefined, height: this.deviceType === 'mobile' ? '98vh' : undefined,
width: this.deviceType === 'mobile' ? '100vw' : '30rem' width: this.deviceType === 'mobile' ? '100vw' : '30rem'
}); }
);
dialogRef dialogRef
.afterClosed() .afterClosed()

2
apps/client/src/app/pages/register/show-access-token-dialog/interfaces/interfaces.ts → apps/client/src/app/pages/register/user-account-registration-dialog/interfaces/interfaces.ts

@ -1,4 +1,4 @@
export interface ShowAccessTokenDialogParams { export interface UserAccountRegistrationDialogParams {
deviceType: string; deviceType: string;
needsToAcceptTermsOfService: boolean; needsToAcceptTermsOfService: boolean;
} }

12
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.component.ts → apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts

@ -30,7 +30,7 @@ import {
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { ShowAccessTokenDialogParams } from './interfaces/interfaces'; import { UserAccountRegistrationDialogParams } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -50,11 +50,11 @@ import { ShowAccessTokenDialogParams } from './interfaces/interfaces';
TextFieldModule TextFieldModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-show-access-token-dialog', selector: 'gf-user-account-registration-dialog',
styleUrls: ['./show-access-token-dialog.scss'], styleUrls: ['./user-account-registration-dialog.scss'],
templateUrl: 'show-access-token-dialog.html' templateUrl: 'user-account-registration-dialog.html'
}) })
export class GfShowAccessTokenDialogComponent { export class GfUserAccountRegistrationDialogComponent {
@ViewChild(MatStepper) stepper!: MatStepper; @ViewChild(MatStepper) stepper!: MatStepper;
public accessToken: string; public accessToken: string;
@ -69,7 +69,7 @@ export class GfShowAccessTokenDialogComponent {
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: ShowAccessTokenDialogParams, @Inject(MAT_DIALOG_DATA) public data: UserAccountRegistrationDialogParams,
private dataService: DataService private dataService: DataService
) { ) {
addIcons({ arrowForwardOutline, checkmarkOutline, copyOutline }); addIcons({ arrowForwardOutline, checkmarkOutline, copyOutline });

0
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html → apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html

0
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.scss → apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.scss

7
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -104,6 +104,7 @@ export class GfActivitiesTableComponent
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() pageIndex: number; @Input() pageIndex: number;
@Input() pageSize = DEFAULT_PAGE_SIZE; @Input() pageSize = DEFAULT_PAGE_SIZE;
@Input() showAccountColumn = true;
@Input() showActions = true; @Input() showActions = true;
@Input() showCheckbox = false; @Input() showCheckbox = false;
@Input() showNameColumn = true; @Input() showNameColumn = true;
@ -194,6 +195,12 @@ export class GfActivitiesTableComponent
'actions' 'actions'
]; ];
if (!this.showAccountColumn) {
this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'account';
});
}
if (!this.showCheckbox) { if (!this.showCheckbox) {
this.displayedColumns = this.displayedColumns.filter((column) => { this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'importStatus' && column !== 'select'; return column !== 'importStatus' && column !== 'select';

7
libs/ui/src/lib/benchmark/benchmark.component.html

@ -12,7 +12,14 @@
Name Name
</th> </th>
<td *matCellDef="let element" class="px-2 text-nowrap" mat-cell> <td *matCellDef="let element" class="px-2 text-nowrap" mat-cell>
<div class="text-truncate">
{{ element?.name }} {{ element?.name }}
</div>
@if (showSymbol) {
<div>
<small class="text-muted">{{ element?.symbol }}</small>
</div>
}
</td> </td>
</ng-container> </ng-container>

1
libs/ui/src/lib/benchmark/benchmark.component.ts

@ -62,6 +62,7 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToDeleteItem: boolean; @Input() hasPermissionToDeleteItem: boolean;
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() showSymbol = true;
@Input() user: User; @Input() user: User;
@Output() itemDeleted = new EventEmitter<AssetProfileIdentifier>(); @Output() itemDeleted = new EventEmitter<AssetProfileIdentifier>();

12
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.199.0", "version": "2.200.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.199.0", "version": "2.200.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -64,7 +64,7 @@
"color": "5.0.0", "color": "5.0.0",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.1.1", "countries-list": "3.1.1",
"countup.js": "2.8.2", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"envalid": "8.1.0", "envalid": "8.1.0",
"fuse.js": "7.1.0", "fuse.js": "7.1.0",
@ -18619,9 +18619,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/countup.js": { "node_modules/countup.js": {
"version": "2.8.2", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.8.2.tgz", "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.9.0.tgz",
"integrity": "sha512-UtRoPH6udaru/MOhhZhI/GZHJKAyAxuKItD2Tr7AbrqrOPBX/uejWBBJt8q86169AMqKkE9h9/24kFWbUk/Bag==", "integrity": "sha512-llqrvyXztRFPp6+i8jx25phHWcVWhrHO4Nlt0uAOSKHB8778zzQswa4MU3qKBvkXfJKftRYFJuVHez67lyKdHg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/create-jest": { "node_modules/create-jest": {

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.199.0", "version": "2.200.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",
@ -110,7 +110,7 @@
"color": "5.0.0", "color": "5.0.0",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.1.1", "countries-list": "3.1.1",
"countup.js": "2.8.2", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"envalid": "8.1.0", "envalid": "8.1.0",
"fuse.js": "7.1.0", "fuse.js": "7.1.0",

17
prisma/migrations/20250915163323_added_asset_profile_resolution/migration.sql

@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "public"."AssetProfileResolution" (
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"currency" TEXT NOT NULL,
"dataSourceOrigin" "public"."DataSource" NOT NULL,
"dataSourceTarget" "public"."DataSource" NOT NULL,
"id" TEXT NOT NULL,
"requestCount" INTEGER NOT NULL DEFAULT 1,
"symbolOrigin" TEXT NOT NULL,
"symbolTarget" TEXT NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AssetProfileResolution_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "AssetProfileResolution_dataSourceOrigin_symbolOrigin_key" ON "public"."AssetProfileResolution"("dataSourceOrigin", "symbolOrigin");

14
prisma/schema.prisma

@ -88,6 +88,20 @@ model ApiKey {
@@index([userId]) @@index([userId])
} }
model AssetProfileResolution {
createdAt DateTime @default(now())
currency String
dataSourceOrigin DataSource
dataSourceTarget DataSource
id String @id @default(uuid())
requestCount Int @default(1)
symbolOrigin String
symbolTarget String
updatedAt DateTime @updatedAt
@@unique([dataSourceOrigin, symbolOrigin])
}
model AuthDevice { model AuthDevice {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
credentialId Bytes credentialId Bytes

Loading…
Cancel
Save