Browse Source

Feature: Refactor access filter dialog and improve filter handling

pull/5848/head
Germán Martín 3 days ago
parent
commit
a2bd17692e
  1. 4
      apps/api/src/app/access/access-filter.dto.ts
  2. 1
      apps/api/src/app/access/access-settings.interface.ts
  3. 22
      apps/api/src/app/access/access.controller.ts
  4. 10
      apps/api/src/app/endpoints/public/public.controller.ts
  5. 151
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  6. 74
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  7. 11
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss

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

@ -8,7 +8,7 @@ import {
ValidateNested
} from 'class-validator';
export class HoldingFilterDto {
class HoldingFilterDto {
@IsEnum(DataSource)
dataSource: DataSource;
@ -29,8 +29,8 @@ export class AccessFilterDto {
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
@Type(() => HoldingFilterDto)
@ValidateNested({ each: true })
holdings?: HoldingFilterDto[];
@IsArray()

1
apps/api/src/app/access/access-settings.interface.ts

@ -1 +0,0 @@
export type { AccessSettings } from '@ghostfolio/common/interfaces';

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

@ -19,14 +19,24 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Access as AccessModel } from '@prisma/client';
import { Access as AccessModel, Prisma } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccessSettings } from './access-settings.interface';
import { AccessService } from './access.service';
import { CreateAccessDto } from './create-access.dto';
import { UpdateAccessDto } from './update-access.dto';
interface AccessFilter {
accountIds?: string[];
assetClasses?: string[];
holdings?: { dataSource: string; symbol: string }[];
tagIds?: string[];
}
interface AccessSettings {
filter?: AccessFilter;
}
@Controller('access')
export class AccessController {
public constructor(
@ -62,7 +72,7 @@ export class AccessController {
? { connect: { id: data.granteeUserId } }
: undefined,
permissions: data.permissions,
settings: settings as any,
settings: settings as Prisma.InputJsonValue,
user: { connect: { id: this.request.user.id } }
});
} catch {
@ -116,20 +126,20 @@ export class AccessController {
if (granteeUser) {
return {
alias,
grantee: granteeUser?.id,
id,
permissions: accessPermissions,
settings: settings as AccessSettings,
grantee: granteeUser?.id,
type: 'PRIVATE'
};
}
return {
alias,
grantee: 'Public',
id,
permissions: accessPermissions,
settings: settings as AccessSettings,
grantee: 'Public',
type: 'PUBLIC'
};
}
@ -177,7 +187,7 @@ export class AccessController {
? { connect: { id: data.granteeUserId } }
: { disconnect: true },
permissions: data.permissions,
settings: settings as any
settings: settings as Prisma.InputJsonValue
},
where: { id }
});

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

