Browse Source

Merge remote-tracking branch 'origin/main' into feature/extend-holdings-endpoint-for-cash

pull/5650/head
KenTandrian 1 week ago
parent
commit
4ad13b71cf
  1. 7
      CHANGELOG.md
  2. 16
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.scss
  3. 70
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  4. 233
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  5. 44
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts
  6. 5
      apps/client/src/app/services/data.service.ts
  7. 4
      libs/common/src/lib/interfaces/info-item.interface.ts
  8. 4
      package-lock.json
  9. 2
      package.json

7
CHANGELOG.md

@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Integrated the endpoint to get all platforms (`GET api/v1/platforms`) into the create or update account dialog
- Extracted the scraper configuration to a dedicated tab in the asset profile details dialog of the admin control panel
## 2.227.0 - 2026-01-02
### Changed
- Initialized the input properties in the _FIRE_ calculator - Initialized the input properties in the _FIRE_ calculator
- Removed the deprecated public _Stripe_ key - Removed the deprecated public _Stripe_ key
- Upgraded `stripe` from version `18.5.0` to `20.1.0` - Upgraded `stripe` from version `18.5.0` to `20.1.0`

16
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.scss

@ -13,21 +13,5 @@
right: 1rem; right: 1rem;
top: 0; top: 0;
} }
.mat-expansion-panel {
--mat-expansion-container-background-color: transparent;
::ng-deep {
.mat-expansion-panel-body {
padding: 0;
}
}
.mat-expansion-panel-header {
&:hover {
--mat-expansion-header-hover-state-layer-color: transparent;
}
}
}
} }
} }

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

@ -38,8 +38,7 @@ import {
Inject, Inject,
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewChild, ViewChild
signal
} from '@angular/core'; } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
@ -60,7 +59,6 @@ import {
MatDialogModule, MatDialogModule,
MatDialogRef MatDialogRef
} from '@angular/material/dialog'; } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
@ -79,6 +77,7 @@ import { format } from 'date-fns';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
codeSlashOutline,
createOutline, createOutline,
ellipsisVertical, ellipsisVertical,
readerOutline, readerOutline,
@ -106,7 +105,6 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,
MatDialogModule, MatDialogModule,
MatExpansionModule,
MatInputModule, MatInputModule,
MatMenuModule, MatMenuModule,
MatSelectModule, MatSelectModule,
@ -233,8 +231,6 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
} }
]; ];
public scraperConfiguationIsExpanded = signal(false);
public sectors: { public sectors: {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
@ -255,7 +251,13 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private userService: UserService private userService: UserService
) { ) {
addIcons({ createOutline, ellipsisVertical, readerOutline, serverOutline }); addIcons({
codeSlashOutline,
createOutline,
ellipsisVertical,
readerOutline,
serverOutline
});
} }
public get canSaveAssetProfileIdentifier() { public get canSaveAssetProfileIdentifier() {
@ -504,7 +506,19 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
if (!scraperConfiguration.selector || !scraperConfiguration.url) { if (!scraperConfiguration.selector || !scraperConfiguration.url) {
scraperConfiguration = undefined; scraperConfiguration = undefined;
} }
} catch {} } catch (error) {
console.error($localize`Could not parse scraper configuration`, error);
this.snackBar.open(
'😞 ' + $localize`Could not parse scraper configuration`,
undefined,
{
duration: ms('3 seconds')
}
);
return;
}
try { try {
sectors = JSON.parse(this.assetProfileForm.get('sectors').value); sectors = JSON.parse(this.assetProfileForm.get('sectors').value);
@ -538,7 +552,16 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
object: assetProfile object: assetProfile
}); });
} catch (error) { } catch (error) {
console.error(error); console.error($localize`Could not validate form`, error);
this.snackBar.open(
'😞 ' + $localize`Could not validate form`,
undefined,
{
duration: ms('3 seconds')
}
);
return; return;
} }
@ -550,8 +573,29 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
}, },
assetProfile assetProfile
) )
.subscribe(() => { .subscribe({
this.initialize(); next: () => {
this.snackBar.open(
'✅ ' + $localize`Asset profile has been saved`,
undefined,
{
duration: ms('3 seconds')
}
);
this.initialize();
},
error: (error) => {
console.error($localize`Could not save asset profile`, error);
this.snackBar.open(
'😞 ' + $localize`Could not save asset profile`,
undefined,
{
duration: ms('3 seconds')
}
);
}
}); });
} }
@ -702,8 +746,8 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
} }
public onTriggerSubmitAssetProfileForm() { public onTriggerSubmitAssetProfileForm() {
if (this.assetProfileForm) { if (this.assetProfileForm.valid) {
this.assetProfileFormElement.nativeElement.requestSubmit(); this.onSubmitAssetProfileForm();
} }
} }

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

