Browse Source

Merge ec3a6ecba0 into e7e3ef601f

pull/6709/merge
Asish Kumar 1 month ago
committed by GitHub
parent
commit
981ea7375a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 175
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.spec.ts
  3. 75
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  4. 13
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

1
CHANGELOG.md

@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed the benchmark toggle in the asset profile details dialog to participate in the regular save flow
- Improved the style of the activity type component - Improved the style of the activity type component
## 2.253.0 - 2026-03-06 ## 2.253.0 - 2026-03-06

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

@ -0,0 +1,175 @@
import { AdminService, DataService } from '@ghostfolio/ui/services';
import { ChangeDetectorRef, DestroyRef } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import 'reflect-metadata';
import { of } from 'rxjs';
import { GfAssetProfileDialogComponent } from './asset-profile-dialog.component';
(globalThis as any).$localize = (
strings: TemplateStringsArray,
...expressions: unknown[]
) => {
return strings.reduce((result, string, index) => {
return result + string + String(expressions[index] ?? '');
}, '');
};
jest.mock('@ghostfolio/common/utils', () => {
const actual = jest.requireActual('@ghostfolio/common/utils');
return {
...actual,
validateObjectForForm: jest.fn().mockResolvedValue(undefined)
};
});
jest.mock('@ghostfolio/client/services/user/user.service', () => ({
UserService: class {}
}));
jest.mock(
'@ghostfolio/client/components/admin-market-data/admin-market-data.service',
() => ({
AdminMarketDataService: class {}
})
);
jest.mock('@ionic/angular/standalone', () => ({
IonIcon: class {}
}));
jest.mock('@ghostfolio/ui/currency-selector', () => ({
GfCurrencySelectorComponent: class {}
}));
jest.mock('@ghostfolio/ui/entity-logo', () => ({
GfEntityLogoComponent: class {}
}));
jest.mock('@ghostfolio/ui/historical-market-data-editor', () => ({
GfHistoricalMarketDataEditorComponent: class {}
}));
jest.mock('@ghostfolio/ui/i18n', () => ({
translate: (value: string) => value
}));
jest.mock('@ghostfolio/ui/line-chart', () => ({
GfLineChartComponent: class {}
}));
jest.mock('@ghostfolio/ui/notifications', () => ({
NotificationService: class {}
}));
jest.mock('@ghostfolio/ui/portfolio-proportion-chart', () => ({
GfPortfolioProportionChartComponent: class {}
}));
jest.mock('@ghostfolio/ui/symbol-autocomplete', () => ({
GfSymbolAutocompleteComponent: class {}
}));
jest.mock('@ghostfolio/ui/value', () => ({
GfValueComponent: class {}
}));
describe('GfAssetProfileDialogComponent', () => {
let component: GfAssetProfileDialogComponent;
let adminService: jest.Mocked<AdminService>;
let dataService: jest.Mocked<DataService>;
beforeEach(() => {
adminService = {
patchAssetProfile: jest.fn().mockReturnValue(of(undefined))
} as unknown as jest.Mocked<AdminService>;
dataService = {
deleteBenchmark: jest.fn().mockReturnValue(of(undefined)),
postBenchmark: jest.fn().mockReturnValue(of(undefined)),
updateInfo: jest.fn()
} as unknown as jest.Mocked<DataService>;
component = new GfAssetProfileDialogComponent(
{} as never,
adminService,
{ markForCheck: jest.fn() } as ChangeDetectorRef,
{ dataSource: 'YAHOO', symbol: 'AAPL' } as never,
dataService,
{} as DestroyRef,
{ close: jest.fn() } as MatDialogRef<GfAssetProfileDialogComponent>,
new FormBuilder(),
{} as never,
{ open: jest.fn() } as unknown as MatSnackBar,
{} as never
);
component.assetProfile = {
id: 'asset-profile-id',
isActive: true
} as never;
component.benchmarks = [];
component.assetProfileForm.patchValue({
countries: '[]',
currency: 'USD',
isActive: true,
name: 'Apple Inc.',
scraperConfiguration: {
defaultMarketPrice: null,
headers: '{}',
locale: '',
mode: '',
selector: '',
url: ''
},
sectors: '[]',
symbolMapping: '{}',
url: ''
});
jest.spyOn(component, 'initialize').mockImplementation();
});
it('only updates the benchmark when the form is submitted', async () => {
component.assetProfileForm.get('isBenchmark')?.setValue(true);
expect(dataService.postBenchmark).not.toHaveBeenCalled();
await component.onSubmitAssetProfileForm();
expect(adminService.patchAssetProfile).toHaveBeenCalledTimes(1);
expect(dataService.postBenchmark).toHaveBeenCalledWith({
dataSource: 'YAHOO',
symbol: 'AAPL'
});
expect(dataService.updateInfo).toHaveBeenCalledTimes(1);
});
it('removes the benchmark when it is unchecked and saved', async () => {
component.benchmarks = [{ id: 'asset-profile-id' }];
component.isBenchmark = true;
component.assetProfileForm.get('isBenchmark')?.setValue(false);
await component.onSubmitAssetProfileForm();
expect(dataService.deleteBenchmark).toHaveBeenCalledWith({
dataSource: 'YAHOO',
symbol: 'AAPL'
});
expect(dataService.postBenchmark).not.toHaveBeenCalled();
});
it('does not call benchmark endpoints when the state is unchanged', async () => {
component.assetProfileForm.get('isBenchmark')?.setValue(false);
await component.onSubmitAssetProfileForm();
expect(dataService.postBenchmark).not.toHaveBeenCalled();
expect(dataService.deleteBenchmark).not.toHaveBeenCalled();
});
});

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

