Browse Source

Bugfix/double submission of forms (#8307)

* Fix double submission of forms

* Update changelog
pull/8321/head^2
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
063a92fa54
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 15
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  3. 1
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  4. 11
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts
  5. 6
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html
  6. 15
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  7. 1
      apps/client/src/app/components/admin-overview/admin-overview.html
  8. 16
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  9. 6
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  10. 6
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  11. 2
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  12. 2
      libs/ui/src/lib/account-balances/account-balances.component.html
  13. 1
      libs/ui/src/lib/account-balances/account-balances.component.ts
  14. 3
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html
  15. 11
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts

4
CHANGELOG.md

@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Deprecated the `api/v1/access` endpoints in favor of the `api/v1/accesses` endpoints - Deprecated the `api/v1/access` endpoints in favor of the `api/v1/accesses` endpoints
### Fixed
- Fixed the double submission of forms in the account detail dialog, the create or update access dialog, the historical market data editor, the import activities dialog, the coupon system and the create asset profile dialog of the admin control panel
## 3.93.0 - 2026-10-25 ## 3.93.0 - 2026-10-25
### Changed ### Changed

15
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -34,7 +34,8 @@ import {
Component, Component,
DestroyRef, DestroyRef,
inject, inject,
OnInit OnInit,
signal
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -57,7 +58,7 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
import { isNumber } from 'lodash-es'; import { isNumber } from 'lodash-es';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { filter, forkJoin } from 'rxjs'; import { filter, finalize, forkJoin } from 'rxjs';
import { import {
AccountDetailDialogParams, AccountDetailDialogParams,
@ -104,6 +105,7 @@ export class GfAccountDetailDialogComponent implements OnInit {
protected interestInBaseCurrencyPrecision = 2; protected interestInBaseCurrencyPrecision = 2;
protected isLoading = true; protected isLoading = true;
protected isLoadingChart: boolean; protected isLoadingChart: boolean;
protected readonly isSubmitting = signal(false);
protected name: string | null; protected name: string | null;
protected netPerformancePercentageWithCurrencyEffect: number; protected netPerformancePercentageWithCurrencyEffect: number;
protected netPerformancePercentageWithCurrencyEffectPrecision = 2; protected netPerformancePercentageWithCurrencyEffectPrecision = 2;
@ -171,9 +173,16 @@ export class GfAccountDetailDialogComponent implements OnInit {
} }
protected onAddAccountBalance(accountBalance: CreateAccountBalanceDto) { protected onAddAccountBalance(accountBalance: CreateAccountBalanceDto) {
this.isSubmitting.set(true);
this.dataService this.dataService
.postAccountBalance(accountBalance) .postAccountBalance(accountBalance)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(
finalize(() => {
this.isSubmitting.set(false);
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => { .subscribe(() => {
this.initialize(); this.initialize();

1
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -213,6 +213,7 @@
[accountCurrency]="currency" [accountCurrency]="currency"
[accountId]="data.accountId" [accountId]="data.accountId"
[currentBalance]="balance" [currentBalance]="balance"
[isSubmitting]="isSubmitting()"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]=" [showActions]="
hasPermissionToDeleteAccountBalance && hasPermissionToDeleteAccountBalance &&

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

@ -14,7 +14,8 @@ import {
Component, Component,
DestroyRef, DestroyRef,
inject, inject,
OnInit OnInit,
signal
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
@ -33,7 +34,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { isISO4217CurrencyCode } from 'class-validator'; import { isISO4217CurrencyCode } from 'class-validator';
import { switchMap } from 'rxjs'; import { finalize, switchMap } from 'rxjs';
import type { import type {
CreateAssetProfileDialogMode, CreateAssetProfileDialogMode,
@ -60,6 +61,7 @@ import type {
export class GfCreateAssetProfileDialogComponent implements OnInit { export class GfCreateAssetProfileDialogComponent implements OnInit {
protected createAssetProfileForm: CreateAssetProfileForm; protected createAssetProfileForm: CreateAssetProfileForm;
protected readonly ghostfolioPrefix = `${ghostfolioPrefix}_`; protected readonly ghostfolioPrefix = `${ghostfolioPrefix}_`;
protected readonly isSubmitting = signal(false);
protected mode: CreateAssetProfileDialogMode; protected mode: CreateAssetProfileDialogMode;
private customCurrencies: string[]; private customCurrencies: string[];
@ -130,6 +132,8 @@ export class GfCreateAssetProfileDialogComponent implements OnInit {
new Set([...this.customCurrencies, currency]) new Set([...this.customCurrencies, currency])
).sort(); ).sort();
this.isSubmitting.set(true);
this.dataService this.dataService
.putAdminSetting(PROPERTY_CURRENCIES, { .putAdminSetting(PROPERTY_CURRENCIES, {
value: JSON.stringify(currencies) value: JSON.stringify(currencies)
@ -141,6 +145,9 @@ export class GfCreateAssetProfileDialogComponent implements OnInit {
symbol: `${DEFAULT_CURRENCY}${currency}` symbol: `${DEFAULT_CURRENCY}${currency}`
}); });
}), }),
finalize(() => {
this.isSubmitting.set(false);
}),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {

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

@ -1,7 +1,7 @@
<form <form
class="d-flex flex-column h-100" class="d-flex flex-column h-100"
[formGroup]="createAssetProfileForm" [formGroup]="createAssetProfileForm"
(keyup.enter)="createAssetProfileForm.valid && onSubmit()" (keyup.enter)="createAssetProfileForm.valid && !isSubmitting() && onSubmit()"
(ngSubmit)="onSubmit()" (ngSubmit)="onSubmit()"
> >
<h1 i18n mat-dialog-title>Add Asset Profile</h1> <h1 i18n mat-dialog-title>Add Asset Profile</h1>
@ -63,7 +63,9 @@
color="primary" color="primary"
mat-flat-button mat-flat-button
type="submit" type="submit"
[disabled]="createAssetProfileForm.hasError('atLeastOneValid')" [disabled]="
createAssetProfileForm.hasError('atLeastOneValid') || isSubmitting()
"
> >
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>

15
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -32,7 +32,8 @@ import {
Component, Component,
DestroyRef, DestroyRef,
inject, inject,
OnInit OnInit,
signal
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -59,7 +60,7 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
import ms, { StringValue } from 'ms'; import ms, { StringValue } from 'ms';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { catchError, of, switchMap } from 'rxjs'; import { catchError, finalize, of, switchMap } from 'rxjs';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -102,6 +103,7 @@ export class GfAdminOverviewComponent implements OnInit {
protected isDataGatheringEnabled: boolean; protected isDataGatheringEnabled: boolean;
protected isLoading = false; protected isLoading = false;
protected isReadOnlyMode: boolean; protected isReadOnlyMode: boolean;
protected readonly isSavingCoupons = signal(false);
protected isUserSignupEnabled: boolean; protected isUserSignupEnabled: boolean;
protected readonly permissions = permissions; protected readonly permissions = permissions;
protected systemMessage: SystemMessage; protected systemMessage: SystemMessage;
@ -391,11 +393,18 @@ export class GfAdminOverviewComponent implements OnInit {
coupons: Coupon[]; coupons: Coupon[];
snackBarMessage?: string; snackBarMessage?: string;
}) { }) {
this.isSavingCoupons.set(true);
this.dataService this.dataService
.putAdminSetting(PROPERTY_COUPONS, { .putAdminSetting(PROPERTY_COUPONS, {
value: JSON.stringify(coupons) value: JSON.stringify(coupons)
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(
finalize(() => {
this.isSavingCoupons.set(false);
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => { .subscribe(() => {
this.couponsDataSource.data = coupons; this.couponsDataSource.data = coupons;

1
apps/client/src/app/components/admin-overview/admin-overview.html

@ -286,6 +286,7 @@
class="h-auto rounded" class="h-auto rounded"
color="primary" color="primary"
mat-flat-button mat-flat-button
[disabled]="isSavingCoupons()"
(click)="onAddCoupon()" (click)="onAddCoupon()"
> >
<ng-container i18n>Add</ng-container> <ng-container i18n>Add</ng-container>

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

@ -37,7 +37,8 @@ import {
Component, Component,
DestroyRef, DestroyRef,
inject, inject,
OnInit OnInit,
signal
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
@ -62,7 +63,7 @@ import { addDays, endOfDay, isValid, startOfDay } from 'date-fns';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { calendarClearOutline } from 'ionicons/icons'; import { calendarClearOutline } from 'ionicons/icons';
import { EMPTY, catchError } from 'rxjs'; import { EMPTY, catchError, finalize } from 'rxjs';
import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
@ -95,6 +96,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
protected accessForm: FormGroup; protected accessForm: FormGroup;
protected readonly baseUrl = window.location.origin; protected readonly baseUrl = window.location.origin;
protected readonly isSubmitting = signal(false);
protected minExpiresAt: Date; protected minExpiresAt: Date;
protected readonly mode: 'create' | 'update'; protected readonly mode: 'create' | 'update';
protected readonly today = startOfDay(new Date()); protected readonly today = startOfDay(new Date());
@ -307,6 +309,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
object: access object: access
}); });
this.isSubmitting.set(true);
this.dataService this.dataService
.postAccess(access) .postAccess(access)
.pipe( .pipe(
@ -319,6 +323,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
return EMPTY; return EMPTY;
}), }),
finalize(() => {
this.isSubmitting.set(false);
}),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {
@ -407,6 +414,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
object: access object: access
}); });
this.isSubmitting.set(true);
this.dataService this.dataService
.putAccess(access) .putAccess(access)
.pipe( .pipe(
@ -419,6 +428,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
return EMPTY; return EMPTY;
}), }),
finalize(() => {
this.isSubmitting.set(false);
}),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {

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

@ -1,7 +1,7 @@
<form <form
class="d-flex flex-column h-100" class="d-flex flex-column h-100"
[formGroup]="accessForm" [formGroup]="accessForm"
(keyup.enter)="accessForm.valid && onSubmit()" (keyup.enter)="accessForm.valid && !isSubmitting() && onSubmit()"
(ngSubmit)="onSubmit()" (ngSubmit)="onSubmit()"
> >
<h1 mat-dialog-title> <h1 mat-dialog-title>
@ -159,9 +159,9 @@
mat-flat-button mat-flat-button
type="submit" type="submit"
[disabled]=" [disabled]="
mode === 'create' (mode === 'create'
? !(accessForm.dirty && accessForm.valid) ? !(accessForm.dirty && accessForm.valid)
: !accessForm.valid : !accessForm.valid) || isSubmitting()
" "
> >
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>

6
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -40,7 +40,8 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
inject inject,
signal
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
@ -181,6 +182,7 @@ export class GfImportActivitiesDialogComponent {
protected isLoading = false; protected isLoading = false;
protected isMaxActivitiesOfGhostfolioAiReached = false; protected isMaxActivitiesOfGhostfolioAiReached = false;
protected readonly isSubmitting = signal(false);
protected readonly language = document.documentElement.lang; protected readonly language = document.documentElement.lang;
protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES; protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES;
protected mode: 'DIVIDEND'; protected mode: 'DIVIDEND';
@ -282,6 +284,8 @@ export class GfImportActivitiesDialogComponent {
} }
protected async onImportActivities() { protected async onImportActivities() {
this.isSubmitting.set(true);
try { try {
this.snackBar.open('⏳ ' + $localize`Importing data...`); this.snackBar.open('⏳ ' + $localize`Importing data...`);

2
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -190,7 +190,7 @@
class="ms-1" class="ms-1"
color="primary" color="primary"
mat-flat-button mat-flat-button
[disabled]="!selectedActivities?.length" [disabled]="!selectedActivities?.length || isSubmitting()"
(click)="onImportActivities()" (click)="onImportActivities()"
> >
<ng-container i18n>Import</ng-container> <ng-container i18n>Import</ng-container>

2
libs/ui/src/lib/account-balances/account-balances.component.html

@ -93,7 +93,7 @@
color="primary" color="primary"
mat-flat-button mat-flat-button
type="submit" type="submit"
[disabled]="accountBalanceForm.invalid" [disabled]="accountBalanceForm.invalid || isSubmitting()"
> >
<span i18n>Add</span> <span i18n>Add</span>
</button> </button>

1
libs/ui/src/lib/account-balances/account-balances.component.ts

@ -72,6 +72,7 @@ export class GfAccountBalancesComponent implements OnChanges {
public readonly accountId = input.required<string>(); public readonly accountId = input.required<string>();
public readonly currentBalance = input<number | null>(); public readonly currentBalance = input<number | null>();
public readonly displayedColumns: string[] = ['date', 'value', 'actions']; public readonly displayedColumns: string[] = ['date', 'value', 'actions'];
public readonly isSubmitting = input(false);
public readonly locale = input(getLocale(), { public readonly locale = input(getLocale(), {
transform: (value?: string) => { transform: (value?: string) => {

3
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html

@ -62,7 +62,8 @@
!historicalDataForm.controls.historicalData.controls.csvString !historicalDataForm.controls.historicalData.controls.csvString
.touched || .touched ||
historicalDataForm.controls.historicalData.controls.csvString historicalDataForm.controls.historicalData.controls.csvString
.value === '' .value === '' ||
isSubmitting()
" "
(click)="onImportHistoricalData()" (click)="onImportHistoricalData()"
> >

11
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts

@ -22,7 +22,8 @@ import {
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
Output Output,
signal
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
@ -46,7 +47,7 @@ import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { parse as csvToJson } from 'papaparse'; import { parse as csvToJson } from 'papaparse';
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError, finalize } from 'rxjs/operators';
import { GfHistoricalMarketDataEditorDialogComponent } from './historical-market-data-editor-dialog/historical-market-data-editor-dialog.component'; import { GfHistoricalMarketDataEditorDialogComponent } from './historical-market-data-editor-dialog/historical-market-data-editor-dialog.component';
import { HistoricalMarketDataEditorDialogParams } from './historical-market-data-editor-dialog/interfaces/interfaces'; import { HistoricalMarketDataEditorDialogParams } from './historical-market-data-editor-dialog/interfaces/interfaces';
@ -95,6 +96,7 @@ export class GfHistoricalMarketDataEditorComponent
protected readonly defaultDateFormat = computed(() => protected readonly defaultDateFormat = computed(() =>
getDateFormatString(this.locale()) getDateFormatString(this.locale())
); );
protected readonly isSubmitting = signal(false);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly deviceDetectorService = inject(DeviceDetectorService);
@ -258,6 +260,8 @@ export class GfHistoricalMarketDataEditorComponent
} }
).data; ).data;
this.isSubmitting.set(true);
this.dataService this.dataService
.postMarketData({ .postMarketData({
dataSource: this.dataSource, dataSource: this.dataSource,
@ -273,6 +277,9 @@ export class GfHistoricalMarketDataEditorComponent
}); });
return EMPTY; return EMPTY;
}), }),
finalize(() => {
this.isSubmitting.set(false);
}),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {

Loading…
Cancel
Save