Browse Source

Merge remote-tracking branch 'upstream/main' into bugfix/average-unit-price-line-alignment

# Conflicts:
#	CHANGELOG.md
pull/7937/head
GabeSilvaDev 2 weeks ago
parent
commit
d86617eaed
  1. 9
      CHANGELOG.md
  2. 9
      apps/api/src/app/auth/auth.module.ts
  3. 20
      apps/api/src/app/auth/oidc.helper.spec.ts
  4. 4
      apps/api/src/app/auth/oidc.helper.ts
  5. 9
      eslint.config.cjs
  6. 25
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts
  7. 34
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html
  8. 1
      nx.json
  9. 6
      package-lock.json
  10. 4
      package.json

9
CHANGELOG.md

@ -7,18 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Fixed
- Fixed the alignment of the average price in the chart of the holding detail dialog for custom asset profiles
## 3.75.0 - 2026-09-28
### Changed ### Changed
- Extended the net performance on the analysis page to include the dividends (experimental) - Extended the net performance on the analysis page to include the dividends (experimental)
- Extended the net performance in the portfolio summary to include the dividends (experimental) - Extended the net performance in the portfolio summary to include the dividends (experimental)
- Improved the get quotes functionality of the _Manual_ service - Improved the get quotes functionality of the _Manual_ service
- Migrated the historical market data editor dialog from `ngModel` to form control
- Migrated the _ESLint_ configuration to the flat config format without `FlatCompat` - Migrated the _ESLint_ configuration to the flat config format without `FlatCompat`
- Upgraded `chartjs-chart-treemap` from version `4.2.0` to `4.2.2` - Upgraded `chartjs-chart-treemap` from version `4.2.0` to `4.2.2`
### Fixed ### Fixed
- Fixed an issue where holdings without a quote have been valued at the unit price of the latest activity instead of the latest market price - Fixed an issue where holdings without a quote have been valued at the unit price of the latest activity instead of the latest market price
- Fixed the alignment of the average price in the chart of the holding detail dialog for custom asset profiles - Fixed the discovery of the _OpenID Connect_ (`OIDC`) configuration for issuer URLs with a trailing slash (experimental)
## 3.74.0 - 2026-09-27 ## 3.74.0 - 2026-09-27

9
apps/api/src/app/auth/auth.module.ts

