Browse Source

Task/migrate account dialogs to dedicated routes (#7473)

* Migrate account dialogs to dedicated routes

* Update changelog
pull/7572/head^2
Thomas Kaul 10 hours ago
committed by GitHub
parent
commit
6b64563391
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 11
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  3. 296
      apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts
  4. 1
      apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts
  5. 199
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  6. 7
      apps/client/src/app/pages/accounts/accounts-page.html
  7. 28
      apps/client/src/app/pages/accounts/accounts-page.routes.ts
  8. 21
      libs/common/src/lib/routes/routes.ts
  9. 24
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  10. 26
      libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts
  11. 30
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  12. 8
      libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts
  13. 3
      libs/ui/src/lib/assistant/assistant.component.ts

1
CHANGELOG.md

@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the check for duplicates in the preview step of the import dividends dialog (regardless of the account) - Improved the check for duplicates in the preview step of the import dividends dialog (regardless of the account)
- Extended the activities import to reuse an existing account of the user by name and currency - Extended the activities import to reuse an existing account of the user by name and currency
- Extended the activities import to resolve an ISIN to the symbol of the data provider - Extended the activities import to resolve an ISIN to the symbol of the data provider
- Migrated the create, detail and edit account dialogs to dedicated routes
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
### Fixed ### Fixed

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

@ -176,6 +176,8 @@ export class GfAccountDetailDialogComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.initialize(); this.initialize();
this.refreshUser();
}); });
} }
@ -195,6 +197,8 @@ export class GfAccountDetailDialogComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.initialize(); this.initialize();
this.refreshUser();
}); });
} }
@ -413,4 +417,11 @@ export class GfAccountDetailDialogComponent implements OnInit {
this.fetchChart(); this.fetchChart();
this.fetchPortfolioHoldings(); this.fetchPortfolioHoldings();
} }
private refreshUser() {
this.userService
.get(true)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe();
}
} }

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

