Browse Source

Merge branch 'ghostfolio:main' into pl-translations

pull/5295/head
Lukas Bauer 3 weeks ago
committed by GitHub
parent
commit
ef71101b69
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 21
      CHANGELOG.md
  2. 2
      apps/api/src/app/export/export.service.ts
  3. 2
      apps/api/src/app/import/import.service.ts
  4. 4
      apps/api/src/app/order/order.controller.ts
  5. 20
      apps/api/src/app/order/order.service.ts
  6. 10
      apps/api/src/app/portfolio/portfolio.service.ts
  7. 4
      apps/api/src/services/tag/tag.service.ts
  8. 3
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  9. 2
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  10. 2
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  11. 2
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  12. 14
      apps/client/src/app/pages/landing/landing-page.html
  13. 306
      apps/client/src/locales/messages.ca.xlf
  14. 306
      apps/client/src/locales/messages.de.xlf
  15. 338
      apps/client/src/locales/messages.es.xlf
  16. 306
      apps/client/src/locales/messages.fr.xlf
  17. 306
      apps/client/src/locales/messages.it.xlf
  18. 306
      apps/client/src/locales/messages.nl.xlf
  19. 306
      apps/client/src/locales/messages.pl.xlf
  20. 306
      apps/client/src/locales/messages.pt.xlf
  21. 306
      apps/client/src/locales/messages.tr.xlf
  22. 306
      apps/client/src/locales/messages.uk.xlf
  23. 305
      apps/client/src/locales/messages.xlf
  24. 306
      apps/client/src/locales/messages.zh.xlf
  25. 32
      apps/client/src/styles.scss
  26. 2
      libs/common/src/lib/config.ts
  27. 0
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  28. 0
      libs/ui/src/lib/accounts-table/accounts-table.component.scss
  29. 146
      libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts
  30. 10
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  31. 1
      libs/ui/src/lib/accounts-table/index.ts
  32. 2
      libs/ui/src/lib/activities-table/activities-table.component.html
  33. 28
      libs/ui/src/lib/activities-table/activities-table.component.ts
  34. 1
      libs/ui/src/lib/i18n.ts
  35. 50
      libs/ui/src/lib/membership-card/membership-card.component.stories.ts
  36. 2
      libs/ui/src/lib/tags-selector/tags-selector.component.stories.ts
  37. 38343
      package-lock.json
  38. 68
      package.json
  39. 4
      prisma/seed.ts

21
CHANGELOG.md

@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.187.0 - 2025-08-02
### Added
- Added support to exclude an activity from analysis based on tags
- Added a _Storybook_ story for the accounts table component
- Added a _Storybook_ story for the membership card component
### Changed
- Moved the support for changing the asset profile identifier (`dataSource` and `symbol`) in the asset profile details dialog of the admin control panel from experimental to general availability
- Improved the balance of headings on the landing page
- Improved the language localization for German (`de`)
- Improved the language localization for Spanish (`es`)
- Upgraded `angular` from version `20.0.7` to `20.1.3`
- Upgraded `Nx` from version `21.2.4` to `21.3.9`
### Fixed
- Fixed the missing localization for "Exclude from Analysis" in the create or update account dialog
## 2.186.0 - 2025-07-30 ## 2.186.0 - 2025-07-30
### Added ### Added

2
apps/api/src/app/export/export.service.ts