@ -390,130 +390,6 @@
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
@if (assetProfile?.dataSource === 'MANUAL') {
<div class="mb-3">
<mat-accordion class="my-3">
<mat-expansion-panel
class="shadow-none"
[expanded]="
assetProfileForm.controls.scraperConfiguration.controls
.selector.value !== '' &&
assetProfileForm.controls.scraperConfiguration.controls
.url.value !== ''
"
(closed)="scraperConfiguationIsExpanded.set(false)"
(opened)="scraperConfiguationIsExpanded.set(true)"
>
<mat-expansion-panel-header class="p-0 pr-3">
<mat-panel-title class="font-weight-bold" i18n
>Scraper Configuration</mat-panel-title
>
</mat-expansion-panel-header>
<div formGroupName="scraperConfiguration">
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Default Market Price</mat-label>
<input
formControlName="defaultMarketPrice"
matInput
type="number"
/>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>HTTP Request Headers</mat-label>
<textarea
cdkTextareaAutosize
formControlName="headers"
matInput
type="text"
[matAutocomplete]="auto"
></textarea>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Locale</mat-label>
<input
formControlName="locale"
matInput
type="text"
/>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Mode</mat-label>
<mat-select formControlName="mode">
@for (modeValue of modeValues; track modeValue) {
<mat-option [value]="modeValue.value">{{
modeValue.viewValue
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label>
<ng-container i18n>Selector</ng-container>*
</mat-label>
<textarea
cdkTextareaAutosize
formControlName="selector"
matInput
type="text"
></textarea>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label>
<ng-container i18n>Url</ng-container>*
</mat-label>
<input formControlName="url" matInput type="text" />
</mat-form-field>
</div>
<div class="my-3 text-right">
<button
color="accent"
mat-flat-button
type="button"
[disabled]="
assetProfileForm.controls.scraperConfiguration
.controls.selector.value === '' ||
assetProfileForm.controls.scraperConfiguration
.controls.url.value === ''
"
(click)="onTestMarketData()"
>
<ng-container i18n>Test</ng-container>
</button>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
}
@if (assetProfile?.dataSource === 'MANUAL') { @if (assetProfile?.dataSource === 'MANUAL') {
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
@ -588,6 +464,115 @@
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
@if (assetProfile?.dataSource === 'MANUAL') {
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="code-slash-outline" />
<div class="d-none d-sm-block ml-2" i18n>Scraper Configuration</div>
</ng-template>
<div class="container mt-3 p-0">
<div [formGroup]="assetProfileForm">
<div formGroupName="scraperConfiguration">
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Default Market Price</mat-label>
<input
formControlName="defaultMarketPrice"
matInput
type="number"
/>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>HTTP Request Headers</mat-label>
<textarea
cdkTextareaAutosize
formControlName="headers"
matInput
type="text"
[matAutocomplete]="auto"
></textarea>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Locale</mat-label>
<input formControlName="locale" matInput type="text" />
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Mode</mat-label>
<mat-select formControlName="mode">
@for (modeValue of modeValues; track modeValue) {
<mat-option [value]="modeValue.value">{{
modeValue.viewValue
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label>
<ng-container i18n>Selector</ng-container>*
</mat-label>
<textarea
cdkTextareaAutosize
formControlName="selector"
matInput
type="text"
></textarea>
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label>
<ng-container i18n>Url</ng-container>*
</mat-label>
<input formControlName="url" matInput type="text" />
</mat-form-field>
</div>
<div class="my-3 text-right">
<button
color="accent"
mat-flat-button
type="button"
[disabled]="
assetProfileForm.controls.scraperConfiguration.controls
.selector.value === '' ||
assetProfileForm.controls.scraperConfiguration.controls
.url.value === ''
"
(click)="onTestMarketData()"
>
<ng-container i18n>Test</ng-container>
</button>
</div>
</div>
</div>
</div>
</mat-tab>
}
</mat-tab-group> </mat-tab-group>
</div> </div>

44
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts

@ -59,7 +59,7 @@ export class GfCreateOrUpdateAccountDialogComponent implements OnDestroy {
public accountForm: FormGroup; public accountForm: FormGroup;
public currencies: string[] = []; public currencies: string[] = [];
public filteredPlatforms: Observable<Platform[]>; public filteredPlatforms: Observable<Platform[]>;
public platforms: Platform[]; public platforms: Platform[] = [];
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -71,10 +71,8 @@ export class GfCreateOrUpdateAccountDialogComponent implements OnDestroy {
) {} ) {}
public ngOnInit() { public ngOnInit() {
const { currencies, platforms } = this.dataService.fetchInfo(); const { currencies } = this.dataService.fetchInfo();
this.currencies = currencies; this.currencies = currencies;
this.platforms = platforms;
this.accountForm = this.formBuilder.group({ this.accountForm = this.formBuilder.group({
accountId: [{ disabled: true, value: this.data.account.id }], accountId: [{ disabled: true, value: this.data.account.id }],
@ -83,23 +81,33 @@ export class GfCreateOrUpdateAccountDialogComponent implements OnDestroy {
currency: [this.data.account.currency, Validators.required], currency: [this.data.account.currency, Validators.required],
isExcluded: [this.data.account.isExcluded], isExcluded: [this.data.account.isExcluded],
name: [this.data.account.name, Validators.required], name: [this.data.account.name, Validators.required],
platformId: [ platformId: [null, this.autocompleteObjectValidator()]
this.platforms.find(({ id }) => {
return id === this.data.account.platformId;
}),
this.autocompleteObjectValidator()
]
}); });
this.filteredPlatforms = this.accountForm this.dataService.fetchPlatforms().subscribe(({ platforms }) => {
.get('platformId') this.platforms = platforms;
.valueChanges.pipe(
startWith(''), const selectedPlatform = this.platforms.find(({ id }) => {
map((value) => { return id === this.data.account.platformId;
const name = typeof value === 'string' ? value : value?.name; });
return name ? this.filter(name as string) : this.platforms.slice();
}) this.accountForm.patchValue(
{
platformId: selectedPlatform
},
{ emitEvent: false }
); );
this.filteredPlatforms = this.accountForm
.get('platformId')
.valueChanges.pipe(
startWith(''),
map((value) => {
const name = typeof value === 'string' ? value : value?.name;
return name ? this.filter(name as string) : this.platforms.slice();
})
);
});
} }
public autoCompleteCheck() { public autoCompleteCheck() {

5
apps/client/src/app/services/data.service.ts

@ -42,6 +42,7 @@ import {
MarketDataDetailsResponse, MarketDataDetailsResponse,
MarketDataOfMarketsResponse, MarketDataOfMarketsResponse,
OAuthResponse, OAuthResponse,
PlatformsResponse,
PortfolioDetails, PortfolioDetails,
PortfolioDividendsResponse, PortfolioDividendsResponse,
PortfolioHoldingResponse, PortfolioHoldingResponse,
@ -521,6 +522,10 @@ export class DataService {
); );
} }
public fetchPlatforms() {
return this.http.get<PlatformsResponse>('/api/v1/platforms');
}
public fetchPortfolioDetails({ public fetchPortfolioDetails({
filters, filters,
withMarkets = false withMarkets = false

4
libs/common/src/lib/interfaces/info-item.interface.ts

@ -18,9 +18,5 @@ export interface InfoItem {
platforms: Platform[]; platforms: Platform[];
statistics: Statistics; statistics: Statistics;
/** @deprecated */
stripePublicKey?: string;
subscriptionOffer?: SubscriptionOffer; subscriptionOffer?: SubscriptionOffer;
} }

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.226.0", "version": "2.227.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.226.0", "version": "2.227.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.226.0", "version": "2.227.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",

Loading…
Cancel
Save