Browse Source

Task/refactor access dialog to enhance filter settings and improve form structure

pull/5848/head
Germán Martín 1 week ago
parent
commit
17a4f313e1
  1. 58
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  2. 121
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

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

@ -1,22 +1,18 @@
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { UpdateAccessDto } from '@ghostfolio/api/app/access/update-access.dto';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import { GfPortfolioFilterFormComponent } from '@ghostfolio/ui/portfolio-filter-form';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
Inject,
OnDestroy,
OnInit
} from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormGroup,
FormsModule,
@ -32,12 +28,13 @@ import {
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { IonIcon } from '@ionic/angular/standalone';
import { AccessPermission } from '@prisma/client';
import { StatusCodes } from 'http-status-codes';
import { addIcons } from 'ionicons';
import { chevronUpOutline, optionsOutline } from 'ionicons/icons';
import { EMPTY, Subject, catchError, takeUntil } from 'rxjs';
import { NotificationService } from '../../../core/notification/notification.service';
import { DataService } from '../../../services/data.service';
import { validateObjectForForm } from '../../../util/form.util';
import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
@Component({
@ -45,8 +42,6 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
host: { class: 'h-100' },
imports: [
FormsModule,
GfPortfolioFilterFormComponent,
IonIcon,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
@ -54,7 +49,6 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
MatSelectModule,
ReactiveFormsModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-create-or-update-access-dialog',
styleUrls: ['./create-or-update-access-dialog.scss'],
templateUrl: 'create-or-update-access-dialog.html'
@ -65,7 +59,6 @@ export class GfCreateOrUpdateAccessDialogComponent
public accessForm: FormGroup;
public mode: 'create' | 'update';
public showFilterPanel = false;
public filterPanelExpanded = false;
// Datos para el filtro
public accounts: AccountWithPlatform[] = [];
@ -84,8 +77,6 @@ export class GfCreateOrUpdateAccessDialogComponent
private notificationService: NotificationService
) {
this.mode = this.data.access?.id ? 'update' : 'create';
addIcons({ chevronUpOutline, optionsOutline });
}
public ngOnInit() {
@ -96,12 +87,17 @@ export class GfCreateOrUpdateAccessDialogComponent
filter: [null],
granteeUserId: [
this.data.access.grantee,
isPublic ? null : Validators.required
isPublic
? null
: [(control: AbstractControl) => Validators.required(control)]
],
permissions: [
this.data.access.permissions[0],
[(control: AbstractControl) => Validators.required(control)]
],
permissions: [this.data.access.permissions[0], Validators.required],
type: [
{ disabled: this.mode === 'update', value: this.data.access.type },
Validators.required
[(control: AbstractControl) => Validators.required(control)]
]
});
@ -111,7 +107,9 @@ export class GfCreateOrUpdateAccessDialogComponent
const filterControl = this.accessForm.get('filter');
if (accessType === 'PRIVATE') {
granteeUserIdControl.setValidators(Validators.required);
granteeUserIdControl.setValidators([
(control: AbstractControl) => Validators.required(control)
]);
this.showFilterPanel = false;
filterControl.setValue(null);
} else {
@ -201,9 +199,11 @@ export class GfCreateOrUpdateAccessDialogComponent
private async createAccess() {
const access: CreateAccessDto = {
alias: this.accessForm.get('alias').value,
granteeUserId: this.accessForm.get('granteeUserId').value,
permissions: [this.accessForm.get('permissions').value]
alias: this.accessForm.get('alias')?.value as string,
granteeUserId: this.accessForm.get('granteeUserId')?.value as string,
permissions: [
this.accessForm.get('permissions')?.value as AccessPermission
]
};
try {
@ -216,8 +216,8 @@ export class GfCreateOrUpdateAccessDialogComponent
this.dataService
.postAccess(access)
.pipe(
catchError((error) => {
if (error.status === StatusCodes.BAD_REQUEST) {
catchError((error: { status?: number }) => {
if (error?.status === StatusCodes.BAD_REQUEST) {
this.notificationService.alert({
title: $localize`Oops! Could not grant access.`
});
@ -237,10 +237,12 @@ export class GfCreateOrUpdateAccessDialogComponent
private async updateAccess() {
const access: UpdateAccessDto = {
alias: this.accessForm.get('alias').value,
granteeUserId: this.accessForm.get('granteeUserId').value,
alias: this.accessForm.get('alias')?.value as string,
granteeUserId: this.accessForm.get('granteeUserId')?.value as string,
id: this.data.access.id,
permissions: [this.accessForm.get('permissions').value]
permissions: [
this.accessForm.get('permissions')?.value as AccessPermission
]
};
try {
@ -253,8 +255,8 @@ export class GfCreateOrUpdateAccessDialogComponent
this.dataService
.putAccess(access)
.pipe(
catchError(({ status }) => {
if (status.status === StatusCodes.BAD_REQUEST) {
catchError((error: { status?: number }) => {
if (error?.status === StatusCodes.BAD_REQUEST) {
this.notificationService.alert({
title: $localize`Oops! Could not update access.`
});

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

@ -4,48 +4,13 @@
(keyup.enter)="accessForm.valid && onSubmit()"
(ngSubmit)="onSubmit()"
>
<div mat-dialog-title>
<div class="d-flex justify-content-between align-items-center">
<h1 class="m-0">
@if (mode === 'create') {
<span i18n>Grant access</span>
} @else {
<span i18n>Edit access</span>
}
</h1>
@if (showFilterPanel) {
<button
class="no-min-width"
mat-button
type="button"
(click)="filterPanelExpanded = !filterPanelExpanded"
>
<ion-icon
class="rotate-90"
size="large"
[name]="
filterPanelExpanded ? 'chevron-up-outline' : 'options-outline'
"
/>
</button>
}
</div>
@if (showFilterPanel && filterPanelExpanded) {
<div class="mt-3">
<p class="text-muted mb-3" i18n style="font-size: 0.875rem">
Configure which accounts, holdings, tags, and asset classes will be
visible in this public access. Leave empty to show all data.
</p>
<gf-portfolio-filter-form
formControlName="filter"
[accounts]="accounts"
[assetClasses]="assetClasses"
[holdings]="holdings"
[tags]="tags"
/>
</div>
<h1 mat-dialog-title>
@if (mode === 'create') {
<span i18n>Grant access</span>
} @else {
<span i18n>Edit access</span>
}
</div>
</h1>
<div class="flex-grow-1 py-3" mat-dialog-content>
<div>
<mat-form-field appearance="outline" class="w-100">
@ -94,6 +59,80 @@
</mat-form-field>
</div>
}
@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">
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>
</div>
}
</div>
<div class="justify-content-end" mat-dialog-actions>
<button mat-button type="button" (click)="onCancel()">

Loading…
Cancel
Save