From 2b0a92c1596b7f7e35c39f94f3a529458a490ab7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:58:52 +0200 Subject: [PATCH] Refactoring --- ...reate-or-update-access-dialog.component.ts | 113 +++------------- .../src/lib/assistant/assistant.component.ts | 116 +++++----------- .../ui/src/lib/portfolio-filter-form/index.ts | 1 + .../portfolio-filter-form.util.ts | 124 ++++++++++++++++++ 4 files changed, 172 insertions(+), 182 deletions(-) create mode 100644 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts 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 df083a5dc..c1e19af8a 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 @@ -1,14 +1,16 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; -import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { AccountWithPlatform } from '@ghostfolio/common/types'; import { validateObjectForForm } from '@ghostfolio/common/utils'; -import { translate } from '@ghostfolio/ui/i18n'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfPortfolioFilterFormComponent, - PortfolioFilterFormValue + getAssetClassFilters, + getFiltersFromPortfolioFilterFormValue, + getHoldingsForFilter, + getPortfolioFilterFormValue, + getTagFilters } from '@ghostfolio/ui/portfolio-filter-form'; import { DataService } from '@ghostfolio/ui/services'; @@ -39,7 +41,7 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { AccessPermission, AssetClass, DataSource } from '@prisma/client'; +import { AccessPermission } from '@prisma/client'; import { StatusCodes } from 'http-status-codes'; import { EMPTY, catchError } from 'rxjs'; @@ -112,17 +114,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { ] }); - this.assetClasses = Object.keys(AssetClass) - .map((assetClass) => { - return { - id: assetClass, - label: translate(assetClass), - type: 'ASSET_CLASS' as const - }; - }) - .sort((a, b) => { - return a.label.localeCompare(b.label); - }); + this.assetClasses = getAssetClassFilters(); this.userService .get() @@ -130,22 +122,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { .subscribe(({ accounts, settings, tags }) => { this.accounts = accounts; this.hasExperimentalFeatures = settings.isExperimentalFeatures ?? false; - - this.tags = - tags - ?.filter(({ isUsed }) => { - return isUsed; - }) - ?.map(({ id, name }) => { - return { - id, - label: translate(name), - type: 'TAG' as const - }; - }) - ?.sort((a, b) => { - return a.label.localeCompare(b.label); - }) ?? []; + this.tags = getTagFilters(tags); this.changeDetectorRef.markForCheck(); }); @@ -197,41 +174,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } private buildFilters(): Filter[] { - const filterValue = this.accessForm.get('filters') - ?.value as PortfolioFilterFormValue | null; - - if (!filterValue) { - return []; - } - - const filters: Filter[] = []; - - if (filterValue.account) { - filters.push({ id: filterValue.account, type: 'ACCOUNT' }); - } - - if (filterValue.assetClass) { - filters.push({ id: filterValue.assetClass, type: 'ASSET_CLASS' }); - } - - if (filterValue.holding) { - filters.push( - { - id: filterValue.holding.assetProfile.dataSource, - type: 'DATA_SOURCE' - }, - { - id: filterValue.holding.assetProfile.symbol, - type: 'SYMBOL' - } - ); - } - - if (filterValue.tag) { - filters.push({ id: filterValue.tag, type: 'TAG' }); - } - - return filters; + return getFiltersFromPortfolioFilterFormValue( + this.accessForm.get('filters')?.value + ); } private loadHoldings() { @@ -239,18 +184,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { .fetchPortfolioHoldings() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { - this.holdings = holdings - .filter(({ assetProfile }) => { - return ( - assetProfile.assetSubClass && - !['CASH'].includes(assetProfile.assetSubClass) - ); - }) - .sort((a, b) => { - return (a.assetProfile.name ?? '').localeCompare( - b.assetProfile.name ?? '' - ); - }); + this.holdings = getHoldingsForFilter(holdings); this.updateFiltersFormControl(this.data.access?.settings?.filters); @@ -348,25 +282,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { return; } - const getFilterId = (type: Filter['type']) => { - return filters.find((filter) => filter.type === type)?.id ?? null; - }; - - const dataSource = getFilterId('DATA_SOURCE') as DataSource; - const symbol = getFilterId('SYMBOL'); - const holding = this.holdings.find(({ assetProfile }) => { - return ( - !!(dataSource && symbol) && - getAssetProfileIdentifier(assetProfile) === - getAssetProfileIdentifier({ dataSource, symbol }) - ); - }); - - this.accessForm.get('filters')?.setValue({ - account: getFilterId('ACCOUNT'), - assetClass: getFilterId('ASSET_CLASS'), - holding: holding ?? null, - tag: getFilterId('TAG') - } satisfies PortfolioFilterFormValue); + this.accessForm + .get('filters') + ?.setValue(getPortfolioFilterFormValue(filters, this.holdings)); } } diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index 3f8efa6ec..205e65c40 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -1,4 +1,3 @@ -import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { Filter, PortfolioPosition, User } from '@ghostfolio/common/interfaces'; import { InternalRoute } from '@ghostfolio/common/routes/interfaces/internal-route.interface'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; @@ -31,7 +30,6 @@ import { MatMenuTrigger } from '@angular/material/menu'; import { MatSelectModule } from '@angular/material/select'; import { RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; -import { AssetClass, DataSource } from '@prisma/client'; import { differenceInYears, eachYearOfInterval, format } from 'date-fns'; import Fuse from 'fuse.js'; import { addIcons } from 'ionicons'; @@ -56,7 +54,12 @@ import { import { translate } from '../i18n'; import { GfPortfolioFilterFormComponent, - PortfolioFilterFormValue + PortfolioFilterFormValue, + getAssetClassFilters, + getFiltersFromPortfolioFilterFormValue, + getHoldingsForFilter, + getPortfolioFilterFormValue, + getTagFilters } from '../portfolio-filter-form'; import { GfAssistantListItemComponent } from './assistant-list-item/assistant-list-item.component'; import { SearchMode } from './enums/search-mode'; @@ -194,17 +197,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { } public ngOnInit() { - this.assetClasses = Object.keys(AssetClass) - .map((assetClass) => { - return { - id: assetClass, - label: translate(assetClass), - type: 'ASSET_CLASS' - } satisfies Filter; - }) - .sort((a, b) => { - return a.label.localeCompare(b.label); - }); + this.assetClasses = getAssetClassFilters(); this.searchFormControl.valueChanges .pipe( @@ -437,21 +430,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { this.portfolioFilterFormControl.disable({ emitEvent: false }); } - this.tags = - this.user?.tags - ?.filter(({ isUsed }) => { - return isUsed; - }) - ?.map(({ id, name }) => { - return { - id, - label: translate(name), - type: 'TAG' - } satisfies Filter; - }) - ?.sort((a, b) => { - return a.label.localeCompare(b.label); - }) ?? []; + this.tags = getTagFilters(this.user?.tags); } public initialize() { @@ -500,18 +479,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { .fetchPortfolioHoldings() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { - this.holdings = holdings - .filter(({ assetProfile }) => { - return ( - assetProfile.assetSubClass && - !['CASH'].includes(assetProfile.assetSubClass) - ); - }) - .sort((a, b) => { - return (a.assetProfile.name ?? '').localeCompare( - b.assetProfile.name ?? '' - ); - }); + this.holdings = getHoldingsForFilter(holdings); this.setPortfolioFilterFormValues(); @@ -520,30 +488,12 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { } public onApplyFilters() { - const filterValue = this.portfolioFilterFormControl.value; - - this.filtersChanged.emit([ - { - id: filterValue?.account ?? '', - type: 'ACCOUNT' - }, - { - id: filterValue?.assetClass ?? '', - type: 'ASSET_CLASS' - }, - { - id: filterValue?.holding?.assetProfile?.dataSource ?? '', - type: 'DATA_SOURCE' - }, - { - id: filterValue?.holding?.assetProfile?.symbol ?? '', - type: 'SYMBOL' - }, - { - id: filterValue?.tag ?? '', - type: 'TAG' - } - ]); + this.filtersChanged.emit( + getFiltersFromPortfolioFilterFormValue( + this.portfolioFilterFormControl.value, + { includeEmpty: true } + ) + ); this.onCloseAssistant(); } @@ -769,25 +719,23 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { } private setPortfolioFilterFormValues() { - const dataSource = this.user?.settings?.[ - 'filters.dataSource' - ] as DataSource; - const symbol = this.user?.settings?.['filters.symbol']; - const selectedHolding = this.holdings.find((holding) => { - return ( - !!(dataSource && symbol) && - getAssetProfileIdentifier({ - dataSource: holding.assetProfile.dataSource, - symbol: holding.assetProfile.symbol - }) === getAssetProfileIdentifier({ dataSource, symbol }) - ); - }); + const settings = this.user?.settings; - this.portfolioFilterFormControl.setValue({ - account: this.user?.settings?.['filters.accounts']?.[0] ?? null, - assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null, - holding: selectedHolding ?? null, - tag: this.user?.settings?.['filters.tags']?.[0] ?? null - }); + const filters = [ + { id: settings?.['filters.accounts']?.[0] ?? '', type: 'ACCOUNT' }, + { + id: settings?.['filters.assetClasses']?.[0] ?? '', + type: 'ASSET_CLASS' + }, + { id: settings?.['filters.dataSource'] ?? '', type: 'DATA_SOURCE' }, + { id: settings?.['filters.symbol'] ?? '', type: 'SYMBOL' }, + { id: settings?.['filters.tags']?.[0] ?? '', type: 'TAG' } + ].filter(({ id }) => { + return !!id; + }) as Filter[]; + + this.portfolioFilterFormControl.setValue( + getPortfolioFilterFormValue(filters, this.holdings) + ); } } diff --git a/libs/ui/src/lib/portfolio-filter-form/index.ts b/libs/ui/src/lib/portfolio-filter-form/index.ts index 51d22c034..b689a9324 100644 --- a/libs/ui/src/lib/portfolio-filter-form/index.ts +++ b/libs/ui/src/lib/portfolio-filter-form/index.ts @@ -1,2 +1,3 @@ export * from './interfaces'; export * from './portfolio-filter-form.component'; +export * from './portfolio-filter-form.util'; diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts new file mode 100644 index 000000000..5679cfbd0 --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts @@ -0,0 +1,124 @@ +import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; +import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; + +import { AssetClass, DataSource, Tag } from '@prisma/client'; + +import { translate } from '../i18n'; +import { PortfolioFilterFormValue } from './interfaces'; + +export function getAssetClassFilters(): Filter[] { + return Object.keys(AssetClass) + .map((assetClass) => { + return { + id: assetClass, + label: translate(assetClass), + type: 'ASSET_CLASS' + } satisfies Filter; + }) + .sort((a, b) => { + return a.label.localeCompare(b.label); + }); +} + +export function getFiltersFromPortfolioFilterFormValue( + value: PortfolioFilterFormValue | null, + { includeEmpty = false }: { includeEmpty?: boolean } = {} +): Filter[] { + const filters: Filter[] = [ + { + id: value?.account ?? '', + type: 'ACCOUNT' + }, + { + id: value?.assetClass ?? '', + type: 'ASSET_CLASS' + }, + { + id: value?.holding?.assetProfile?.dataSource ?? '', + type: 'DATA_SOURCE' + }, + { + id: value?.holding?.assetProfile?.symbol ?? '', + type: 'SYMBOL' + }, + { + id: value?.tag ?? '', + type: 'TAG' + } + ]; + + return includeEmpty + ? filters + : filters.filter(({ id }) => { + return !!id; + }); +} + +export function getHoldingsForFilter( + holdings: PortfolioPosition[] = [] +): PortfolioPosition[] { + return holdings + .filter(({ assetProfile }) => { + return ( + assetProfile.assetSubClass && + !['CASH'].includes(assetProfile.assetSubClass) + ); + }) + .sort((a, b) => { + return (a.assetProfile.name ?? '').localeCompare( + b.assetProfile.name ?? '' + ); + }); +} + +export function getPortfolioFilterFormValue( + filters: Filter[], + holdings: PortfolioPosition[] +): PortfolioFilterFormValue { + const getFilterId = (type: Filter['type']) => { + return ( + filters?.find((filter) => { + return filter.type === type; + })?.id ?? null + ); + }; + + const dataSource = getFilterId('DATA_SOURCE') as DataSource; + const symbol = getFilterId('SYMBOL'); + + const holding = holdings.find(({ assetProfile }) => { + return ( + !!(dataSource && symbol) && + getAssetProfileIdentifier(assetProfile) === + getAssetProfileIdentifier({ dataSource, symbol }) + ); + }); + + return { + account: getFilterId('ACCOUNT'), + assetClass: getFilterId('ASSET_CLASS'), + holding: holding ?? null, + tag: getFilterId('TAG') + }; +} + +export function getTagFilters( + tags: (Tag & { isUsed: boolean })[] = [] +): Filter[] { + return ( + tags + ?.filter(({ isUsed }) => { + return isUsed; + }) + ?.map(({ id, name }) => { + return { + id, + label: translate(name), + type: 'TAG' + } satisfies Filter; + }) + ?.sort((a, b) => { + return a.label.localeCompare(b.label); + }) ?? [] + ); +}