Browse Source

Merge branch 'main' into task/upgrade-prettier-to-version-3.8.2

pull/6733/head
Thomas Kaul 6 days ago
committed by GitHub
parent
commit
142d45b6c6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      CHANGELOG.md
  2. 16
      apps/api/src/app/import/import.controller.ts
  3. 74
      apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
  4. 299
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  5. 4
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  6. 44
      apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.component.ts
  7. 8
      apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/interfaces/interfaces.ts
  8. 54
      apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
  9. 2
      apps/client/src/app/components/home-watchlist/home-watchlist.html
  10. 16
      apps/client/src/app/pages/faq/overview/faq-overview-page.component.ts
  11. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  12. 8
      package-lock.json
  13. 2
      package.json

9
CHANGELOG.md

@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upgraded `angular` from version `21.1.1` to `21.2.7`
- Upgraded `Nx` from version `22.5.3` to `22.6.4`
- Upgraded `prettier` from version `3.8.1` to `3.8.2`
- Upgraded `yahoo-finance2` from version `3.13.2` to `3.14.0`
### Fixed
- Fixed the missing value column of the accounts table component on mobile
## 2.254.0 - 2026-03-10
@ -6045,10 +6050,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed the alias from the user interface as a preparation to remove it from the `User` database schema
- Removed the activities import limit for users with a subscription
### Todo
- Rename the environment variable from `MAX_ORDERS_TO_IMPORT` to `MAX_ACTIVITIES_TO_IMPORT`
## 1.169.0 - 14.07.2022
### Added

16
apps/api/src/app/import/import.controller.ts

@ -3,6 +3,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.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 { SubscriptionType } from '@ghostfolio/common/enums';
import { ImportResponse } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -62,7 +63,7 @@ export class ImportController {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Premium'
this.request.user.subscription.type === SubscriptionType.Premium
) {
maxActivitiesToImport = Number.MAX_SAFE_INTEGER;
}
@ -100,6 +101,17 @@ export class ImportController {
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<ImportResponse> {
let maxActivitiesToImport = this.configurationService.get(
'MAX_ACTIVITIES_TO_IMPORT'
);
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === SubscriptionType.Premium
) {
maxActivitiesToImport = Number.MAX_SAFE_INTEGER;
}
const activities = await this.importService.getDividends({
dataSource,
symbol,
@ -107,6 +119,6 @@ export class ImportController {
userId: this.request.user.id
});
return { activities };
return { activities: activities.slice(0, maxActivitiesToImport) };
}
}

74
apps/client/src/app/components/admin-jobs/admin-jobs.component.ts

