Feature/refactor transactions to activities table (#600)

* Refactor transactions to activities table with attributes and routes

* Update changelog
This commit is contained in:
Thomas Kaul 2021-12-30 18:56:51 +01:00 committed by GitHub
parent bee702302f
commit b291d9e031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 90 additions and 96 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Start refactoring _transactions_ to _activities_
- Upgraded `angular` from version `13.0.2` to `13.1.1` - Upgraded `angular` from version `13.0.2` to `13.1.1`
- Upgraded `Nx` from version `13.3.0` to `13.4.1` - Upgraded `Nx` from version `13.3.0` to `13.4.1`

View File

@ -66,6 +66,13 @@ const routes: Routes = [
(m) => m.PortfolioPageModule (m) => m.PortfolioPageModule
) )
}, },
{
path: 'portfolio/activities',
loadChildren: () =>
import('./pages/portfolio/transactions/transactions-page.module').then(
(m) => m.TransactionsPageModule
)
},
{ {
path: 'portfolio/allocations', path: 'portfolio/allocations',
loadChildren: () => loadChildren: () =>
@ -87,13 +94,6 @@ const routes: Routes = [
(m) => m.ReportPageModule (m) => m.ReportPageModule
) )
}, },
{
path: 'portfolio/transactions',
loadChildren: () =>
import('./pages/portfolio/transactions/transactions-page.module').then(
(m) => m.TransactionsPageModule
)
},
{ {
path: 'pricing', path: 'pricing',
loadChildren: () => loadChildren: () =>

View File

@ -5,10 +5,10 @@ import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
import { AccountsTableComponent } from './accounts-table.component'; import { AccountsTableComponent } from './accounts-table.component';
@NgModule({ @NgModule({

View File

@ -25,8 +25,8 @@
class="mt-3" class="mt-3"
i18n i18n
mat-button mat-button
[routerLink]="['/portfolio', 'transactions']" [routerLink]="['/portfolio', 'activities']"
>Manage Transactions...</a >Manage Activities...</a
> >
</div> </div>
</div> </div>

View File

@ -125,19 +125,19 @@
</div> </div>
</div> </div>
<gf-transactions-table <gf-activities-table
*ngIf="orders?.length > 0" *ngIf="orders?.length > 0"
[activities]="orders"
[baseCurrency]="data.baseCurrency" [baseCurrency]="data.baseCurrency"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateOrder]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToImportOrders]="false" [hasPermissionToImportActivities]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="data.locale" [locale]="data.locale"
[showActions]="false" [showActions]="false"
[showSymbolColumn]="false" [showSymbolColumn]="false"
[transactions]="orders" ></gf-activities-table>
></gf-transactions-table>
</div> </div>
<gf-dialog-footer <gf-dialog-footer

View File

@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
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 { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -16,10 +16,10 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
exports: [], exports: [],
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesTableModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
GfLineChartModule, GfLineChartModule,
GfTransactionsTableModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,

View File

@ -3,16 +3,16 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<mat-card class="mb-3"> <mat-card class="mb-3">
<h4 i18n>Transactions</h4> <h4 i18n>Activities</h4>
<p class="mb-0">Manage your transactions.</p> <p class="mb-0">Manage your activities.</p>
<p class="text-right"> <p class="text-right">
<a <a
color="primary" color="primary"
i18n
mat-button mat-button
[routerLink]="['/portfolio', 'transactions']" [routerLink]="['/portfolio', 'activities']"
> >
Open Transactions → <span i18n>Open Activities</span>
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
</a> </a>
</p> </p>
</mat-card> </mat-card>
@ -31,12 +31,12 @@
<p class="text-right"> <p class="text-right">
<a <a
color="primary" color="primary"
i18n
mat-button mat-button
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'" [disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
[routerLink]="['/portfolio', 'allocations']" [routerLink]="['/portfolio', 'allocations']"
> >
Open Allocations → <span i18n>Open Allocations</span>
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
</a> </a>
</p> </p>
</mat-card> </mat-card>
@ -57,12 +57,12 @@
<p class="text-right"> <p class="text-right">
<a <a
color="primary" color="primary"
i18n
mat-button mat-button
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'" [disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
[routerLink]="['/portfolio', 'analysis']" [routerLink]="['/portfolio', 'analysis']"
> >
Open Analysis → <span i18n>Open Analysis</span>
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
</a> </a>
</p> </p>
</mat-card> </mat-card>
@ -84,12 +84,12 @@
<p class="text-right"> <p class="text-right">
<a <a
color="primary" color="primary"
i18n
mat-button mat-button
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'" [disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
[routerLink]="['/portfolio', 'report']" [routerLink]="['/portfolio', 'report']"
> >
Open X-ray → <span i18n>Open X-ray</span>
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
</a> </a>
</p> </p>
</mat-card> </mat-card>

View File

@ -1,6 +1,6 @@
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100"> <form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1> <h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update activity</h1>
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1> <h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add activity</h1>
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">

View File

@ -1,21 +1,21 @@
<div class="container"> <div class="container">
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">
<h3 class="d-flex justify-content-center mb-3" i18n>Transactions</h3> <h3 class="d-flex justify-content-center mb-3" i18n>Activities</h3>
<gf-transactions-table <gf-activities-table
[activities]="transactions"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
[hasPermissionToImportOrders]="hasPermissionToImportOrders" [hasPermissionToImportActivities]="hasPermissionToImportOrders"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView" [showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"
[transactions]="transactions" (activityDeleted)="onDeleteTransaction($event)"
(activityToClone)="onCloneTransaction($event)"
(activityToUpdate)="onUpdateTransaction($event)"
(export)="onExport()" (export)="onExport()"
(import)="onImport()" (import)="onImport()"
(transactionDeleted)="onDeleteTransaction($event)" ></gf-activities-table>
(transactionToClone)="onCloneTransaction($event)"
(transactionToUpdate)="onUpdateTransaction($event)"
></gf-transactions-table>
</div> </div>
</div> </div>

View File

@ -3,8 +3,8 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service'; import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfCreateOrUpdateTransactionDialogModule } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.module'; import { GfCreateOrUpdateTransactionDialogModule } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.module';
import { GfImportTransactionDialogModule } from './import-transaction-dialog/import-transaction-dialog.module'; import { GfImportTransactionDialogModule } from './import-transaction-dialog/import-transaction-dialog.module';
@ -16,9 +16,9 @@ import { TransactionsPageComponent } from './transactions-page.component';
exports: [], exports: [],
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesTableModule,
GfCreateOrUpdateTransactionDialogModule, GfCreateOrUpdateTransactionDialogModule,
GfImportTransactionDialogModule, GfImportTransactionDialogModule,
GfTransactionsTableModule,
MatButtonModule, MatButtonModule,
MatSnackBarModule, MatSnackBarModule,
RouterModule, RouterModule,

View File

@ -230,14 +230,14 @@
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
[matMenuTriggerFor]="transactionsMenu" [matMenuTriggerFor]="activitiesMenu"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
> >
<ion-icon name="ellipsis-vertical"></ion-icon> <ion-icon name="ellipsis-vertical"></ion-icon>
</button> </button>
<mat-menu #transactionsMenu="matMenu" xPosition="before"> <mat-menu #activitiesMenu="matMenu" xPosition="before">
<button <button
*ngIf="hasPermissionToImportOrders" *ngIf="hasPermissionToImportActivities"
class="align-items-center d-flex" class="align-items-center d-flex"
mat-menu-item mat-menu-item
(click)="onImport()" (click)="onImport()"
@ -259,19 +259,19 @@
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
[matMenuTriggerFor]="transactionMenu" [matMenuTriggerFor]="activityMenu"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
> >
<ion-icon name="ellipsis-vertical"></ion-icon> <ion-icon name="ellipsis-vertical"></ion-icon>
</button> </button>
<mat-menu #transactionMenu="matMenu" xPosition="before"> <mat-menu #activityMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onUpdateTransaction(element)"> <button i18n mat-menu-item (click)="onUpdateActivity(element)">
Edit Edit
</button> </button>
<button i18n mat-menu-item (click)="onCloneTransaction(element)"> <button i18n mat-menu-item (click)="onCloneActivity(element)">
Clone Clone
</button> </button>
<button i18n mat-menu-item (click)="onDeleteTransaction(element.id)"> <button i18n mat-menu-item (click)="onDeleteActivity(element.id)">
Delete Delete
</button> </button>
</mat-menu> </mat-menu>
@ -305,7 +305,7 @@
<div <div
*ngIf=" *ngIf="
dataSource.data.length === 0 && hasPermissionToCreateOrder && !isLoading dataSource.data.length === 0 && hasPermissionToCreateActivity && !isLoading
" "
class="p-3 text-center" class="p-3 text-center"
> >

View File

@ -30,30 +30,28 @@ const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...';
const SEARCH_STRING_SEPARATOR = ','; const SEARCH_STRING_SEPARATOR = ',';
@Component({ @Component({
selector: 'gf-transactions-table',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './transactions-table.component.html', selector: 'gf-activities-table',
styleUrls: ['./transactions-table.component.scss'] styleUrls: ['./activities-table.component.scss'],
templateUrl: './activities-table.component.html'
}) })
export class TransactionsTableComponent export class ActivitiesTableComponent implements OnChanges, OnDestroy {
implements OnChanges, OnDestroy, OnInit @Input() activities: OrderWithAccount[];
{
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToCreateOrder: boolean; @Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToFilter = true; @Input() hasPermissionToFilter = true;
@Input() hasPermissionToImportOrders: boolean; @Input() hasPermissionToImportActivities: boolean;
@Input() hasPermissionToOpenDetails = true; @Input() hasPermissionToOpenDetails = true;
@Input() locale: string; @Input() locale: string;
@Input() showActions: boolean; @Input() showActions: boolean;
@Input() showSymbolColumn = true; @Input() showSymbolColumn = true;
@Input() transactions: OrderWithAccount[];
@Output() activityDeleted = new EventEmitter<string>();
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@Output() export = new EventEmitter<void>(); @Output() export = new EventEmitter<void>();
@Output() import = new EventEmitter<void>(); @Output() import = new EventEmitter<void>();
@Output() transactionDeleted = new EventEmitter<string>();
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete; @ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>; @ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
@ -124,8 +122,6 @@ export class TransactionsTableComponent
this.searchControl.setValue(null); this.searchControl.setValue(null);
} }
public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
this.displayedColumns = [ this.displayedColumns = [
'count', 'count',
@ -152,8 +148,8 @@ export class TransactionsTableComponent
this.isLoading = true; this.isLoading = true;
if (this.transactions) { if (this.activities) {
this.dataSource = new MatTableDataSource(this.transactions); this.dataSource = new MatTableDataSource(this.activities);
this.dataSource.filterPredicate = (data, filter) => { this.dataSource.filterPredicate = (data, filter) => {
const dataString = this.getFilterableValues(data) const dataString = this.getFilterableValues(data)
.join(' ') .join(' ')
@ -171,13 +167,15 @@ export class TransactionsTableComponent
} }
} }
public onDeleteTransaction(aId: string) { public onCloneActivity(aActivity: OrderWithAccount) {
const confirmation = confirm( this.activityToClone.emit(aActivity);
'Do you really want to delete this transaction?' }
);
public onDeleteActivity(aId: string) {
const confirmation = confirm('Do you really want to delete this activity?');
if (confirmation) { if (confirmation) {
this.transactionDeleted.emit(aId); this.activityDeleted.emit(aId);
} }
} }
@ -195,12 +193,8 @@ export class TransactionsTableComponent
}); });
} }
public onUpdateTransaction(aTransaction: OrderWithAccount) { public onUpdateActivity(aActivity: OrderWithAccount) {
this.transactionToUpdate.emit(aTransaction); this.activityToUpdate.emit(aActivity);
}
public onCloneTransaction(aTransaction: OrderWithAccount) {
this.transactionToClone.emit(aTransaction);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -217,7 +211,7 @@ export class TransactionsTableComponent
this.placeholder = this.placeholder =
lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : ''; lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : '';
this.allFilters = this.getSearchableFieldValues(this.transactions).filter( this.allFilters = this.getSearchableFieldValues(this.activities).filter(
(item) => { (item) => {
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase()); return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
} }
@ -226,11 +220,11 @@ export class TransactionsTableComponent
this.filters$.next(this.allFilters); this.filters$.next(this.allFilters);
} }
private getSearchableFieldValues(transactions: OrderWithAccount[]): string[] { private getSearchableFieldValues(activities: OrderWithAccount[]): string[] {
const fieldValues = new Set<string>(); const fieldValues = new Set<string>();
for (const transaction of transactions) { for (const activity of activities) {
this.getFilterableValues(transaction, fieldValues); this.getFilterableValues(activity, fieldValues);
} }
return [...fieldValues] return [...fieldValues]
@ -255,15 +249,15 @@ export class TransactionsTableComponent
} }
private getFilterableValues( private getFilterableValues(
transaction: OrderWithAccount, activity: OrderWithAccount,
fieldValues: Set<string> = new Set<string>() fieldValues: Set<string> = new Set<string>()
): string[] { ): string[] {
fieldValues.add(transaction.currency); fieldValues.add(activity.currency);
fieldValues.add(transaction.symbol); fieldValues.add(activity.symbol);
fieldValues.add(transaction.type); fieldValues.add(activity.type);
fieldValues.add(transaction.Account?.name); fieldValues.add(activity.Account?.name);
fieldValues.add(transaction.Account?.Platform?.name); fieldValues.add(activity.Account?.Platform?.name);
fieldValues.add(format(transaction.date, 'yyyy')); fieldValues.add(format(activity.date, 'yyyy'));
return [...fieldValues].filter((item) => { return [...fieldValues].filter((item) => {
return item !== undefined; return item !== undefined;

View File

@ -9,17 +9,17 @@ 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 { 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 { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module'; import { ActivitiesTableComponent } from './activities-table.component';
import { TransactionsTableComponent } from './transactions-table.component';
@NgModule({ @NgModule({
declarations: [TransactionsTableComponent], declarations: [ActivitiesTableComponent],
exports: [TransactionsTableComponent], exports: [ActivitiesTableComponent],
imports: [ imports: [
CommonModule, CommonModule,
GfNoTransactionsInfoModule, GfNoTransactionsInfoModule,
@ -37,7 +37,6 @@ import { TransactionsTableComponent } from './transactions-table.component';
ReactiveFormsModule, ReactiveFormsModule,
RouterModule RouterModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfTransactionsTableModule {} export class GfActivitiesTableModule {}

View File

@ -5,10 +5,10 @@
<a <a
class="align-items-center justify-content-center" class="align-items-center justify-content-center"
color="primary" color="primary"
[routerLink]="['/portfolio', 'transactions']" [routerLink]="['/portfolio', 'activities']"
[queryParams]="{ createDialog: true }" [queryParams]="{ createDialog: true }"
mat-button mat-button
> >
<span i18n>Time to add your first transaction.</span> <span i18n>Time to add your first activity.</span>
</a> </a>
</div> </div>