Browse Source

Merge branch 'main' into feature/add-lazy-loaded-activities-table-to-import-activities-dialog

pull/2754/head
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
5162f9c661
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 1
      apps/api/src/app/export/export.controller.ts
  3. 13
      apps/api/src/app/export/export.service.ts
  4. 12
      apps/api/src/app/import/import.service.ts
  5. 58
      apps/api/src/app/order/order.service.ts
  6. 84
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  7. 18
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  8. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts
  9. 48
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
  10. 25
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
  11. 2
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts
  12. 1
      libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.html
  13. 1
      libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts

2
CHANGELOG.md

@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Introduced the lazy-loaded activities table to the account detail dialog (experimental)
- Introduced the lazy-loaded activities table to the import activities dialog (experimental)
- Introduced the lazy-loaded activities table to the position detail dialog (experimental)
- Improved the font weight in the value component
- Improved the language localization for Türkçe (`tr`)
- Upgraded to _Inter_ 4 font family

1
apps/api/src/app/export/export.controller.ts

@ -20,6 +20,7 @@ export class ExportController {
): Promise<Export> {
return this.exportService.export({
activityIds,
userCurrency: this.request.user.Settings.settings.baseCurrency,
userId: this.request.user.id
});
}

13
apps/api/src/app/export/export.service.ts

