Net Performance
-
(ROAI)
+ @if (
+ user?.settings?.performanceCalculationType ===
+ PerformanceCalculationType.ROAI
+ ) {
+
(ROAI)
+ }
Date: Mon, 7 Sep 2026 20:43:49 +0200
Subject: [PATCH 3/8] Task/include closed holdings by default in holdings
endpoint (#7843)
* Return active and closed holdings in holdings endpoint
* Update changelog
---
CHANGELOG.md | 1 +
.../app/portfolio/portfolio.service.spec.ts | 136 +++++++++++++++++-
.../src/app/portfolio/portfolio.service.ts | 37 +++--
.../account-detail-dialog.component.ts | 8 +-
.../home-holdings/home-holdings.component.ts | 4 +-
...reate-or-update-access-dialog.component.ts | 4 +-
...ate-or-update-activity-dialog.component.ts | 4 +-
.../import-activities-dialog.component.ts | 4 +
.../analysis/analysis-page.component.ts | 5 +-
.../src/lib/assistant/assistant.component.ts | 4 +-
10 files changed, 180 insertions(+), 27 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46214cfba6..dd97e5e31c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the loading state of the activity count in the portfolio summary
+- Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types
- Upgraded `zod` from version `4.4.3` to `4.5.4`
## 3.68.0 - 2026-09-06
diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts
index aed14e090f..0882ff7e64 100644
--- a/apps/api/src/app/portfolio/portfolio.service.spec.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts
@@ -355,8 +355,13 @@ describe('PortfolioService', () => {
describe('getDetails', () => {
const setUpCashOnlyPortfolio = ({
baseCurrency = 'CHF',
- emergencyFund
- }: { baseCurrency?: string; emergencyFund?: number } = {}) => {
+ emergencyFund,
+ quantity = 2000
+ }: {
+ baseCurrency?: string;
+ emergencyFund?: number;
+ quantity?: number;
+ } = {}) => {
const cashAccount: AccountWithBalance = {
balance: 2000,
comment: null,
@@ -421,7 +426,7 @@ describe('PortfolioService', () => {
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffectMap: {},
netPerformanceWithCurrencyEffectMap: {},
- quantity: new Big(2000),
+ quantity: new Big(quantity),
symbol: 'USD',
tags: [],
timeWeightedInvestment: new Big(0),
@@ -493,6 +498,131 @@ describe('PortfolioService', () => {
expect(holdings[0].assetProfile.symbol).toBe('USD');
expect(holdings[0].valueInBaseCurrency).toBe(1000);
});
+
+ it('should include closed holdings when all holdings are requested', async () => {
+ setUpCashOnlyPortfolio({ quantity: 0 });
+
+ const { holdings } = await portfolioService.getDetails({
+ filters: [],
+ includeAllHoldings: true,
+ userId: userDummyData.id
+ });
+
+ expect(holdings).toHaveLength(1);
+ expect(holdings[0].quantity).toBe(0);
+ });
+
+ it.each([
+ { holdingType: 'ACTIVE', quantity: 2000 },
+ { holdingType: 'CLOSED', quantity: 0 }
+ ])(
+ 'should return $holdingType holdings when the holding type is specified',
+ async ({ holdingType, quantity }) => {
+ setUpCashOnlyPortfolio({ quantity });
+
+ const { holdings } = await portfolioService.getDetails({
+ filters: [{ id: holdingType, type: 'HOLDING_TYPE' }],
+ userId: userDummyData.id
+ });
+
+ expect(holdings).toHaveLength(1);
+ expect(holdings[0].quantity).toBe(quantity);
+ }
+ );
+
+ it('should remove the holding type only from the snapshot filters', async () => {
+ setUpCashOnlyPortfolio({ quantity: 0 });
+
+ await portfolioService.getDetails({
+ filters: [
+ { id: AssetClass.EQUITY, type: 'ASSET_CLASS' },
+ { id: 'CLOSED', type: 'HOLDING_TYPE' }
+ ],
+ userId: userDummyData.id
+ });
+
+ expect(portfolioCalculatorFactory.createCalculator).toHaveBeenCalledWith(
+ expect.objectContaining({
+ filters: [{ id: AssetClass.EQUITY, type: 'ASSET_CLASS' }]
+ })
+ );
+ expect(
+ activitiesService.getActivitiesForPortfolioCalculator
+ ).toHaveBeenCalledWith({
+ filters: [{ id: AssetClass.EQUITY, type: 'ASSET_CLASS' }],
+ userCurrency: 'CHF',
+ userId: userDummyData.id
+ });
+ });
+ });
+
+ describe('getHoldings', () => {
+ const activeHolding = {
+ assetProfile: {
+ isin: 'US0378331005',
+ name: 'Apple',
+ symbol: 'AAPL'
+ },
+ quantity: 1
+ };
+
+ const closedHolding = {
+ assetProfile: {
+ isin: 'US5949181045',
+ name: 'Microsoft',
+ symbol: 'MSFT'
+ },
+ quantity: 0
+ };
+
+ beforeEach(() => {
+ jest.spyOn(portfolioService, 'getDetails').mockResolvedValue({
+ holdings: [activeHolding, closedHolding]
+ } as unknown as Awaited>);
+ });
+
+ it('should request all holdings when the holding type is not specified', async () => {
+ const holdings = await portfolioService.getHoldings({
+ dateRange: 'max',
+ userId: userDummyData.id
+ });
+
+ expect(holdings).toEqual([activeHolding, closedHolding]);
+ expect(portfolioService.getDetails).toHaveBeenCalledWith({
+ dateRange: 'max',
+ filters: undefined,
+ includeAllHoldings: true,
+ userId: userDummyData.id
+ });
+ });
+
+ it('should find a closed holding when the holding type is not specified', async () => {
+ const holdings = await portfolioService.getHoldings({
+ dateRange: 'max',
+ filters: [{ id: 'Microsoft', type: 'SEARCH_QUERY' }],
+ userId: userDummyData.id
+ });
+
+ expect(holdings).toEqual([closedHolding]);
+ });
+
+ it.each(['ACTIVE', 'CLOSED'])(
+ 'should not request all holdings when the holding type is %s',
+ async (holdingType) => {
+ await portfolioService.getHoldings({
+ dateRange: 'max',
+ filters: [{ id: holdingType, type: 'HOLDING_TYPE' }],
+ userId: userDummyData.id
+ });
+
+ expect(portfolioService.getDetails).toHaveBeenCalledWith({
+ dateRange: 'max',
+ filters: [{ id: holdingType, type: 'HOLDING_TYPE' }],
+ includeAllHoldings: false,
+ userId: userDummyData.id
+ });
+ }
+ );
});
describe('getHolding', () => {
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 19c2dd7948..60dab50329 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -477,12 +477,12 @@ export class PortfolioService {
filters?: Filter[];
userId: string;
}) {
- const { SEARCH_QUERY: [filterBySearchQuery] = [] } = groupBy(
- filters,
- ({ type }) => {
- return type;
- }
- );
+ const {
+ HOLDING_TYPE: [filterByHoldingType] = [],
+ SEARCH_QUERY: [filterBySearchQuery] = []
+ } = groupBy(filters, ({ type }) => {
+ return type;
+ });
const filtersWithoutSearchQueryFilter = filters?.filter(({ type }) => {
return type !== 'SEARCH_QUERY';
@@ -491,7 +491,8 @@ export class PortfolioService {
let { holdings } = await this.getDetails({
dateRange,
userId,
- filters: filtersWithoutSearchQueryFilter
+ filters: filtersWithoutSearchQueryFilter,
+ includeAllHoldings: !filterByHoldingType
});
if (filterBySearchQuery) {
@@ -592,6 +593,7 @@ export class PortfolioService {
public async getDetails({
dateRange = DEFAULT_DATE_RANGE,
filters,
+ includeAllHoldings = false,
user: userFromCaller,
userId,
withExcludedAccounts = false,
@@ -600,6 +602,7 @@ export class PortfolioService {
}: {
dateRange?: DateRange;
filters?: Filter[];
+ includeAllHoldings?: boolean;
user?: UserWithSettings;
userId: string;
withExcludedAccounts?: boolean;
@@ -614,19 +617,23 @@ export class PortfolioService {
(user.settings?.settings as UserSettings)?.emergencyFund ?? 0
);
+ const portfolioSnapshotFilters = filters?.filter(({ type }) => {
+ return type !== 'HOLDING_TYPE';
+ });
+
const { activities } =
await this.activitiesService.getActivitiesForPortfolioCalculator({
- filters,
userCurrency,
- userId
+ userId,
+ filters: portfolioSnapshotFilters
});
const portfolioCalculator = this.calculatorFactory.createCalculator({
activities,
- filters,
userId,
calculationType: this.getUserPerformanceCalculationType(user),
- currency: userCurrency
+ currency: userCurrency,
+ filters: portfolioSnapshotFilters
});
const { createdAt, currentValueInBaseCurrency, hasErrors, positions } =
@@ -706,13 +713,13 @@ export class PortfolioService {
tags,
valueInBaseCurrency
} of positions) {
- if (isFilteredByClosedHoldings === true) {
- if (!quantity.eq(0)) {
+ if (!includeAllHoldings) {
+ if (isFilteredByClosedHoldings && !quantity.eq(0)) {
// Ignore positions with a quantity
continue;
}
- } else {
- if (quantity.eq(0)) {
+
+ if (!isFilteredByClosedHoldings && quantity.eq(0)) {
// Ignore positions without any quantity
continue;
}
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
index 518458e8a4..75186b1744 100644
--- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
+++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
@@ -393,8 +393,12 @@ export class GfAccountDetailDialogComponent implements OnInit {
.fetchPortfolioHoldings({
filters: [
{
- type: 'ACCOUNT',
- id: this.data.accountId
+ id: this.data.accountId,
+ type: 'ACCOUNT'
+ },
+ {
+ id: 'ACTIVE',
+ type: 'HOLDING_TYPE'
}
]
})
diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts
index 7f02cfb64e..f079e1c9c6 100644
--- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts
+++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts
@@ -153,9 +153,7 @@ export class GfHomeHoldingsComponent implements OnInit {
private fetchHoldings() {
const filters = this.userService.getFilters();
- if (this.holdingType === 'CLOSED') {
- filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
- }
+ filters.push({ id: this.holdingType, type: 'HOLDING_TYPE' });
return this.dataService.fetchPortfolioHoldings({
filters,
diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
index 573d4e5bb6..63e7872c1e 100644
--- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
+++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
@@ -367,7 +367,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
private loadHoldings() {
this.dataService
- .fetchPortfolioHoldings()
+ .fetchPortfolioHoldings({
+ filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }]
+ })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => {
this.holdings = getHoldingsForFilter(holdings);
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
index 853787f804..3b7938c8c4 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
@@ -153,7 +153,9 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.defaultDateFormat = getDateFormatString(this.locale);
this.dataService
- .fetchPortfolioHoldings()
+ .fetchPortfolioHoldings({
+ filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }]
+ })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => {
this.defaultLookupItems = holdings
diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
index 48f1d5abd0..98e0c0ad82 100644
--- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
@@ -144,6 +144,10 @@ export class GfImportActivitiesDialogComponent {
{
id: AssetClass.FIXED_INCOME,
type: 'ASSET_CLASS'
+ },
+ {
+ id: 'ACTIVE',
+ type: 'HOLDING_TYPE'
}
],
range: DEFAULT_DATE_RANGE
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
index 3a3c44269d..26fb711bb7 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
@@ -377,7 +377,10 @@ export class GfAnalysisPageComponent implements OnInit {
this.dataService
.fetchPortfolioHoldings({
- filters: this.userService.getFilters(),
+ filters: [
+ ...this.userService.getFilters(),
+ { id: 'ACTIVE', type: 'HOLDING_TYPE' }
+ ],
range: this.user?.settings?.dateRange
})
.pipe(takeUntilDestroyed(this.destroyRef))
diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts
index bdbaa6cdb5..6babc10398 100644
--- a/libs/ui/src/lib/assistant/assistant.component.ts
+++ b/libs/ui/src/lib/assistant/assistant.component.ts
@@ -479,7 +479,9 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.setIsOpen(true);
this.dataService
- .fetchPortfolioHoldings()
+ .fetchPortfolioHoldings({
+ filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }]
+ })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => {
this.holdings = getHoldingsForFilter(holdings);
From eee5d08084a90282a9bf5dcf07591a92e6e8d679 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 7 Sep 2026 20:45:11 +0200
Subject: [PATCH 4/8] Task/upgrade bull-board to version 9.5.0 (#7844)
* Update bull-board to version 9.5.0
* Update changelog
---
CHANGELOG.md | 1 +
package-lock.json | 42 +++++++++++++++++++++---------------------
package.json | 6 +++---
3 files changed, 25 insertions(+), 24 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd97e5e31c..f363f4cc5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the loading state of the activity count in the portfolio summary
- Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types
+- Upgraded `bull-board` from version `9.0.1` to `9.5.0`
- Upgraded `zod` from version `4.4.3` to `4.5.4`
## 3.68.0 - 2026-09-06
diff --git a/package-lock.json b/package-lock.json
index 563f7d4e2d..ba53c3025b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,9 +21,9 @@
"@angular/platform-browser-dynamic": "22.1.2",
"@angular/router": "22.1.2",
"@angular/service-worker": "22.1.2",
- "@bull-board/api": "9.0.1",
- "@bull-board/express": "9.0.1",
- "@bull-board/nestjs": "9.0.1",
+ "@bull-board/api": "9.5.0",
+ "@bull-board/express": "9.5.0",
+ "@bull-board/nestjs": "9.5.0",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.7",
@@ -6166,17 +6166,17 @@
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@bull-board/api": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-9.0.1.tgz",
- "integrity": "sha512-QZ+SDOlD1MAePNqy8qfs+7zExZyJCW/+nEncGE6Mt/+MlxT9mqvyZWmPUm38QZvIBJGSKaA9i9ZElR9wDbbPhw==",
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-9.5.0.tgz",
+ "integrity": "sha512-fmOyOy1lZiLZFyDJjI2osZrpTMO+JSgmGNhDf3MrmOf3oF084o6w0wpGefs0OqEfyUT1F5/1arE5AwmEUuxZ+A==",
"license": "MIT",
"dependencies": {
"redis-info": "^3.1.0"
},
"peerDependencies": {
- "@bull-board/ui": "9.0.1",
+ "@bull-board/ui": "9.5.0",
"bull": "^4.16.5",
- "bullmq": "^5.79.2 || ^6.0.0"
+ "bullmq": "^5.56.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"bull": {
@@ -6188,13 +6188,13 @@
}
},
"node_modules/@bull-board/express": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-9.0.1.tgz",
- "integrity": "sha512-eblNP2imr639K9KG0n1sBaJp6lw6pu4/j7q3W+bydxtw83v4wobCcuVUxuhQkRaLTrIUMR/Snulc7+/vtxCshw==",
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-9.5.0.tgz",
+ "integrity": "sha512-EKeXQzeb1zF9ra0hfYfPArU1JlWKl2vyL4Rb3gQW0XzU7SkEFgYm3727gDrQZdU8dvc5g/FLEqc0dr6UkDUHhg==",
"license": "MIT",
"dependencies": {
- "@bull-board/api": "9.0.1",
- "@bull-board/ui": "9.0.1",
+ "@bull-board/api": "9.5.0",
+ "@bull-board/ui": "9.5.0",
"ejs": "^6.0.1",
"express": "^5.2.1"
}
@@ -6212,12 +6212,12 @@
}
},
"node_modules/@bull-board/nestjs": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-9.0.1.tgz",
- "integrity": "sha512-pVcMZyuZ/X8SLjoayW8aREM1f4RFdI9WlwvHmT9RwlPhRnFLTnGMbnC5LOk+cYR/9WTB/Oeo1SDnzblIR99JtQ==",
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-9.5.0.tgz",
+ "integrity": "sha512-0P/Ru3cbOnf6GwnDGv4iPJ/NEjbcaicdpYMISpp6K7JZ24RVkCnmoUIKt8KbQdjZGqfH2EWmyIRC2WhiUE3T+g==",
"license": "MIT",
"peerDependencies": {
- "@bull-board/api": "^9.0.1",
+ "@bull-board/api": "^9.5.0",
"@nestjs/bull-shared": "^10.0.0 || ^11.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
@@ -6226,12 +6226,12 @@
}
},
"node_modules/@bull-board/ui": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-9.0.1.tgz",
- "integrity": "sha512-yGg0CMAFCA49IM1zPS+JFvUJW/whftaLtRyMWiVtvyLRd53uX0QW/uTaPcHy0V4bApp6U0FTa9+L9rk/49ciwg==",
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-9.5.0.tgz",
+ "integrity": "sha512-qrq0e906M8Ps/mV3bQXmj7EXJWLaFs5mNz+oBoLR/7EShBi5YooROzQ/SEFFedg/L9h6QWDWJ9Ph0AH7NrG9uw==",
"license": "MIT",
"dependencies": {
- "@bull-board/api": "9.0.1"
+ "@bull-board/api": "9.5.0"
}
},
"node_modules/@cacheable/utils": {
diff --git a/package.json b/package.json
index 1ddbf7ea64..1f13ab86c2 100644
--- a/package.json
+++ b/package.json
@@ -65,9 +65,9 @@
"@angular/platform-browser-dynamic": "22.1.2",
"@angular/router": "22.1.2",
"@angular/service-worker": "22.1.2",
- "@bull-board/api": "9.0.1",
- "@bull-board/express": "9.0.1",
- "@bull-board/nestjs": "9.0.1",
+ "@bull-board/api": "9.5.0",
+ "@bull-board/express": "9.5.0",
+ "@bull-board/nestjs": "9.5.0",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.7",
From ceaef13b70d5293c90e6d18518f45b3363e85135 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 7 Sep 2026 20:58:52 +0200
Subject: [PATCH 5/8] Task/migrate watchlist to dedicated routes (#7837)
* Migrate create dialog to dedicated route
* Update changelog
---
CHANGELOG.md | 1 +
.../home-watchlist.component.ts | 66 ++--------
.../home-watchlist/home-watchlist.html | 8 +-
.../home-watchlist/home-watchlist.service.ts | 15 +++
.../watchlist-dialog-host.component.ts | 114 ++++++++++++++++++
.../src/app/pages/home/home-page.routes.ts | 9 ++
libs/common/src/lib/routes/routes.ts | 7 ++
7 files changed, 162 insertions(+), 58 deletions(-)
create mode 100644 apps/client/src/app/components/home-watchlist/home-watchlist.service.ts
create mode 100644 apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f363f4cc5f..9bac4d2e12 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the loading state of the activity count in the portfolio summary
+- Migrated the create dialog of the watchlist to a dedicated route
- Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types
- Upgraded `bull-board` from version `9.0.1` to `9.5.0`
- Upgraded `zod` from version `4.4.3` to `4.5.4`
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
index d3a7fb203f..b56d8d66ea 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
@@ -6,6 +6,7 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
+import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfFabComponent } from '@ghostfolio/ui/fab';
@@ -23,12 +24,10 @@ import {
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { MatDialog } from '@angular/material/dialog';
-import { ActivatedRoute, Router, RouterModule } from '@angular/router';
+import { RouterModule } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector';
-import { GfCreateWatchlistItemDialogComponent } from './create-watchlist-item-dialog/create-watchlist-item-dialog.component';
-import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/interfaces/interfaces';
+import { HomeWatchlistService } from './home-watchlist.service';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -38,6 +37,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
GfPremiumIndicatorComponent,
RouterModule
],
+ providers: [HomeWatchlistService],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-home-watchlist',
styleUrls: ['./home-watchlist.scss'],
@@ -45,6 +45,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
})
export class GfHomeWatchlistComponent implements OnInit {
protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE;
+ protected readonly internalRoutes = internalRoutes;
protected hasPermissionToCreateWatchlistItem: boolean;
protected hasPermissionToDeleteWatchlistItem: boolean;
@@ -59,18 +60,14 @@ export class GfHomeWatchlistComponent implements OnInit {
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
- private readonly dialog = inject(MatDialog);
- private readonly route = inject(ActivatedRoute);
- private readonly router = inject(Router);
+ private readonly homeWatchlistService = inject(HomeWatchlistService);
private readonly userService = inject(UserService);
public constructor() {
- this.route.queryParams
+ this.homeWatchlistService.refresh$
.pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((params) => {
- if (params['createWatchlistItemDialog']) {
- this.openCreateWatchlistItemDialog();
- }
+ .subscribe(() => {
+ this.loadWatchlistData();
});
this.userService.stateChanged
@@ -130,49 +127,4 @@ export class GfHomeWatchlistComponent implements OnInit {
}
});
}
-
- private openCreateWatchlistItemDialog() {
- this.userService
- .get()
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((user) => {
- this.user = user;
-
- if (
- !hasPermission(user?.permissions, permissions.createWatchlistItem) ||
- !hasScope(user?.scopes, scopes.watchlistCreate)
- ) {
- this.router.navigate(['.'], { relativeTo: this.route });
-
- return;
- }
-
- const dialogRef = this.dialog.open<
- GfCreateWatchlistItemDialogComponent,
- CreateWatchlistItemDialogParams
- >(GfCreateWatchlistItemDialogComponent, {
- data: {
- deviceType: this.deviceType(),
- locale: this.user?.settings?.locale ?? DEFAULT_LOCALE
- },
- width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
- });
-
- dialogRef
- .afterClosed()
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe(({ dataSource, symbol } = {}) => {
- if (dataSource && symbol) {
- this.dataService
- .postWatchlistItem({ dataSource, symbol })
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe({
- next: () => this.loadWatchlistData()
- });
- }
-
- this.router.navigate(['.'], { relativeTo: this.route });
- });
- });
- }
}
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html
index fdd894de4e..11b2d9c2fe 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.html
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html
@@ -22,5 +22,11 @@
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts
new file mode 100644
index 0000000000..47eafe6059
--- /dev/null
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts
@@ -0,0 +1,15 @@
+import { Service } from '@angular/core';
+import { Subject } from 'rxjs';
+
+@Service({ autoProvided: false })
+export class HomeWatchlistService {
+ private readonly refreshSubject = new Subject