@ -20,12 +20,13 @@ import {
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnInit,
ViewChild
viewChild
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule
@ -74,19 +75,25 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
templateUrl: './admin-jobs.html'
})
export class GfAdminJobsComponent implements OnInit {
@ViewChild(MatSort) sort: MatSort;
protected readonly sort = viewChild.required(MatSort);
public DATA_GATHERING_QUEUE_PRIORITY_LOW = DATA_GATHERING_QUEUE_PRIORITY_LOW;
public DATA_GATHERING_QUEUE_PRIORITY_HIGH =
protected readonly DATA_GATHERING_QUEUE_PRIORITY_HIGH =
DATA_GATHERING_QUEUE_PRIORITY_HIGH;
public DATA_GATHERING_QUEUE_PRIORITY_MEDIUM =
protected readonly DATA_GATHERING_QUEUE_PRIORITY_LOW =
DATA_GATHERING_QUEUE_PRIORITY_LOW;
protected readonly DATA_GATHERING_QUEUE_PRIORITY_MEDIUM =
DATA_GATHERING_QUEUE_PRIORITY_MEDIUM;
public dataSource = new MatTableDataSource<AdminJobs['jobs'][0]>();
public defaultDateTimeFormat: string;
public filterForm: FormGroup;
protected dataSource = new MatTableDataSource<AdminJobs['jobs'][0]>();
protected defaultDateTimeFormat: string;
protected readonly filterForm = new FormGroup({
status: new FormControl<JobStatus | null>(null)
});
public displayedColumns = [
protected readonly displayedColumns = [
'index',
'type',
'symbol',
@ -99,21 +106,20 @@ export class GfAdminJobsComponent implements OnInit {
'actions'
];
public hasPermissionToAccessBullBoard = false;
public isLoading = false;
public statusFilterOptions = QUEUE_JOB_STATUS_LIST;
protected hasPermissionToAccessBullBoard = false;
protected isLoading = false;
protected readonly statusFilterOptions = QUEUE_JOB_STATUS_LIST;
private user: User;
public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
private destroyRef: DestroyRef,
private formBuilder: FormBuilder,
private notificationService: NotificationService,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
private readonly adminService = inject(AdminService);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
private readonly notificationService = inject(NotificationService);
private readonly tokenStorageService = inject(TokenStorageService);
private readonly userService = inject(UserService);
public constructor() {
this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
@ -148,21 +154,17 @@ export class GfAdminJobsComponent implements OnInit {
}
public ngOnInit() {
this.filterForm = this.formBuilder.group({
status: []
});
this.filterForm.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
const currentFilter = this.filterForm.get('status').value;
const currentFilter = this.filterForm.controls.status.value;
this.fetchJobs(currentFilter ? [currentFilter] : undefined);
});
this.fetchJobs();
}
public onDeleteJob(aId: string) {
protected onDeleteJob(aId: string) {
this.adminService
.deleteJob(aId)
.pipe(takeUntilDestroyed(this.destroyRef))
@ -171,18 +173,18 @@ export class GfAdminJobsComponent implements OnInit {
});
}
public onDeleteJobs() {
const currentFilter = this.filterForm.get('status').value;
protected onDeleteJobs() {
const currentFilter = this.filterForm.controls.status.value;
this.adminService
.deleteJobs({ status: currentFilter ? [currentFilter] : undefined })
.deleteJobs({ status: currentFilter ? [currentFilter] : [] })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.fetchJobs(currentFilter ? [currentFilter] : undefined);
});
}
public onExecuteJob(aId: string) {
protected onExecuteJob(aId: string) {
this.adminService
.executeJob(aId)
.pipe(takeUntilDestroyed(this.destroyRef))
@ -191,7 +193,7 @@ export class GfAdminJobsComponent implements OnInit {
});
}
public onOpenBullBoard() {
protected onOpenBullBoard() {
const token = this.tokenStorageService.getToken();
document.cookie = [
@ -203,13 +205,13 @@ export class GfAdminJobsComponent implements OnInit {
window.open(BULL_BOARD_ROUTE, '_blank');
}
public onViewData(aData: AdminJobs['jobs'][0]['data']) {
protected onViewData(aData: AdminJobs['jobs'][0]['data']) {
this.notificationService.alert({
title: JSON.stringify(aData, null, ' ')
});
}
public onViewStacktrace(aStacktrace: AdminJobs['jobs'][0]['stacktrace']) {
protected onViewStacktrace(aStacktrace: AdminJobs['jobs'][0]['stacktrace']) {
this.notificationService.alert({
title: JSON.stringify(aStacktrace, null, ' ')
});
@ -223,7 +225,7 @@ export class GfAdminJobsComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ jobs }) => {
this.dataSource = new MatTableDataSource(jobs);
this.dataSource.sort = this.sort;
this.dataSource.sort = this.sort();
this.dataSource.sortingDataAccessor = get;
this.isLoading = false;

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

@ -87,6 +87,7 @@ import {
readerOutline,
serverOutline
} from 'ionicons/icons';
import { isBoolean } from 'lodash';
import ms from 'ms';
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
@ -129,25 +130,26 @@ export class GfAssetProfileDialogComponent implements OnInit {
)};123.45`;
@ViewChild('assetProfileFormElement')
assetProfileFormElement: ElementRef<HTMLFormElement>;
public readonly assetProfileFormElement: ElementRef<HTMLFormElement>;
public assetClassLabel: string;
public assetSubClassLabel: string;
protected assetClassLabel: string;
protected assetSubClassLabel: string;
public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass)
.map((id) => {
return { id, label: translate(id) } as AssetClassSelectorOption;
})
.sort((a, b) => {
return a.label.localeCompare(b.label);
});
protected readonly assetClassOptions: AssetClassSelectorOption[] =
Object.keys(AssetClass)
.map((id) => {
return { id, label: translate(id) } as AssetClassSelectorOption;
})
.sort((a, b) => {
return a.label.localeCompare(b.label);
});
public assetSubClassOptions: AssetClassSelectorOption[] = [];
public assetProfile: AdminMarketDataDetails['assetProfile'];
protected assetSubClassOptions: AssetClassSelectorOption[] = [];
protected assetProfile: AdminMarketDataDetails['assetProfile'];
public assetProfileForm = this.formBuilder.group({
assetClass: new FormControl<AssetClass>(undefined),
assetSubClass: new FormControl<AssetSubClass>(undefined),
protected readonly assetProfileForm = this.formBuilder.group({
assetClass: new FormControl<AssetClass | null>(null),
assetSubClass: new FormControl<AssetSubClass | null>(null),
comment: '',
countries: ['', jsonValidator()],
currency: '',
@ -156,11 +158,15 @@ export class GfAssetProfileDialogComponent implements OnInit {
}),
isActive: [true],
name: ['', Validators.required],
scraperConfiguration: this.formBuilder.group({
defaultMarketPrice: null,
headers: [JSON.stringify({}), jsonValidator()],
scraperConfiguration: this.formBuilder.group<
Omit<ScraperConfiguration, 'headers'> & {
headers: FormControl<string | null>;
}
>({
defaultMarketPrice: undefined,
headers: new FormControl(JSON.stringify({}), jsonValidator()),
locale: '',
mode: '',
mode: 'lazy',
selector: '',
url: ''
}),
@ -169,12 +175,11 @@ export class GfAssetProfileDialogComponent implements OnInit {
url: ''
});
public assetProfileIdentifierForm = this.formBuilder.group(
protected readonly assetProfileIdentifierForm = this.formBuilder.group(
{
assetProfileIdentifier: new FormControl<AssetProfileIdentifier>(
{ symbol: null, dataSource: null },
[Validators.required]
)
assetProfileIdentifier: new FormControl<
AssetProfileIdentifier | { dataSource: null; symbol: null }
>({ dataSource: null, symbol: null }, [Validators.required])
},
{
validators: (control) => {
@ -183,16 +188,15 @@ export class GfAssetProfileDialogComponent implements OnInit {
}
);
public benchmarks: Partial<SymbolProfile>[];
public canEditAssetProfile = true;
protected canEditAssetProfile = true;
public countries: {
protected countries: {
[code: string]: { name: string; value: number };
};
public currencies: string[] = [];
protected currencies: string[] = [];
public dateRangeOptions = [
protected readonly dateRangeOptions = [
{
label: $localize`Current week` + ' (' + $localize`WTD` + ')',
value: 'wtd'
@ -218,14 +222,14 @@ export class GfAssetProfileDialogComponent implements OnInit {
value: 'max'
}
];
public historicalDataItems: LineChartItem[];
public isBenchmark = false;
public isDataGatheringEnabled: boolean;
public isEditAssetProfileIdentifierMode = false;
public isUUID = isUUID;
public marketDataItems: MarketData[] = [];
public modeValues = [
protected historicalDataItems: LineChartItem[];
protected isBenchmark = false;
protected isDataGatheringEnabled: boolean;
protected isEditAssetProfileIdentifierMode = false;
protected readonly isUUID = isUUID;
protected marketDataItems: MarketData[] = [];
protected readonly modeValues = [
{
value: 'lazy',
viewValue: $localize`Lazy` + ' (' + $localize`end of day` + ')'
@ -236,20 +240,22 @@ export class GfAssetProfileDialogComponent implements OnInit {
}
];
public sectors: {
protected sectors: {
[name: string]: { name: string; value: number };
};
public user: User;
protected user: User;
private benchmarks: Partial<SymbolProfile>[];
public constructor(
public adminMarketDataService: AdminMarketDataService,
protected adminMarketDataService: AdminMarketDataService,
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
@Inject(MAT_DIALOG_DATA) protected data: AssetProfileDialogParams,
private dataService: DataService,
private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfAssetProfileDialogComponent>,
private dialogRef: MatDialogRef<GfAssetProfileDialogComponent>,
private formBuilder: FormBuilder,
private notificationService: NotificationService,
private snackBar: MatSnackBar,
@ -264,7 +270,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
});
}
public get canSaveAssetProfileIdentifier() {
protected get canSaveAssetProfileIdentifier() {
return !this.assetProfileForm.dirty && this.canEditAssetProfile;
}
@ -277,8 +283,8 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.initialize();
}
public initialize() {
this.historicalDataItems = undefined;
protected initialize() {
this.historicalDataItems = [];
this.adminService
.fetchAdminData()
@ -298,10 +304,13 @@ export class GfAssetProfileDialogComponent implements OnInit {
}
});
this.assetProfileForm
.get('assetClass')
.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
this.assetProfileForm.controls.assetClass.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((assetClass) => {
if (!assetClass) {
return;
}
const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? [];
this.assetSubClassOptions = assetSubClasses
@ -313,7 +322,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
})
.sort((a, b) => a.label.localeCompare(b.label));
this.assetProfileForm.get('assetSubClass').setValue(null);
this.assetProfileForm.controls.assetSubClass.setValue(null);
this.changeDetectorRef.markForCheck();
});
@ -327,8 +336,10 @@ export class GfAssetProfileDialogComponent implements OnInit {
.subscribe(({ assetProfile, marketData }) => {
this.assetProfile = assetProfile;
this.assetClassLabel = translate(this.assetProfile?.assetClass);
this.assetSubClassLabel = translate(this.assetProfile?.assetSubClass);
this.assetClassLabel = translate(this.assetProfile?.assetClass ?? '');
this.assetSubClassLabel = translate(
this.assetProfile?.assetSubClass ?? ''
);
this.canEditAssetProfile = !isCurrency(
getCurrencyFromSymbol(this.data.symbol)
@ -350,7 +361,10 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.marketDataItems = marketData;
this.sectors = {};
if (this.assetProfile?.countries?.length > 0) {
if (
this.assetProfile?.countries &&
this.assetProfile.countries.length > 0
) {
for (const { code, name, weight } of this.assetProfile.countries) {
this.countries[code] = {
name,
@ -359,7 +373,10 @@ export class GfAssetProfileDialogComponent implements OnInit {
}
}
if (this.assetProfile?.sectors?.length > 0) {
if (
this.assetProfile?.sectors &&
this.assetProfile.sectors.length > 0
) {
for (const { name, weight } of this.assetProfile.sectors) {
this.sectors[name] = {
name,
@ -377,12 +394,14 @@ export class GfAssetProfileDialogComponent implements OnInit {
return { code, weight };
}) ?? []
),
currency: this.assetProfile?.currency,
currency: this.assetProfile?.currency ?? null,
historicalData: {
csvString: GfAssetProfileDialogComponent.HISTORICAL_DATA_TEMPLATE
},
isActive: this.assetProfile?.isActive,
name: this.assetProfile.name ?? this.assetProfile.symbol,
isActive: isBoolean(this.assetProfile?.isActive)
? this.assetProfile.isActive
: null,
name: this.assetProfile.name ?? this.assetProfile.symbol ?? null,
scraperConfiguration: {
defaultMarketPrice:
this.assetProfile?.scraperConfiguration?.defaultMarketPrice ??
@ -410,7 +429,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
});
}
public onCancelEditAssetProfileIdentifierMode() {
protected onCancelEditAssetProfileIdentifierMode() {
this.isEditAssetProfileIdentifierMode = false;
if (this.canEditAssetProfile) {
@ -420,17 +439,20 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.assetProfileIdentifierForm.reset();
}
public onClose() {
protected onClose() {
this.dialogRef.close();
}
public onDeleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) {
protected onDeleteProfileData({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
this.dialogRef.close();
}
public onGatherProfileDataBySymbol({
protected onGatherProfileDataBySymbol({
dataSource,
symbol
}: AssetProfileIdentifier) {
@ -440,7 +462,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
.subscribe();
}
public onGatherSymbol({
protected onGatherSymbol({
dataSource,
range,
symbol
@ -453,13 +475,13 @@ export class GfAssetProfileDialogComponent implements OnInit {
.subscribe();
}
public onMarketDataChanged(withRefresh: boolean = false) {
protected onMarketDataChanged(withRefresh: boolean = false) {
if (withRefresh) {
this.initialize();
}
}
public onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
protected onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService
.postBenchmark({ dataSource, symbol })
.pipe(takeUntilDestroyed(this.destroyRef))
@ -472,50 +494,48 @@ export class GfAssetProfileDialogComponent implements OnInit {
});
}
public onSetEditAssetProfileIdentifierMode() {
protected onSetEditAssetProfileIdentifierMode() {
this.isEditAssetProfileIdentifierMode = true;
this.assetProfileForm.disable();
}
public async onSubmitAssetProfileForm() {
let countries = [];
let scraperConfiguration: ScraperConfiguration = {
protected async onSubmitAssetProfileForm() {
let countries: Prisma.InputJsonArray = [];
let scraperConfiguration: Prisma.InputJsonObject | undefined = {
selector: '',
url: ''
};
let sectors = [];
let symbolMapping = {};
let sectors: Prisma.InputJsonArray = [];
let symbolMapping: Record<string, string> = {};
try {
countries = JSON.parse(this.assetProfileForm.get('countries').value);
countries = JSON.parse(
this.assetProfileForm.controls.countries.value ?? '[]'
) as Prisma.InputJsonArray;
} catch {}
try {
scraperConfiguration = {
defaultMarketPrice:
(this.assetProfileForm.controls['scraperConfiguration'].controls[
'defaultMarketPrice'
].value as number) || undefined,
this.assetProfileForm.controls.scraperConfiguration.controls
.defaultMarketPrice?.value ?? undefined,
headers: JSON.parse(
this.assetProfileForm.controls['scraperConfiguration'].controls[
'headers'
].value
),
this.assetProfileForm.controls.scraperConfiguration.controls.headers
.value ?? '{}'
) as Record<string, string>,
locale:
this.assetProfileForm.controls['scraperConfiguration'].controls[
'locale'
].value || undefined,
mode: this.assetProfileForm.controls['scraperConfiguration'].controls[
'mode'
].value as ScraperConfiguration['mode'],
this.assetProfileForm.controls.scraperConfiguration.controls.locale
?.value ?? undefined,
mode:
this.assetProfileForm.controls.scraperConfiguration.controls.mode
?.value ?? undefined,
selector:
this.assetProfileForm.controls['scraperConfiguration'].controls[
'selector'
].value,
url: this.assetProfileForm.controls['scraperConfiguration'].controls[
'url'
].value
this.assetProfileForm.controls.scraperConfiguration.controls.selector
.value ?? '',
url:
this.assetProfileForm.controls.scraperConfiguration.controls.url
.value ?? ''
};
if (!scraperConfiguration.selector || !scraperConfiguration.url) {
@ -536,28 +556,32 @@ export class GfAssetProfileDialogComponent implements OnInit {
}
try {
sectors = JSON.parse(this.assetProfileForm.get('sectors').value);
sectors = JSON.parse(
this.assetProfileForm.controls.sectors.value ?? '[]'
) as Prisma.InputJsonArray;
} catch {}
try {
symbolMapping = JSON.parse(
this.assetProfileForm.get('symbolMapping').value
);
this.assetProfileForm.controls.symbolMapping.value ?? '{}'
) as Record<string, string>;
} catch {}
const assetProfile: UpdateAssetProfileDto = {
countries,
scraperConfiguration,
sectors,
symbolMapping,
assetClass: this.assetProfileForm.get('assetClass').value,
assetSubClass: this.assetProfileForm.get('assetSubClass').value,
comment: this.assetProfileForm.get('comment').value || null,
currency: this.assetProfileForm.get('currency').value,
isActive: this.assetProfileForm.get('isActive').value,
name: this.assetProfileForm.get('name').value,
scraperConfiguration:
scraperConfiguration as unknown as Prisma.InputJsonObject,
url: this.assetProfileForm.get('url').value || null
assetClass: this.assetProfileForm.controls.assetClass.value ?? undefined,
assetSubClass:
this.assetProfileForm.controls.assetSubClass.value ?? undefined,
comment: this.assetProfileForm.controls.comment.value ?? undefined,
currency: this.assetProfileForm.controls.currency.value ?? undefined,
isActive: isBoolean(this.assetProfileForm.controls.isActive.value)
? this.assetProfileForm.controls.isActive.value
: undefined,
name: this.assetProfileForm.controls.name.value ?? undefined,
url: this.assetProfileForm.controls.url.value ?? undefined
};
try {
@ -600,7 +624,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.initialize();
},
error: (error) => {
error: (error: HttpErrorResponse) => {
console.error($localize`Could not save asset profile`, error);
this.snackBar.open(
@ -614,12 +638,14 @@ export class GfAssetProfileDialogComponent implements OnInit {
});
}
public async onSubmitAssetProfileIdentifierForm() {
protected async onSubmitAssetProfileIdentifierForm() {
const assetProfileIdentifier: UpdateAssetProfileDto = {
dataSource: this.assetProfileIdentifierForm.get('assetProfileIdentifier')
.value.dataSource,
symbol: this.assetProfileIdentifierForm.get('assetProfileIdentifier')
.value.symbol
dataSource:
this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value
?.dataSource ?? undefined,
symbol:
this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value
?.symbol ?? undefined
};
try {
@ -676,38 +702,33 @@ export class GfAssetProfileDialogComponent implements OnInit {
});
}
public onTestMarketData() {
protected onTestMarketData() {
this.adminService
.testMarketData({
dataSource: this.data.dataSource,
scraperConfiguration: {
defaultMarketPrice: this.assetProfileForm.controls[
'scraperConfiguration'
].controls['defaultMarketPrice'].value as number,
defaultMarketPrice:
this.assetProfileForm.controls.scraperConfiguration.controls
.defaultMarketPrice?.value,
headers: JSON.parse(
this.assetProfileForm.controls['scraperConfiguration'].controls[
'headers'
].value
),
this.assetProfileForm.controls.scraperConfiguration.controls.headers
.value ?? '{}'
) as Record<string, string>,
locale:
this.assetProfileForm.controls['scraperConfiguration'].controls[
'locale'
].value || undefined,
mode: this.assetProfileForm.controls['scraperConfiguration'].controls[
'mode'
].value,
this.assetProfileForm.controls.scraperConfiguration.controls.locale
?.value || undefined,
mode: this.assetProfileForm.controls.scraperConfiguration.controls
.mode?.value,
selector:
this.assetProfileForm.controls['scraperConfiguration'].controls[
'selector'
].value,
url: this.assetProfileForm.controls['scraperConfiguration'].controls[
'url'
].value
this.assetProfileForm.controls.scraperConfiguration.controls
.selector.value,
url: this.assetProfileForm.controls.scraperConfiguration.controls.url
.value
},
symbol: this.data.symbol
})
.pipe(
catchError(({ error }) => {
catchError(({ error }: HttpErrorResponse) => {
this.notificationService.alert({
message: error?.message,
title: $localize`Error`
@ -723,26 +744,26 @@ export class GfAssetProfileDialogComponent implements OnInit {
' ' +
price +
' ' +
this.assetProfileForm.get('currency').value
this.assetProfileForm.controls.currency.value
});
});
}
public onToggleIsActive({ checked }: MatCheckboxChange) {
protected onToggleIsActive({ checked }: MatCheckboxChange) {
if (checked) {
this.assetProfileForm.get('isActive')?.setValue(true);
this.assetProfileForm.controls.isActive.setValue(true);
} else {
this.assetProfileForm.get('isActive')?.setValue(false);
this.assetProfileForm.controls.isActive.setValue(false);
}
if (checked === this.assetProfile.isActive) {
this.assetProfileForm.get('isActive')?.markAsPristine();
this.assetProfileForm.controls.isActive.markAsPristine();
} else {
this.assetProfileForm.get('isActive')?.markAsDirty();
this.assetProfileForm.controls.isActive.markAsDirty();
}
}
public onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
protected onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService
.deleteBenchmark({ dataSource, symbol })
.pipe(takeUntilDestroyed(this.destroyRef))
@ -755,15 +776,15 @@ export class GfAssetProfileDialogComponent implements OnInit {
});
}
public onTriggerSubmitAssetProfileForm() {
protected onTriggerSubmitAssetProfileForm() {
if (this.assetProfileForm.valid) {
this.onSubmitAssetProfileForm();
}
}
private isNewSymbolValid(control: AbstractControl): ValidationErrors {
private isNewSymbolValid(control: AbstractControl): ValidationErrors | null {
const currentAssetProfileIdentifier: AssetProfileIdentifier | undefined =
control.get('assetProfileIdentifier').value;
control.get('assetProfileIdentifier')?.value;
if (
currentAssetProfileIdentifier?.dataSource === this.data?.dataSource &&
@ -773,5 +794,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
equalsPreviousProfileIdentifier: true
};
}
return null;
}
}

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

@ -419,11 +419,11 @@
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Url</mat-label>
<input formControlName="url" matInput type="text" />
@if (assetProfileForm.get('url').value) {
@if (assetProfileForm.controls.url.value) {
<gf-entity-logo
class="mr-3"
matSuffix
[url]="assetProfileForm.get('url').value"
[url]="assetProfileForm.controls.url.value"
/>
}
</mat-form-field>

44
apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.component.ts

@ -1,9 +1,8 @@
import type { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
FormsModule,
@ -15,6 +14,8 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { CreateWatchlistItemForm } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'h-100' },
@ -30,44 +31,41 @@ import { MatFormFieldModule } from '@angular/material/form-field';
styleUrls: ['./create-watchlist-item-dialog.component.scss'],
templateUrl: 'create-watchlist-item-dialog.html'
})
export class GfCreateWatchlistItemDialogComponent implements OnInit {
public createWatchlistItemForm: FormGroup;
public constructor(
public readonly dialogRef: MatDialogRef<GfCreateWatchlistItemDialogComponent>,
public readonly formBuilder: FormBuilder
) {}
public ngOnInit() {
this.createWatchlistItemForm = this.formBuilder.group(
export class GfCreateWatchlistItemDialogComponent {
protected readonly createWatchlistItemForm: CreateWatchlistItemForm =
new FormGroup(
{
searchSymbol: new FormControl(null, [Validators.required])
searchSymbol: new FormControl<AssetProfileIdentifier | null>(null, [
Validators.required
])
},
{
validators: this.validator
}
);
}
public onCancel() {
private readonly dialogRef =
inject<MatDialogRef<GfCreateWatchlistItemDialogComponent>>(MatDialogRef);
protected onCancel() {
this.dialogRef.close();
}
public onSubmit() {
protected onSubmit() {
this.dialogRef.close({
dataSource:
this.createWatchlistItemForm.get('searchSymbol').value.dataSource,
symbol: this.createWatchlistItemForm.get('searchSymbol').value.symbol
this.createWatchlistItemForm.controls.searchSymbol.value?.dataSource,
symbol: this.createWatchlistItemForm.controls.searchSymbol.value?.symbol
});
}
private validator(control: AbstractControl): ValidationErrors {
const searchSymbolControl = control.get('searchSymbol');
private validator(control: CreateWatchlistItemForm): ValidationErrors {
const searchSymbolControl = control.controls.searchSymbol;
if (
searchSymbolControl.valid &&
searchSymbolControl.value.dataSource &&
searchSymbolControl.value.symbol
searchSymbolControl.value?.dataSource &&
searchSymbolControl.value?.symbol
) {
return { incomplete: false };
}

8
apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/interfaces/interfaces.ts

@ -1,4 +1,12 @@
import type { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import type { FormControl, FormGroup } from '@angular/forms';
export interface CreateWatchlistItemDialogParams {
deviceType: string;
locale: string;
}
export type CreateWatchlistItemForm = FormGroup<{
searchSymbol: FormControl<AssetProfileIdentifier | null>;
}>;

54
apps/client/src/app/components/home-watchlist/home-watchlist.component.ts

@ -1,5 +1,6 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { locale as defaultLocale } from '@ghostfolio/common/config';
import {
AssetProfileIdentifier,
Benchmark,
@ -14,8 +15,10 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
DestroyRef,
inject,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -45,26 +48,29 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
templateUrl: './home-watchlist.html'
})
export class GfHomeWatchlistComponent implements OnInit {
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateWatchlistItem: boolean;
public hasPermissionToDeleteWatchlistItem: boolean;
public user: User;
public watchlist: Benchmark[];
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
protected hasImpersonationId: boolean;
protected hasPermissionToCreateWatchlistItem: boolean;
protected hasPermissionToDeleteWatchlistItem: boolean;
protected user: User;
protected watchlist: Benchmark[];
protected readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly dialog = inject(MatDialog);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly userService = inject(UserService);
public constructor() {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef))
@ -110,7 +116,7 @@ export class GfHomeWatchlistComponent implements OnInit {
this.loadWatchlistData();
}
public onWatchlistItemDeleted({
protected onWatchlistItemDeleted({
dataSource,
symbol
}: AssetProfileIdentifier) {
@ -148,10 +154,10 @@ export class GfHomeWatchlistComponent implements OnInit {
>(GfCreateWatchlistItemDialogComponent, {
autoFocus: false,
data: {
deviceType: this.deviceType,
locale: this.user?.settings?.locale
deviceType: this.deviceType(),
locale: this.user?.settings?.locale ?? defaultLocale
},
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
dialogRef

2
apps/client/src/app/components/home-watchlist/home-watchlist.html

@ -11,7 +11,7 @@
<div class="col-xs-12 col-md-10 offset-md-1">
<gf-benchmark
[benchmarks]="watchlist"
[deviceType]="deviceType"
[deviceType]="deviceType()"
[hasPermissionToDeleteItem]="hasPermissionToDeleteWatchlistItem"
[locale]="user?.settings?.locale || undefined"
[user]="user"

16
apps/client/src/app/pages/faq/overview/faq-overview-page.component.ts

@ -7,11 +7,11 @@ import {
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
OnDestroy
DestroyRef
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
@ -21,21 +21,20 @@ import { Subject, takeUntil } from 'rxjs';
styleUrls: ['./faq-overview-page.scss'],
templateUrl: './faq-overview-page.html'
})
export class GfFaqOverviewPageComponent implements OnDestroy {
export class GfFaqOverviewPageComponent {
public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${publicRoutes.pricing.path}`;
public routerLinkFeatures = publicRoutes.features.routerLink;
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private destroyRef: DestroyRef,
private userService: UserService
) {}
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
@ -44,9 +43,4 @@ export class GfFaqOverviewPageComponent implements OnDestroy {
}
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

2
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -64,7 +64,7 @@ export class GfAccountsTableComponent {
public readonly showBalance = input(true);
public readonly showFooter = input(true);
public readonly showValue = input(true);
public readonly showValueInBaseCurrency = input(false);
public readonly showValueInBaseCurrency = input(true);
public readonly totalBalanceInBaseCurrency = input<number>();
public readonly totalValueInBaseCurrency = input<number>();

8
package-lock.json

@ -93,7 +93,7 @@
"svgmap": "2.19.2",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.0",
"yahoo-finance2": "3.13.2",
"yahoo-finance2": "3.14.0",
"zone.js": "0.16.1"
},
"devDependencies": {
@ -39587,9 +39587,9 @@
}
},
"node_modules/yahoo-finance2": {
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.13.2.tgz",
"integrity": "sha512-aAOJEjuLClfDxVPRKxjcwFoyzMr8BE/svgUqr5IjnQR+kppYbKO92Wl3SbAGz5DRghy6FiUfqi5FBDSBA/e2jg==",
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.14.0.tgz",
"integrity": "sha512-gsT/tqgeizKtMxbIIWFiFyuhM/6MZE4yEyNLmPekr88AX14JL2HWw0/QNMOR081jVtzTjihqDW0zV7IayH1Wcw==",
"license": "MIT",
"dependencies": {
"@deno/shim-deno": "~0.18.0",

2
package.json

@ -138,7 +138,7 @@
"svgmap": "2.19.2",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.0",
"yahoo-finance2": "3.13.2",
"yahoo-finance2": "3.14.0",
"zone.js": "0.16.1"
},
"devDependencies": {

Loading…
Cancel
Save