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