diff --git a/apps/api/src/app/access/access-filter.dto.ts b/apps/api/src/app/access/access-filter.dto.ts deleted file mode 100644 index 22c2ce603..000000000 --- a/apps/api/src/app/access/access-filter.dto.ts +++ /dev/null @@ -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[]; -} diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index edc1b4751..727bf821e 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/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({ diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index b760e54ff..8f1c80761 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/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, 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 39e78b1c1..f9778be26 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,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 = {}; - 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] diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index 27d4788e6..504066e97 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -58,19 +58,9 @@ /> - } - - - @if (showFilterPanel) { -

- Filter Settings (Optional) -

-
-

- Configure which accounts, holdings, tags, and asset classes will be - visible in this public access. Leave empty to show all data. -

- + } @else if (hasExperimentalFeatures) { +

Filters

+
} - +