From 063a92fa541bd833bcaa040889cbe7a0245e70d9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:31:56 +0100 Subject: [PATCH] Bugfix/double submission of forms (#8307) * Fix double submission of forms * Update changelog --- CHANGELOG.md | 4 ++++ .../account-detail-dialog.component.ts | 15 ++++++++++++--- .../account-detail-dialog.html | 1 + .../create-asset-profile-dialog.component.ts | 11 +++++++++-- .../create-asset-profile-dialog.html | 6 ++++-- .../admin-overview/admin-overview.component.ts | 15 ++++++++++++--- .../admin-overview/admin-overview.html | 1 + .../create-or-update-access-dialog.component.ts | 16 ++++++++++++++-- .../create-or-update-access-dialog.html | 6 +++--- .../import-activities-dialog.component.ts | 6 +++++- .../import-activities-dialog.html | 2 +- .../account-balances.component.html | 2 +- .../account-balances.component.ts | 1 + .../historical-market-data-editor.component.html | 3 ++- .../historical-market-data-editor.component.ts | 11 +++++++++-- 15 files changed, 79 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d520269e0f..42d7fb1710 100644 --- a/CHANGELOG.md +++ b/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 +### 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 ### Changed diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index bc5818062d..ce31645986 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -34,7 +34,8 @@ import { Component, DestroyRef, inject, - OnInit + OnInit, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; @@ -57,7 +58,7 @@ import { } from 'ionicons/icons'; import { isNumber } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { filter, forkJoin } from 'rxjs'; +import { filter, finalize, forkJoin } from 'rxjs'; import { AccountDetailDialogParams, @@ -104,6 +105,7 @@ export class GfAccountDetailDialogComponent implements OnInit { protected interestInBaseCurrencyPrecision = 2; protected isLoading = true; protected isLoadingChart: boolean; + protected readonly isSubmitting = signal(false); protected name: string | null; protected netPerformancePercentageWithCurrencyEffect: number; protected netPerformancePercentageWithCurrencyEffectPrecision = 2; @@ -171,9 +173,16 @@ export class GfAccountDetailDialogComponent implements OnInit { } protected onAddAccountBalance(accountBalance: CreateAccountBalanceDto) { + this.isSubmitting.set(true); + this.dataService .postAccountBalance(accountBalance) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe( + finalize(() => { + this.isSubmitting.set(false); + }), + takeUntilDestroyed(this.destroyRef) + ) .subscribe(() => { this.initialize(); diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index 7d35884705..c5f82d294a 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -213,6 +213,7 @@ [accountCurrency]="currency" [accountId]="data.accountId" [currentBalance]="balance" + [isSubmitting]="isSubmitting()" [locale]="user?.settings?.locale" [showActions]=" hasPermissionToDeleteAccountBalance && diff --git a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts index 8b7ef793fc..e2a830b8ee 100644 --- a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts @@ -14,7 +14,8 @@ import { Component, DestroyRef, inject, - OnInit + OnInit, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { @@ -33,7 +34,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { isISO4217CurrencyCode } from 'class-validator'; -import { switchMap } from 'rxjs'; +import { finalize, switchMap } from 'rxjs'; import type { CreateAssetProfileDialogMode, @@ -60,6 +61,7 @@ import type { export class GfCreateAssetProfileDialogComponent implements OnInit { protected createAssetProfileForm: CreateAssetProfileForm; protected readonly ghostfolioPrefix = `${ghostfolioPrefix}_`; + protected readonly isSubmitting = signal(false); protected mode: CreateAssetProfileDialogMode; private customCurrencies: string[]; @@ -130,6 +132,8 @@ export class GfCreateAssetProfileDialogComponent implements OnInit { new Set([...this.customCurrencies, currency]) ).sort(); + this.isSubmitting.set(true); + this.dataService .putAdminSetting(PROPERTY_CURRENCIES, { value: JSON.stringify(currencies) @@ -141,6 +145,9 @@ export class GfCreateAssetProfileDialogComponent implements OnInit { symbol: `${DEFAULT_CURRENCY}${currency}` }); }), + finalize(() => { + this.isSubmitting.set(false); + }), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { diff --git a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html index 06fa59df07..80a1ff9dd5 100644 --- a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -1,7 +1,7 @@

Add Asset Profile

@@ -63,7 +63,9 @@ color="primary" mat-flat-button type="submit" - [disabled]="createAssetProfileForm.hasError('atLeastOneValid')" + [disabled]=" + createAssetProfileForm.hasError('atLeastOneValid') || isSubmitting() + " > Save diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index fd1b72fcaf..5824bda366 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -32,7 +32,8 @@ import { Component, DestroyRef, inject, - OnInit + OnInit, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -59,7 +60,7 @@ import { } from 'ionicons/icons'; import ms, { StringValue } from 'ms'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { catchError, of, switchMap } from 'rxjs'; +import { catchError, finalize, of, switchMap } from 'rxjs'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -102,6 +103,7 @@ export class GfAdminOverviewComponent implements OnInit { protected isDataGatheringEnabled: boolean; protected isLoading = false; protected isReadOnlyMode: boolean; + protected readonly isSavingCoupons = signal(false); protected isUserSignupEnabled: boolean; protected readonly permissions = permissions; protected systemMessage: SystemMessage; @@ -391,11 +393,18 @@ export class GfAdminOverviewComponent implements OnInit { coupons: Coupon[]; snackBarMessage?: string; }) { + this.isSavingCoupons.set(true); + this.dataService .putAdminSetting(PROPERTY_COUPONS, { value: JSON.stringify(coupons) }) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe( + finalize(() => { + this.isSavingCoupons.set(false); + }), + takeUntilDestroyed(this.destroyRef) + ) .subscribe(() => { this.couponsDataSource.data = coupons; diff --git a/apps/client/src/app/components/admin-overview/admin-overview.html b/apps/client/src/app/components/admin-overview/admin-overview.html index dc81c6309f..269fe291ac 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -286,6 +286,7 @@ class="h-auto rounded" color="primary" mat-flat-button + [disabled]="isSavingCoupons()" (click)="onAddCoupon()" > Add 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 17d671c9b2..11b82ac881 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 @@ -37,7 +37,8 @@ import { Component, DestroyRef, inject, - OnInit + OnInit, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { @@ -62,7 +63,7 @@ import { addDays, endOfDay, isValid, startOfDay } from 'date-fns'; import { StatusCodes } from 'http-status-codes'; import { addIcons } from 'ionicons'; import { calendarClearOutline } from 'ionicons/icons'; -import { EMPTY, catchError } from 'rxjs'; +import { EMPTY, catchError, finalize } from 'rxjs'; import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; @@ -95,6 +96,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { protected accessForm: FormGroup; protected readonly baseUrl = window.location.origin; + protected readonly isSubmitting = signal(false); protected minExpiresAt: Date; protected readonly mode: 'create' | 'update'; protected readonly today = startOfDay(new Date()); @@ -307,6 +309,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { object: access }); + this.isSubmitting.set(true); + this.dataService .postAccess(access) .pipe( @@ -319,6 +323,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { return EMPTY; }), + finalize(() => { + this.isSubmitting.set(false); + }), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { @@ -407,6 +414,8 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { object: access }); + this.isSubmitting.set(true); + this.dataService .putAccess(access) .pipe( @@ -419,6 +428,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { return EMPTY; }), + finalize(() => { + this.isSubmitting.set(false); + }), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { 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 6b078a8bd0..6489865cf2 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 @@ -1,7 +1,7 @@

@@ -159,9 +159,9 @@ mat-flat-button type="submit" [disabled]=" - mode === 'create' + (mode === 'create' ? !(accessForm.dirty && accessForm.valid) - : !accessForm.valid + : !accessForm.valid) || isSubmitting() " > Save diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 5f0ca17ed4..ecd184f20c 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -40,7 +40,8 @@ import { ChangeDetectorRef, Component, DestroyRef, - inject + inject, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { @@ -181,6 +182,7 @@ export class GfImportActivitiesDialogComponent { protected isLoading = false; protected isMaxActivitiesOfGhostfolioAiReached = false; + protected readonly isSubmitting = signal(false); protected readonly language = document.documentElement.lang; protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES; protected mode: 'DIVIDEND'; @@ -282,6 +284,8 @@ export class GfImportActivitiesDialogComponent { } protected async onImportActivities() { + this.isSubmitting.set(true); + try { this.snackBar.open('⏳ ' + $localize`Importing data...`); diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html index 4f46afc20a..e977be904f 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -190,7 +190,7 @@ class="ms-1" color="primary" mat-flat-button - [disabled]="!selectedActivities?.length" + [disabled]="!selectedActivities?.length || isSubmitting()" (click)="onImportActivities()" > Import diff --git a/libs/ui/src/lib/account-balances/account-balances.component.html b/libs/ui/src/lib/account-balances/account-balances.component.html index c00857a079..4884db4f43 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.html +++ b/libs/ui/src/lib/account-balances/account-balances.component.html @@ -93,7 +93,7 @@ color="primary" mat-flat-button type="submit" - [disabled]="accountBalanceForm.invalid" + [disabled]="accountBalanceForm.invalid || isSubmitting()" > Add diff --git a/libs/ui/src/lib/account-balances/account-balances.component.ts b/libs/ui/src/lib/account-balances/account-balances.component.ts index 715b4af0ec..4e72647b13 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.ts +++ b/libs/ui/src/lib/account-balances/account-balances.component.ts @@ -72,6 +72,7 @@ export class GfAccountBalancesComponent implements OnChanges { public readonly accountId = input.required(); public readonly currentBalance = input(); public readonly displayedColumns: string[] = ['date', 'value', 'actions']; + public readonly isSubmitting = input(false); public readonly locale = input(getLocale(), { transform: (value?: string) => { diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html index 8cffb62a9a..2581c120e0 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -62,7 +62,8 @@ !historicalDataForm.controls.historicalData.controls.csvString .touched || historicalDataForm.controls.historicalData.controls.csvString - .value === '' + .value === '' || + isSubmitting() " (click)="onImportHistoricalData()" > diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts index cefc96b8b2..03df8837c3 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts @@ -22,7 +22,8 @@ import { Input, OnChanges, OnInit, - Output + Output, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; @@ -46,7 +47,7 @@ import ms from 'ms'; import { DeviceDetectorService } from 'ngx-device-detector'; import { parse as csvToJson } from 'papaparse'; 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 { HistoricalMarketDataEditorDialogParams } from './historical-market-data-editor-dialog/interfaces/interfaces'; @@ -95,6 +96,7 @@ export class GfHistoricalMarketDataEditorComponent protected readonly defaultDateFormat = computed(() => getDateFormatString(this.locale()) ); + protected readonly isSubmitting = signal(false); private readonly destroyRef = inject(DestroyRef); private readonly deviceDetectorService = inject(DeviceDetectorService); @@ -258,6 +260,8 @@ export class GfHistoricalMarketDataEditorComponent } ).data; + this.isSubmitting.set(true); + this.dataService .postMarketData({ dataSource: this.dataSource, @@ -273,6 +277,9 @@ export class GfHistoricalMarketDataEditorComponent }); return EMPTY; }), + finalize(() => { + this.isSubmitting.set(false); + }), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => {