Browse Source

Refactoring

pull/7146/head
Thomas Kaul 3 weeks ago
parent
commit
f15363d08d
  1. 27
      apps/api/src/app/access/access-filter.dto.ts
  2. 8
      apps/api/src/app/access/access.controller.ts
  3. 85
      apps/api/src/app/endpoints/public/public.controller.ts
  4. 169
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  5. 18
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  6. 10
      libs/common/src/lib/dtos/asset-profile-identifier.dto.ts
  7. 14
      libs/common/src/lib/dtos/create-access.dto.ts
  8. 2
      libs/common/src/lib/dtos/index.ts
  9. 14
      libs/common/src/lib/dtos/update-access.dto.ts
  10. 4
      libs/common/src/lib/dtos/update-bulk-market-data.dto.ts
  11. 11
      libs/common/src/lib/interfaces/access.interface.ts
  12. 3
      libs/common/src/lib/interfaces/index.ts

27
apps/api/src/app/access/access-filter.dto.ts

@ -1,27 +0,0 @@
import { AssetProfileIdentifierDto } from '@ghostfolio/common/dtos';
import { Type } from 'class-transformer';
import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator';
export class AccessFilterDto {
@IsArray()
@IsOptional()
@IsString({ each: true })
accountIds?: string[];
@IsArray()
@IsOptional()
@IsString({ each: true })
assetClasses?: string[];
@IsArray()
@IsOptional()
@Type(() => AssetProfileIdentifierDto)
@ValidateNested({ each: true })
holdings?: AssetProfileIdentifierDto[];
@IsArray()
@IsOptional()
@IsString({ each: true })
tagIds?: string[];
}

8
apps/api/src/app/access/access.controller.ts