@ -0,0 +1,296 @@
import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import {
AccountDetailDialogParams,
AccountDetailDialogResult
} from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos';
import { AccountResponse, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { DataService } from '@ghostfolio/ui/services';
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
OnDestroy,
OnInit,
inject
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Observable, of, Subject } from 'rxjs';
import {
distinctUntilChanged,
map,
switchMap,
takeUntil,
tap
} from 'rxjs/operators';
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 { AccountDialogMode } from './types/account-dialog-mode.type';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'gf-account-dialog-host',
template: ''
})
export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
private dialogRef: MatDialogRef<
GfAccountDetailDialogComponent | GfCreateOrUpdateAccountDialogComponent
>;
private readonly deviceType = computed(() => {
return this.deviceDetectorService.deviceInfo().deviceType;
});
private readonly dialogClosed = new Subject<void>();
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly dialog = inject(MatDialog);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly userService = inject(UserService);
public ngOnInit() {
const mode = this.route.snapshot.data.mode as AccountDialogMode;
// The router reuses this component when only the account id changes, so
// the parameters are observed instead of read from the snapshot once
this.route.paramMap
.pipe(
map((paramMap) => {
return paramMap.get('accountId');
}),
distinctUntilChanged(),
tap(() => {
this.closeDialog();
}),
switchMap((accountId) => {
const account$: Observable<AccountResponse | undefined> =
mode === 'update' && accountId
? this.dataService.fetchAccount(accountId)
: of(undefined);
return this.userService.get().pipe(
switchMap((user) => {
return account$.pipe(
map((account) => {
return { account, accountId, user };
})
);
})
);
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe({
error: () => {
this.navigateBack();
},
next: ({ account, accountId, user }) => {
if (mode === 'detail') {
this.openAccountDetailDialog({ accountId, user });
return;
}
if (mode === 'update') {
if (
!account ||
!hasPermission(user?.permissions, permissions.updateAccount) ||
this.isReadOnlyMode(user)
) {
this.navigateBack();
return;
}
const { balance, comment, currency, id, name, platformId, tags } =
account;
this.openCreateOrUpdateAccountDialog({
user,
account: {
balance,
comment,
currency,
id,
name,
platformId,
tags
},
isUpdate: true
});
return;
}
if (
!hasPermission(user?.permissions, permissions.createAccount) ||
this.isReadOnlyMode(user)
) {
this.navigateBack();
return;
}
this.openCreateOrUpdateAccountDialog({
user,
account: {
balance: 0,
comment: null,
currency: user?.settings?.baseCurrency ?? null,
id: null,
name: null,
platformId: null,
tags: []
},
isUpdate: false
});
}
});
}
public ngOnDestroy() {
// The dialog lives in an overlay outside of this component, so it needs to
// be closed explicitly when leaving the route (for example via the browser
// navigation)
this.dialogRef?.close();
this.dialogClosed.complete();
}
private closeDialog() {
// Tear down the subscription of the dialog which is about to be replaced,
// so that its result is not mistaken for the user closing it
this.dialogClosed.next();
this.dialogRef?.close();
}
private isReadOnlyMode(user: User) {
return (
!!this.impersonationStorageService.getId() ||
!!user?.settings?.isRestrictedView
);
}
private navigateBack() {
void this.router.navigate(internalRoutes.accounts.routerLink);
}
private openAccountDetailDialog({
accountId,
user
}: {
accountId: string | null;
user: User;
}) {
if (!accountId) {
this.navigateBack();
return;
}
const impersonationId = this.impersonationStorageService.getId();
const dialogRef = this.dialog.open<
GfAccountDetailDialogComponent,
AccountDetailDialogParams,
AccountDetailDialogResult
>(GfAccountDetailDialogComponent, {
autoFocus: false,
data: {
accountId,
impersonationId,
deviceType: this.deviceType(),
hasPermissionToCreateActivity:
!impersonationId &&
hasPermission(user?.permissions, permissions.createActivity) &&
!user?.settings?.isRestrictedView
},
height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
this.dialogRef = dialogRef;
dialogRef
.afterClosed()
.pipe(takeUntil(this.dialogClosed), takeUntilDestroyed(this.destroyRef))
.subscribe((result) => {
if (result?.isNavigating) {
return;
}
this.navigateBack();
});
}
private openCreateOrUpdateAccountDialog({
account,
isUpdate,
user
}: {
account: CreateOrUpdateAccountDialogParams['account'];
isUpdate: boolean;
user: User;
}) {
const dialogRef = this.dialog.open<
GfCreateOrUpdateAccountDialogComponent,
CreateOrUpdateAccountDialogParams,
CreateAccountDto | UpdateAccountDto | null
>(GfCreateOrUpdateAccountDialogComponent, {
data: {
account,
user
},
height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
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;
}
const request$: Observable<unknown> = isUpdate
? this.dataService.putAccount(result as UpdateAccountDto)
: this.dataService.postAccount(result as CreateAccountDto);
request$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
error: () => {
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();
}
});
});
}
}

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

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

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

@ -1,17 +1,9 @@
import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import {
AccountDetailDialogParams,
AccountDetailDialogResult
} 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 { import { TransferBalanceDto } from '@ghostfolio/common/dtos';
CreateAccountDto,
TransferBalanceDto,
UpdateAccountDto
} from '@ghostfolio/common/dtos';
import { User } from '@ghostfolio/common/interfaces'; import { 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 { 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';
@ -30,13 +22,10 @@ import {
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Tag } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
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 { TransferBalanceDialogParams } from './transfer-balance/interfaces/interfaces'; import { TransferBalanceDialogParams } from './transfer-balance/interfaces/interfaces';
import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component'; import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component';
@ -54,10 +43,13 @@ export class GfAccountsPageComponent implements OnInit {
protected hasPermissionToCreateAccount: boolean; protected hasPermissionToCreateAccount: boolean;
protected hasPermissionToUpdateAccount: boolean; protected hasPermissionToUpdateAccount: boolean;
protected impersonationId: string | null; protected impersonationId: string | null;
protected readonly internalRoutes = internalRoutes;
protected totalBalanceInBaseCurrency = 0; protected totalBalanceInBaseCurrency = 0;
protected totalValueInBaseCurrency = 0; protected totalValueInBaseCurrency = 0;
protected user: User; protected user: User;
private isInitialFetch = true;
private readonly deviceType = computed( private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType () => this.deviceDetectorService.deviceInfo().deviceType
); );
@ -79,26 +71,7 @@ export class GfAccountsPageComponent implements OnInit {
this.route.queryParams this.route.queryParams
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
if (params['accountId'] && params['accountDetailDialog']) { if (params['transferBalanceDialog']) {
this.openAccountDetailDialog(params['accountId']);
} else if (
params['createDialog'] &&
this.hasPermissionToCreateAccount
) {
this.openCreateAccountDialog();
} else if (params['editDialog']) {
if (this.accounts) {
const account = this.accounts.find(({ id }) => {
return id === params['accountId'];
});
if (account) {
this.openUpdateAccountDialog(account);
}
} else {
this.router.navigate(['.'], { relativeTo: this.route });
}
} else if (params['transferBalanceDialog']) {
this.openTransferBalanceDialog(); this.openTransferBalanceDialog();
} }
}); });
@ -130,12 +103,12 @@ export class GfAccountsPageComponent implements OnInit {
this.user.permissions, this.user.permissions,
permissions.updateAccount permissions.updateAccount
); );
this.fetchAccounts();
} }
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.fetchAccounts();
} }
protected onDeleteAccount(aId: string) { protected onDeleteAccount(aId: string) {
@ -149,8 +122,6 @@ export class GfAccountsPageComponent implements OnInit {
.get(true) .get(true)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchAccounts();
}); });
} }
@ -160,12 +131,6 @@ export class GfAccountsPageComponent implements OnInit {
}); });
} }
protected onUpdateAccount(aAccount: AccountWithValue) {
this.router.navigate([], {
queryParams: { accountId: aAccount.id, editDialog: true }
});
}
private fetchAccounts() { private fetchAccounts() {
this.dataService this.dataService
.fetchAccounts() .fetchAccounts()
@ -182,149 +147,21 @@ export class GfAccountsPageComponent implements OnInit {
this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency; this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency;
this.totalValueInBaseCurrency = totalValueInBaseCurrency; this.totalValueInBaseCurrency = totalValueInBaseCurrency;
if (this.accounts?.length <= 0) { if (
this.router.navigate([], { queryParams: { createDialog: true } }); this.accounts?.length <= 0 &&
this.hasPermissionToCreateAccount &&
this.isInitialFetch
) {
void this.router.navigate(
internalRoutes.accounts.subRoutes.create.routerLink
);
} }
this.changeDetectorRef.markForCheck(); this.isInitialFetch = false;
}
);
}
private openUpdateAccountDialog({
balance,
comment,
currency,
id,
name,
platformId,
tags
}: AccountWithValue & { tags?: Tag[] }) {
const dialogRef = this.dialog.open<
GfCreateOrUpdateAccountDialogComponent,
CreateOrUpdateAccountDialogParams
>(GfCreateOrUpdateAccountDialogComponent, {
data: {
account: {
balance,
comment,
currency,
id,
name,
platformId,
tags
},
user: this.user
},
height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((account: UpdateAccountDto | null) => {
if (account) {
this.reset();
this.dataService
.putAccount(account)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe();
this.fetchAccounts();
});
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
);
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open<
GfAccountDetailDialogComponent,
AccountDetailDialogParams,
AccountDetailDialogResult
>(GfAccountDetailDialogComponent, {
autoFocus: false,
data: {
accountId: aAccountId,
deviceType: this.deviceType(),
hasPermissionToCreateActivity:
!this.hasImpersonationId &&
hasPermission(this.user?.permissions, permissions.createActivity) &&
!this.user?.settings?.isRestrictedView,
impersonationId: this.impersonationId
},
height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((result) => {
if (result?.isNavigating) {
return;
}
this.fetchAccounts();
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private openCreateAccountDialog() {
const dialogRef = this.dialog.open<
GfCreateOrUpdateAccountDialogComponent,
CreateOrUpdateAccountDialogParams
>(GfCreateOrUpdateAccountDialogComponent, {
data: {
account: {
balance: 0,
comment: null,
currency: this.user?.settings?.baseCurrency ?? null,
id: null,
name: null,
platformId: null,
tags: []
},
user: this.user
} satisfies CreateOrUpdateAccountDialogParams,
height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((account: CreateAccountDto | null) => {
if (account) {
this.reset();
this.dataService
.postAccount(account)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe();
this.fetchAccounts();
});
this.changeDetectorRef.markForCheck();
}
this.router.navigate(['.'], { relativeTo: this.route });
});
} }
private openTransferBalanceDialog() { private openTransferBalanceDialog() {

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

@ -15,7 +15,6 @@
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
(accountDeleted)="onDeleteAccount($event)" (accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)"
(transferBalance)="onTransferBalance()" (transferBalance)="onTransferBalance()"
/> />
</div> </div>
@ -26,6 +25,10 @@
hasPermissionToCreateAccount && hasPermissionToCreateAccount &&
!user.settings.isRestrictedView !user.settings.isRestrictedView
) { ) {
<gf-fab [queryParams]="{ createDialog: true }" /> <gf-fab
[routerLink]="internalRoutes.accounts.subRoutes.create.routerLink"
/>
} }
</div> </div>
<router-outlet />

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

@ -3,11 +3,39 @@ import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
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;
export const routes: Routes = [ export const routes: Routes = [
{ {
canActivate: [AuthGuard], canActivate: [AuthGuard],
children: [
{
component: GfAccountDialogHostComponent,
data: { mode: 'create' },
path: create.path,
title: create.title
},
{
children: [
{
component: GfAccountDialogHostComponent,
data: { mode: 'detail' },
path: '',
title: detail.title
},
{
component: GfAccountDialogHostComponent,
data: { mode: 'update' },
path: update.path,
title: update.title
}
],
path: ':accountId'
}
],
component: GfAccountsPageComponent, component: GfAccountsPageComponent,
path: '', path: '',
title: internalRoutes.accounts.title title: internalRoutes.accounts.title

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

@ -66,6 +66,27 @@ export const internalRoutes = {
accounts: { accounts: {
path: 'accounts', path: 'accounts',
routerLink: ['/accounts'], routerLink: ['/accounts'],
subRoutes: {
create: {
path: 'create',
routerLink: ['/accounts', 'create'],
title: $localize`Add Account`
},
detail: {
path: undefined, // Default sub route
routerLink: (aAccountId: string) => {
return ['/accounts', aAccountId];
},
title: $localize`Account`
},
update: {
path: 'update',
routerLink: (aAccountId: string) => {
return ['/accounts', aAccountId, 'update'];
},
title: $localize`Update Account`
}
},
title: $localize`Accounts` title: $localize`Accounts`
}, },
api: { api: {

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

@ -307,18 +307,26 @@
<ion-icon name="ellipsis-horizontal" /> <ion-icon name="ellipsis-horizontal" />
</button> </button>
<mat-menu #accountMenu="matMenu" xPosition="before"> <mat-menu #accountMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onOpenAccountDetailDialog(element.id)"> @if (hasPermissionToOpenDetails()) {
<span class="align-items-center d-flex"> <a
<ion-icon class="mr-2" name="wallet-outline" /> mat-menu-item
<span><ng-container i18n>View Details</ng-container>...</span> [routerLink]="accountDialogRouterLinks().get(element.id)?.detail"
</span> >
</button> <span class="align-items-center d-flex">
<button mat-menu-item (click)="onUpdateAccount(element)"> <ion-icon class="mr-2" name="wallet-outline" />
<span><ng-container i18n>View Details</ng-container>...</span>
</span>
</a>
}
<a
mat-menu-item
[routerLink]="accountDialogRouterLinks().get(element.id)?.update"
>
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" /> <ion-icon class="mr-2" name="create-outline" />
<span><ng-container i18n>Edit</ng-container>...</span> <span><ng-container i18n>Edit</ng-container>...</span>
</span> </span>
</button> </a>
<hr class="m-0" /> <hr class="m-0" />
<button <button
mat-menu-item mat-menu-item

26
libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts

@ -5,9 +5,9 @@ import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { provideRouter, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { moduleMetadata } from '@storybook/angular'; import { applicationConfig, moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -92,6 +92,9 @@ export default {
title: 'Accounts Table', title: 'Accounts Table',
component: GfAccountsTableComponent, component: GfAccountsTableComponent,
decorators: [ decorators: [
applicationConfig({
providers: [provideRouter([])]
}),
moduleMetadata({ moduleMetadata({
imports: [ imports: [
CommonModule, CommonModule,
@ -147,6 +150,25 @@ export const Default: Story = {
} }
}; };
export const Actions: Story = {
args: {
accounts,
activitiesCount: 12,
baseCurrency: 'USD',
hasPermissionToOpenDetails: true,
locale: 'en-US',
showActions: true,
showActivitiesCount: true,
showAllocationInPercentage: false,
showBalance: true,
showFooter: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 12428.2,
totalValueInBaseCurrency: 107971.70321466809
}
};
export const WithoutFooter: Story = { export const WithoutFooter: Story = {
args: { args: {
accounts, accounts,

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

@ -4,6 +4,7 @@ import {
getLowercase, getLowercase,
isAccountExcluded isAccountExcluded
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { AccountWithValue } from '@ghostfolio/common/types'; import { AccountWithValue } from '@ghostfolio/common/types';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
@ -71,11 +72,28 @@ 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 accountToUpdate = output<AccountWithValue>();
public readonly transferBalance = output<void>(); public readonly transferBalance = output<void>();
public readonly sort = viewChild.required(MatSort); public readonly sort = viewChild.required(MatSort);
protected readonly accountDialogRouterLinks = computed(() => {
const { detail, update } = internalRoutes.accounts.subRoutes;
const routerLinks = new Map<
string,
{ detail: string[]; update: string[] }
>();
for (const { id } of this.accounts() ?? []) {
routerLinks.set(id, {
detail: detail.routerLink(id),
update: update.routerLink(id)
});
}
return routerLinks;
});
protected readonly dataSource = new MatTableDataSource<AccountWithValue>([]); protected readonly dataSource = new MatTableDataSource<AccountWithValue>([]);
protected readonly displayedColumns = computed(() => { protected readonly displayedColumns = computed(() => {
@ -159,9 +177,9 @@ export class GfAccountsTableComponent {
protected onOpenAccountDetailDialog(accountId: string) { protected onOpenAccountDetailDialog(accountId: string) {
if (this.hasPermissionToOpenDetails()) { if (this.hasPermissionToOpenDetails()) {
this.router.navigate([], { void this.router.navigate(
queryParams: { accountId, accountDetailDialog: true } internalRoutes.accounts.subRoutes.detail.routerLink(accountId)
}); );
} }
} }
@ -174,8 +192,4 @@ export class GfAccountsTableComponent {
protected onTransferBalance() { protected onTransferBalance() {
this.transferBalance.emit(); this.transferBalance.emit();
} }
protected onUpdateAccount(aAccount: AccountWithValue) {
this.accountToUpdate.emit(aAccount);
}
} }

8
libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts

@ -52,12 +52,8 @@ export class GfAssistantListItemComponent
public ngOnChanges() { public ngOnChanges() {
if (this.item?.mode === SearchMode.ACCOUNT) { if (this.item?.mode === SearchMode.ACCOUNT) {
this.queryParams = { this.queryParams = {};
accountDetailDialog: true, this.routerLink = this.item.routerLink;
accountId: this.item.id
};
this.routerLink = internalRoutes.accounts.routerLink;
} else if (this.item?.mode === SearchMode.ASSET_PROFILE) { } else if (this.item?.mode === SearchMode.ASSET_PROFILE) {
this.queryParams = { this.queryParams = {
assetProfileDialog: true, assetProfileDialog: true,

3
libs/ui/src/lib/assistant/assistant.component.ts

@ -612,7 +612,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
return { return {
id, id,
name, name,
routerLink: internalRoutes.accounts.routerLink, routerLink:
internalRoutes.accounts.subRoutes.detail.routerLink(id),
mode: SearchMode.ACCOUNT as const mode: SearchMode.ACCOUNT as const
}; };
}); });

Loading…
Cancel
Save