Browse Source

feat: frontend form logic and error handling

pull/4469/head
tobikugel 4 weeks ago
committed by Thomas Kaul
parent
commit
1825a1f4b9
  1. 12
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  2. 74
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  3. 21
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  4. 3
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

12
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -389,9 +389,15 @@ export class AdminMarketDataComponent
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => { .subscribe(
this.router.navigate(['.'], { relativeTo: this.route }); (newAssetProfileIdentifer: AssetProfileIdentifier | undefined) => {
}); if (newAssetProfileIdentifer) {
this.onOpenAssetProfileDialog(newAssetProfileIdentifer);
} else {
this.router.navigate(['.'], { relativeTo: this.route });
}
}
);
}); });
} }

74
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -15,6 +15,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { HttpErrorResponse } from '@angular/common/http';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
@ -26,7 +27,13 @@ import {
ViewChild, ViewChild,
signal signal
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms'; import {
AbstractControl,
FormBuilder,
FormControl,
ValidationErrors,
Validators
} from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -63,12 +70,18 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
return { id: assetSubClass, label: translate(assetSubClass) }; return { id: assetSubClass, label: translate(assetSubClass) };
}); });
public assetProfile: AdminMarketDataDetails['assetProfile']; public assetProfile: AdminMarketDataDetails['assetProfile'];
public assetProfileIdentifierForm = this.formBuilder.group({ public assetProfileIdentifierForm = this.formBuilder.group(
editedSearchSymbol: new FormControl<AssetProfileIdentifier>( {
{ symbol: null, dataSource: null }, editedSearchSymbol: new FormControl<AssetProfileIdentifier>(
[Validators.required] { symbol: null, dataSource: null },
) [Validators.required]
}); )
},
{
validators: (control) => this.isNewSymbolValid(control)
}
);
public assetProfileForm = this.formBuilder.group({ public assetProfileForm = this.formBuilder.group({
assetClass: new FormControl<AssetClass>(undefined), assetClass: new FormControl<AssetClass>(undefined),
assetSubClass: new FormControl<AssetSubClass>(undefined), assetSubClass: new FormControl<AssetSubClass>(undefined),
@ -240,6 +253,21 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}); });
} }
private isNewSymbolValid(control: AbstractControl): ValidationErrors {
const currentAssetProfileIdentifier: AssetProfileIdentifier | undefined =
control.get('editedSearchSymbol').value;
console.log(this.assetProfileIdentifierForm?.valid);
if (
currentAssetProfileIdentifier.dataSource === this.data?.dataSource &&
currentAssetProfileIdentifier.symbol === this.data?.symbol
) {
return {
isPreviousIdentifier: true
};
}
}
public get isSymbolEditable() { public get isSymbolEditable() {
return !this.assetProfileForm.dirty; return !this.assetProfileForm.dirty;
} }
@ -337,22 +365,36 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
...assetProfileIdentifierData ...assetProfileIdentifierData
}) })
.pipe( .pipe(
catchError(() => { catchError((error: HttpErrorResponse) => {
this.snackBar.open('Conflict', undefined, { if (error.status === 409) {
duration: ms('3 seconds') this.snackBar.open(
}); $localize`This symbol is already in use`,
undefined,
{
duration: ms('3 seconds')
}
);
} else {
this.snackBar.open(
$localize`An error occurred while updating the symbol`,
undefined,
{
duration: ms('3 seconds')
}
);
}
return EMPTY; return EMPTY;
}), }),
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
.subscribe(() => { .subscribe(() => {
this.onOpenAssetProfileDialog({ const newAssetProfileIdentifer = {
dataSource: assetProfileIdentifierData.dataSource, dataSource: assetProfileIdentifierData.dataSource,
symbol: assetProfileIdentifierData.symbol symbol: assetProfileIdentifierData.symbol
}); };
this.dialogRef.close(); this.dialogRef.close(newAssetProfileIdentifer);
}); });
} }
@ -521,8 +563,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}); });
} }
public triggerIdentifierSubmit() { public triggerProfileFormSubmit() {
if (this.assetProfileIdentifierForm) { if (this.assetProfileForm) {
this.assetProfileFormElement.nativeElement.requestSubmit(); this.assetProfileFormElement.nativeElement.requestSubmit();
} }
} }

21
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -87,8 +87,9 @@
<div class="row"> <div class="row">
@if (isEditSymbolMode) { @if (isEditSymbolMode) {
<div class="col-12 mb-3"> <div class="col-12 mb-4">
<form <form
class="align-items-center d-flex"
[formGroup]="assetProfileIdentifierForm" [formGroup]="assetProfileIdentifierForm"
(keyup.enter)=" (keyup.enter)="
assetProfileIdentifierForm.valid && assetProfileIdentifierForm.valid &&
@ -96,7 +97,7 @@
" "
(ngSubmit)="onSubmitAssetProfileIdentifierForm()" (ngSubmit)="onSubmitAssetProfileIdentifierForm()"
> >
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" class="gf-spacer without-hint">
<mat-label i18n>Name, symbol or ISIN</mat-label> <mat-label i18n>Name, symbol or ISIN</mat-label>
<gf-symbol-autocomplete <gf-symbol-autocomplete
formControlName="editedSearchSymbol" formControlName="editedSearchSymbol"
@ -108,7 +109,12 @@
color="primary" color="primary"
mat-flat-button mat-flat-button
type="submit" type="submit"
[disabled]="!assetProfileIdentifierForm.valid" [disabled]="
assetProfileIdentifierForm.hasError(
'invalidData',
'editedSearchSymbol'
) || assetProfileIdentifierForm.hasError('isPreviousIdentifier')
"
> >
Apply Apply
</button> </button>
@ -128,8 +134,9 @@
>Symbol</gf-value >Symbol</gf-value
> >
</div> </div>
<div class="col-4 mb-3"> <div class="col-6 mb-3 d-flex justify-content-between">
<gf-value <gf-value
class="col-11"
i18n i18n
size="medium" size="medium"
[value]=" [value]="
@ -137,10 +144,8 @@
" "
>Data Source</gf-value >Data Source</gf-value
> >
</div>
<div class="col-1 mb-3">
<button <button
class="mx-1 no-min-width px-2" class="col-1 mx-1 no-min-width px-2"
mat-button mat-button
type="button" type="button"
[disabled]="!isSymbolEditable" [disabled]="!isSymbolEditable"
@ -507,7 +512,7 @@
color="primary" color="primary"
mat-flat-button mat-flat-button
[disabled]="!(assetProfileForm.dirty && assetProfileForm.valid)" [disabled]="!(assetProfileForm.dirty && assetProfileForm.valid)"
(click)="triggerIdentifierSubmit()" (click)="triggerProfileFormSubmit()"
> >
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>

3
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

@ -194,6 +194,9 @@ export class GfSymbolAutocompleteComponent
} }
private validateRequired() { private validateRequired() {
console.log(`dataSource: ${super.value?.dataSource}`);
console.log(`symbol: ${super.value?.symbol}`);
const requiredCheck = super.required const requiredCheck = super.required
? !super.value?.dataSource || !super.value?.symbol ? !super.value?.dataSource || !super.value?.symbol
: false; : false;

Loading…
Cancel
Save