@ -13,9 +13,11 @@ export class ExportService {
public async export({
activityIds,
userCurrency,
userId
}: {
activityIds?: string[];
userCurrency: string;
userId: string;
}): Promise<Export> {
const accounts = (
@ -39,10 +41,13 @@ export class ExportService {
}
);
let activities = await this.orderService.orders({
include: { SymbolProfile: true },
orderBy: { date: 'desc' },
where: { userId }
let { activities } = await this.orderService.getOrders({
userCurrency,
userId,
includeDrafts: true,
sortColumn: 'date',
sortDirection: 'asc',
withExcludedAccounts: true
});
if (activityIds) {

12
apps/api/src/app/import/import.service.ts

@ -236,6 +236,7 @@ export class ImportService {
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
activitiesDto,
userCurrency,
userId
});
@ -459,15 +460,18 @@ export class ImportService {
private async extendActivitiesWithErrors({
activitiesDto,
userCurrency,
userId
}: {
activitiesDto: Partial<CreateOrderDto>[];
userCurrency: string;
userId: string;
}): Promise<Partial<Activity>[]> {
const existingActivities = await this.orderService.orders({
include: { SymbolProfile: true },
orderBy: { date: 'desc' },
where: { userId }
let { activities: existingActivities } = await this.orderService.getOrders({
userCurrency,
userId,
includeDrafts: true,
withExcludedAccounts: true
});
return activitiesDto.map(

58
apps/api/src/app/order/order.service.ts

@ -25,7 +25,7 @@ import { endOfToday, isAfter } from 'date-fns';
import { groupBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { Activities, Activity } from './interfaces/activities.interface';
import { Activities } from './interfaces/activities.interface';
@Injectable()
export class OrderService {
@ -37,34 +37,6 @@ export class OrderService {
private readonly symbolProfileService: SymbolProfileService
) {}
public async order(
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
): Promise<Order | null> {
return this.prismaService.order.findUnique({
where: orderWhereUniqueInput
});
}
public async orders(params: {
include?: Prisma.OrderInclude;
skip?: number;
take?: number;
cursor?: Prisma.OrderWhereUniqueInput;
where?: Prisma.OrderWhereInput;
orderBy?: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput>;
}): Promise<OrderWithAccount[]> {
const { include, skip, take, cursor, where, orderBy } = params;
return this.prismaService.order.findMany({
cursor,
include,
orderBy,
skip,
take,
where
});
}
public async createOrder(
data: Prisma.OrderCreateInput & {
accountId?: string;
@ -379,6 +351,14 @@ export class OrderService {
return { activities, count };
}
public async order(
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
): Promise<Order | null> {
return this.prismaService.order.findUnique({
where: orderWhereUniqueInput
});
}
public async updateOrder({
data,
where
@ -455,4 +435,24 @@ export class OrderService {
where
});
}
private async orders(params: {
include?: Prisma.OrderInclude;
skip?: number;
take?: number;
cursor?: Prisma.OrderWhereUniqueInput;
where?: Prisma.OrderWhereInput;
orderBy?: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput>;
}): Promise<OrderWithAccount[]> {
const { include, skip, take, cursor, where, orderBy } = params;
return this.prismaService.order.findMany({
cursor,
include,
orderBy,
skip,
take,
where
});
}
}

84
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -7,6 +7,8 @@ import {
OnInit
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -16,6 +18,7 @@ import {
HistoricalDataItem,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { OrderWithAccount } from '@ghostfolio/common/types';
import Big from 'big.js';
import { format, parseISO } from 'date-fns';
@ -24,7 +27,6 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@Component({
host: { class: 'd-flex flex-column h-100' },
@ -38,6 +40,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
public activities: OrderWithAccount[];
public balance: number;
public currency: string;
public dataSource: MatTableDataSource<OrderWithAccount>;
public equity: number;
public hasImpersonationId: boolean;
public hasPermissionToDeleteAccountBalance: boolean;
@ -46,6 +49,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
public isLoadingChart: boolean;
public name: string;
public platformName: string;
public sortColumn = 'date';
public sortDirection: SortDirection = 'desc';
public totalItems: number;
public transactionCount: number;
public user: User;
public valueInBaseCurrency: number;
@ -77,8 +83,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
}
public ngOnInit() {
this.isLoadingActivities = true;
this.dataService
.fetchAccount(this.data.accountId)
.pipe(takeUntil(this.unsubscribeSubject))
@ -110,19 +114,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
}
);
this.dataService
.fetchActivities({
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck();
});
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
@ -131,6 +122,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
});
this.fetchAccountBalances();
this.fetchActivities();
this.fetchPortfolioPerformance();
}
@ -151,12 +143,20 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
}
public onExport() {
let activityIds = [];
if (this.user?.settings?.isExperimentalFeatures === true) {
activityIds = this.dataSource.data.map(({ id }) => {
return id;
});
} else {
activityIds = this.activities.map(({ id }) => {
return id;
});
}
this.dataService
.fetchExport(
this.activities.map(({ id }) => {
return id;
})
)
.fetchExport(activityIds)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile({
@ -172,6 +172,13 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
});
}
public onSortChanged({ active, direction }: Sort) {
this.sortColumn = active;
this.sortDirection = direction;
this.fetchActivities();
}
private fetchAccountBalances() {
this.dataService
.fetchAccountBalances(this.data.accountId)
@ -183,6 +190,41 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
});
}
private fetchActivities() {
this.isLoadingActivities = true;
if (this.user?.settings?.isExperimentalFeatures === true) {
this.dataService
.fetchActivities({
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }],
sortColumn: this.sortColumn,
sortDirection: this.sortDirection
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities, count }) => {
this.dataSource = new MatTableDataSource(activities);
this.totalItems = count;
this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck();
});
} else {
this.dataService
.fetchActivities({
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck();
});
}
}
private fetchPortfolioPerformance() {
this.isLoadingChart = true;

18
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -71,7 +71,25 @@
>
<mat-tab>
<ng-template i18n mat-tab-label>Activities</ng-template>
<gf-activities-table-lazy
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
[showActions]="false"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[totalItems]="totalItems"
(export)="onExport()"
(sortChanged)="onSortChanged($event)"
></gf-activities-table-lazy>
<gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"

2
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts

@ -7,6 +7,7 @@ import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-foote
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -19,6 +20,7 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
CommonModule,
GfAccountBalancesModule,
GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfInvestmentChartModule,

48
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts

@ -7,12 +7,16 @@ import {
OnInit
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
import {
DataProviderInfo,
EnhancedSymbolProfile,
LineChartItem
LineChartItem,
User
} from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
@ -31,6 +35,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
styleUrls: ['./position-detail-dialog.component.scss']
})
export class PositionDetailDialog implements OnDestroy, OnInit {
public activities: OrderWithAccount[];
public assetClass: string;
public assetSubClass: string;
public averagePrice: number;
@ -39,6 +44,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
[code: string]: { name: string; value: number };
};
public dataProviderInfo: DataProviderInfo;
public dataSource: MatTableDataSource<OrderWithAccount>;
public dividendInBaseCurrency: number;
public feeInBaseCurrency: number;
public firstBuyDate: string;
@ -51,16 +57,19 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public minPrice: number;
public netPerformance: number;
public netPerformancePercent: number;
public orders: OrderWithAccount[];
public quantity: number;
public quantityPrecision = 2;
public reportDataGlitchMail: string;
public sectors: {
[name: string]: { name: string; value: number };
};
public sortColumn = 'date';
public sortDirection: SortDirection = 'desc';
public SymbolProfile: EnhancedSymbolProfile;
public tags: Tag[];
public totalItems: number;
public transactionCount: number;
public user: User;
public value: number;
private unsubscribeSubject = new Subject<void>();
@ -69,7 +78,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
public dialogRef: MatDialogRef<PositionDetailDialog>,
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams,
private userService: UserService
) {}
public ngOnInit(): void {
@ -102,10 +112,12 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
transactionCount,
value
}) => {
this.activities = orders;
this.averagePrice = averagePrice;
this.benchmarkDataItems = [];
this.countries = {};
this.dataProviderInfo = dataProviderInfo;
this.dataSource = new MatTableDataSource(orders.reverse());
this.dividendInBaseCurrency = dividendInBaseCurrency;
this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate;
@ -130,7 +142,6 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.minPrice = minPrice;
this.netPerformance = netPerformance;
this.netPerformancePercent = netPerformancePercent;
this.orders = orders;
this.quantity = quantity;
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
this.sectors = {};
@ -142,6 +153,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
};
});
this.transactionCount = transactionCount;
this.totalItems = transactionCount;
this.value = value;
if (SymbolProfile?.assetClass) {
@ -239,6 +251,16 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
}
);
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public onClose(): void {
@ -246,12 +268,20 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
}
public onExport() {
let activityIds = [];
if (this.user?.settings?.isExperimentalFeatures === true) {
activityIds = this.dataSource.data.map(({ id }) => {
return id;
});
} else {
activityIds = this.activities.map(({ id }) => {
return id;
});
}
this.dataService
.fetchExport(
this.orders.map((order) => {
return order.id;
})
)
.fetchExport(activityIds)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile({

25
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html

@ -246,11 +246,30 @@
</div>
</div>
<div class="row" [ngClass]="{ 'd-none': !orders?.length }">
<div class="row" [ngClass]="{ 'd-none': !activities?.length }">
<div class="col mb-3">
<div class="h5 mb-0" i18n>Activities</div>
<gf-activities-table-lazy
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="data.baseCurrency"
[dataSource]="dataSource"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data.locale"
[showActions]="false"
[showNameColumn]="false"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[sortDisabled]="true"
[totalItems]="totalItems"
(export)="onExport()"
></gf-activities-table-lazy>
<gf-activities-table
[activities]="orders"
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="data.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
@ -277,7 +296,7 @@
</div>
<div
*ngIf="data.hasPermissionToReportDataGlitch === true && orders?.length > 0"
*ngIf="activities?.length > 0 && data.hasPermissionToReportDataGlitch === true"
class="row"
>
<div class="col">

2
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts

@ -5,6 +5,7 @@ import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
@ -19,6 +20,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
imports: [
CommonModule,
GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDataProviderCreditsModule,
GfDialogFooterModule,
GfDialogHeaderModule,

1
libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.html

@ -72,6 +72,7 @@
[dataSource]="dataSource"
[matSortActive]="sortColumn"
[matSortDirection]="sortDirection"
[matSortDisabled]="sortDisabled"
>
<ng-container matColumnDef="select">
<th *matHeaderCellDef class="px-1" mat-header-cell>

1
libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts

@ -48,6 +48,7 @@ export class ActivitiesTableLazyComponent
@Input() showNameColumn = true;
@Input() sortColumn: string;
@Input() sortDirection: SortDirection;
@Input() sortDisabled = false;
@Input() totalItems = Number.MAX_SAFE_INTEGER;
@Output() activityDeleted = new EventEmitter<string>();

Loading…
Cancel
Save