@ -93,8 +93,8 @@ export class AccessController {
}
try {
const settings: AccessSettings = data.filter
? { filter: data.filter }
const settings: AccessSettings = data.filters?.length
? { filters: data.filters }
: {};
return this.accessService.createAccess({
@ -144,8 +144,8 @@ export class AccessController {
}
try {
const settings: AccessSettings = data.filter
? { filter: data.filter }
const settings: AccessSettings = data.filters?.length
? { filters: data.filters }
: {};
return this.accessService.updateAccess({

85
apps/api/src/app/endpoints/public/public.controller.ts

@ -11,7 +11,6 @@ import { SubscriptionType } from '@ghostfolio/common/enums';
import { getSum } from '@ghostfolio/common/helper';
import {
AccessSettings,
Filter,
PublicPortfolioResponse
} from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -70,60 +69,9 @@ export class PublicController {
hasDetails = user.subscription.type === SubscriptionType.Premium;
}
// Get filter configuration from access settings
const { filter: accessFilter } = (access.settings ?? {}) as AccessSettings;
// Convert access filter to portfolio filters
const portfolioFilters: Filter[] = [];
if (accessFilter) {
// Add account filters
if (accessFilter.accountIds?.length > 0) {
portfolioFilters.push(
...accessFilter.accountIds.map((accountId) => ({
id: accountId,
type: 'ACCOUNT' as const
}))
);
}
// Add asset class filters
if (accessFilter.assetClasses?.length > 0) {
portfolioFilters.push(
...accessFilter.assetClasses.map((assetClass) => ({
id: assetClass,
type: 'ASSET_CLASS' as const
}))
);
}
// Add holding filters (symbol + dataSource)
// Each holding needs both DATA_SOURCE and SYMBOL filters
if (accessFilter.holdings?.length > 0) {
for (const { dataSource, symbol } of accessFilter.holdings) {
portfolioFilters.push(
{
id: dataSource,
type: 'DATA_SOURCE' as const
},
{
id: symbol,
type: 'SYMBOL' as const
}
);
}
}
// Add tag filters
if (accessFilter.tagIds?.length > 0) {
portfolioFilters.push(
...accessFilter.tagIds.map((tagId) => ({
id: tagId,
type: 'TAG' as const
}))
);
}
}
// Get the portfolio filters from the access settings
const { filters: portfolioFilters = [] } = (access.settings ??
{}) as AccessSettings;
const [
{ createdAt, holdings, markets },
@ -159,33 +107,8 @@ export class PublicController {
})
);
// Use filters for activities, but exclude DATA_SOURCE/SYMBOL filters
// if there are multiple holdings (the service can't handle multiple symbol filters)
const hasMultipleHoldingFilters = accessFilter?.holdings?.length > 1;
const activityFilters = portfolioFilters.filter((filter) => {
// Always include ACCOUNT, ASSET_CLASS, TAG filters
if (
filter.type === 'ACCOUNT' ||
filter.type === 'ASSET_CLASS' ||
filter.type === 'TAG'
) {
return true;
}
// Include DATA_SOURCE and SYMBOL only if there's a single holding filter
if (
!hasMultipleHoldingFilters &&
(filter.type === 'DATA_SOURCE' || filter.type === 'SYMBOL')
) {
return true;
}
return false;
});
const { activities } = await this.activitiesService.getActivities({
filters: activityFilters.length > 0 ? activityFilters : undefined,
filters: portfolioFilters.length > 0 ? portfolioFilters : undefined,
sortColumn: 'date',
sortDirection: 'desc',
take: 10,

169
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -1,10 +1,6 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import {
AssetProfileIdentifier,
Filter,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import { validateObjectForForm } from '@ghostfolio/common/utils';
import { NotificationService } from '@ghostfolio/ui/notifications';
@ -65,7 +61,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
templateUrl: 'create-or-update-access-dialog.html'
})
export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
public showFilterPanel = false;
public hasExperimentalFeatures = false;
public accounts: AccountWithPlatform[] = [];
public assetClasses: Filter[] = [];
@ -111,6 +107,26 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
]
});
this.userService
.get()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ accounts, settings, tags }) => {
this.accounts = accounts;
this.hasExperimentalFeatures = settings.isExperimentalFeatures ?? false;
this.tags = tags
.filter(({ isUsed }) => isUsed)
.map(({ id, name }) => ({
id,
label: name,
type: 'TAG' as const
}));
this.updateFiltersFormControl(this.data.access?.settings?.filters);
this.changeDetectorRef.markForCheck();
});
this.accessForm
.get('type')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
@ -122,7 +138,6 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
granteeUserIdControl?.setValidators([
(control: AbstractControl) => Validators.required(control)
]);
this.showFilterPanel = false;
this.accessForm.get('filters')?.setValue(null);
} else {
granteeUserIdControl?.clearValidators();
@ -130,8 +145,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
permissionsControl?.setValue(
access?.permissions[0] ?? AccessPermission.READ_RESTRICTED
);
this.showFilterPanel = true;
this.loadFilterData();
if (this.hasExperimentalFeatures) {
this.loadHoldings();
}
}
granteeUserIdControl?.updateValueAndValidity();
@ -139,9 +156,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
this.changeDetectorRef.markForCheck();
});
if (isPublic) {
this.showFilterPanel = true;
this.loadFilterData();
if (isPublic && this.hasExperimentalFeatures) {
this.loadHoldings();
}
}
@ -157,76 +173,45 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
}
}
private buildFilterObject():
| {
accountIds?: string[];
assetClasses?: string[];
holdings?: AssetProfileIdentifier[];
tagIds?: string[];
}
| undefined {
private buildFilters(): Filter[] {
const filterValue = this.accessForm.get('filters')
?.value as PortfolioFilterFormValue | null;
if (
!filterValue ||
(!filterValue.account &&
!filterValue.assetClass &&
!filterValue.holding &&
!filterValue.tag)
) {
return undefined;
if (!filterValue) {
return [];
}
const filter: {
accountIds?: string[];
assetClasses?: string[];
holdings?: AssetProfileIdentifier[];
tagIds?: string[];
} = {};
const filters: Filter[] = [];
if (filterValue.account) {
filter.accountIds = [filterValue.account];
filters.push({ id: filterValue.account, type: 'ACCOUNT' });
}
if (filterValue.assetClass) {
filter.assetClasses = [filterValue.assetClass];
filters.push({ id: filterValue.assetClass, type: 'ASSET_CLASS' });
}
if (filterValue.holding) {
filter.holdings = [
filters.push(
{
dataSource: filterValue.holding.assetProfile.dataSource,
symbol: filterValue.holding.assetProfile.symbol
id: filterValue.holding.assetProfile.dataSource,
type: 'DATA_SOURCE'
},
{
id: filterValue.holding.assetProfile.symbol,
type: 'SYMBOL'
}
];
);
}
if (filterValue.tag) {
filter.tagIds = [filterValue.tag];
filters.push({ id: filterValue.tag, type: 'TAG' });
}
return filter;
return filters;
}
private loadFilterData() {
const existingFilter = this.data.access?.settings?.filter;
this.userService
.get()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.accounts = user.accounts;
this.tags = user.tags
.filter(({ isUsed }) => isUsed)
.map(({ id, name }) => ({
id,
label: name,
type: 'TAG' as const
}));
this.updateFiltersFormControl(existingFilter);
});
private loadHoldings() {
this.dataService
.fetchPortfolioDetails({})
.pipe(takeUntilDestroyed(this.destroyRef))
@ -246,50 +231,56 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
type: 'ASSET_CLASS' as const
}));
this.updateFiltersFormControl(existingFilter);
this.updateFiltersFormControl(this.data.access?.settings?.filters);
}
this.changeDetectorRef.markForCheck();
});
}
private updateFiltersFormControl(
existingFilter:
| {
accountIds?: string[];
assetClasses?: string[];
holdings?: AssetProfileIdentifier[];
tagIds?: string[];
}
| undefined
) {
if (!existingFilter) {
private updateFiltersFormControl(existingFilters: Filter[] | undefined) {
if (!existingFilters?.length) {
return;
}
const filterValue: Partial<PortfolioFilterFormValue> = {};
if (existingFilter.accountIds?.[0] && this.accounts.length > 0) {
filterValue.account = existingFilter.accountIds[0];
const accountFilter = existingFilters.find(({ type }) => {
return type === 'ACCOUNT';
});
if (accountFilter && this.accounts.length > 0) {
filterValue.account = accountFilter.id;
}
if (existingFilter.assetClasses?.[0] && this.assetClasses.length > 0) {
filterValue.assetClass = existingFilter.assetClasses[0];
const assetClassFilter = existingFilters.find(({ type }) => {
return type === 'ASSET_CLASS';
});
if (assetClassFilter && this.assetClasses.length > 0) {
filterValue.assetClass = assetClassFilter.id;
}
if (existingFilter.holdings?.[0] && this.holdings.length > 0) {
const holdingData = existingFilter.holdings[0];
const holding = this.holdings.find(
(h) =>
h.assetProfile.dataSource === holdingData.dataSource &&
h.assetProfile.symbol === holdingData.symbol
);
const dataSourceFilter = existingFilters.find(({ type }) => {
return type === 'DATA_SOURCE';
});
const symbolFilter = existingFilters.find(({ type }) => {
return type === 'SYMBOL';
});
if (dataSourceFilter && symbolFilter && this.holdings.length > 0) {
const holding = this.holdings.find(({ assetProfile }) => {
return (
assetProfile.dataSource === dataSourceFilter.id &&
assetProfile.symbol === symbolFilter.id
);
});
if (holding) {
filterValue.holding = holding;
}
}
if (existingFilter.tagIds?.[0] && this.tags.length > 0) {
filterValue.tag = existingFilter.tagIds[0];
const tagFilter = existingFilters.find(({ type }) => {
return type === 'TAG';
});
if (tagFilter && this.tags.length > 0) {
filterValue.tag = tagFilter.id;
}
if (Object.keys(filterValue).length > 0) {
@ -299,11 +290,11 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
}
private async createAccess() {
const filter = this.showFilterPanel ? this.buildFilterObject() : undefined;
const filters = this.buildFilters();
const access: CreateAccessDto = {
alias: this.accessForm.get('alias')?.value,
filter,
filters: filters.length > 0 ? filters : undefined,
granteeUserId: this.accessForm.get('granteeUserId')?.value,
permissions: [this.accessForm.get('permissions')?.value]
};
@ -344,11 +335,11 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
return;
}
const filter = this.showFilterPanel ? this.buildFilterObject() : undefined;
const filters = this.buildFilters();
const access: UpdateAccessDto = {
alias: this.accessForm.get('alias')?.value,
filter,
filters: filters.length > 0 ? filters : undefined,
granteeUserId: this.accessForm.get('granteeUserId')?.value,
id: accessId,
permissions: [this.accessForm.get('permissions')?.value]

18
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -58,19 +58,9 @@
/>
</mat-form-field>
</div>
}
</div>
<span>
@if (showFilterPanel) {
<h1 class="filter-settings-title" i18n mat-dialog-title>
Filter Settings (Optional)
</h1>
<div class="flex-grow-1 py-0" mat-dialog-content>
<p class="filter-settings-description text-muted" i18n>
Configure which accounts, holdings, tags, and asset classes will be
visible in this public access. Leave empty to show all data.
</p>
} @else if (hasExperimentalFeatures) {
<h2 class="h6" i18n>Filters</h2>
<div class="flex-grow-1 py-0">
<gf-portfolio-filter-form
appearance="outline"
class="w-100"
@ -82,7 +72,7 @@
/>
</div>
}
</span>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button mat-button type="button" (click)="onCancel()">
@if (accessForm.dirty) {

10
libs/common/src/lib/dtos/asset-profile-identifier.dto.ts

@ -1,10 +0,0 @@
import { DataSource } from '@prisma/client';
import { IsEnum, IsString } from 'class-validator';
export class AssetProfileIdentifierDto {
@IsEnum(DataSource)
dataSource: DataSource;
@IsString()
symbol: string;
}

14
libs/common/src/lib/dtos/create-access.dto.ts

@ -1,22 +1,16 @@
import { AccessFilter } from '@ghostfolio/common/interfaces';
import { Filter } from '@ghostfolio/common/interfaces';
import { AccessPermission } from '@prisma/client';
import {
IsEnum,
IsObject,
IsOptional,
IsString,
IsUUID
} from 'class-validator';
import { IsArray, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
export class CreateAccessDto {
@IsOptional()
@IsString()
alias?: string;
@IsArray()
@IsOptional()
@IsObject()
filter?: AccessFilter;
filters?: Filter[];
@IsOptional()
@IsUUID()

2
libs/common/src/lib/dtos/index.ts

@ -1,4 +1,3 @@
import { AssetProfileIdentifierDto } from './asset-profile-identifier.dto';
import { AuthDeviceDto } from './auth-device.dto';
import { CreateAccessDto } from './create-access.dto';
import { CreateAccountBalanceDto } from './create-account-balance.dto';
@ -26,7 +25,6 @@ import { UpdateTagDto } from './update-tag.dto';
import { UpdateUserSettingDto } from './update-user-setting.dto';
export {
AssetProfileIdentifierDto,
AuthDeviceDto,
CreateAccessDto,
CreateAccountBalanceDto,

14
libs/common/src/lib/dtos/update-access.dto.ts

@ -1,22 +1,16 @@
import { AccessFilter } from '@ghostfolio/common/interfaces';
import { Filter } from '@ghostfolio/common/interfaces';
import { AccessPermission } from '@prisma/client';
import {
IsEnum,
IsObject,
IsOptional,
IsString,
IsUUID
} from 'class-validator';
import { IsArray, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
export class UpdateAccessDto {
@IsOptional()
@IsString()
alias?: string;
@IsArray()
@IsOptional()
@IsObject()
filter?: AccessFilter;
filters?: Filter[];
@IsOptional()
@IsUUID()

4
libs/common/src/lib/dtos/update-bulk-market-data.dto.ts

@ -1,8 +1,8 @@
import { UpdateMarketDataDto } from '@ghostfolio/common/dtos';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray } from 'class-validator';
import { UpdateMarketDataDto } from './update-market-data.dto';
export class UpdateBulkMarketDataDto {
@ArrayNotEmpty()
@IsArray()

11
libs/common/src/lib/interfaces/access.interface.ts

@ -2,17 +2,10 @@ import { AccessType } from '@ghostfolio/common/types';
import { AccessPermission } from '@prisma/client';
import { AssetProfileIdentifier } from './asset-profile-identifier.interface';
export interface AccessFilter {
accountIds?: string[];
assetClasses?: string[];
holdings?: AssetProfileIdentifier[];
tagIds?: string[];
}
import { Filter } from './filter.interface';
export interface AccessSettings {
filter?: AccessFilter;
filters?: Filter[];
}
export interface Access {

3
libs/common/src/lib/interfaces/index.ts

@ -1,4 +1,4 @@
import type { Access, AccessFilter, AccessSettings } from './access.interface';
import type { Access, AccessSettings } from './access.interface';
import type { AccountBalance } from './account-balance.interface';
import type { Activity, ActivityError } from './activities.interface';
import type { AdminData } from './admin-data.interface';
@ -101,7 +101,6 @@ import type { XRayRulesSettings } from './x-ray-rules-settings.interface';
export {
Access,
AccessFilter,
AccessSettings,
AccessTokenResponse,
AccountBalance,

Loading…
Cancel
Save