@ -22,6 +22,7 @@ import { AuthController } from './auth.controller';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { GoogleStrategy } from './google.strategy'; import { GoogleStrategy } from './google.strategy';
import { JwtStrategy } from './jwt.strategy'; import { JwtStrategy } from './jwt.strategy';
import { getOidcDiscoveryUrl } from './oidc.helper';
import { OidcStrategy } from './oidc.strategy'; import { OidcStrategy } from './oidc.strategy';
@Module({ @Module({
@ -94,9 +95,15 @@ import { OidcStrategy } from './oidc.strategy';
// Fetch OIDC configuration from discovery endpoint // Fetch OIDC configuration from discovery endpoint
try { try {
const response = await fetchService.fetch( const response = await fetchService.fetch(
`${issuer}/.well-known/openid-configuration` getOidcDiscoveryUrl(issuer)
); );
if (!response.ok) {
throw new Error(
`OIDC discovery request failed with status ${response.status}`
);
}
const config = (await response.json()) as { const config = (await response.json()) as {
authorization_endpoint: string; authorization_endpoint: string;
token_endpoint: string; token_endpoint: string;

20
apps/api/src/app/auth/oidc.helper.spec.ts

@ -0,0 +1,20 @@
import { getOidcDiscoveryUrl } from './oidc.helper';
describe('getOidcDiscoveryUrl', () => {
it.each([
[
'https://auth.example.com',
'https://auth.example.com/.well-known/openid-configuration'
],
[
'https://auth.example.com/',
'https://auth.example.com/.well-known/openid-configuration'
],
[
'https://auth.example.com/application/o/ghostfolio/',
'https://auth.example.com/application/o/ghostfolio/.well-known/openid-configuration'
]
])('creates the discovery URL for %s', (issuer, expected) => {
expect(getOidcDiscoveryUrl(issuer)).toBe(expected);
});
});

4
apps/api/src/app/auth/oidc.helper.ts

@ -0,0 +1,4 @@
export function getOidcDiscoveryUrl(issuer: string) {
// Remove trailing slashes
return `${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`;
}

9
eslint.config.cjs

@ -1,6 +1,7 @@
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const importPlugin = require('eslint-plugin-import'); const importPlugin = require('eslint-plugin-import');
const storybook = require('eslint-plugin-storybook'); const storybook = require('eslint-plugin-storybook');
const { defineConfig } = require('eslint/config');
const typescriptEslint = require('typescript-eslint'); const typescriptEslint = require('typescript-eslint');
module.exports = [ module.exports = [
@ -25,14 +26,12 @@ module.exports = [
], ],
enforceBuildableLibDependency: true enforceBuildableLibDependency: true
} }
], ]
'@typescript-eslint/no-extra-semi': 'error',
'no-extra-semi': 'off'
} }
}, },
...nxEslintPlugin.configs['flat/typescript'], ...nxEslintPlugin.configs['flat/typescript'],
...nxEslintPlugin.configs['flat/javascript'], ...nxEslintPlugin.configs['flat/javascript'],
...typescriptEslint.config({ ...defineConfig({
files: ['**/*.ts'], files: ['**/*.ts'],
plugins: { import: importPlugin }, plugins: { import: importPlugin },
extends: [ extends: [
@ -91,8 +90,6 @@ module.exports = [
} }
], ],
'@typescript-eslint/unified-signatures': 'error', '@typescript-eslint/unified-signatures': 'error',
'@typescript-eslint/no-loss-of-precision': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
'arrow-body-style': 'off', 'arrow-body-style': 'off',
'constructor-super': 'error', 'constructor-super': 'error',
eqeqeq: ['error', 'smart'], eqeqeq: ['error', 'smart'],

25
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts

@ -7,11 +7,15 @@ import {
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
DestroyRef, DestroyRef,
OnInit, OnInit,
inject, inject
signal
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@ -33,7 +37,6 @@ import { HistoricalMarketDataEditorDialogParams } from './interfaces/interfaces'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'h-100' }, host: { class: 'h-100' },
imports: [ imports: [
FormsModule,
IonIcon, IonIcon,
MatButtonModule, MatButtonModule,
MatDatepickerModule, MatDatepickerModule,
@ -51,7 +54,10 @@ export class GfHistoricalMarketDataEditorDialogComponent implements OnInit {
public readonly data = public readonly data =
inject<HistoricalMarketDataEditorDialogParams>(MAT_DIALOG_DATA); inject<HistoricalMarketDataEditorDialogParams>(MAT_DIALOG_DATA);
protected readonly marketPrice = signal(this.data.marketPrice); protected readonly historicalMarketDataForm = new FormGroup({
date: new FormControl({ disabled: true, value: this.data.dateString }),
marketPrice: new FormControl(this.data.marketPrice, Validators.required)
});
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly locale = private readonly locale =
@ -84,14 +90,19 @@ export class GfHistoricalMarketDataEditorDialogComponent implements OnInit {
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ marketPrice }) => { .subscribe(({ marketPrice }) => {
this.marketPrice.set(marketPrice); this.historicalMarketDataForm.controls.marketPrice.setValue(
marketPrice
);
this.historicalMarketDataForm.controls.marketPrice.markAsDirty();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
public onUpdate() { public onUpdate() {
const marketPrice = this.marketPrice(); const marketPrice =
this.historicalMarketDataForm.controls.marketPrice.value;
if (isNil(marketPrice)) { if (isNil(marketPrice)) {
return; return;

34
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html

@ -1,16 +1,14 @@
<form class="d-flex flex-column h-100"> <form
class="d-flex flex-column h-100"
[formGroup]="historicalMarketDataForm"
(ngSubmit)="onUpdate()"
>
<h1 i18n mat-dialog-title>Details for {{ data.symbol }}</h1> <h1 i18n mat-dialog-title>Details for {{ data.symbol }}</h1>
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div class="mb-3"> <div class="mb-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label> <mat-label i18n>Date</mat-label>
<input <input formControlName="date" matInput [matDatepicker]="date" />
disabled
matInput
name="date"
[matDatepicker]="date"
[(ngModel)]="data.dateString"
/>
<mat-datepicker-toggle class="mr-2" matSuffix [for]="date"> <mat-datepicker-toggle class="mr-2" matSuffix [for]="date">
<ion-icon <ion-icon
class="text-muted" class="text-muted"
@ -24,13 +22,7 @@
<div class="align-items-start d-flex"> <div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Market Price</mat-label> <mat-label i18n>Market Price</mat-label>
<input <input formControlName="marketPrice" matInput type="number" />
matInput
name="marketPrice"
type="number"
[ngModel]="marketPrice()"
(ngModelChange)="marketPrice.set($event)"
/>
<span class="ml-2" matTextSuffix>{{ data.currency }}</span> <span class="ml-2" matTextSuffix>{{ data.currency }}</span>
</mat-form-field> </mat-form-field>
<button <button
@ -38,6 +30,7 @@
i18n-title i18n-title
mat-button mat-button
title="Fetch market price" title="Fetch market price"
type="button"
(click)="onFetchSymbolForDate()" (click)="onFetchSymbolForDate()"
> >
<ion-icon class="text-muted" name="refresh-outline" /> <ion-icon class="text-muted" name="refresh-outline" />
@ -45,8 +38,15 @@
</div> </div>
</div> </div>
<div class="justify-content-end" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button color="primary" mat-flat-button (click)="onUpdate()"> <button
color="primary"
mat-flat-button
type="submit"
[disabled]="
!(historicalMarketDataForm.dirty && historicalMarketDataForm.valid)
"
>
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>
</div> </div>

1
nx.json

@ -98,6 +98,7 @@
"cache": true "cache": true
}, },
"lint": { "lint": {
"inputs": ["default", "^default", "{workspaceRoot}/eslint.config.cjs"],
"cache": true "cache": true
}, },
"@nx/jest:jest": { "@nx/jest:jest": {

6
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.74.0", "version": "3.75.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.74.0", "version": "3.75.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -152,8 +152,6 @@
"@types/papaparse": "5.5.2", "@types/papaparse": "5.5.2",
"@types/passport-google-oauth20": "2.0.17", "@types/passport-google-oauth20": "2.0.17",
"@types/passport-openidconnect": "0.1.3", "@types/passport-openidconnect": "0.1.3",
"@typescript-eslint/eslint-plugin": "8.67.0",
"@typescript-eslint/parser": "8.67.0",
"eslint": "9.35.0", "eslint": "9.35.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.74.0", "version": "3.75.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",
@ -196,8 +196,6 @@
"@types/papaparse": "5.5.2", "@types/papaparse": "5.5.2",
"@types/passport-google-oauth20": "2.0.17", "@types/passport-google-oauth20": "2.0.17",
"@types/passport-openidconnect": "0.1.3", "@types/passport-openidconnect": "0.1.3",
"@typescript-eslint/eslint-plugin": "8.67.0",
"@typescript-eslint/parser": "8.67.0",
"eslint": "9.35.0", "eslint": "9.35.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",

Loading…
Cancel
Save