diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a4eff54..f682d51db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ 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 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 an issue in the calculation of the portfolio summary caused by future liabilities diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index 783bbdbc4..5016a4205 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -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 { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { @@ -258,7 +259,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit { }); } - public onSubmit() { + public async onSubmit() { let countries = []; let scraperConfiguration = {}; let sectors = []; @@ -299,6 +300,17 @@ export class AssetProfileDialog implements OnDestroy, OnInit { 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 .patchAssetProfile({ ...assetProfileData, diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.ts b/apps/client/src/app/components/admin-platform/admin-platform.component.ts index 6a2a3de3c..8a2004f8d 100644 --- a/apps/client/src/app/components/admin-platform/admin-platform.component.ts +++ b/apps/client/src/app/components/admin-platform/admin-platform.component.ts @@ -143,9 +143,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data) => { - const platform: CreatePlatformDto = data?.platform; - + .subscribe((platform: CreatePlatformDto | null) => { if (platform) { this.adminService .postPlatform(platform) @@ -182,9 +180,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data) => { - const platform: UpdatePlatformDto = data?.platform; - + .subscribe((platform: UpdatePlatformDto | null) => { if (platform) { this.adminService .putPlatform(platform) diff --git a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.component.ts b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.component.ts index 14d893900..518b6dd52 100644 --- a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.component.ts +++ b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.component.ts @@ -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 { Subject } from 'rxjs'; @@ -11,18 +21,54 @@ import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces'; styleUrls: ['./create-or-update-platform-dialog.scss'], templateUrl: 'create-or-update-platform-dialog.html' }) -export class CreateOrUpdatePlatformDialog { +export class CreateOrUpdatePlatformDialog implements OnDestroy { + public platformForm: FormGroup; + private unsubscribeSubject = new Subject(); public constructor( @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams, - public dialogRef: MatDialogRef - ) {} + public dialogRef: MatDialogRef, + private formBuilder: FormBuilder + ) { + this.platformForm = this.formBuilder.group({ + name: [this.data.platform.name, Validators.required], + url: [this.data.platform.url, Validators.required] + }); + } public onCancel() { 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() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html index bcd3d0340..0e85cff2c 100644 --- a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html +++ b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html @@ -1,17 +1,30 @@ -
+

Update platform

Add platform

Name - +
Url - + @if (data.platform.url) {
- + diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.ts b/apps/client/src/app/components/admin-tag/admin-tag.component.ts index a8bb7720a..5b5d5aa84 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.ts +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.ts @@ -142,9 +142,7 @@ export class AdminTagComponent implements OnInit, OnDestroy { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data) => { - const tag: CreateTagDto = data?.tag; - + .subscribe((tag: CreateTagDto | null) => { if (tag) { this.adminService .postTag(tag) @@ -180,9 +178,7 @@ export class AdminTagComponent implements OnInit, OnDestroy { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data) => { - const tag: UpdateTagDto = data?.tag; - + .subscribe((tag: UpdateTagDto | null) => { if (tag) { this.adminService .putTag(tag) diff --git a/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts b/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts index aaa5a0221..f50974358 100644 --- a/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts +++ b/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts @@ -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 { Subject } from 'rxjs'; @@ -11,18 +21,52 @@ import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces'; styleUrls: ['./create-or-update-tag-dialog.scss'], templateUrl: 'create-or-update-tag-dialog.html' }) -export class CreateOrUpdateTagDialog { +export class CreateOrUpdateTagDialog implements OnDestroy { + public tagForm: FormGroup; + private unsubscribeSubject = new Subject(); public constructor( @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams, - public dialogRef: MatDialogRef - ) {} + public dialogRef: MatDialogRef, + private formBuilder: FormBuilder + ) { + this.tagForm = this.formBuilder.group({ + name: [this.data.tag.name] + }); + } public onCancel() { 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() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html b/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html index c2e8f4ee1..46ab1a39e 100644 --- a/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html +++ b/apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -1,21 +1,30 @@ - +

Update tag

Add tag

Name - +
- + 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 5e08635e0..d7a41f62f 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,5 +1,6 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { ChangeDetectionStrategy, @@ -40,22 +41,22 @@ export class CreateOrUpdateAccessDialog implements OnDestroy { alias: [this.data.access.alias], permissions: [this.data.access.permissions[0], 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) => { + const granteeUserIdControl = this.accessForm.get('granteeUserId'); const permissionsControl = this.accessForm.get('permissions'); - const userIdControl = this.accessForm.get('userId'); if (accessType === 'PRIVATE') { + granteeUserIdControl.setValidators(Validators.required); permissionsControl.setValidators(Validators.required); - userIdControl.setValidators(Validators.required); } else { - userIdControl.clearValidators(); + granteeUserIdControl.clearValidators(); } + granteeUserIdControl.updateValueAndValidity(); permissionsControl.updateValueAndValidity(); - userIdControl.updateValueAndValidity(); this.changeDetectorRef.markForCheck(); }); @@ -65,28 +66,38 @@ export class CreateOrUpdateAccessDialog implements OnDestroy { this.dialogRef.close(); } - public onSubmit() { + public async onSubmit() { const access: CreateAccessDto = { alias: this.accessForm.get('alias').value, - granteeUserId: this.accessForm.get('userId').value, + granteeUserId: this.accessForm.get('granteeUserId').value, permissions: [this.accessForm.get('permissions').value] }; - 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 }); + try { + await validateObjectForForm({ + classDto: CreateAccessDto, + form: this.accessForm, + object: 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() { 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 a6f20f2f4..951079717 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 @@ -45,7 +45,7 @@ Ghostfolio User ID { + dialogRef.afterClosed().subscribe((access: CreateAccessDto | null) => { if (access) { this.update(); } diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index e1b53acd3..e445863b4 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -189,9 +189,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data: any) => { - const account: UpdateAccountDto = data?.account; - + .subscribe((account: UpdateAccountDto | null) => { if (account) { this.dataService .putAccount(account) @@ -258,9 +256,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data: any) => { - const account: CreateAccountDto = data?.account; - + .subscribe((account: CreateAccountDto | null) => { if (account) { this.dataService .postAccount(account) 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 6c3624344..91e0769fc 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 @@ -123,6 +123,8 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { form: this.accountForm, object: account }); + + this.dialogRef.close(account as UpdateAccountDto); } else { delete (account as CreateAccountDto).id; @@ -131,9 +133,9 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { form: this.accountForm, object: account }); - } - this.dialogRef.close({ account }); + this.dialogRef.close(account as CreateAccountDto); + } } catch (error) { console.error(error); } diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 190fc673e..f6f0feded 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -287,9 +287,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data: any) => { - const transaction: UpdateOrderDto = data?.activity; - + .subscribe((transaction: UpdateOrderDto | null) => { if (transaction) { this.dataService .putOrder(transaction) @@ -338,9 +336,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((data: any) => { - const transaction: CreateOrderDto = data?.activity; - + .subscribe((transaction: CreateOrderDto | null) => { if (transaction) { this.dataService.postOrder(transaction).subscribe({ next: () => { 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 700e997e1..f49860028 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 @@ -475,6 +475,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ignoreFields: ['dataSource', 'date'], object: activity as UpdateOrderDto }); + + this.dialogRef.close(activity as UpdateOrderDto); } else { (activity as CreateOrderDto).updateAccountBalance = this.activityForm.get('updateAccountBalance').value; @@ -485,9 +487,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ignoreFields: ['dataSource', 'date'], object: activity }); - } - this.dialogRef.close({ activity }); + this.dialogRef.close(activity as CreateOrderDto); + } } catch (error) { console.error(error); } diff --git a/apps/client/src/app/util/form.util.ts b/apps/client/src/app/util/form.util.ts index d11490c7e..f629cc4a2 100644 --- a/apps/client/src/app/util/form.util.ts +++ b/apps/client/src/app/util/form.util.ts @@ -32,6 +32,14 @@ export async function validateObjectForForm({ validationError: Object.values(constraints)[0] }); } + + const formControlInCustomCurrency = form.get(`${property}InCustomCurrency`); + + if (formControlInCustomCurrency) { + formControlInCustomCurrency.setErrors({ + validationError: Object.values(constraints)[0] + }); + } } return Promise.reject(nonIgnoredErrors);