Browse Source

Refactoring

pull/7146/head
Thomas Kaul 3 weeks ago
parent
commit
2b0a92c159
  1. 113
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  2. 116
      libs/ui/src/lib/assistant/assistant.component.ts
  3. 1
      libs/ui/src/lib/portfolio-filter-form/index.ts
  4. 124
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.util.ts

113
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { AccountWithPlatform } from '@ghostfolio/common/types'; import { AccountWithPlatform } from '@ghostfolio/common/types';
import { validateObjectForForm } from '@ghostfolio/common/utils'; import { validateObjectForForm } from '@ghostfolio/common/utils';
import { translate } from '@ghostfolio/ui/i18n';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
import { import {
GfPortfolioFilterFormComponent, GfPortfolioFilterFormComponent,
PortfolioFilterFormValue getAssetClassFilters,
getFiltersFromPortfolioFilterFormValue,
getHoldingsForFilter,
getPortfolioFilterFormValue,
getTagFilters
} from '@ghostfolio/ui/portfolio-filter-form'; } from '@ghostfolio/ui/portfolio-filter-form';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
@ -39,7 +41,7 @@ import {
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; 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 { StatusCodes } from 'http-status-codes';
import { EMPTY, catchError } from 'rxjs'; import { EMPTY, catchError } from 'rxjs';
@ -112,17 +114,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
] ]
}); });
this.assetClasses = Object.keys(AssetClass) this.assetClasses = getAssetClassFilters();
.map((assetClass) => {
return {
id: assetClass,
label: translate(assetClass),
type: 'ASSET_CLASS' as const
};
})
.sort((a, b) => {
return a.label.localeCompare(b.label);
});
this.userService this.userService
.get() .get()
@ -130,22 +122,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
.subscribe(({ accounts, settings, tags }) => { .subscribe(({ accounts, settings, tags }) => {
this.accounts = accounts; this.accounts = accounts;
this.hasExperimentalFeatures = settings.isExperimentalFeatures ?? false; this.hasExperimentalFeatures = settings.isExperimentalFeatures ?? false;
this.tags = getTagFilters(tags);
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.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -197,41 +174,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} }
private buildFilters(): Filter[] { private buildFilters(): Filter[] {
const filterValue = this.accessForm.get('filters') return getFiltersFromPortfolioFilterFormValue(
?.value as PortfolioFilterFormValue | null; this.accessForm.get('filters')?.value
);
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;
} }
private loadHoldings() { private loadHoldings() {
@ -239,18 +184,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
.fetchPortfolioHoldings() .fetchPortfolioHoldings()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.holdings = holdings this.holdings = getHoldingsForFilter(holdings);
.filter(({ assetProfile }) => {
return (
assetProfile.assetSubClass &&
!['CASH'].includes(assetProfile.assetSubClass)
);
})
.sort((a, b) => {
return (a.assetProfile.name ?? '').localeCompare(
b.assetProfile.name ?? ''
);
});
this.updateFiltersFormControl(this.data.access?.settings?.filters); this.updateFiltersFormControl(this.data.access?.settings?.filters);
@ -348,25 +282,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
return; return;
} }
const getFilterId = (type: Filter['type']) => { this.accessForm
return filters.find((filter) => filter.type === type)?.id ?? null; .get('filters')
}; ?.setValue(getPortfolioFilterFormValue(filters, this.holdings));
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);
} }
} }

116
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 { Filter, PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { InternalRoute } from '@ghostfolio/common/routes/interfaces/internal-route.interface'; import { InternalRoute } from '@ghostfolio/common/routes/interfaces/internal-route.interface';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
@ -31,7 +30,6 @@ import { MatMenuTrigger } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { AssetClass, DataSource } from '@prisma/client';
import { differenceInYears, eachYearOfInterval, format } from 'date-fns'; import { differenceInYears, eachYearOfInterval, format } from 'date-fns';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
@ -56,7 +54,12 @@ import {
import { translate } from '../i18n'; import { translate } from '../i18n';
import { import {
GfPortfolioFilterFormComponent, GfPortfolioFilterFormComponent,
PortfolioFilterFormValue PortfolioFilterFormValue,
getAssetClassFilters,
getFiltersFromPortfolioFilterFormValue,
getHoldingsForFilter,
getPortfolioFilterFormValue,
getTagFilters
} from '../portfolio-filter-form'; } from '../portfolio-filter-form';
import { GfAssistantListItemComponent } from './assistant-list-item/assistant-list-item.component'; import { GfAssistantListItemComponent } from './assistant-list-item/assistant-list-item.component';
import { SearchMode } from './enums/search-mode'; import { SearchMode } from './enums/search-mode';
@ -194,17 +197,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.assetClasses = Object.keys(AssetClass) this.assetClasses = getAssetClassFilters();
.map((assetClass) => {
return {
id: assetClass,
label: translate(assetClass),
type: 'ASSET_CLASS'
} satisfies Filter;
})
.sort((a, b) => {
return a.label.localeCompare(b.label);
});
this.searchFormControl.valueChanges this.searchFormControl.valueChanges
.pipe( .pipe(
@ -437,21 +430,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.portfolioFilterFormControl.disable({ emitEvent: false }); this.portfolioFilterFormControl.disable({ emitEvent: false });
} }
this.tags = this.tags = getTagFilters(this.user?.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);
}) ?? [];
} }
public initialize() { public initialize() {
@ -500,18 +479,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
.fetchPortfolioHoldings() .fetchPortfolioHoldings()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.holdings = holdings this.holdings = getHoldingsForFilter(holdings);
.filter(({ assetProfile }) => {
return (
assetProfile.assetSubClass &&
!['CASH'].includes(assetProfile.assetSubClass)
);
})
.sort((a, b) => {
return (a.assetProfile.name ?? '').localeCompare(
b.assetProfile.name ?? ''
);
});
this.setPortfolioFilterFormValues(); this.setPortfolioFilterFormValues();
@ -520,30 +488,12 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
} }
public onApplyFilters() { public onApplyFilters() {
const filterValue = this.portfolioFilterFormControl.value; this.filtersChanged.emit(
getFiltersFromPortfolioFilterFormValue(
this.filtersChanged.emit([ this.portfolioFilterFormControl.value,
{ { includeEmpty: true }
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.onCloseAssistant(); this.onCloseAssistant();
} }
@ -769,25 +719,23 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
} }
private setPortfolioFilterFormValues() { private setPortfolioFilterFormValues() {
const dataSource = this.user?.settings?.[ const settings = 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 })
);
});
this.portfolioFilterFormControl.setValue({ const filters = [
account: this.user?.settings?.['filters.accounts']?.[0] ?? null, { id: settings?.['filters.accounts']?.[0] ?? '', type: 'ACCOUNT' },
assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null, {
holding: selectedHolding ?? null, id: settings?.['filters.assetClasses']?.[0] ?? '',
tag: this.user?.settings?.['filters.tags']?.[0] ?? null 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)
);
} }
} }

1
libs/ui/src/lib/portfolio-filter-form/index.ts

@ -1,2 +1,3 @@
export * from './interfaces'; export * from './interfaces';
export * from './portfolio-filter-form.component'; export * from './portfolio-filter-form.component';
export * from './portfolio-filter-form.util';

124
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);
}) ?? []
);
}
Loading…
Cancel
Save