Browse Source

Task/migrate transfer cash balance dialog to dedicated route (#7791)

* Migrate transfer cash balance dialog to dedicated route

* Update changelog
pull/7806/head
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
94c2c6cd06
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 84
      apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts
  3. 3
      apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts
  4. 86
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  5. 1
      apps/client/src/app/pages/accounts/accounts-page.html
  6. 9
      apps/client/src/app/pages/accounts/accounts-page.routes.ts
  7. 4
      apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts
  8. 5
      libs/common/src/lib/routes/routes.ts
  9. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  10. 8
      libs/ui/src/lib/accounts-table/accounts-table.component.ts

1
CHANGELOG.md

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental) - Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental)
- Migrated the transfer cash balance dialog to a dedicated route
- Improved the language localization for Italian (`it`) - Improved the language localization for Italian (`it`)
- Upgraded `prisma` from version `7.9.1` to `7.10.0` - Upgraded `prisma` from version `7.9.1` to `7.10.0`

84
apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts

@ -5,11 +5,17 @@ import {
} from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; import {
CreateAccountDto,
TransferBalanceDto,
UpdateAccountDto
} from '@ghostfolio/common/dtos';
import { AccountResponse, User } from '@ghostfolio/common/interfaces'; import { AccountResponse, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { Scope, hasScope, scopes } from '@ghostfolio/common/scopes'; import { Scope, hasScope, scopes } from '@ghostfolio/common/scopes';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { import {
@ -36,6 +42,8 @@ import {
import { GfCreateOrUpdateAccountDialogComponent } from '../create-or-update-account-dialog/create-or-update-account-dialog.component'; import { GfCreateOrUpdateAccountDialogComponent } from '../create-or-update-account-dialog/create-or-update-account-dialog.component';
import { CreateOrUpdateAccountDialogParams } from '../create-or-update-account-dialog/interfaces/interfaces'; import { CreateOrUpdateAccountDialogParams } from '../create-or-update-account-dialog/interfaces/interfaces';
import { TransferBalanceDialogParams } from '../transfer-balance/interfaces/interfaces';
import { GfTransferBalanceDialogComponent } from '../transfer-balance/transfer-balance-dialog.component';
import { AccountDialogMode } from './types/account-dialog-mode.type'; import { AccountDialogMode } from './types/account-dialog-mode.type';
@Component({ @Component({
@ -45,7 +53,9 @@ import { AccountDialogMode } from './types/account-dialog-mode.type';
}) })
export class GfAccountDialogHostComponent implements OnDestroy, OnInit { export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
private dialogRef: MatDialogRef< private dialogRef: MatDialogRef<
GfAccountDetailDialogComponent | GfCreateOrUpdateAccountDialogComponent | GfAccountDetailDialogComponent
| GfCreateOrUpdateAccountDialogComponent
| GfTransferBalanceDialogComponent
>; >;
private readonly deviceType = computed(() => { private readonly deviceType = computed(() => {
@ -61,6 +71,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
private readonly impersonationStorageService = inject( private readonly impersonationStorageService = inject(
ImpersonationStorageService ImpersonationStorageService
); );
private readonly notificationService = inject(NotificationService);
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
@ -108,6 +119,24 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
return; return;
} }
if (mode === 'transferCashBalance') {
const accounts = user?.accounts ?? [];
if (
accounts.length < 2 ||
!hasPermission(user?.permissions, permissions.updateAccount) ||
this.isWriteRestricted(user, scopes.accountUpdate)
) {
this.navigateBack();
return;
}
this.openTransferBalanceDialog({ accounts });
return;
}
if (mode === 'update') { if (mode === 'update') {
if ( if (
!account || !account ||
@ -298,4 +327,55 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
}); });
}); });
} }
private openTransferBalanceDialog({
accounts
}: {
accounts: AccountWithPlatform[];
}) {
const dialogRef = this.dialog.open<
GfTransferBalanceDialogComponent,
TransferBalanceDialogParams,
TransferBalanceDto | null
>(GfTransferBalanceDialogComponent, {
data: {
accounts
},
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
this.dialogRef = dialogRef;
dialogRef
.afterClosed()
.pipe(takeUntil(this.dialogClosed), takeUntilDestroyed(this.destroyRef))
.subscribe((result) => {
if (!result) {
this.navigateBack();
return;
}
this.dataService
.transferAccountBalance(result)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
error: () => {
this.notificationService.alert({
title: $localize`Oops, cash balance transfer has failed.`
});
this.navigateBack();
},
next: () => {
// Deliberately not bound to the destroy reference: navigating
// back destroys this component and the refreshed user is what
// makes the accounts page reload its data
this.userService.get(true).subscribe();
this.navigateBack();
}
});
});
}
} }

