From 1d69cda530194bd0d616e16e721899a481bbc7ef Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 6 Dec 2023 19:50:47 +0100 Subject: [PATCH] Introduce lazy-loaded activities table --- .../activities/activities-page.component.ts | 3 + .../portfolio/activities/activities-page.html | 19 + .../activities/activities-page.module.ts | 2 + .../activities-table-lazy.component.html | 496 ++++++++++++++++++ .../activities-table-lazy.component.scss | 19 + .../activities-table-lazy.component.ts | 241 +++++++++ .../activities-table-lazy.module.ts | 42 ++ 7 files changed, 822 insertions(+) create mode 100644 libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.html create mode 100644 libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.scss create mode 100644 libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts create mode 100644 libs/ui/src/lib/activities-table-lazy/activities-table-lazy.module.ts diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 259611c0d..f568eb5f7 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; import { ActivatedRoute, Router } from '@angular/router'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; @@ -30,6 +31,7 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa }) export class ActivitiesPageComponent implements OnDestroy, OnInit { public activities: Activity[]; + public dataSource: MatTableDataSource; public defaultAccountId: string; public deviceType: string; public hasImpersonationId: boolean; @@ -108,6 +110,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ activities }) => { this.activities = activities; + this.dataSource = new MatTableDataSource(activities); if ( this.hasPermissionToCreateActivity && diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html index 8c2cf9bd5..e651450ff 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -2,7 +2,26 @@

Activities

+ + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ +
+
+ Name + +
+
+ {{ element.SymbolProfile?.name }} + Draft +
+
+
+ {{ + element.SymbolProfile?.symbol | gfSymbol + }} +
+
+ Type + + + + Date + +
+ {{ element.date | date: defaultDateFormat }} +
+
+ Quantity + +
+ +
+
+ Unit Price + +
+ +
+
+ Fee + +
+ +
+
+ Value + +
+ +
+
+ Currency + + {{ element.SymbolProfile?.currency }} + + Value + +
+ +
+
+ Account + +
+ + {{ element.Account?.name }} +
+
+ + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ +
diff --git a/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.scss b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.scss new file mode 100644 index 000000000..ea3dad292 --- /dev/null +++ b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.scss @@ -0,0 +1,19 @@ +@import 'apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; + + .activities { + overflow-x: auto; + + .mat-mdc-table { + th { + ::ng-deep { + .mat-sort-header-container { + justify-content: inherit; + } + } + } + } + } +} diff --git a/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts new file mode 100644 index 000000000..711857935 --- /dev/null +++ b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts @@ -0,0 +1,241 @@ +import { SelectionModel } from '@angular/cdk/collections'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + ViewChild +} from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { Router } from '@angular/router'; +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; +import { getDateFormatString } from '@ghostfolio/common/helper'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { OrderWithAccount } from '@ghostfolio/common/types'; +import { isUUID } from 'class-validator'; +import { endOfToday, isAfter } from 'date-fns'; +import { get } from 'lodash'; +import { Subject, Subscription, takeUntil } from 'rxjs'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-activities-table-lazy', + styleUrls: ['./activities-table-lazy.component.scss'], + templateUrl: './activities-table-lazy.component.html' +}) +export class ActivitiesTableLazyComponent + implements OnChanges, OnDestroy, OnInit +{ + @Input() baseCurrency: string; + @Input() dataSource: MatTableDataSource; + @Input() deviceType: string; + @Input() hasPermissionToCreateActivity: boolean; + @Input() hasPermissionToExportActivities: boolean; + @Input() hasPermissionToOpenDetails = true; + @Input() locale: string; + @Input() pageSize = DEFAULT_PAGE_SIZE; + @Input() showActions = true; + @Input() showCheckbox = false; + @Input() showFooter = true; + @Input() showNameColumn = true; + + @Output() activityDeleted = new EventEmitter(); + @Output() activityToClone = new EventEmitter(); + @Output() activityToUpdate = new EventEmitter(); + @Output() deleteAllActivities = new EventEmitter(); + @Output() export = new EventEmitter(); + @Output() exportDrafts = new EventEmitter(); + @Output() import = new EventEmitter(); + @Output() importDividends = new EventEmitter(); + @Output() selectedActivities = new EventEmitter(); + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + public defaultDateFormat: string; + public displayedColumns = []; + public endOfToday = endOfToday(); + public hasDrafts = false; + public hasErrors = false; + public isAfter = isAfter; + public isLoading = true; + public isUUID = isUUID; + public pageIndex = 0; + public routeQueryParams: Subscription; + public searchKeywords: string[] = []; + public selectedRows = new SelectionModel(true, []); + + private unsubscribeSubject = new Subject(); + + public constructor(private router: Router) {} + + public ngOnInit() { + if (this.showCheckbox) { + this.toggleAllRows(); + this.selectedRows.changed + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((selectedRows) => { + this.selectedActivities.emit(selectedRows.source.selected); + }); + } + } + + public areAllRowsSelected() { + const numSelectedRows = this.selectedRows.selected.length; + const numTotalRows = this.dataSource.data.length; + return numSelectedRows === numTotalRows; + } + + public ngOnChanges() { + this.defaultDateFormat = getDateFormatString(this.locale); + + this.displayedColumns = [ + 'select', + 'importStatus', + 'nameWithSymbol', + 'type', + 'date', + 'quantity', + 'unitPrice', + 'fee', + 'value', + 'currency', + 'valueInBaseCurrency', + 'account', + 'comment', + 'actions' + ]; + + if (!this.showCheckbox) { + this.displayedColumns = this.displayedColumns.filter((column) => { + return column !== 'importStatus' && column !== 'select'; + }); + } + + if (!this.showNameColumn) { + this.displayedColumns = this.displayedColumns.filter((column) => { + return column !== 'nameWithSymbol'; + }); + } + + if (this.dataSource) { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.dataSource.sortingDataAccessor = get; + + this.isLoading = false; + } + } + + public onChangePage(page: PageEvent) { + this.pageIndex = page.pageIndex; + } + + public onClickActivity(activity: Activity) { + if (this.showCheckbox) { + if (!activity.error) { + this.selectedRows.toggle(activity); + } + } else if ( + this.hasPermissionToOpenDetails && + !activity.isDraft && + activity.type !== 'FEE' && + activity.type !== 'INTEREST' && + activity.type !== 'ITEM' && + activity.type !== 'LIABILITY' + ) { + this.onOpenPositionDialog({ + dataSource: activity.SymbolProfile.dataSource, + symbol: activity.SymbolProfile.symbol + }); + } + } + + public onCloneActivity(aActivity: OrderWithAccount) { + this.activityToClone.emit(aActivity); + } + + public onDeleteActivity(aId: string) { + const confirmation = confirm( + $localize`Do you really want to delete this activity?` + ); + + if (confirmation) { + this.activityDeleted.emit(aId); + } + } + + public onExport() { + if (this.searchKeywords.length > 0) { + this.export.emit( + this.dataSource.filteredData.map((activity) => { + return activity.id; + }) + ); + } else { + this.export.emit(); + } + } + + public onExportDraft(aActivityId: string) { + this.exportDrafts.emit([aActivityId]); + } + + public onExportDrafts() { + this.exportDrafts.emit( + this.dataSource.filteredData + .filter((activity) => { + return activity.isDraft; + }) + .map((activity) => { + return activity.id; + }) + ); + } + + public onDeleteAllActivities() { + this.deleteAllActivities.emit(); + } + + public onImport() { + this.import.emit(); + } + + public onImportDividends() { + this.importDividends.emit(); + } + + public onOpenComment(aComment: string) { + alert(aComment); + } + + public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void { + this.router.navigate([], { + queryParams: { dataSource, symbol, positionDetailDialog: true } + }); + } + + public onUpdateActivity(aActivity: OrderWithAccount) { + this.activityToUpdate.emit(aActivity); + } + + public toggleAllRows() { + this.areAllRowsSelected() + ? this.selectedRows.clear() + : this.dataSource.data.forEach((row) => this.selectedRows.select(row)); + + this.selectedActivities.emit(this.selectedRows.selected); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.module.ts b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.module.ts new file mode 100644 index 000000000..22c9f6354 --- /dev/null +++ b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.module.ts @@ -0,0 +1,42 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { RouterModule } from '@angular/router'; +import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; +import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; +import { GfActivityTypeModule } from '@ghostfolio/ui/activity-type'; +import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; +import { GfValueModule } from '@ghostfolio/ui/value'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +import { ActivitiesTableLazyComponent } from './activities-table-lazy.component'; + +@NgModule({ + declarations: [ActivitiesTableLazyComponent], + exports: [ActivitiesTableLazyComponent], + imports: [ + CommonModule, + GfActivityTypeModule, + GfNoTransactionsInfoModule, + GfSymbolIconModule, + GfSymbolModule, + GfValueModule, + MatButtonModule, + MatCheckboxModule, + MatMenuModule, + MatPaginatorModule, + MatSortModule, + MatTableModule, + MatTooltipModule, + NgxSkeletonLoaderModule, + RouterModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfActivitiesTableLazyModule {}