From 9241c04d5a2099665b925b735d3319307b429c52 Mon Sep 17 00:00:00 2001 From: Fedron <40535546+Fedron@users.noreply.github.com> Date: Sun, 14 Apr 2024 18:52:41 +0100 Subject: [PATCH] Feature/add form validation against DTO for activity and account (#3230) * Add form validation against DTO for activity and account * Update changelog --- CHANGELOG.md | 5 +++ ...eate-or-update-account-dialog.component.ts | 31 +++++++++++---- ...ate-or-update-activity-dialog.component.ts | 35 +++++++++++++---- apps/client/src/app/util/form.util.ts | 38 +++++++++++++++++++ 4 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 apps/client/src/app/util/form.util.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fec3111c..8e26c1369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added a form validation against the DTO in the create or update account dialog +- Added a form validation against the DTO in the create or update activity dialog + ### Changed - Moved the dividend calculations into the portfolio calculator diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts index c97bbf113..4e3ef335e 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts @@ -1,6 +1,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { Currency } from '@ghostfolio/common/interfaces'; import { @@ -102,7 +103,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { this.dialogRef.close(); } - public onSubmit() { + public async onSubmit() { const account: CreateAccountDto | UpdateAccountDto = { balance: this.accountForm.controls['balance'].value, comment: this.accountForm.controls['comment'].value, @@ -113,13 +114,29 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { platformId: this.accountForm.controls['platformId'].value?.id ?? null }; - if (this.data.account.id) { - (account as UpdateAccountDto).id = this.data.account.id; - } else { - delete (account as CreateAccountDto).id; - } + try { + if (this.data.account.id) { + (account as UpdateAccountDto).id = this.data.account.id; + + await validateObjectForForm({ + classDto: UpdateAccountDto, + form: this.accountForm, + object: account + }); + } else { + delete (account as CreateAccountDto).id; + + await validateObjectForForm({ + classDto: CreateAccountDto, + form: this.accountForm, + object: account + }); + } - this.dialogRef.close({ account }); + this.dialogRef.close({ account }); + } catch (error) { + console.error(error); + } } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 21a2ca920..1196de58c 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -1,6 +1,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { getDateFormatString } from '@ghostfolio/common/helper'; import { translate } from '@ghostfolio/ui/i18n'; @@ -451,7 +452,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ); } - public onSubmit() { + public async onSubmit() { const activity: CreateOrderDto | UpdateOrderDto = { accountId: this.activityForm.controls['accountId'].value, assetClass: this.activityForm.controls['assetClass'].value, @@ -474,14 +475,32 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { unitPrice: this.activityForm.controls['unitPrice'].value }; - if (this.data.activity.id) { - (activity as UpdateOrderDto).id = this.data.activity.id; - } else { - (activity as CreateOrderDto).updateAccountBalance = - this.activityForm.controls['updateAccountBalance'].value; - } + try { + if (this.data.activity.id) { + (activity as UpdateOrderDto).id = this.data.activity.id; - this.dialogRef.close({ activity }); + await validateObjectForForm({ + classDto: UpdateOrderDto, + form: this.activityForm, + ignoreFields: ['dataSource', 'date'], + object: activity as UpdateOrderDto + }); + } else { + (activity as CreateOrderDto).updateAccountBalance = + this.activityForm.controls['updateAccountBalance'].value; + + await validateObjectForForm({ + classDto: CreateOrderDto, + form: this.activityForm, + ignoreFields: ['dataSource', 'date'], + object: activity + }); + } + + this.dialogRef.close({ activity }); + } catch (error) { + console.error(error); + } } public ngOnDestroy() { diff --git a/apps/client/src/app/util/form.util.ts b/apps/client/src/app/util/form.util.ts new file mode 100644 index 000000000..d11490c7e --- /dev/null +++ b/apps/client/src/app/util/form.util.ts @@ -0,0 +1,38 @@ +import { FormGroup } from '@angular/forms'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; + +export async function validateObjectForForm({ + classDto, + form, + ignoreFields = [], + object +}: { + classDto: { new (): T }; + form: FormGroup; + ignoreFields?: string[]; + object: T; +}): Promise { + const objectInstance = plainToInstance(classDto, object); + const errors = await validate(objectInstance as object); + + const nonIgnoredErrors = errors.filter(({ property }) => { + return !ignoreFields.includes(property); + }); + + if (nonIgnoredErrors.length === 0) { + return Promise.resolve(); + } + + for (const { constraints, property } of nonIgnoredErrors) { + const formControl = form.get(property); + + if (formControl) { + formControl.setErrors({ + validationError: Object.values(constraints)[0] + }); + } + } + + return Promise.reject(nonIgnoredErrors); +}