Browse Source

Feature/move accounts table to ui 20250729

* Move accounts table component to @ghostfolio/ui library
* Add Storybook story with sample data from live demo
* Update imports in client application
pull/5278/head
David Requeno 4 weeks ago
parent
commit
1e177770a0
  1. 84
      apps/client/src/app/components/access-table/access-table.component.html
  2. 11
      apps/client/src/app/components/access-table/access-table.component.scss
  3. 111
      apps/client/src/app/components/access-table/access-table.component.ts
  4. 13
      apps/client/src/app/components/accounts-table/accounts-table.component.scss
  5. 2
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  6. 2
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  7. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  8. 13
      libs/ui/src/lib/accounts-table/accounts-table.component.scss
  9. 199
      libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts
  10. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  11. 1
      libs/ui/src/lib/accounts-table/index.ts

84
apps/client/src/app/components/access-table/access-table.component.html

@ -1,84 +0,0 @@
<div class="overflow-x-auto">
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="alias">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
<td *matCellDef="let element" class="px-1" mat-cell>
{{ element.alias }}
</td>
</ng-container>
<ng-container matColumnDef="grantee">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
<td *matCellDef="let element" class="px-1" mat-cell>
{{ element.grantee }}
</td>
</ng-container>
<ng-container matColumnDef="type">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<div class="align-items-center d-flex">
@if (element.permissions.includes('READ')) {
<ion-icon class="mr-1" name="lock-open-outline" />
<ng-container i18n>View</ng-container>
} @else if (element.permissions.includes('READ_RESTRICTED')) {
<ion-icon class="mr-1" name="lock-closed-outline" />
<ng-container i18n>Restricted view</ng-container>
}
</div>
</td>
</ng-container>
<ng-container matColumnDef="details">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
@if (element.type === 'PUBLIC') {
<div class="align-items-center d-flex">
<ion-icon class="mr-1" name="link-outline" />
<a target="_blank" [href]="getPublicUrl(element.id)">{{
getPublicUrl(element.id)
}}</a>
</div>
@if (user?.settings?.isExperimentalFeatures) {
<div>
<code
>GET {{ baseUrl }}/api/v1/public/{{
element.id
}}/portfolio</code
>
</div>
}
}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="transactionMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #transactionMenu="matMenu" xPosition="before">
@if (element.type === 'PUBLIC') {
<button mat-menu-item (click)="onCopyUrlToClipboard(element.id)">
<ng-container i18n>Copy link to clipboard</ng-container>
</button>
<hr class="my-0" />
}
<button mat-menu-item (click)="onDeleteAccess(element.id)">
<ng-container i18n>Revoke</ng-container>
</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
</div>

11
apps/client/src/app/components/access-table/access-table.component.scss

