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. 68
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  4. 189
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  5. 26
      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
- 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
- Removed the deprecated public _Stripe_ key
- 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;
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;
}
}
}
}
}

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

@ -38,8 +38,7 @@ import {
Inject,
OnDestroy,
OnInit,
ViewChild,
signal
ViewChild
} from '@angular/core';
import {
AbstractControl,
@ -60,7 +59,6 @@ import {
MatDialogModule,
MatDialogRef
} from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
@ -79,6 +77,7 @@ import { format } from 'date-fns';
import { StatusCodes } from 'http-status-codes';
import { addIcons } from 'ionicons';
import {
codeSlashOutline,
createOutline,
ellipsisVertical,
readerOutline,
@ -106,7 +105,6 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatExpansionModule,
MatInputModule,
MatMenuModule,
MatSelectModule,
@ -233,8 +231,6 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
}
];
public scraperConfiguationIsExpanded = signal(false);
public sectors: {
[name: string]: { name: string; value: number };
};
@ -255,7 +251,13 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
private snackBar: MatSnackBar,
private userService: UserService
) {
addIcons({ createOutline, ellipsisVertical, readerOutline, serverOutline });
addIcons({
codeSlashOutline,
createOutline,
ellipsisVertical,
readerOutline,
serverOutline
});
}
public get canSaveAssetProfileIdentifier() {
@ -504,7 +506,19 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
if (!scraperConfiguration.selector || !scraperConfiguration.url) {
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 {
sectors = JSON.parse(this.assetProfileForm.get('sectors').value);
@ -538,7 +552,16 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
object: assetProfile
});
} 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;
}
@ -550,8 +573,29 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
},
assetProfile
)
.subscribe(() => {
.subscribe({
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() {
if (this.assetProfileForm) {
this.assetProfileFormElement.nativeElement.requestSubmit();
if (this.assetProfileForm.valid) {
this.onSubmitAssetProfileForm();
}
}

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

@ -391,24 +391,87 @@
</mat-form-field>
</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>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Sectors</mat-label>
<textarea
cdkTextareaAutosize
formControlName="sectors"
matInput
type="text"
></textarea>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Countries</mat-label>
<textarea
cdkTextareaAutosize
formControlName="countries"
matInput
type="text"
></textarea>
</mat-form-field>
</div>
}
<div>
<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) {
<gf-entity-logo
class="mr-3"
matSuffix
[url]="assetProfileForm.get('url').value"
/>
}
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Note</mat-label>
<textarea
cdkAutosizeMinRows="2"
cdkTextareaAutosize
formControlName="comment"
matInput
(keyup.enter)="$event.stopPropagation()"
></textarea>
</mat-form-field>
</div>
</form>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="server-outline" />
<div class="d-none d-sm-block ml-2" i18n>Historical Market Data</div>
</ng-template>
<div class="container mt-3 p-0">
<div class="no-gutters row w-100">
<div class="col-12">
<gf-historical-market-data-editor
[currency]="assetProfile?.currency"
[dataSource]="data.dataSource"
[dateOfFirstActivity]="assetProfile?.dateOfFirstActivity"
[locale]="data.locale"
[marketData]="marketDataItems"
[symbol]="data.symbol"
[user]="user"
(marketDataChanged)="onMarketDataChanged($event)"
/>
</div>
</div>
</div>
</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
@ -444,11 +507,7 @@
class="w-100 without-hint"
>
<mat-label i18n>Locale</mat-label>
<input
formControlName="locale"
matInput
type="text"
/>
<input formControlName="locale" matInput type="text" />
</mat-form-field>
</div>
<div class="mt-3">
@ -499,10 +558,10 @@
mat-flat-button
type="button"
[disabled]="
assetProfileForm.controls.scraperConfiguration
.controls.selector.value === '' ||
assetProfileForm.controls.scraperConfiguration
.controls.url.value === ''
assetProfileForm.controls.scraperConfiguration.controls
.selector.value === '' ||
assetProfileForm.controls.scraperConfiguration.controls
.url.value === ''
"
(click)="onTestMarketData()"
>
@ -510,84 +569,10 @@
</button>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
}
@if (assetProfile?.dataSource === 'MANUAL') {
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Sectors</mat-label>
<textarea
cdkTextareaAutosize
formControlName="sectors"
matInput
type="text"
></textarea>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Countries</mat-label>
<textarea
cdkTextareaAutosize
formControlName="countries"
matInput
type="text"
></textarea>
</mat-form-field>
</div>
}
<div>
<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) {
<gf-entity-logo
class="mr-3"
matSuffix
[url]="assetProfileForm.get('url').value"
/>
}
</mat-form-field>
</div>
<div class="mt-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Note</mat-label>
<textarea
cdkAutosizeMinRows="2"
cdkTextareaAutosize
formControlName="comment"
matInput
(keyup.enter)="$event.stopPropagation()"
></textarea>
</mat-form-field>
</div>
</form>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="server-outline" />
<div class="d-none d-sm-block ml-2" i18n>Historical Market Data</div>
</ng-template>
<div class="container mt-3 p-0">
<div class="no-gutters row w-100">
<div class="col-12">
<gf-historical-market-data-editor
[currency]="assetProfile?.currency"
[dataSource]="data.dataSource"
[dateOfFirstActivity]="assetProfile?.dateOfFirstActivity"
[locale]="data.locale"
[marketData]="marketDataItems"
[symbol]="data.symbol"
[user]="user"
(marketDataChanged)="onMarketDataChanged($event)"
/>
</div>
</div>
</div>
</mat-tab>
}
</mat-tab-group>
</div>

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

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

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

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

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

4
package-lock.json

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

2
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.226.0",
"version": "2.227.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",

Loading…
Cancel
Save