@ -41,7 +41,7 @@ export class ExportService {
includeDrafts: true, includeDrafts: true,
sortColumn: 'date', sortColumn: 'date',
sortDirection: 'asc', sortDirection: 'asc',
withExcludedAccounts: true withExcludedAccountsAndActivities: true
}); });
if (activityIds?.length > 0) { if (activityIds?.length > 0) {

2
apps/api/src/app/import/import.service.ts

@ -533,7 +533,7 @@ export class ImportService {
userCurrency, userCurrency,
userId, userId,
includeDrafts: true, includeDrafts: true,
withExcludedAccounts: true withExcludedAccountsAndActivities: true
}); });
return activitiesDto.map( return activitiesDto.map(

4
apps/api/src/app/order/order.controller.ts

@ -144,7 +144,7 @@ export class OrderController {
skip: isNaN(skip) ? undefined : skip, skip: isNaN(skip) ? undefined : skip,
take: isNaN(take) ? undefined : take, take: isNaN(take) ? undefined : take,
userId: impersonationUserId || this.request.user.id, userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true withExcludedAccountsAndActivities: true
}); });
return { activities, count }; return { activities, count };
@ -165,7 +165,7 @@ export class OrderController {
const { activities } = await this.orderService.getOrders({ const { activities } = await this.orderService.getOrders({
userCurrency, userCurrency,
userId: impersonationUserId || this.request.user.id, userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true withExcludedAccountsAndActivities: true
}); });
const activity = activities.find((activity) => { const activity = activities.find((activity) => {

20
apps/api/src/app/order/order.service.ts

@ -9,7 +9,8 @@ import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_HIGH,
GATHER_ASSET_PROFILE_PROCESS_JOB_NAME, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS, GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
ghostfolioPrefix ghostfolioPrefix,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { import {
@ -275,7 +276,7 @@ export class OrderService {
userId, userId,
includeDrafts: true, includeDrafts: true,
userCurrency: undefined, userCurrency: undefined,
withExcludedAccounts: true withExcludedAccountsAndActivities: true
}); });
const { count } = await this.prismaService.order.deleteMany({ const { count } = await this.prismaService.order.deleteMany({
@ -332,7 +333,7 @@ export class OrderService {
types, types,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts = false withExcludedAccountsAndActivities = false
}: { }: {
endDate?: Date; endDate?: Date;
filters?: Filter[]; filters?: Filter[];
@ -345,7 +346,7 @@ export class OrderService {
types?: ActivityType[]; types?: ActivityType[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccountsAndActivities?: boolean;
}): Promise<Activities> { }): Promise<Activities> {
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [ let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
{ date: 'asc' }, { date: 'asc' },
@ -491,11 +492,18 @@ export class OrderService {
where.type = { in: types }; where.type = { in: types };
} }
if (withExcludedAccounts === false) { if (withExcludedAccountsAndActivities === false) {
where.OR = [ where.OR = [
{ account: null }, { account: null },
{ account: { NOT: { isExcluded: true } } } { account: { NOT: { isExcluded: true } } }
]; ];
where.tags = {
...where.tags,
none: {
id: TAG_ID_EXCLUDE_FROM_ANALYSIS
}
};
} }
const [orders, count] = await Promise.all([ const [orders, count] = await Promise.all([
@ -609,7 +617,7 @@ export class OrderService {
filters, filters,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts: false // TODO withExcludedAccountsAndActivities: false // TODO
}); });
} }

10
apps/api/src/app/portfolio/portfolio.service.ts

@ -33,6 +33,7 @@ import {
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
TAG_ID_EMERGENCY_FUND, TAG_ID_EMERGENCY_FUND,
TAG_ID_EXCLUDE_FROM_ANALYSIS,
UNKNOWN_KEY UNKNOWN_KEY
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
@ -1799,14 +1800,19 @@ export class PortfolioService {
const { activities } = await this.orderService.getOrders({ const { activities } = await this.orderService.getOrders({
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts: true withExcludedAccountsAndActivities: true
}); });
const excludedActivities: Activity[] = []; const excludedActivities: Activity[] = [];
const nonExcludedActivities: Activity[] = []; const nonExcludedActivities: Activity[] = [];
for (const activity of activities) { for (const activity of activities) {
if (activity.account?.isExcluded) { if (
activity.account?.isExcluded ||
activity.tags?.some(({ id }) => {
return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
})
) {
excludedActivities.push(activity); excludedActivities.push(activity);
} else { } else {
nonExcludedActivities.push(activity); nonExcludedActivities.push(activity);

4
apps/api/src/services/tag/tag.service.ts

@ -1,4 +1,5 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, Tag } from '@prisma/client'; import { Prisma, Tag } from '@prisma/client';
@ -79,7 +80,8 @@ export class TagService {
id, id,
name, name,
userId, userId,
isUsed: _count.activities > 0 isUsed:
_count.activities > 0 && ![TAG_ID_EXCLUDE_FROM_ANALYSIS].includes(id)
})); }));
} }

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

@ -233,8 +233,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
public get canEditAssetProfileIdentifier() { public get canEditAssetProfileIdentifier() {
return ( return (
this.assetProfile?.assetClass && this.assetProfile?.assetClass &&
!['MANUAL'].includes(this.assetProfile?.dataSource) && !['MANUAL'].includes(this.assetProfile?.dataSource)
this.user?.settings?.isExperimentalFeatures
); );
} }

2
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -1,5 +1,4 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { GfAccountsTableComponent } from '@ghostfolio/client/components/accounts-table/accounts-table.component';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -15,6 +14,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits'; import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits';
import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor'; import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor';

2
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -4,13 +4,13 @@ import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module'; import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { GfAccountsTableComponent } from '@ghostfolio/client/components/accounts-table/accounts-table.component';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';

2
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -86,7 +86,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3 px-2"> <div class="mb-3 px-2">
<mat-checkbox color="primary" formControlName="isExcluded" <mat-checkbox color="primary" formControlName="isExcluded" i18n
>Exclude from Analysis</mat-checkbox >Exclude from Analysis</mat-checkbox
> >
</div> </div>

14
apps/client/src/app/pages/landing/landing-page.html

@ -2,11 +2,11 @@
<div class="row"> <div class="row">
<div class="col text-center"> <div class="col text-center">
<div> <div>
<h1 class="font-weight-bold intro" i18n> <h1 class="font-weight-bold gf-text-wrap-balance intro" i18n>
Manage your wealth like a boss Manage your wealth like a boss
</h1> </h1>
</div> </div>
<p class="lead mb-4" i18n> <p class="gf-text-wrap-balance lead mb-4" i18n>
Ghostfolio is a privacy-first, open source dashboard for your personal Ghostfolio is a privacy-first, open source dashboard for your personal
finances. Break down your asset allocation, know your net worth and make finances. Break down your asset allocation, know your net worth and make
solid, data-driven investment decisions. solid, data-driven investment decisions.
@ -222,11 +222,11 @@
<div class="pt-3 row"> <div class="pt-3 row">
<div class="col text-center"> <div class="col text-center">
<h2 class="h4 mb-1 text-center" i18n> <h2 class="gf-text-wrap-balance h4 mb-1 text-center" i18n>
Protect your <strong>assets</strong>. Refine your Protect your <strong>assets</strong>. Refine your
<strong>personal investment strategy</strong>. <strong>personal investment strategy</strong>.
</h2> </h2>
<p class="lead m-0" i18n> <p class="gf-text-wrap-balance lead m-0" i18n>
Ghostfolio empowers busy people to keep track of stocks, ETFs or Ghostfolio empowers busy people to keep track of stocks, ETFs or
cryptocurrencies without being tracked. cryptocurrencies without being tracked.
</p> </p>
@ -270,7 +270,7 @@
<div class="row my-5"> <div class="row my-5">
<div class="col-md-6 offset-md-3"> <div class="col-md-6 offset-md-3">
<h2 class="h4 mb-1 text-center" i18n>Why <strong>Ghostfolio</strong>?</h2> <h2 class="h4 mb-1 text-center" i18n>Why <strong>Ghostfolio</strong>?</h2>
<p class="lead mb-3 text-center" i18n> <p class="gf-text-wrap-balance lead mb-3 text-center" i18n>
Ghostfolio is for you if you are... Ghostfolio is for you if you are...
</p> </p>
<ul class="list-unstyled"> <ul class="list-unstyled">
@ -363,7 +363,7 @@
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<div class="row my-5"> <div class="row my-5">
<div class="col-12"> <div class="col-12">
<h2 class="h4 text-center" i18n> <h2 class="gf-text-wrap-balance h4 text-center" i18n>
Members from around the globe are using Members from around the globe are using
<a href="pricing"><strong>Ghostfolio Premium</strong></a> <a href="pricing"><strong>Ghostfolio Premium</strong></a>
</h2> </h2>
@ -431,7 +431,7 @@
<h2 class="h4 mb-1 text-center" i18n> <h2 class="h4 mb-1 text-center" i18n>
Are <strong>you</strong> ready? Are <strong>you</strong> ready?
</h2> </h2>
<p class="lead mb-3 text-center" i18n> <p class="gf-text-wrap-balance lead mb-3 text-center" i18n>
Join now Join now
@if (hasPermissionForDemo) { @if (hasPermissionForDemo) {
or check out the example account or check out the example account

306
apps/client/src/locales/messages.ca.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.de.xlf

File diff suppressed because it is too large

338
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.it.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.pl.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.uk.xlf

File diff suppressed because it is too large

305
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

306
apps/client/src/locales/messages.zh.xlf

File diff suppressed because it is too large

32
apps/client/src/styles.scss

@ -267,6 +267,30 @@ body {
background-color: rgba(var(--palette-foreground-base-dark), 0.02); background-color: rgba(var(--palette-foreground-base-dark), 0.02);
} }
.mat-mdc-slide-toggle {
.mdc-switch__track {
--mat-slide-toggle-selected-focus-track-color: rgba(
255,
255,
255,
0.12
);
--mat-slide-toggle-selected-hover-track-color: rgba(
255,
255,
255,
0.12
);
--mat-slide-toggle-selected-pressed-track-color: rgba(
255,
255,
255,
0.12
);
--mat-slide-toggle-selected-track-color: rgba(255, 255, 255, 0.12);
}
}
.mdc-button { .mdc-button {
&.mat-accent, &.mat-accent,
&.mat-primary { &.mat-primary {
@ -488,12 +512,12 @@ ngx-skeleton-loader {
} }
} }
/**
* Fix for https://github.com/angular/components/issues/26818
*/
.mat-mdc-slide-toggle { .mat-mdc-slide-toggle {
.mdc-switch__track { .mdc-switch__track {
background-color: rgba(var(--palette-primary-500), 1); --mat-slide-toggle-selected-focus-track-color: rgba(0, 0, 0, 0.12);
--mat-slide-toggle-selected-hover-track-color: rgba(0, 0, 0, 0.12);
--mat-slide-toggle-selected-pressed-track-color: rgba(0, 0, 0, 0.12);
--mat-slide-toggle-selected-track-color: rgba(0, 0, 0, 0.12);
} }
} }

2
libs/common/src/lib/config.ts

@ -200,6 +200,8 @@ export const SUPPORTED_LANGUAGE_CODES = [
]; ];
export const TAG_ID_EMERGENCY_FUND = '4452656d-9fa4-4bd0-ba38-70492e31d180'; export const TAG_ID_EMERGENCY_FUND = '4452656d-9fa4-4bd0-ba38-70492e31d180';
export const TAG_ID_EXCLUDE_FROM_ANALYSIS =
'f2e868af-8333-459f-b161-cbc6544c24bd';
export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874'; export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874';
export const UNKNOWN_KEY = 'UNKNOWN'; export const UNKNOWN_KEY = 'UNKNOWN';

0
apps/client/src/app/components/accounts-table/accounts-table.component.html → libs/ui/src/lib/accounts-table/accounts-table.component.html

0
apps/client/src/app/components/accounts-table/accounts-table.component.scss → libs/ui/src/lib/accounts-table/accounts-table.component.scss

146
libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts

@ -0,0 +1,146 @@
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular';
import { NotificationService } from 'apps/client/src/app/core/notification/notification.service';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfEntityLogoComponent } from '../entity-logo';
import { GfValueComponent } from '../value';
import { GfAccountsTableComponent } from './accounts-table.component';
const accounts = [
{
allocationInPercentage: null,
balance: 278,
balanceInBaseCurrency: 278,
comment: null,
createdAt: new Date('2025-06-01T06:52:49.063Z'),
currency: 'USD',
id: '460d7401-ca43-4ed4-b08e-349f1822e9db',
isExcluded: false,
name: 'Coinbase Account',
platform: {
id: '8dc24b88-bb92-4152-af25-fe6a31643e26',
name: 'Coinbase',
url: 'https://www.coinbase.com'
},
platformId: '8dc24b88-bb92-4152-af25-fe6a31643e26',
transactionCount: 0,
updatedAt: new Date('2025-06-01T06:52:49.063Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e',
value: 278,
valueInBaseCurrency: 278
},
{
allocationInPercentage: null,
balance: 12000,
balanceInBaseCurrency: 12000,
comment: null,
createdAt: new Date('2025-06-01T06:48:53.055Z'),
currency: 'USD',
id: '6d773e31-0583-4c85-a247-e69870b4f1ee',
isExcluded: false,
name: 'Private Banking Account',
platform: {
id: '43e8fcd1-5b79-4100-b678-d2229bd1660d',
name: 'J.P. Morgan',
url: 'https://www.jpmorgan.com'
},
platformId: '43e8fcd1-5b79-4100-b678-d2229bd1660d',
transactionCount: 0,
updatedAt: new Date('2025-06-01T06:48:53.055Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e',
value: 12000,
valueInBaseCurrency: 12000
},
{
allocationInPercentage: null,
balance: 150.2,
balanceInBaseCurrency: 150.2,
comment: null,
createdAt: new Date('2025-05-31T13:00:13.940Z'),
currency: 'USD',
id: '776bd1e9-b2f6-4f7e-933d-18756c2f0625',
isExcluded: false,
name: 'Trading Account',
platform: {
id: '9da3a8a7-4795-43e3-a6db-ccb914189737',
name: 'Interactive Brokers',
url: 'https://interactivebrokers.com'
},
platformId: '9da3a8a7-4795-43e3-a6db-ccb914189737',
transactionCount: 12,
valueInBaseCurrency: 95693.70321466809,
updatedAt: new Date('2025-06-01T06:53:10.569Z'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e',
value: 95693.70321466809
}
];
export default {
title: 'Accounts Table',
component: GfAccountsTableComponent,
decorators: [
moduleMetadata({
imports: [
CommonModule,
GfEntityLogoComponent,
GfValueComponent,
IonIcon,
MatButtonModule,
MatMenuModule,
MatSortModule,
MatTableModule,
NgxSkeletonLoaderModule,
RouterModule.forChild([])
],
providers: [NotificationService]
})
]
} as Meta<GfAccountsTableComponent>;
type Story = StoryObj<GfAccountsTableComponent>;
export const Default: Story = {
args: {
accounts,
baseCurrency: 'USD',
deviceType: 'desktop',
locale: 'en-US',
showActions: false,
showAllocationInPercentage: false,
showBalance: true,
showFooter: true,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 12428.2,
totalValueInBaseCurrency: 107971.70321466809,
transactionCount: 12
}
};
export const WithoutFooter: Story = {
args: {
accounts,
baseCurrency: 'USD',
deviceType: 'desktop',
locale: 'en-US',
showActions: false,
showAllocationInPercentage: false,
showBalance: true,
showFooter: false,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 12428.2,
totalValueInBaseCurrency: 107971.70321466809,
transactionCount: 12
}
};

10
apps/client/src/app/components/accounts-table/accounts-table.component.ts → libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -21,7 +21,7 @@ import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router'; import { Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { Account as AccountModel } from '@prisma/client'; import { Account } from '@prisma/client';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
arrowRedoOutline, arrowRedoOutline,
@ -54,7 +54,7 @@ import { Subject, Subscription } from 'rxjs';
templateUrl: './accounts-table.component.html' templateUrl: './accounts-table.component.html'
}) })
export class GfAccountsTableComponent implements OnChanges, OnDestroy { export class GfAccountsTableComponent implements OnChanges, OnDestroy {
@Input() accounts: AccountModel[]; @Input() accounts: Account[];
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToOpenDetails = true; @Input() hasPermissionToOpenDetails = true;
@ -71,12 +71,12 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy {
@Input() transactionCount: number; @Input() transactionCount: number;
@Output() accountDeleted = new EventEmitter<string>(); @Output() accountDeleted = new EventEmitter<string>();
@Output() accountToUpdate = new EventEmitter<AccountModel>(); @Output() accountToUpdate = new EventEmitter<Account>();
@Output() transferBalance = new EventEmitter<void>(); @Output() transferBalance = new EventEmitter<void>();
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public dataSource = new MatTableDataSource<AccountModel>(); public dataSource = new MatTableDataSource<Account>();
public displayedColumns = []; public displayedColumns = [];
public isLoading = true; public isLoading = true;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
@ -167,7 +167,7 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy {
this.transferBalance.emit(); this.transferBalance.emit();
} }
public onUpdateAccount(aAccount: AccountModel) { public onUpdateAccount(aAccount: Account) {
this.accountToUpdate.emit(aAccount); this.accountToUpdate.emit(aAccount);
} }

1
libs/ui/src/lib/accounts-table/index.ts

@ -0,0 +1 @@
export * from './accounts-table.component';

2
libs/ui/src/lib/activities-table/activities-table.component.html

@ -468,7 +468,7 @@
[ngClass]="{ [ngClass]="{
'cursor-pointer': 'cursor-pointer':
hasPermissionToOpenDetails && hasPermissionToOpenDetails &&
row.account?.isExcluded !== true && isExcludedFromAnalysis(row) === false &&
row.isDraft === false && row.isDraft === false &&
['BUY', 'DIVIDEND', 'SELL'].includes(row.type) ['BUY', 'DIVIDEND', 'SELL'].includes(row.type)
}" }"

28
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -2,7 +2,10 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import {
DEFAULT_PAGE_SIZE,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config';
import { getDateFormatString, getLocale } from '@ghostfolio/common/helper'; import { getDateFormatString, getLocale } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
@ -171,12 +174,6 @@ export class GfActivitiesTableComponent
}); });
} }
public areAllRowsSelected() {
const numSelectedRows = this.selectedRows.selected.length;
const numTotalRows = this.dataSource.data.length;
return numSelectedRows === numTotalRows;
}
public ngOnChanges() { public ngOnChanges() {
this.defaultDateFormat = getDateFormatString(this.locale); this.defaultDateFormat = getDateFormatString(this.locale);
@ -215,6 +212,21 @@ export class GfActivitiesTableComponent
} }
} }
public areAllRowsSelected() {
const numSelectedRows = this.selectedRows.selected.length;
const numTotalRows = this.dataSource.data.length;
return numSelectedRows === numTotalRows;
}
public isExcludedFromAnalysis(activity: Activity) {
return (
activity.account?.isExcluded ||
activity.tags?.some(({ id }) => {
return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
})
);
}
public onChangePage(page: PageEvent) { public onChangePage(page: PageEvent) {
this.pageChanged.emit(page); this.pageChanged.emit(page);
} }
@ -226,7 +238,7 @@ export class GfActivitiesTableComponent
} }
} else if ( } else if (
this.hasPermissionToOpenDetails && this.hasPermissionToOpenDetails &&
activity.account?.isExcluded !== true && this.isExcludedFromAnalysis(activity) === false &&
activity.isDraft === false && activity.isDraft === false &&
['BUY', 'DIVIDEND', 'SELL'].includes(activity.type) ['BUY', 'DIVIDEND', 'SELL'].includes(activity.type)
) { ) {

1
libs/ui/src/lib/i18n.ts

@ -13,6 +13,7 @@ const locales = {
DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`,
DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source or Ghostfolio Basic easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source or Ghostfolio Basic easily`,
EMERGENCY_FUND: $localize`Emergency Fund`, EMERGENCY_FUND: $localize`Emergency Fund`,
EXCLUDE_FROM_ANALYSIS: $localize`Exclude from Analysis`,
Global: $localize`Global`, Global: $localize`Global`,
GRANT: $localize`Grant`, GRANT: $localize`Grant`,
HIGHER_RISK: $localize`Higher Risk`, HIGHER_RISK: $localize`Higher Risk`,

50
libs/ui/src/lib/membership-card/membership-card.component.stories.ts

@ -0,0 +1,50 @@
import { CommonModule } from '@angular/common';
import '@angular/localize/init';
import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular';
import { addYears } from 'date-fns';
import { GfLogoComponent } from '../logo';
import { GfMembershipCardComponent } from './membership-card.component';
export default {
title: 'Membership Card',
component: GfMembershipCardComponent,
decorators: [
moduleMetadata({
imports: [
CommonModule,
GfLogoComponent,
IonIcon,
MatButtonModule,
RouterModule.forChild([])
],
providers: [{ provide: ActivatedRoute, useValue: {} }]
})
],
argTypes: {
name: {
control: { type: 'select' },
options: ['Basic', 'Premium']
}
}
} as Meta<GfMembershipCardComponent>;
type Story = StoryObj<GfMembershipCardComponent>;
export const Basic: Story = {
args: {
name: 'Basic'
}
};
export const Premium: Story = {
args: {
expiresAt: addYears(new Date(), 1).toLocaleDateString(),
hasPermissionToCreateApiKey: true,
name: 'Premium'
}
};

2
libs/ui/src/lib/tags-selector/tags-selector.component.stories.ts

@ -21,7 +21,7 @@ const OPTIONS = [
{ {
id: '3ef7e6d9-4598-4eb2-b0e8-00e61cfc0ea6', id: '3ef7e6d9-4598-4eb2-b0e8-00e61cfc0ea6',
name: 'Gambling', name: 'Gambling',
userId: 'c6a71541-d0e3-4e22-ae83-b5e5611b6695' userId: '081aa387-487d-4438-83a4-3060eb2a016e'
}, },
{ {
id: 'EMERGENCY_FUND', id: 'EMERGENCY_FUND',

38343
package-lock.json

File diff suppressed because it is too large

68
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.186.0", "version": "2.187.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",
@ -56,17 +56,17 @@
"workspace-generator": "nx workspace-generator" "workspace-generator": "nx workspace-generator"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "20.0.7", "@angular/animations": "20.1.3",
"@angular/cdk": "20.0.6", "@angular/cdk": "20.1.3",
"@angular/common": "20.0.7", "@angular/common": "20.1.3",
"@angular/compiler": "20.0.7", "@angular/compiler": "20.1.3",
"@angular/core": "20.0.7", "@angular/core": "20.1.3",
"@angular/forms": "20.0.7", "@angular/forms": "20.1.3",
"@angular/material": "20.0.6", "@angular/material": "20.1.3",
"@angular/platform-browser": "20.0.7", "@angular/platform-browser": "20.1.3",
"@angular/platform-browser-dynamic": "20.0.7", "@angular/platform-browser-dynamic": "20.1.3",
"@angular/router": "20.0.7", "@angular/router": "20.1.3",
"@angular/service-worker": "20.0.7", "@angular/service-worker": "20.1.3",
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.0", "@date-fns/utc": "2.1.0",
"@dfinity/agent": "0.15.7", "@dfinity/agent": "0.15.7",
@ -143,33 +143,33 @@
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "20.0.6", "@angular-devkit/build-angular": "20.1.3",
"@angular-devkit/core": "20.0.6", "@angular-devkit/core": "20.1.3",
"@angular-devkit/schematics": "20.0.6", "@angular-devkit/schematics": "20.1.3",
"@angular-eslint/eslint-plugin": "20.1.1", "@angular-eslint/eslint-plugin": "20.1.1",
"@angular-eslint/eslint-plugin-template": "20.1.1", "@angular-eslint/eslint-plugin-template": "20.1.1",
"@angular-eslint/template-parser": "20.1.1", "@angular-eslint/template-parser": "20.1.1",
"@angular/cli": "20.0.6", "@angular/cli": "20.1.3",
"@angular/compiler-cli": "20.0.7", "@angular/compiler-cli": "20.1.3",
"@angular/language-service": "20.0.7", "@angular/language-service": "20.1.3",
"@angular/localize": "20.0.7", "@angular/localize": "20.1.3",
"@angular/pwa": "20.0.6", "@angular/pwa": "20.1.3",
"@eslint/eslintrc": "3.3.1", "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.24.0", "@eslint/js": "9.24.0",
"@nestjs/schematics": "11.0.5", "@nestjs/schematics": "11.0.5",
"@nestjs/testing": "11.1.3", "@nestjs/testing": "11.1.3",
"@nx/angular": "21.2.4", "@nx/angular": "21.3.9",
"@nx/cypress": "21.2.4", "@nx/cypress": "21.3.9",
"@nx/eslint-plugin": "21.2.4", "@nx/eslint-plugin": "21.3.9",
"@nx/jest": "21.2.4", "@nx/jest": "21.3.9",
"@nx/js": "21.2.4", "@nx/js": "21.3.9",
"@nx/module-federation": "21.2.4", "@nx/module-federation": "21.3.9",
"@nx/nest": "21.2.4", "@nx/nest": "21.3.9",
"@nx/node": "21.2.4", "@nx/node": "21.3.9",
"@nx/storybook": "21.2.4", "@nx/storybook": "21.3.9",
"@nx/web": "21.2.4", "@nx/web": "21.3.9",
"@nx/workspace": "21.2.4", "@nx/workspace": "21.3.9",
"@schematics/angular": "20.0.6", "@schematics/angular": "20.1.3",
"@storybook/addon-docs": "9.0.17", "@storybook/addon-docs": "9.0.17",
"@storybook/angular": "9.0.17", "@storybook/angular": "9.0.17",
"@trivago/prettier-plugin-sort-imports": "5.2.2", "@trivago/prettier-plugin-sort-imports": "5.2.2",
@ -193,7 +193,7 @@
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.6.0", "jest-preset-angular": "14.6.0",
"nx": "21.2.4", "nx": "21.3.9",
"prettier": "3.6.2", "prettier": "3.6.2",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.12.0", "prisma": "6.12.0",
@ -202,7 +202,7 @@
"replace-in-file": "8.3.0", "replace-in-file": "8.3.0",
"shx": "0.3.4", "shx": "0.3.4",
"storybook": "9.0.17", "storybook": "9.0.17",
"ts-jest": "29.1.0", "ts-jest": "29.4.0",
"ts-node": "10.9.2", "ts-node": "10.9.2",
"tslib": "2.8.1", "tslib": "2.8.1",
"typescript": "5.8.3", "typescript": "5.8.3",

4
prisma/seed.ts

@ -8,6 +8,10 @@ async function main() {
{ {
id: '4452656d-9fa4-4bd0-ba38-70492e31d180', id: '4452656d-9fa4-4bd0-ba38-70492e31d180',
name: 'EMERGENCY_FUND' name: 'EMERGENCY_FUND'
},
{
id: 'f2e868af-8333-459f-b161-cbc6544c24bd',
name: 'EXCLUDE_FROM_ANALYSIS'
} }
], ],
skipDuplicates: true skipDuplicates: true

Loading…
Cancel
Save