mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* Move accounts table component to @ghostfolio/ui library * Add Storybook story with sample data from live demo * Update imports in client applicationpull/5278/head
11 changed files with 217 additions and 223 deletions
@ -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> |
|
@ -1,11 +0,0 @@ |
|||||
:host { |
|
||||
display: block; |
|
||||
|
|
||||
a { |
|
||||
color: rgba(var(--palette-primary-500), 1); |
|
||||
|
|
||||
&:hover { |
|
||||
color: rgba(var(--palette-primary-300), 1); |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -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?` |
|
||||
}); |
|
||||
} |
|
||||
} |
|
@ -1,13 +0,0 @@ |
|||||
:host { |
|
||||
display: block; |
|
||||
|
|
||||
.gf-table { |
|
||||
th { |
|
||||
::ng-deep { |
|
||||
.mat-sort-header-container { |
|
||||
justify-content: inherit; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,13 @@ |
|||||
|
:host { |
||||
|
display: block; |
||||
|
|
||||
|
.gf-table { |
||||
|
th { |
||||
|
::ng-deep { |
||||
|
.mat-sort-header-container { |
||||
|
justify-content: inherit; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
}; |
@ -0,0 +1 @@ |
|||||
|
export * from './accounts-table.component'; |
Loading…
Reference in new issue