Browse Source

Bugfix/double submission of forms (#8307)

* Fix double submission of forms

* Update changelog
pull/1946/merge
Thomas Kaul 23 hours 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
### 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

15
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();

1
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 &&

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,
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(() => {

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

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

15
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;

1
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()"
>
<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,
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(() => {

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

2
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()"
>
<ng-container i18n>Import</ng-container>

2
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()"
>
<span i18n>Add</span>
</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 currentBalance = input<number | null>();
public readonly displayedColumns: string[] = ['date', 'value', 'actions'];
public readonly isSubmitting = input(false);
public readonly locale = input(getLocale(), {
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
.touched ||
historicalDataForm.controls.historicalData.controls.csvString
.value === ''
.value === '' ||
isSubmitting()
"
(click)="onImportHistoricalData()"
>

11
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(() => {

Loading…
Cancel
Save