Feature/validate forms using DTO for access, asset profile, tag and platform management (#3337)

* Validate forms using DTO for access, asset profile, tag and platform management

* Update changelog
pull/3348/head^2
Fedron 1 month ago committed by GitHub
parent 4efd5cefd8
commit 2173c418a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added a form validation against the DTO in the create or update access dialog
- Added a form validation against the DTO in the asset profile details dialog of the admin control
- Added a form validation against the DTO in the platform management of the admin control panel
- Added a form validation against the DTO in the tag management of the admin control panel
### Fixed ### Fixed
- Fixed an issue in the calculation of the portfolio summary caused by future liabilities - Fixed an issue in the calculation of the portfolio summary caused by future liabilities

@ -3,6 +3,7 @@ import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-dat
import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service'; import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config'; import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
@ -258,7 +259,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}); });
} }
public onSubmit() { public async onSubmit() {
let countries = []; let countries = [];
let scraperConfiguration = {}; let scraperConfiguration = {};
let sectors = []; let sectors = [];
@ -299,6 +300,17 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
url: this.assetProfileForm.get('url').value || null url: this.assetProfileForm.get('url').value || null
}; };
try {
await validateObjectForForm({
classDto: UpdateAssetProfileDto,
form: this.assetProfileForm,
object: assetProfileData
});
} catch (error) {
console.error(error);
return;
}
this.adminService this.adminService
.patchAssetProfile({ .patchAssetProfile({
...assetProfileData, ...assetProfileData,

@ -143,9 +143,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((platform: CreatePlatformDto | null) => {
const platform: CreatePlatformDto = data?.platform;
if (platform) { if (platform) {
this.adminService this.adminService
.postPlatform(platform) .postPlatform(platform)
@ -182,9 +180,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((platform: UpdatePlatformDto | null) => {
const platform: UpdatePlatformDto = data?.platform;
if (platform) { if (platform) {
this.adminService this.adminService
.putPlatform(platform) .putPlatform(platform)

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import {
ChangeDetectionStrategy,
Component,
Inject,
OnDestroy
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -11,18 +21,54 @@ import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces';
styleUrls: ['./create-or-update-platform-dialog.scss'], styleUrls: ['./create-or-update-platform-dialog.scss'],
templateUrl: 'create-or-update-platform-dialog.html' templateUrl: 'create-or-update-platform-dialog.html'
}) })
export class CreateOrUpdatePlatformDialog { export class CreateOrUpdatePlatformDialog implements OnDestroy {
public platformForm: FormGroup;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams, @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams,
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog> public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>,
) {} private formBuilder: FormBuilder
) {
this.platformForm = this.formBuilder.group({
name: [this.data.platform.name, Validators.required],
url: [this.data.platform.url, Validators.required]
});
}
public onCancel() { public onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }
public async onSubmit() {
try {
const platform: CreatePlatformDto | UpdatePlatformDto = {
name: this.platformForm.get('name')?.value,
url: this.platformForm.get('url')?.value
};
if (this.data.platform.id) {
(platform as UpdatePlatformDto).id = this.data.platform.id;
await validateObjectForForm({
classDto: UpdatePlatformDto,
form: this.platformForm,
object: platform
});
} else {
await validateObjectForForm({
classDto: CreatePlatformDto,
form: this.platformForm,
object: platform
});
}
this.dialogRef.close(platform);
} catch (error) {
console.error(error);
}
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

@ -1,17 +1,30 @@
<form #addPlatformForm="ngForm" class="d-flex flex-column h-100"> <form
class="d-flex flex-column h-100"
[formGroup]="platformForm"
(keyup.enter)="platformForm.valid && onSubmit()"
(ngSubmit)="onSubmit()"
>
<h1 *ngIf="data.platform.id" i18n mat-dialog-title>Update platform</h1> <h1 *ngIf="data.platform.id" i18n mat-dialog-title>Update platform</h1>
<h1 *ngIf="!data.platform.id" i18n mat-dialog-title>Add platform</h1> <h1 *ngIf="!data.platform.id" i18n mat-dialog-title>Add platform</h1>
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label> <mat-label i18n>Name</mat-label>
<input matInput name="name" required [(ngModel)]="data.platform.name" /> <input
formControlName="name"
matInput
(keydown.enter)="$event.stopPropagation()"
/>
</mat-form-field> </mat-form-field>
</div> </div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Url</mat-label> <mat-label i18n>Url</mat-label>
<input matInput name="url" required [(ngModel)]="data.platform.url" /> <input
formControlName="url"
matInput
(keydown.enter)="$event.stopPropagation()"
/>
@if (data.platform.url) { @if (data.platform.url) {
<gf-asset-profile-icon <gf-asset-profile-icon
class="mr-3" class="mr-3"
@ -23,12 +36,12 @@
</div> </div>
</div> </div>
<div class="justify-content-end" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button <button
color="primary" color="primary"
mat-flat-button mat-flat-button
[disabled]="!addPlatformForm.form.valid" type="submit"
[mat-dialog-close]="data" [disabled]="!platformForm.valid"
> >
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>

@ -142,9 +142,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((tag: CreateTagDto | null) => {
const tag: CreateTagDto = data?.tag;
if (tag) { if (tag) {
this.adminService this.adminService
.postTag(tag) .postTag(tag)
@ -180,9 +178,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((tag: UpdateTagDto | null) => {
const tag: UpdateTagDto = data?.tag;
if (tag) { if (tag) {
this.adminService this.adminService
.putTag(tag) .putTag(tag)

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import {
ChangeDetectionStrategy,
Component,
Inject,
OnDestroy
} from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -11,18 +21,52 @@ import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces';
styleUrls: ['./create-or-update-tag-dialog.scss'], styleUrls: ['./create-or-update-tag-dialog.scss'],
templateUrl: 'create-or-update-tag-dialog.html' templateUrl: 'create-or-update-tag-dialog.html'
}) })
export class CreateOrUpdateTagDialog { export class CreateOrUpdateTagDialog implements OnDestroy {
public tagForm: FormGroup;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams, @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams,
public dialogRef: MatDialogRef<CreateOrUpdateTagDialog> public dialogRef: MatDialogRef<CreateOrUpdateTagDialog>,
) {} private formBuilder: FormBuilder
) {
this.tagForm = this.formBuilder.group({
name: [this.data.tag.name]
});
}
public onCancel() { public onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }
public async onSubmit() {
try {
const tag: CreateTagDto | UpdateTagDto = {
name: this.tagForm.get('name')?.value
};
if (this.data.tag.id) {
(tag as UpdateTagDto).id = this.data.tag.id;
await validateObjectForForm({
classDto: UpdateTagDto,
form: this.tagForm,
object: tag
});
} else {
await validateObjectForForm({
classDto: CreateTagDto,
form: this.tagForm,
object: tag
});
}
this.dialogRef.close(tag);
} catch (error) {
console.error(error);
}
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

@ -1,21 +1,30 @@
<form #addTagForm="ngForm" class="d-flex flex-column h-100"> <form
class="d-flex flex-column h-100"
[formGroup]="tagForm"
(keyup.enter)="tagForm.valid && onSubmit()"
(ngSubmit)="onSubmit()"
>
<h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1> <h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1>
<h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1> <h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1>
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label> <mat-label i18n>Name</mat-label>
<input matInput name="name" required [(ngModel)]="data.tag.name" /> <input
formControlName="name"
matInput
(keydown.enter)="$event.stopPropagation()"
/>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="justify-content-end" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button <button
color="primary" color="primary"
mat-flat-button mat-flat-button
[disabled]="!addTagForm.form.valid" type="submit"
[mat-dialog-close]="data" [disabled]="!tagForm.valid"
> >
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>

@ -1,5 +1,6 @@
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -40,22 +41,22 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
alias: [this.data.access.alias], alias: [this.data.access.alias],
permissions: [this.data.access.permissions[0], Validators.required], permissions: [this.data.access.permissions[0], Validators.required],
type: [this.data.access.type, Validators.required], type: [this.data.access.type, Validators.required],
userId: [this.data.access.grantee, Validators.required] granteeUserId: [this.data.access.grantee, Validators.required]
}); });
this.accessForm.get('type').valueChanges.subscribe((accessType) => { this.accessForm.get('type').valueChanges.subscribe((accessType) => {
const granteeUserIdControl = this.accessForm.get('granteeUserId');
const permissionsControl = this.accessForm.get('permissions'); const permissionsControl = this.accessForm.get('permissions');
const userIdControl = this.accessForm.get('userId');
if (accessType === 'PRIVATE') { if (accessType === 'PRIVATE') {
granteeUserIdControl.setValidators(Validators.required);
permissionsControl.setValidators(Validators.required); permissionsControl.setValidators(Validators.required);
userIdControl.setValidators(Validators.required);
} else { } else {
userIdControl.clearValidators(); granteeUserIdControl.clearValidators();
} }
granteeUserIdControl.updateValueAndValidity();
permissionsControl.updateValueAndValidity(); permissionsControl.updateValueAndValidity();
userIdControl.updateValueAndValidity();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -65,28 +66,38 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
this.dialogRef.close(); this.dialogRef.close();
} }
public onSubmit() { public async onSubmit() {
const access: CreateAccessDto = { const access: CreateAccessDto = {
alias: this.accessForm.get('alias').value, alias: this.accessForm.get('alias').value,
granteeUserId: this.accessForm.get('userId').value, granteeUserId: this.accessForm.get('granteeUserId').value,
permissions: [this.accessForm.get('permissions').value] permissions: [this.accessForm.get('permissions').value]
}; };
this.dataService try {
.postAccess(access) await validateObjectForForm({
.pipe( classDto: CreateAccessDto,
catchError((error) => { form: this.accessForm,
if (error.status === StatusCodes.BAD_REQUEST) { object: access
alert($localize`Oops! Could not grant access.`);
}
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => {
this.dialogRef.close({ access });
}); });
this.dataService
.postAccess(access)
.pipe(
catchError((error) => {
if (error.status === StatusCodes.BAD_REQUEST) {
alert($localize`Oops! Could not grant access.`);
}
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => {
this.dialogRef.close(access);
});
} catch (error) {
console.error(error);
}
} }
public ngOnDestroy() { public ngOnDestroy() {

@ -45,7 +45,7 @@
Ghostfolio <ng-container i18n>User ID</ng-container> Ghostfolio <ng-container i18n>User ID</ng-container>
</mat-label> </mat-label>
<input <input
formControlName="userId" formControlName="granteeUserId"
matInput matInput
type="text" type="text"
(keydown.enter)="$event.stopPropagation()" (keydown.enter)="$event.stopPropagation()"

@ -1,3 +1,4 @@
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
@ -113,7 +114,7 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef.afterClosed().subscribe((access) => { dialogRef.afterClosed().subscribe((access: CreateAccessDto | null) => {
if (access) { if (access) {
this.update(); this.update();
} }

@ -189,9 +189,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data: any) => { .subscribe((account: UpdateAccountDto | null) => {
const account: UpdateAccountDto = data?.account;
if (account) { if (account) {
this.dataService this.dataService
.putAccount(account) .putAccount(account)
@ -258,9 +256,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data: any) => { .subscribe((account: CreateAccountDto | null) => {
const account: CreateAccountDto = data?.account;
if (account) { if (account) {
this.dataService this.dataService
.postAccount(account) .postAccount(account)

@ -123,6 +123,8 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
form: this.accountForm, form: this.accountForm,
object: account object: account
}); });
this.dialogRef.close(account as UpdateAccountDto);
} else { } else {
delete (account as CreateAccountDto).id; delete (account as CreateAccountDto).id;
@ -131,9 +133,9 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
form: this.accountForm, form: this.accountForm,
object: account object: account
}); });
}
this.dialogRef.close({ account }); this.dialogRef.close(account as CreateAccountDto);
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

@ -287,9 +287,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data: any) => { .subscribe((transaction: UpdateOrderDto | null) => {
const transaction: UpdateOrderDto = data?.activity;
if (transaction) { if (transaction) {
this.dataService this.dataService
.putOrder(transaction) .putOrder(transaction)
@ -338,9 +336,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data: any) => { .subscribe((transaction: CreateOrderDto | null) => {
const transaction: CreateOrderDto = data?.activity;
if (transaction) { if (transaction) {
this.dataService.postOrder(transaction).subscribe({ this.dataService.postOrder(transaction).subscribe({
next: () => { next: () => {

@ -475,6 +475,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
ignoreFields: ['dataSource', 'date'], ignoreFields: ['dataSource', 'date'],
object: activity as UpdateOrderDto object: activity as UpdateOrderDto
}); });
this.dialogRef.close(activity as UpdateOrderDto);
} else { } else {
(activity as CreateOrderDto).updateAccountBalance = (activity as CreateOrderDto).updateAccountBalance =
this.activityForm.get('updateAccountBalance').value; this.activityForm.get('updateAccountBalance').value;
@ -485,9 +487,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
ignoreFields: ['dataSource', 'date'], ignoreFields: ['dataSource', 'date'],
object: activity object: activity
}); });
}
this.dialogRef.close({ activity }); this.dialogRef.close(activity as CreateOrderDto);
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

@ -32,6 +32,14 @@ export async function validateObjectForForm<T>({
validationError: Object.values(constraints)[0] validationError: Object.values(constraints)[0]
}); });
} }
const formControlInCustomCurrency = form.get(`${property}InCustomCurrency`);
if (formControlInCustomCurrency) {
formControlInCustomCurrency.setErrors({
validationError: Object.values(constraints)[0]
});
}
} }
return Promise.reject(nonIgnoredErrors); return Promise.reject(nonIgnoredErrors);

Loading…
Cancel
Save