@ -1,11 +0,0 @@
:host {
display: block;
a {
color: rgba(var(--palette-primary-500), 1);
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}

111
apps/client/src/app/components/access-table/access-table.component.ts

@ -1,111 +0,0 @@
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { Access, User } from '@ghostfolio/common/interfaces';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
CUSTOM_ELEMENTS_SCHEMA,
EventEmitter,
Input,
OnChanges,
Output
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import {
ellipsisHorizontal,
linkOutline,
lockClosedOutline,
lockOpenOutline
} from 'ionicons/icons';
import ms from 'ms';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ClipboardModule,
CommonModule,
IonIcon,
MatButtonModule,
MatMenuModule,
MatTableModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-access-table',
templateUrl: './access-table.component.html',
styleUrls: ['./access-table.component.scss']
})
export class GfAccessTableComponent implements OnChanges {
@Input() accesses: Access[];
@Input() showActions: boolean;
@Input() user: User;
@Output() accessDeleted = new EventEmitter<string>();
public baseUrl = window.location.origin;
public dataSource: MatTableDataSource<Access>;
public displayedColumns = [];
public constructor(
private clipboard: Clipboard,
private notificationService: NotificationService,
private snackBar: MatSnackBar
) {
addIcons({
ellipsisHorizontal,
linkOutline,
lockClosedOutline,
lockOpenOutline
});
}
public ngOnChanges() {
this.displayedColumns = ['alias', 'grantee', 'type', 'details'];
if (this.showActions) {
this.displayedColumns.push('actions');
}
if (this.accesses) {
this.dataSource = new MatTableDataSource(this.accesses);
}
}
public getPublicUrl(aId: string): string {
const languageCode = this.user.settings.language;
return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`;
}
public onCopyUrlToClipboard(aId: string): void {
this.clipboard.copy(this.getPublicUrl(aId));
this.snackBar.open(
'✅ ' + $localize`Link has been copied to the clipboard`,
undefined,
{
duration: ms('3 seconds')
}
);
}
public onDeleteAccess(aId: string) {
this.notificationService.confirm({
confirmFn: () => {
this.accessDeleted.emit(aId);
},
confirmType: ConfirmationDialogType.Warn,
title: $localize`Do you really want to revoke this granted access?`
});
}
}

13
apps/client/src/app/components/accounts-table/accounts-table.component.scss

@ -1,13 +0,0 @@
:host {
display: block;
.gf-table {
th {
::ng-deep {
.mat-sort-header-container {
justify-content: inherit;
}
}
}
}
}

2
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -1,5 +1,5 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { GfAccountsTableComponent } from '@ghostfolio/client/components/accounts-table/accounts-table.component'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';

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

@ -4,7 +4,7 @@ import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module'; import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { GfAccountsTableComponent } from '@ghostfolio/client/components/accounts-table/accounts-table.component'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';

2
apps/client/src/app/components/accounts-table/accounts-table.component.html → libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -353,4 +353,4 @@
width: '100%' width: '100%'
}" }"
/> />
} }

13
libs/ui/src/lib/accounts-table/accounts-table.component.scss

@ -0,0 +1,13 @@
:host {
display: block;
.gf-table {
th {
::ng-deep {
.mat-sort-header-container {
justify-content: inherit;
}
}
}
}
}

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

@ -0,0 +1,199 @@
import { moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { CommonModule } from '@angular/common';
import { GfAccountsTableComponent } from './accounts-table.component';
import { GfEntityLogoComponent } from '../entity-logo';
import { GfValueComponent } from '../value';
const mockAccounts = [
{
id: '1',
name: 'Checking Account',
currency: 'USD',
balance: 15000,
value: 15000,
valueInBaseCurrency: 15000,
transactionCount: 25,
allocationInPercentage: 0.15,
isExcluded: false,
comment: 'Primary checking account',
platform: {
name: 'Bank of America',
url: 'https://www.bankofamerica.com'
}
},
{
id: '2',
name: 'Trading Account',
currency: 'USD',
balance: 5000,
value: 125000,
valueInBaseCurrency: 125000,
transactionCount: 127,
allocationInPercentage: 0.65,
isExcluded: false,
comment: null,
platform: {
name: 'Interactive Brokers',
url: 'https://www.interactivebrokers.com'
}
},
{
id: '3',
name: 'Savings Account',
currency: 'EUR',
balance: 20000,
value: 20000,
valueInBaseCurrency: 21600,
transactionCount: 8,
allocationInPercentage: 0.2,
isExcluded: false,
comment: 'Emergency fund',
platform: {
name: 'Deutsche Bank',
url: 'https://www.deutsche-bank.de'
}
},
{
id: '4',
name: 'Excluded Account',
currency: 'USD',
balance: 1000,
value: 1000,
valueInBaseCurrency: 1000,
transactionCount: 3,
allocationInPercentage: 0,
isExcluded: true,
comment: null,
platform: {
name: 'Local Credit Union',
url: null
}
}
];
export default {
title: 'Accounts Table',
component: GfAccountsTableComponent,
decorators: [
moduleMetadata({
imports: [
CommonModule,
NgxSkeletonLoaderModule,
MatButtonModule,
MatMenuModule,
MatSortModule,
MatTableModule,
RouterModule.forRoot([]),
IonIcon,
GfEntityLogoComponent,
GfValueComponent
]
})
]
} as Meta<GfAccountsTableComponent>;
type Story = StoryObj<GfAccountsTableComponent>;
export const Loading: Story = {
args: {
accounts: [],
baseCurrency: 'USD',
deviceType: 'web',
locale: 'en-US',
showActions: false,
showAllocationInPercentage: false,
showBalance: true,
showFooter: true,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 0,
totalValueInBaseCurrency: 0,
transactionCount: 0
}
};
export const Default: Story = {
args: {
accounts: mockAccounts,
baseCurrency: 'USD',
deviceType: 'web',
locale: 'en-US',
showActions: false,
showAllocationInPercentage: false,
showBalance: true,
showFooter: true,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 56600,
totalValueInBaseCurrency: 161600,
transactionCount: 163
}
};
export const WithActions: Story = {
args: {
accounts: mockAccounts,
baseCurrency: 'USD',
deviceType: 'web',
locale: 'en-US',
showActions: true,
showAllocationInPercentage: true,
showBalance: true,
showFooter: true,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 56600,
totalValueInBaseCurrency: 161600,
transactionCount: 163
}
};
export const MobileView: Story = {
args: {
accounts: mockAccounts,
baseCurrency: 'USD',
deviceType: 'mobile',
locale: 'en-US',
showActions: false,
showAllocationInPercentage: false,
showBalance: false,
showFooter: false,
showTransactions: true,
showValue: false,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 56600,
totalValueInBaseCurrency: 161600,
transactionCount: 163
}
};
export const WithoutFooter: Story = {
args: {
accounts: mockAccounts,
baseCurrency: 'USD',
deviceType: 'web',
locale: 'en-US',
showActions: false,
showAllocationInPercentage: true,
showBalance: true,
showFooter: false,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 56600,
totalValueInBaseCurrency: 161600,
transactionCount: 163
}
};

2
apps/client/src/app/components/accounts-table/accounts-table.component.ts → libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -175,4 +175,4 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
} }

1
libs/ui/src/lib/accounts-table/index.ts

@ -0,0 +1 @@
export * from './accounts-table.component';
Loading…
Cancel
Save