@ -88,8 +88,8 @@ import {
serverOutline serverOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import ms from 'ms'; import ms from 'ms';
import { EMPTY } from 'rxjs'; import { EMPTY, of } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError, switchMap, tap } from 'rxjs/operators';
import { AssetProfileDialogParams } from './interfaces/interfaces'; import { AssetProfileDialogParams } from './interfaces/interfaces';
@ -155,6 +155,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
csvString: '' csvString: ''
}), }),
isActive: [true], isActive: [true],
isBenchmark: [false],
name: ['', Validators.required], name: ['', Validators.required],
scraperConfiguration: this.formBuilder.group({ scraperConfiguration: this.formBuilder.group({
defaultMarketPrice: null, defaultMarketPrice: null,
@ -382,6 +383,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
csvString: GfAssetProfileDialogComponent.HISTORICAL_DATA_TEMPLATE csvString: GfAssetProfileDialogComponent.HISTORICAL_DATA_TEMPLATE
}, },
isActive: this.assetProfile?.isActive, isActive: this.assetProfile?.isActive,
isBenchmark: this.isBenchmark,
name: this.assetProfile.name ?? this.assetProfile.symbol, name: this.assetProfile.name ?? this.assetProfile.symbol,
scraperConfiguration: { scraperConfiguration: {
defaultMarketPrice: defaultMarketPrice:
@ -459,19 +461,6 @@ export class GfAssetProfileDialogComponent implements OnInit {
} }
} }
public onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService
.postBenchmark({ dataSource, symbol })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.dataService.updateInfo();
this.isBenchmark = true;
this.changeDetectorRef.markForCheck();
});
}
public onSetEditAssetProfileIdentifierMode() { public onSetEditAssetProfileIdentifierMode() {
this.isEditAssetProfileIdentifierMode = true; this.isEditAssetProfileIdentifierMode = true;
@ -559,6 +548,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
scraperConfiguration as unknown as Prisma.InputJsonObject, scraperConfiguration as unknown as Prisma.InputJsonObject,
url: this.assetProfileForm.get('url').value || null url: this.assetProfileForm.get('url').value || null
}; };
const isBenchmark = !!this.assetProfileForm.get('isBenchmark').value;
try { try {
await validateObjectForForm({ await validateObjectForForm({
@ -588,6 +578,15 @@ export class GfAssetProfileDialogComponent implements OnInit {
}, },
assetProfile assetProfile
) )
.pipe(
switchMap(() => {
if (isBenchmark === this.isBenchmark) {
return of(undefined);
}
return this.updateBenchmark(isBenchmark);
})
)
.subscribe({ .subscribe({
next: () => { next: () => {
this.snackBar.open( this.snackBar.open(
@ -742,19 +741,6 @@ export class GfAssetProfileDialogComponent implements OnInit {
} }
} }
public onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService
.deleteBenchmark({ dataSource, symbol })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.dataService.updateInfo();
this.isBenchmark = false;
this.changeDetectorRef.markForCheck();
});
}
public onTriggerSubmitAssetProfileForm() { public onTriggerSubmitAssetProfileForm() {
if (this.assetProfileForm.valid) { if (this.assetProfileForm.valid) {
this.onSubmitAssetProfileForm(); this.onSubmitAssetProfileForm();
@ -774,4 +760,37 @@ export class GfAssetProfileDialogComponent implements OnInit {
}; };
} }
} }
private updateBenchmark(isBenchmark: boolean) {
const benchmark$ = isBenchmark
? this.dataService.postBenchmark({
dataSource: this.data.dataSource,
symbol: this.data.symbol
})
: this.dataService.deleteBenchmark({
dataSource: this.data.dataSource,
symbol: this.data.symbol
});
return benchmark$.pipe(
tap(() => {
this.dataService.updateInfo();
this.isBenchmark = isBenchmark;
if (this.assetProfile?.id) {
this.benchmarks = isBenchmark
? [
...this.benchmarks.filter(({ id }) => {
return id !== this.assetProfile.id;
}),
{ id: this.assetProfile.id }
]
: this.benchmarks.filter(({ id }) => {
return id !== this.assetProfile.id;
});
}
})
);
}
} }

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

@ -358,21 +358,10 @@
<div class="w-50"> <div class="w-50">
<mat-checkbox <mat-checkbox
color="primary" color="primary"
[checked]="isBenchmark" formControlName="isBenchmark"
[disabled]=" [disabled]="
!canEditAssetProfile || isEditAssetProfileIdentifierMode !canEditAssetProfile || isEditAssetProfileIdentifierMode
" "
(change)="
isBenchmark
? onUnsetBenchmark({
dataSource: data.dataSource,
symbol: data.symbol
})
: onSetBenchmark({
dataSource: data.dataSource,
symbol: data.symbol
})
"
> >
<ng-container i18n>Include in</ng-container> <ng-container i18n>Include in</ng-container>
<ng-container>&nbsp;</ng-container> <ng-container>&nbsp;</ng-container>

Loading…
Cancel
Save