3
apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts

@ -1 +1,2 @@
export type AccountDialogMode = 'create' | 'detail' | 'update'; export type AccountDialogMode =
'create' | 'detail' | 'transferCashBalance' | 'update';

86
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -1,7 +1,6 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { TransferBalanceDto } from '@ghostfolio/common/dtos';
import { AccountsResponse, User } from '@ghostfolio/common/interfaces'; import { AccountsResponse, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
@ -9,27 +8,19 @@ import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { AccountWithValue } from '@ghostfolio/common/types'; import { AccountWithValue } from '@ghostfolio/common/types';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { GfFabComponent } from '@ghostfolio/ui/fab'; import { GfFabComponent } from '@ghostfolio/ui/fab';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
DestroyRef, DestroyRef,
inject, inject,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog'; import { Router, RouterModule } from '@angular/router';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { filter, switchMap, tap } from 'rxjs';
import { DeviceDetectorService } from 'ngx-device-detector';
import { filter, of, switchMap, tap } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { TransferBalanceDialogParams } from './transfer-balance/interfaces/interfaces';
import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -54,33 +45,15 @@ export class GfAccountsPageComponent implements OnInit {
private hasImpersonationId: boolean; private hasImpersonationId: boolean;
private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly dialog = inject(MatDialog);
private readonly impersonationStorageService = inject( private readonly impersonationStorageService = inject(
ImpersonationStorageService ImpersonationStorageService
); );
private readonly notificationService = inject(NotificationService);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
public constructor() {
this.route.queryParams
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => {
if (params['transferBalanceDialog']) {
this.openTransferBalanceDialog();
}
});
}
public ngOnInit() { public ngOnInit() {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
@ -135,67 +108,12 @@ export class GfAccountsPageComponent implements OnInit {
}); });
} }
protected onTransferBalance() {
this.router.navigate([], {
queryParams: { transferBalanceDialog: true }
});
}
private fetchAccounts() { private fetchAccounts() {
return this.dataService.fetchAccounts({ return this.dataService.fetchAccounts({
filters: this.userService.getFilters() filters: this.userService.getFilters()
}); });
} }
private openTransferBalanceDialog() {
const dialogRef = this.dialog.open<
GfTransferBalanceDialogComponent,
TransferBalanceDialogParams
>(GfTransferBalanceDialogComponent, {
data: {
accounts: this.user?.accounts
},
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data: any) => {
if (data) {
this.reset();
const { accountIdFrom, accountIdTo, balance }: TransferBalanceDto =
data?.account;
this.dataService
.transferAccountBalance({
accountIdFrom,
accountIdTo,
balance
})
.pipe(
catchError(() => {
this.notificationService.alert({
title: $localize`Oops, cash balance transfer has failed.`
});
return of(undefined);
}),
switchMap(() => this.fetchAccounts()),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((response) => {
this.updateAccounts(response);
});
this.changeDetectorRef.markForCheck();
}
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private reset() { private reset() {
this.accounts = undefined; this.accounts = undefined;
this.activitiesCount = 0; this.activitiesCount = 0;

1
apps/client/src/app/pages/accounts/accounts-page.html

@ -16,7 +16,6 @@
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
(accountDeleted)="onDeleteAccount($event)" (accountDeleted)="onDeleteAccount($event)"
(transferBalance)="onTransferBalance()"
/> />
</div> </div>
</div> </div>

9
apps/client/src/app/pages/accounts/accounts-page.routes.ts

@ -6,7 +6,8 @@ import { Routes } from '@angular/router';
import { GfAccountDialogHostComponent } from './account-dialog-host/account-dialog-host.component'; import { GfAccountDialogHostComponent } from './account-dialog-host/account-dialog-host.component';
import { GfAccountsPageComponent } from './accounts-page.component'; import { GfAccountsPageComponent } from './accounts-page.component';
const { create, detail, update } = internalRoutes.accounts.subRoutes; const { create, detail, transferCashBalance, update } =
internalRoutes.accounts.subRoutes;
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -18,6 +19,12 @@ export const routes: Routes = [
path: create.path, path: create.path,
title: create.title title: create.title
}, },
{
component: GfAccountDialogHostComponent,
data: { mode: 'transferCashBalance' },
path: transferCashBalance.path,
title: transferCashBalance.title
},
{ {
children: [ children: [
{ {

4
apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts

@ -99,13 +99,13 @@ export class GfTransferBalanceDialogComponent {
} }
protected onSubmit() { protected onSubmit() {
const account: TransferBalanceDto = { const transferBalance: TransferBalanceDto = {
accountIdFrom: this.transferBalanceForm.controls.fromAccount.value ?? '', accountIdFrom: this.transferBalanceForm.controls.fromAccount.value ?? '',
accountIdTo: this.transferBalanceForm.controls.toAccount.value ?? '', accountIdTo: this.transferBalanceForm.controls.toAccount.value ?? '',
balance: Number(this.transferBalanceForm.controls.balance.value) balance: Number(this.transferBalanceForm.controls.balance.value)
}; };
this.dialogRef.close({ account }); this.dialogRef.close(transferBalance);
} }
private getAccountById(aId: string | null) { private getAccountById(aId: string | null) {

5
libs/common/src/lib/routes/routes.ts

@ -93,6 +93,11 @@ export const internalRoutes = {
}, },
title: $localize`Account` title: $localize`Account`
}, },
transferCashBalance: {
path: 'transfer-cash-balance',
routerLink: ['/accounts', 'transfer-cash-balance'],
title: $localize`Transfer Cash Balance`
},
update: { update: {
path: 'update', path: 'update',
routerLink: (aAccountId: string) => { routerLink: (aAccountId: string) => {

2
libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -4,7 +4,7 @@
class="align-items-center d-flex" class="align-items-center d-flex"
mat-stroked-button mat-stroked-button
[disabled]="dataSource?.data.length < 2" [disabled]="dataSource?.data.length < 2"
(click)="onTransferBalance()" [routerLink]="transferCashBalanceRouterLink"
> >
<ion-icon class="mr-2" name="arrow-redo-outline" /> <ion-icon class="mr-2" name="arrow-redo-outline" />
<span><ng-container i18n>Transfer Cash Balance</ng-container>...</span> <span><ng-container i18n>Transfer Cash Balance</ng-container>...</span>

8
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -75,7 +75,6 @@ export class GfAccountsTableComponent {
public readonly totalValueInBaseCurrency = input<number>(); public readonly totalValueInBaseCurrency = input<number>();
public readonly accountDeleted = output<string>(); public readonly accountDeleted = output<string>();
public readonly transferBalance = output<void>();
public readonly sort = viewChild.required(MatSort); public readonly sort = viewChild.required(MatSort);
@ -141,6 +140,9 @@ export class GfAccountsTableComponent {
protected readonly isLoading = computed(() => !this.accounts()); protected readonly isLoading = computed(() => !this.accounts());
protected readonly transferCashBalanceRouterLink =
internalRoutes.accounts.subRoutes.transferCashBalance.routerLink;
private readonly notificationService = inject(NotificationService); private readonly notificationService = inject(NotificationService);
private readonly router = inject(Router); private readonly router = inject(Router);
@ -197,8 +199,4 @@ export class GfAccountsTableComponent {
title: aComment title: aComment
}); });
} }
protected onTransferBalance() {
this.transferBalance.emit();
}
} }

Loading…
Cancel
Save