@ -74,7 +74,7 @@ export class PublicController {
if (accessFilter) {
// Add account filters
if (accessFilter.accountIds && accessFilter.accountIds.length > 0) {
if (accessFilter.accountIds?.length > 0) {
portfolioFilters.push(
...accessFilter.accountIds.map((accountId) => ({
id: accountId,
@ -84,7 +84,7 @@ export class PublicController {
}
// Add asset class filters
if (accessFilter.assetClasses && accessFilter.assetClasses.length > 0) {
if (accessFilter.assetClasses?.length > 0) {
portfolioFilters.push(
...accessFilter.assetClasses.map((assetClass) => ({
id: assetClass,
@ -94,7 +94,7 @@ export class PublicController {
}
// Add tag filters
if (accessFilter.tagIds && accessFilter.tagIds.length > 0) {
if (accessFilter.tagIds?.length > 0) {
portfolioFilters.push(
...accessFilter.tagIds.map((tagId) => ({
id: tagId,
@ -105,7 +105,7 @@ export class PublicController {
// Add holding filters (symbol + dataSource)
// Each holding needs both DATA_SOURCE and SYMBOL filters
if (accessFilter.holdings && accessFilter.holdings.length > 0) {
if (accessFilter.holdings?.length > 0) {
accessFilter.holdings.forEach((holding) => {
portfolioFilters.push(
{
@ -143,7 +143,6 @@ export class PublicController {
})
]);
// Filter out only the base currency cash holdings
const baseCurrency =
user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
const filteredHoldings = Object.fromEntries(
@ -193,7 +192,6 @@ export class PublicController {
withExcludedAccountsAndActivities: false
});
// If multiple holdings, filter activities manually
let filteredActivities = activities;
if (hasMultipleHoldingFilters && accessFilter.holdings) {
filteredActivities = activities.filter((activity) => {

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

@ -2,6 +2,10 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { UpdateAccessDto } from '@ghostfolio/api/app/access/update-access.dto';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import {
GfPortfolioFilterFormComponent,
PortfolioFilterFormValue
} from '@ghostfolio/ui/portfolio-filter-form';
import {
ChangeDetectionStrategy,
@ -42,6 +46,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
host: { class: 'h-100' },
imports: [
FormsModule,
GfPortfolioFilterFormComponent,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
@ -60,7 +65,6 @@ export class GfCreateOrUpdateAccessDialogComponent
public mode: 'create' | 'update';
public showFilterPanel = false;
// Datos para el filtro
public accounts: AccountWithPlatform[] = [];
public assetClasses: Filter[] = [];
public holdings: PortfolioPosition[] = [];
@ -69,12 +73,12 @@ export class GfCreateOrUpdateAccessDialogComponent
private unsubscribeSubject = new Subject<void>();
public constructor(
public dialogRef: MatDialogRef<GfCreateOrUpdateAccessDialogComponent>,
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) private data: CreateOrUpdateAccessDialogParams,
private dataService: DataService,
private formBuilder: FormBuilder,
private notificationService: NotificationService
private notificationService: NotificationService,
public dialogRef: MatDialogRef<GfCreateOrUpdateAccessDialogComponent>
) {
this.mode = this.data.access?.id ? 'update' : 'create';
}
@ -84,10 +88,7 @@ export class GfCreateOrUpdateAccessDialogComponent
this.accessForm = this.formBuilder.group({
alias: [this.data.access.alias],
filterAccount: [null],
filterAssetClass: [null],
filterHolding: [null],
filterTag: [null],
filters: [null],
granteeUserId: [
this.data.access.grantee,
isPublic
@ -113,11 +114,7 @@ export class GfCreateOrUpdateAccessDialogComponent
(control: AbstractControl) => Validators.required(control)
]);
this.showFilterPanel = false;
// Limpiar los filtros
this.accessForm.get('filterAccount')?.setValue(null);
this.accessForm.get('filterAssetClass')?.setValue(null);
this.accessForm.get('filterHolding')?.setValue(null);
this.accessForm.get('filterTag')?.setValue(null);
this.accessForm.get('filters')?.setValue(null);
} else {
granteeUserIdControl.clearValidators();
granteeUserIdControl.setValue(null);
@ -131,7 +128,6 @@ export class GfCreateOrUpdateAccessDialogComponent
this.changeDetectorRef.markForCheck();
});
// Si ya es público al iniciar, mostrar el panel y cargar datos
if (isPublic) {
this.showFilterPanel = true;
this.loadFilterData();
@ -163,19 +159,16 @@ export class GfCreateOrUpdateAccessDialogComponent
tagIds?: string[];
}
| undefined {
const filterAccount = this.accessForm.get('filterAccount')?.value as
| string
| null;
const filterAssetClass = this.accessForm.get('filterAssetClass')?.value as
| string
| null;
const filterHolding = this.accessForm.get('filterHolding')?.value as
| string
| null;
const filterTag = this.accessForm.get('filterTag')?.value as string | null;
// Solo retornar filtro si hay al menos un campo con valor
if (!filterAccount && !filterAssetClass && !filterHolding && !filterTag) {
const filterValue = this.accessForm.get('filters')
?.value as PortfolioFilterFormValue | null;
if (
!filterValue ||
(!filterValue.account &&
!filterValue.assetClass &&
!filterValue.holding &&
!filterValue.tag)
) {
return undefined;
}
@ -186,29 +179,25 @@ export class GfCreateOrUpdateAccessDialogComponent
tagIds?: string[];
} = {};
if (filterAccount) {
filter.accountIds = [filterAccount];
if (filterValue.account) {
filter.accountIds = [filterValue.account];
}
if (filterAssetClass) {
filter.assetClasses = [filterAssetClass];
if (filterValue.assetClass) {
filter.assetClasses = [filterValue.assetClass];
}
if (filterHolding) {
// Buscar el holding seleccionado para obtener dataSource y symbol
const holding = this.holdings.find((h) => h.symbol === filterHolding);
if (holding) {
filter.holdings = [
{
dataSource: holding.dataSource,
symbol: holding.symbol
}
];
}
if (filterValue.holding) {
filter.holdings = [
{
dataSource: filterValue.holding.dataSource,
symbol: filterValue.holding.symbol
}
];
}
if (filterTag) {
filter.tagIds = [filterTag];
if (filterValue.tag) {
filter.tagIds = [filterValue.tag];
}
return filter;
@ -223,15 +212,7 @@ export class GfCreateOrUpdateAccessDialogComponent
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.accounts = response.accounts;
// Si existe un filtro de cuenta, establecerlo
if (existingFilter?.accountIds?.[0]) {
this.accessForm
.get('filterAccount')
?.setValue(existingFilter.accountIds[0]);
}
this.changeDetectorRef.markForCheck();
this.updateFiltersFormControl(existingFilter);
});
// Cargar holdings y asset classes
@ -255,19 +236,7 @@ export class GfCreateOrUpdateAccessDialogComponent
type: 'ASSET_CLASS' as const
}));
// Si existe un filtro de asset class, establecerlo
if (existingFilter?.assetClasses?.[0]) {
this.accessForm
.get('filterAssetClass')
?.setValue(existingFilter.assetClasses[0]);
}
// Si existe un filtro de holding, establecerlo
if (existingFilter?.holdings?.[0]?.symbol) {
this.accessForm
.get('filterHolding')
?.setValue(existingFilter.holdings[0].symbol);
}
this.updateFiltersFormControl(existingFilter);
}
this.changeDetectorRef.markForCheck();
});
@ -283,15 +252,57 @@ export class GfCreateOrUpdateAccessDialogComponent
type: 'TAG' as const
}));
// Si existe un filtro de tag, establecerlo
if (existingFilter?.tagIds?.[0]) {
this.accessForm.get('filterTag')?.setValue(existingFilter.tagIds[0]);
}
this.updateFiltersFormControl(existingFilter);
this.changeDetectorRef.markForCheck();
});
}
private updateFiltersFormControl(
existingFilter:
| {
accountIds?: string[];
assetClasses?: string[];
holdings?: { dataSource: string; symbol: string }[];
tagIds?: string[];
}
| undefined
) {
if (!existingFilter) {
return;
}
const filterValue: Partial<PortfolioFilterFormValue> = {};
if (existingFilter.accountIds?.[0] && this.accounts.length > 0) {
filterValue.account = existingFilter.accountIds[0];
}
if (existingFilter.assetClasses?.[0] && this.assetClasses.length > 0) {
filterValue.assetClass = existingFilter.assetClasses[0];
}
if (existingFilter.holdings?.[0] && this.holdings.length > 0) {
const holdingData = existingFilter.holdings[0];
const holding = this.holdings.find(
(h) =>
h.dataSource === holdingData.dataSource &&
h.symbol === holdingData.symbol
);
if (holding) {
filterValue.holding = holding;
}
}
if (existingFilter.tagIds?.[0] && this.tags.length > 0) {
filterValue.tag = existingFilter.tagIds[0];
}
if (Object.keys(filterValue).length > 0) {
this.accessForm.get('filters')?.setValue(filterValue);
this.changeDetectorRef.markForCheck();
}
}
private async createAccess() {
// Construir el objeto filter si estamos en modo PUBLIC
const filter = this.showFilterPanel ? this.buildFilterObject() : undefined;

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

@ -62,75 +62,19 @@
@if (showFilterPanel) {
<div class="mt-4">
<h2 class="mb-3" i18n style="font-size: 1.125rem; font-weight: 500">
Filter Settings (Optional)
</h2>
<p class="text-muted mb-3" i18n style="font-size: 0.875rem">
<h2 class="filter-settings-title" i18n>Filter Settings (Optional)</h2>
<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>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Account</mat-label>
<mat-select formControlName="filterAccount">
<mat-option [value]="null" />
@for (account of accounts; track account.id) {
<mat-option [value]="account.id">
{{ account.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Holding</mat-label>
<mat-select formControlName="filterHolding">
<mat-option [value]="null" />
@for (holding of holdings; track holding.symbol) {
<mat-option [value]="holding.symbol">
<div class="line-height-1 text-truncate">
<span
><b>{{ holding.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol }} · {{ holding.currency }}</small
>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Tag</mat-label>
<mat-select formControlName="filterTag">
<mat-option [value]="null" />
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="filterAssetClass">
<mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id">{{
assetClass.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<gf-portfolio-filter-form
formControlName="filters"
[accounts]="accounts"
[assetClasses]="assetClasses"
[holdings]="holdings"
[tags]="tags"
/>
</div>
}
</div>

11
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss

@ -10,4 +10,15 @@
transform: rotate(90deg);
}
}
.filter-settings-title {
font-size: 1.125rem;
font-weight: 500;
margin-bottom: 1rem;
}
.filter-settings-description {
font-size: 0.875rem;
margin-bottom: 1rem;
}
}

Loading…
Cancel
Save