Browse Source

Merge pull request #51 from dandevaud/feature/Merge-upstream-in-dockerpush

Feature/merge upstream in dockerpush
pull/5027/head
dandevaud 2 years ago
committed by GitHub
parent
commit
529a6f7fff
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 46
      CHANGELOG.md
  2. 3
      apps/api/project.json
  3. 3
      apps/api/src/app/access/access.controller.ts
  4. 7
      apps/api/src/app/account-balance/account-balance.controller.ts
  5. 13
      apps/api/src/app/benchmark/benchmark.service.ts
  6. 1
      apps/api/src/app/export/export.controller.ts
  7. 13
      apps/api/src/app/export/export.service.ts
  8. 12
      apps/api/src/app/import/import.service.ts
  9. 1
      apps/api/src/app/order/interfaces/activities.interface.ts
  10. 10
      apps/api/src/app/order/order.controller.ts
  11. 88
      apps/api/src/app/order/order.service.ts
  12. 45
      apps/api/src/app/portfolio/portfolio.service.ts
  13. 14
      apps/api/src/helper/object.helper.ts
  14. 1
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  15. 6
      apps/api/webpack.config.js
  16. 26
      apps/client/project.json
  17. 84
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  18. 18
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  19. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts
  20. 10
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  21. 8
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  22. 48
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
  23. 25
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
  24. 2
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts
  25. 77
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  26. 26
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  27. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.module.ts
  28. 12
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  29. 23
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  30. 2
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.module.ts
  31. 5
      apps/client/src/app/services/admin.service.ts
  32. 49
      apps/client/src/app/services/data.service.ts
  33. BIN
      apps/client/src/assets/fonts/Inter-Black.woff
  34. BIN
      apps/client/src/assets/fonts/Inter-Black.woff2
  35. BIN
      apps/client/src/assets/fonts/Inter-BlackItalic.woff
  36. BIN
      apps/client/src/assets/fonts/Inter-BlackItalic.woff2
  37. BIN
      apps/client/src/assets/fonts/Inter-Bold.woff
  38. BIN
      apps/client/src/assets/fonts/Inter-Bold.woff2
  39. BIN
      apps/client/src/assets/fonts/Inter-BoldItalic.woff
  40. BIN
      apps/client/src/assets/fonts/Inter-BoldItalic.woff2
  41. BIN
      apps/client/src/assets/fonts/Inter-ExtraBold.woff
  42. BIN
      apps/client/src/assets/fonts/Inter-ExtraBold.woff2
  43. BIN
      apps/client/src/assets/fonts/Inter-ExtraBoldItalic.woff
  44. BIN
      apps/client/src/assets/fonts/Inter-ExtraBoldItalic.woff2
  45. BIN
      apps/client/src/assets/fonts/Inter-ExtraLight.woff
  46. BIN
      apps/client/src/assets/fonts/Inter-ExtraLight.woff2
  47. BIN
      apps/client/src/assets/fonts/Inter-ExtraLightItalic.woff
  48. BIN
      apps/client/src/assets/fonts/Inter-ExtraLightItalic.woff2
  49. BIN
      apps/client/src/assets/fonts/Inter-Italic.woff
  50. BIN
      apps/client/src/assets/fonts/Inter-Italic.woff2
  51. BIN
      apps/client/src/assets/fonts/Inter-Light.woff
  52. BIN
      apps/client/src/assets/fonts/Inter-Light.woff2
  53. BIN
      apps/client/src/assets/fonts/Inter-LightItalic.woff
  54. BIN
      apps/client/src/assets/fonts/Inter-LightItalic.woff2
  55. BIN
      apps/client/src/assets/fonts/Inter-Medium.woff
  56. BIN
      apps/client/src/assets/fonts/Inter-Medium.woff2
  57. BIN
      apps/client/src/assets/fonts/Inter-MediumItalic.woff
  58. BIN
      apps/client/src/assets/fonts/Inter-MediumItalic.woff2
  59. BIN
      apps/client/src/assets/fonts/Inter-Regular.woff
  60. BIN
      apps/client/src/assets/fonts/Inter-Regular.woff2
  61. BIN
      apps/client/src/assets/fonts/Inter-SemiBold.woff
  62. BIN
      apps/client/src/assets/fonts/Inter-SemiBold.woff2
  63. BIN
      apps/client/src/assets/fonts/Inter-SemiBoldItalic.woff
  64. BIN
      apps/client/src/assets/fonts/Inter-SemiBoldItalic.woff2
  65. BIN
      apps/client/src/assets/fonts/Inter-Thin.woff
  66. BIN
      apps/client/src/assets/fonts/Inter-Thin.woff2
  67. BIN
      apps/client/src/assets/fonts/Inter-ThinItalic.woff
  68. BIN
      apps/client/src/assets/fonts/Inter-ThinItalic.woff2
  69. BIN
      apps/client/src/assets/fonts/Inter-italic.var.woff2
  70. BIN
      apps/client/src/assets/fonts/Inter-roman.var.woff2
  71. BIN
      apps/client/src/assets/fonts/Inter.var.woff2
  72. BIN
      apps/client/src/assets/fonts/InterDisplay-Black.woff2
  73. BIN
      apps/client/src/assets/fonts/InterDisplay-BlackItalic.woff2
  74. BIN
      apps/client/src/assets/fonts/InterDisplay-Bold.woff2
  75. BIN
      apps/client/src/assets/fonts/InterDisplay-BoldItalic.woff2
  76. BIN
      apps/client/src/assets/fonts/InterDisplay-ExtraBold.woff2
  77. BIN
      apps/client/src/assets/fonts/InterDisplay-ExtraBoldItalic.woff2
  78. BIN
      apps/client/src/assets/fonts/InterDisplay-ExtraLight.woff2
  79. BIN
      apps/client/src/assets/fonts/InterDisplay-ExtraLightItalic.woff2
  80. BIN
      apps/client/src/assets/fonts/InterDisplay-Italic.woff2
  81. BIN
      apps/client/src/assets/fonts/InterDisplay-Light.woff2
  82. BIN
      apps/client/src/assets/fonts/InterDisplay-LightItalic.woff2
  83. BIN
      apps/client/src/assets/fonts/InterDisplay-Medium.woff2
  84. BIN
      apps/client/src/assets/fonts/InterDisplay-MediumItalic.woff2
  85. BIN
      apps/client/src/assets/fonts/InterDisplay-Regular.woff2
  86. BIN
      apps/client/src/assets/fonts/InterDisplay-SemiBold.woff2
  87. BIN
      apps/client/src/assets/fonts/InterDisplay-SemiBoldItalic.woff2
  88. BIN
      apps/client/src/assets/fonts/InterDisplay-Thin.woff2
  89. BIN
      apps/client/src/assets/fonts/InterDisplay-ThinItalic.woff2
  90. BIN
      apps/client/src/assets/fonts/InterVariable-Italic.woff2
  91. BIN
      apps/client/src/assets/fonts/InterVariable.woff2
  92. 261
      apps/client/src/assets/fonts/inter.css
  93. 346
      apps/client/src/locales/messages.tr.xlf
  94. 4
      apps/client/src/styles/theme.scss
  95. 6
      libs/common/src/lib/helper.ts
  96. 2
      libs/common/src/lib/interfaces/benchmark.interface.ts
  97. 492
      libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.html
  98. 9
      libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.scss
  99. 250
      libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts
  100. 42
      libs/ui/src/lib/activities-table-lazy/activities-table-lazy.module.ts

46
CHANGELOG.md

@ -9,6 +9,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Set the select column of the lazy-loaded activities table to stick at the end (experimental)
- Improved the validation of the currency management in the admin control panel
- Improved the performance of the value redaction interceptor for the impersonation mode by eliminating `cloneDeep`
### Fixed
- Reset the letter spacing in buttons
## 2.31.0 - 2023-12-16
### 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 `angular` from version `17.0.4` to `17.0.7`
- Upgraded to _Inter_ 4 font family
- Upgraded `Nx` from version `17.0.2` to `17.2.5`
### Fixed
- Fixed the loading state in the lazy-loaded activities table on the portfolio activities page (experimental)
- Fixed the edit of activity in the lazy-loaded activities table on the portfolio activities page (experimental)
## 2.30.0 - 2023-12-12
### Added
- Added support for column sorting to the lazy-loaded activities table on the portfolio activities page (experimental)
- Extended the benchmarks of the markets overview by the current market condition (all time high)
### Changed
- Adjusted the threshold to skip the data enhancement (_Trackinsight_) if data is inaccurate
- Upgraded `prisma` from version `5.6.0` to `5.7.0`
## 2.29.0 - 2023-12-09
### Added
- Introduced a lazy-loaded activities table on the portfolio activities page (experimental)
### Changed
- Set the actions columns of various tables to stick at the end
- Increased the height of the tabs on mobile
- Improved the language localization for German (`de`)

3
apps/api/project.json

@ -14,7 +14,8 @@
"tsConfig": "apps/api/tsconfig.app.json",
"assets": ["apps/api/src/assets"],
"target": "node",
"compiler": "tsc"
"compiler": "tsc",
"webpackConfig": "apps/api/webpack.config.js"
},
"configurations": {
"production": {

3
apps/api/src/app/access/access.controller.ts

@ -17,7 +17,6 @@ import { AuthGuard } from '@nestjs/passport';
import { Access as AccessModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccessModule } from './access.module';
import { AccessService } from './access.service';
import { CreateAccessDto } from './create-access.dto';
@ -83,7 +82,7 @@ export class AccessController {
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
public async deleteAccess(@Param('id') id: string): Promise<AccessModule> {
public async deleteAccess(@Param('id') id: string): Promise<AccessModel> {
const access = await this.accessService.access({ id });
if (

7
apps/api/src/app/account-balance/account-balance.controller.ts

@ -1,3 +1,4 @@
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
@ -8,11 +9,11 @@ import {
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AccountBalanceService } from './account-balance.service';
import { AuthGuard } from '@nestjs/passport';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccountBalance } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccountBalanceService } from './account-balance.service';
@Controller('account-balance')
export class AccountBalanceController {

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

@ -14,6 +14,7 @@ import {
calculateBenchmarkTrend
} from '@ghostfolio/common/helper';
import {
Benchmark,
BenchmarkMarketDataDetails,
BenchmarkProperty,
BenchmarkResponse,
@ -339,7 +340,15 @@ export class BenchmarkService {
};
}
private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
private getMarketCondition(
aPerformanceInPercent: number
): Benchmark['marketCondition'] {
if (aPerformanceInPercent === 0) {
return 'ALL_TIME_HIGH';
} else if (aPerformanceInPercent <= -0.2) {
return 'BEAR_MARKET';
} else {
return 'NEUTRAL_MARKET';
}
}
}

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(

1
apps/api/src/app/order/interfaces/activities.interface.ts

@ -2,6 +2,7 @@ import { OrderWithAccount } from '@ghostfolio/common/types';
export interface Activities {
activities: Activity[];
count: number;
}
export interface Activity extends OrderWithAccount {

10
apps/api/src/app/order/order.controller.ts

@ -24,7 +24,7 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Order as OrderModel } from '@prisma/client';
import { Order as OrderModel, Prisma } from '@prisma/client';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -90,6 +90,8 @@ export class OrderController {
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('skip') skip?: number,
@Query('sortColumn') sortColumn?: string,
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
@Query('tags') filterByTags?: string,
@Query('take') take?: number
): Promise<Activities> {
@ -103,8 +105,10 @@ export class OrderController {
await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const activities = await this.orderService.getOrders({
const { activities, count } = await this.orderService.getOrders({
filters,
sortColumn,
sortDirection,
userCurrency,
includeDrafts: true,
skip: isNaN(skip) ? undefined : skip,
@ -113,7 +117,7 @@ export class OrderController {
withExcludedAccounts: true
});
return { activities };
return { activities, count };
}
@Post()

88
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 { 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.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;
@ -231,6 +203,8 @@ export class OrderService {
filters,
includeDrafts = false,
skip,
sortColumn,
sortDirection,
take = Number.MAX_SAFE_INTEGER,
types,
userCurrency,
@ -240,12 +214,17 @@ export class OrderService {
filters?: Filter[];
includeDrafts?: boolean;
skip?: number;
sortColumn?: string;
sortDirection?: Prisma.SortOrder;
take?: number;
types?: TypeOfOrder[];
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<Activity[]> {
}): Promise<Activities> {
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
{ date: 'asc' }
];
const where: Prisma.OrderWhereInput = { userId };
const {
@ -328,6 +307,10 @@ export class OrderService {
];
}
if (sortColumn) {
orderBy = [{ [sortColumn]: sortDirection }];
}
if (types) {
where.OR = types.map((type) => {
return {
@ -338,8 +321,9 @@ export class OrderService {
});
}
return (
await this.orders({
const [orders, count] = await Promise.all([
this.orders({
orderBy,
skip,
take,
where,
@ -357,10 +341,12 @@ export class OrderService {
}
},
tags: true
},
orderBy: { date: 'asc' }
})
)
}
}),
this.prismaService.order.count({ where })
]);
const activities = orders
.filter((order) => {
return (
withExcludedAccounts ||
@ -386,6 +372,16 @@ 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({
@ -464,4 +460,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
});
}
}

45
apps/api/src/app/portfolio/portfolio.service.ts

@ -226,7 +226,7 @@ export class PortfolioService {
}): Promise<InvestmentItem[]> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const activities = await this.orderService.getOrders({
const { activities } = await this.orderService.getOrders({
filters,
userId,
types: ['DIVIDEND'],
@ -752,13 +752,13 @@ export class PortfolioService {
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
const orders = (
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ SymbolProfile }) => {
const { activities } = await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
});
const orders = activities.filter(({ SymbolProfile }) => {
return (
SymbolProfile.dataSource === aDataSource &&
SymbolProfile.symbol === aSymbol
@ -1758,12 +1758,11 @@ export class PortfolioService {
userId
});
const ordersRaw = await this.orderService.getOrders({
const { activities } = await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
});
const activities: Activity[] = [];
const excludedActivities: Activity[] = [];
let dividend = 0;
let fees = 0;
@ -1774,7 +1773,7 @@ export class PortfolioService {
let totalBuy = 0;
let totalSell = 0;
for (let order of ordersRaw) {
for (let order of activities) {
if (order.Account?.isExcluded ?? false) {
excludedActivities.push(order);
} else {
@ -1811,7 +1810,6 @@ export class PortfolioService {
}
}
}
const emergencyFund = new Big(
Math.max(
emergencyFundPositionsValueInBaseCurrency,
@ -1957,7 +1955,7 @@ export class PortfolioService {
const userCurrency =
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
const orders = await this.orderService.getOrders({
const { activities, count } = await this.orderService.getOrders({
filters,
includeDrafts,
userCurrency,
@ -1966,11 +1964,11 @@ export class PortfolioService {
types: ['BUY', 'SELL', 'STAKE']
});
if (orders.length <= 0) {
if (count <= 0) {
return { transactionPoints: [], orders: [], portfolioOrders: [] };
}
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({
currency: order.SymbolProfile.currency,
dataSource: order.SymbolProfile.dataSource,
date: format(order.date, DATE_FORMAT),
@ -2004,8 +2002,8 @@ export class PortfolioService {
portfolioCalculator.computeTransactionPoints();
return {
orders,
portfolioOrders,
orders: activities,
transactionPoints: portfolioCalculator.getTransactionPoints()
};
}
@ -2040,13 +2038,14 @@ export class PortfolioService {
userId: string;
withExcludedAccounts?: boolean;
}) {
const ordersOfTypeItemOrLiability = await this.orderService.getOrders({
filters,
userCurrency,
userId,
withExcludedAccounts,
types: ['ITEM', 'LIABILITY']
});
const { activities: ordersOfTypeItemOrLiability } =
await this.orderService.getOrders({
filters,
userCurrency,
userId,
withExcludedAccounts,
types: ['ITEM', 'LIABILITY']
});
const accounts: PortfolioDetails['accounts'] = {};
const platforms: PortfolioDetails['platforms'] = {};

14
apps/api/src/helper/object.helper.ts

@ -32,9 +32,11 @@ export function nullifyValuesInObjects<T>(aObjects: T[], keys: string[]): T[] {
}
export function redactAttributes({
isFirstRun = true,
object,
options
}: {
isFirstRun?: boolean;
object: any;
options: { attribute: string; valueMap: { [key: string]: any } }[];
}): any {
@ -42,7 +44,10 @@ export function redactAttributes({
return object;
}
const redactedObject = cloneDeep(object);
// Create deep clone
const redactedObject = isFirstRun
? JSON.parse(JSON.stringify(object))
: object;
for (const option of options) {
if (redactedObject.hasOwnProperty(option.attribute)) {
@ -59,7 +64,11 @@ export function redactAttributes({
if (isArray(redactedObject[property])) {
redactedObject[property] = redactedObject[property].map(
(currentObject) => {
return redactAttributes({ options, object: currentObject });
return redactAttributes({
options,
isFirstRun: false,
object: currentObject
});
}
);
} else if (
@ -69,6 +78,7 @@ export function redactAttributes({
// Recursively call the function on the nested object
redactedObject[property] = redactAttributes({
options,
isFirstRun: false,
object: redactedObject[property]
});
}

1
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -13,6 +13,7 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private static countriesMapping = {
'Russian Federation': 'Russia'
};
private static holdingsWeightTreshold = 0.85;
private static sectorsMapping = {
'Consumer Discretionary': 'Consumer Cyclical',
'Consumer Defensive': 'Consumer Staples',

6
apps/api/webpack.config.js

@ -0,0 +1,6 @@
const { composePlugins, withNx } = require('@nx/webpack');
module.exports = composePlugins(withNx(), (config, { options, context }) => {
// Customize webpack config here
return config;
});

26
apps/client/project.json

@ -150,48 +150,48 @@
}
},
"serve": {
"executor": "@nx/angular:webpack-dev-server",
"executor": "@nx/angular:dev-server",
"options": {
"proxyConfig": "apps/client/proxy.conf.json",
"browserTarget": "client:build"
"buildTarget": "client:build"
},
"configurations": {
"development-de": {
"browserTarget": "client:build:development-de"
"buildTarget": "client:build:development-de"
},
"development-en": {
"browserTarget": "client:build:development-en"
"buildTarget": "client:build:development-en"
},
"development-es": {
"browserTarget": "client:build:development-es"
"buildTarget": "client:build:development-es"
},
"development-fr": {
"browserTarget": "client:build:development-fr"
"buildTarget": "client:build:development-fr"
},
"development-it": {
"browserTarget": "client:build:development-it"
"buildTarget": "client:build:development-it"
},
"development-nl": {
"browserTarget": "client:build:development-nl"
"buildTarget": "client:build:development-nl"
},
"development-pl": {
"browserTarget": "client:build:development-pl"
"buildTarget": "client:build:development-pl"
},
"development-pt": {
"browserTarget": "client:build:development-pt"
"buildTarget": "client:build:development-pt"
},
"development-tr": {
"browserTarget": "client:build:development-tr"
"buildTarget": "client:build:development-tr"
},
"production": {
"browserTarget": "client:build:production"
"buildTarget": "client:build:production"
}
}
},
"extract-i18n": {
"executor": "ng-extract-i18n-merge:ng-extract-i18n-merge",
"options": {
"browserTarget": "client:build",
"buildTarget": "client:build",
"includeContext": true,
"outputPath": "src/locales",
"targetFiles": [

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,

10
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -9,7 +9,7 @@ import {
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort } from '@angular/material/sort';
import { MatSort, Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { AdminService } from '@ghostfolio/client/services/admin.service';
@ -19,7 +19,7 @@ import { getDateFormatString } from '@ghostfolio/common/helper';
import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { translate } from '@ghostfolio/ui/i18n';
import { AssetSubClass, DataSource, Prisma } from '@prisma/client';
import { AssetSubClass, DataSource } from '@prisma/client';
import { isUUID } from 'class-validator';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
@ -160,7 +160,7 @@ export class AdminMarketDataComponent
this.loadData({
sortColumn,
sortDirection: <Prisma.SortOrder>direction,
sortDirection: direction,
pageIndex: this.paginator.pageIndex
});
}
@ -175,7 +175,7 @@ export class AdminMarketDataComponent
this.loadData({
pageIndex: page.pageIndex,
sortColumn: this.sort.active,
sortDirection: <Prisma.SortOrder>this.sort.direction
sortDirection: this.sort.direction
});
}
@ -262,7 +262,7 @@ export class AdminMarketDataComponent
}: {
pageIndex: number;
sortColumn?: string;
sortDirection?: Prisma.SortOrder;
sortDirection?: SortDirection;
} = { pageIndex: 0 }
) {
this.isLoading = true;

8
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -119,8 +119,12 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
const currency = prompt($localize`Please add a currency:`);
if (currency) {
const currencies = uniq([...this.customCurrencies, currency]);
this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies });
if (currency.length === 3) {
const currencies = uniq([...this.customCurrencies, currency]);
this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies });
} else {
alert($localize`${currency} is an invalid currency!`);
}
}
}

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 stakeRewards: number;
public feeInBaseCurrency: number;
@ -52,7 +58,6 @@ 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 stakePrecision = 2;
@ -60,9 +65,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
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>();
@ -71,7 +80,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 {
@ -105,10 +115,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.stakeRewards = stakeRewards;
this.feeInBaseCurrency = feeInBaseCurrency;
@ -134,7 +146,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 = {};
@ -146,6 +157,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
};
});
this.transactionCount = transactionCount;
this.totalItems = transactionCount;
this.value = value;
if (SymbolProfile?.assetClass) {
@ -251,6 +263,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 {
@ -258,12 +280,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

@ -263,11 +263,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"
@ -294,7 +313,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,

77
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -1,5 +1,8 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { PageEvent } from '@angular/material/paginator';
import { Sort, SortDirection } from '@angular/material/sort';
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';
@ -10,6 +13,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -30,12 +34,18 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa
})
export class ActivitiesPageComponent implements OnDestroy, OnInit {
public activities: Activity[];
public dataSource: MatTableDataSource<Activity>;
public defaultAccountId: string;
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateActivity: boolean;
public hasPermissionToDeleteActivity: boolean;
public pageIndex = 0;
public pageSize = DEFAULT_PAGE_SIZE;
public routeQueryParams: Subscription;
public sortColumn = 'date';
public sortDirection: SortDirection = 'desc';
public totalItems: number;
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -62,6 +72,12 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
return id === params['activityId'];
});
this.openUpdateActivityDialog(activity);
} else if (this.dataSource) {
const activity = this.dataSource.data.find(({ id }) => {
return id === params['activityId'];
});
this.openUpdateActivityDialog(activity);
} else {
this.router.navigate(['.'], { relativeTo: this.route });
@ -103,21 +119,48 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
}
public fetchActivities() {
this.dataService
.fetchActivities({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
if (this.user?.settings?.isExperimentalFeatures === true) {
this.dataService
.fetchActivities({
skip: this.pageIndex * this.pageSize,
sortColumn: this.sortColumn,
sortDirection: this.sortDirection,
take: this.pageSize
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities, count }) => {
this.dataSource = new MatTableDataSource(activities);
this.totalItems = count;
if (
this.hasPermissionToCreateActivity &&
this.activities?.length <= 0
) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
if (this.hasPermissionToCreateActivity && this.totalItems <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
});
} else {
this.dataService
.fetchActivities({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
if (
this.hasPermissionToCreateActivity &&
this.activities?.length <= 0
) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
});
}
}
public onChangePage(page: PageEvent) {
this.pageIndex = page.pageIndex;
this.fetchActivities();
}
public onCloneActivity(aActivity: Activity) {
@ -225,6 +268,14 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
});
}
public onSortChanged({ active, direction }: Sort) {
this.pageIndex = 0;
this.sortColumn = active;
this.sortDirection = direction;
this.fetchActivities();
}
public onUpdateActivity(aActivity: OrderModel) {
this.router.navigate([], {
queryParams: { activityId: aActivity.id, editDialog: true }

26
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -2,7 +2,33 @@
<div class="mb-3 row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
<gf-activities-table-lazy
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
[hasPermissionToExportActivities]="!hasImpersonationId"
[locale]="user?.settings?.locale"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[totalItems]="totalItems"
(activityDeleted)="onDeleteActivity($event)"
(activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)"
(deleteAllActivities)="onDeleteAllActivities()"
(export)="onExport($event)"
(exportDrafts)="onExportDrafts($event)"
(import)="onImport()"
(importDividends)="onImportDividends()"
(pageChanged)="onChangePage($event)"
(sortChanged)="onSortChanged($event)"
></gf-activities-table-lazy>
<gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"

2
apps/client/src/app/pages/portfolio/activities/activities-page.module.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router';
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { ActivitiesPageRoutingModule } from './activities-page-routing.module';
@ -17,6 +18,7 @@ import { GfImportActivitiesDialogModule } from './import-activities-dialog/impor
ActivitiesPageRoutingModule,
CommonModule,
GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfCreateOrUpdateActivityDialogModule,
GfImportActivitiesDialogModule,
MatButtonModule,

12
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -12,7 +12,9 @@ import {
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SortDirection } from '@angular/material/sort';
import { MatStepper } from '@angular/material/stepper';
import { MatTableDataSource } from '@angular/material/table';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
@ -35,6 +37,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
export class ImportActivitiesDialog implements OnDestroy {
public accounts: CreateAccountDto[] = [];
public activities: Activity[] = [];
public dataSource: MatTableDataSource<Activity>;
public details: any[] = [];
public deviceType: string;
public dialogTitle = $localize`Import Activities`;
@ -45,7 +48,10 @@ export class ImportActivitiesDialog implements OnDestroy {
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
public mode: 'DIVIDEND';
public selectedActivities: Activity[] = [];
public sortColumn = 'date';
public sortDirection: SortDirection = 'desc';
public stepperOrientation: StepperOrientation;
public totalItems: number;
public uniqueAssetForm: FormGroup;
private unsubscribeSubject = new Subject<void>();
@ -173,6 +179,8 @@ export class ImportActivitiesDialog implements OnDestroy {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
this.dataSource = new MatTableDataSource(activities.reverse());
this.totalItems = activities.length;
aStepper.next();
@ -260,6 +268,8 @@ export class ImportActivitiesDialog implements OnDestroy {
isDryRun: true
});
this.activities = activities;
this.dataSource = new MatTableDataSource(activities.reverse());
this.totalItems = activities.length;
} catch (error) {
console.error(error);
this.handleImportError({ error, activities: content.activities });
@ -276,6 +286,8 @@ export class ImportActivitiesDialog implements OnDestroy {
userAccounts: this.data.user.accounts
});
this.activities = data.activities;
this.dataSource = new MatTableDataSource(data.activities.reverse());
this.totalItems = data.activities.length;
} catch (error) {
console.error(error);
this.handleImportError({

23
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -119,8 +119,29 @@
</ng-template>
<div class="pt-3">
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
<gf-activities-table-lazy
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="data?.user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="data?.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="false"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data?.user?.settings?.locale"
[pageSize]="maxSafeInteger"
[showActions]="false"
[showCheckbox]="true"
[showFooter]="false"
[showSymbolColumn]="false"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[sortDisabled]="true"
[totalItems]="totalItems"
(selectedActivities)="updateSelection($event)"
></gf-activities-table-lazy>
<gf-activities-table
*ngIf="importStep === 1"
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="data?.user?.settings?.baseCurrency"
[deviceType]="data?.deviceType"

2
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.module.ts

@ -13,6 +13,7 @@ import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-heade
import { GfFileDropModule } from '@ghostfolio/client/directives/file-drop/file-drop.module';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { ImportActivitiesDialog } from './import-activities-dialog.component';
@ -22,6 +23,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
CommonModule,
FormsModule,
GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfFileDropModule,

5
apps/client/src/app/services/admin.service.ts

@ -1,5 +1,6 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
@ -17,7 +18,7 @@ import {
Filter,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { DataSource, MarketData, Platform, Prisma, Tag } from '@prisma/client';
import { DataSource, MarketData, Platform, Tag } from '@prisma/client';
import { JobStatus } from 'bull';
import { format, parseISO } from 'date-fns';
import { Observable, map } from 'rxjs';
@ -84,7 +85,7 @@ export class AdminService {
filters?: Filter[];
skip?: number;
sortColumn?: string;
sortDirection?: Prisma.SortOrder;
sortDirection?: SortDirection;
take: number;
}) {
let params = this.dataService.buildFiltersAsQueryParams({ filters });

49
apps/client/src/app/services/data.service.ts

@ -1,5 +1,6 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
@ -149,23 +150,45 @@ export class DataService {
}
public fetchActivities({
filters
filters,
skip,
sortColumn,
sortDirection,
take
}: {
filters?: Filter[];
skip?: number;
sortColumn?: string;
sortDirection?: SortDirection;
take?: number;
}): Observable<Activities> {
return this.http
.get<any>('/api/v1/order', {
params: this.buildFiltersAsQueryParams({ filters })
let params = this.buildFiltersAsQueryParams({ filters });
if (skip) {
params = params.append('skip', skip);
}
if (sortColumn) {
params = params.append('sortColumn', sortColumn);
}
if (sortDirection) {
params = params.append('sortDirection', sortDirection);
}
if (take) {
params = params.append('take', take);
}
return this.http.get<any>('/api/v1/order', { params }).pipe(
map(({ activities, count }) => {
for (const activity of activities) {
activity.createdAt = parseISO(activity.createdAt);
activity.date = parseISO(activity.date);
}
return { activities, count };
})
.pipe(
map(({ activities }) => {
for (const activity of activities) {
activity.createdAt = parseISO(activity.createdAt);
activity.date = parseISO(activity.date);
}
return { activities };
})
);
);
}
public fetchDividends({

BIN
apps/client/src/assets/fonts/Inter-Black.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Black.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-BlackItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-BlackItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Bold.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Bold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-BoldItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-BoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraBold.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraBold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraBoldItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraBoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraLight.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraLight.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraLightItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraLightItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Italic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Italic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Light.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Light.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-LightItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-LightItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Medium.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Medium.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-MediumItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-MediumItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Regular.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Regular.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-SemiBold.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-SemiBold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-SemiBoldItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-SemiBoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Thin.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Thin.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ThinItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ThinItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-italic.var.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-roman.var.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter.var.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-Black.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-BlackItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-Bold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-BoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-ExtraBold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-ExtraBoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-ExtraLight.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-ExtraLightItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-Italic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-Light.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-LightItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-Medium.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-MediumItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-Regular.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-SemiBold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-SemiBoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-Thin.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterDisplay-ThinItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterVariable-Italic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/InterVariable.woff2

Binary file not shown.

261
apps/client/src/assets/fonts/inter.css

@ -1,226 +1,273 @@
/* Variable fonts usage:
:root { font-family: "Inter", sans-serif; }
@supports (font-variation-settings: normal) {
:root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; }
} */
@font-face {
font-family: InterVariable;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('InterVariable.woff2') format('woff2');
}
@font-face {
font-family: InterVariable;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('InterVariable-Italic.woff2') format('woff2');
}
/* static fonts */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src:
url('Inter-Thin.woff2?v=3.19') format('woff2'),
url('Inter-Thin.woff?v=3.19') format('woff');
src: url('Inter-Thin.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src:
url('Inter-ThinItalic.woff2?v=3.19') format('woff2'),
url('Inter-ThinItalic.woff?v=3.19') format('woff');
src: url('Inter-ThinItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src:
url('Inter-ExtraLight.woff2?v=3.19') format('woff2'),
url('Inter-ExtraLight.woff?v=3.19') format('woff');
src: url('Inter-ExtraLight.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src:
url('Inter-ExtraLightItalic.woff2?v=3.19') format('woff2'),
url('Inter-ExtraLightItalic.woff?v=3.19') format('woff');
src: url('Inter-ExtraLightItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src:
url('Inter-Light.woff2?v=3.19') format('woff2'),
url('Inter-Light.woff?v=3.19') format('woff');
src: url('Inter-Light.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src:
url('Inter-LightItalic.woff2?v=3.19') format('woff2'),
url('Inter-LightItalic.woff?v=3.19') format('woff');
src: url('Inter-LightItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src:
url('Inter-Regular.woff2?v=3.19') format('woff2'),
url('Inter-Regular.woff?v=3.19') format('woff');
src: url('Inter-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src:
url('Inter-Italic.woff2?v=3.19') format('woff2'),
url('Inter-Italic.woff?v=3.19') format('woff');
src: url('Inter-Italic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src:
url('Inter-Medium.woff2?v=3.19') format('woff2'),
url('Inter-Medium.woff?v=3.19') format('woff');
src: url('Inter-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src:
url('Inter-MediumItalic.woff2?v=3.19') format('woff2'),
url('Inter-MediumItalic.woff?v=3.19') format('woff');
src: url('Inter-MediumItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src:
url('Inter-SemiBold.woff2?v=3.19') format('woff2'),
url('Inter-SemiBold.woff?v=3.19') format('woff');
src: url('Inter-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src:
url('Inter-SemiBoldItalic.woff2?v=3.19') format('woff2'),
url('Inter-SemiBoldItalic.woff?v=3.19') format('woff');
src: url('Inter-SemiBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src:
url('Inter-Bold.woff2?v=3.19') format('woff2'),
url('Inter-Bold.woff?v=3.19') format('woff');
src: url('Inter-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src:
url('Inter-BoldItalic.woff2?v=3.19') format('woff2'),
url('Inter-BoldItalic.woff?v=3.19') format('woff');
src: url('Inter-BoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src:
url('Inter-ExtraBold.woff2?v=3.19') format('woff2'),
url('Inter-ExtraBold.woff?v=3.19') format('woff');
src: url('Inter-ExtraBold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src:
url('Inter-ExtraBoldItalic.woff2?v=3.19') format('woff2'),
url('Inter-ExtraBoldItalic.woff?v=3.19') format('woff');
src: url('Inter-ExtraBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src:
url('Inter-Black.woff2?v=3.19') format('woff2'),
url('Inter-Black.woff?v=3.19') format('woff');
src: url('Inter-Black.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src:
url('Inter-BlackItalic.woff2?v=3.19') format('woff2'),
url('Inter-BlackItalic.woff?v=3.19') format('woff');
src: url('Inter-BlackItalic.woff2') format('woff2');
}
/* -------------------------------------------------------
Variable font.
Usage:
html { font-family: 'Inter', sans-serif; }
@supports (font-variation-settings: normal) {
html { font-family: 'Inter var', sans-serif; }
}
*/
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-family: 'InterDisplay';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url('InterDisplay-Thin.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url('InterDisplay-ThinItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-named-instance: 'Regular';
src: url('Inter-roman.var.woff2?v=3.19') format('woff2');
font-weight: 200;
font-display: swap;
src: url('InterDisplay-ExtraLight.woff2') format('woff2');
}
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-family: 'InterDisplay';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url('InterDisplay-ExtraLightItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('InterDisplay-Light.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-named-instance: 'Italic';
src: url('Inter-italic.var.woff2?v=3.19') format('woff2');
font-weight: 300;
font-display: swap;
src: url('InterDisplay-LightItalic.woff2') format('woff2');
}
/* --------------------------------------------------------------------------
[EXPERIMENTAL] Multi-axis, single variable font.
Slant axis is not yet widely supported (as of February 2019) and thus this
multi-axis single variable font is opt-in rather than the default.
When using this, you will probably need to set font-variation-settings
explicitly, e.g.
* { font-variation-settings: "slnt" 0deg }
.italic { font-variation-settings: "slnt" 10deg }
*/
@font-face {
font-family: 'Inter var experimental';
font-weight: 100 900;
font-family: 'InterDisplay';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('InterDisplay-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('InterDisplay-Italic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('InterDisplay-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('InterDisplay-MediumItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('InterDisplay-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('InterDisplay-SemiBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('InterDisplay-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('InterDisplay-BoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('InterDisplay-ExtraBold.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('InterDisplay-ExtraBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url('InterDisplay-Black.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 900;
font-display: swap;
font-style: oblique 0deg 10deg;
src: url('Inter.var.woff2?v=3.19') format('woff2');
src: url('InterDisplay-BlackItalic.woff2') format('woff2');
}

346
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

4
apps/client/src/styles/theme.scss

@ -111,5 +111,7 @@ $gf-theme-dark: mat.define-dark-theme(
--gf-theme-secondary-500: #3686cf;
--gf-theme-secondary-500-rgb: 78, 208, 94;
--mdc-typography-button-letter-spacing: normal;
--mdc-filled-button-label-text-tracking: normal;
--mdc-outlined-button-label-text-tracking: normal;
--mdc-text-button-label-text-tracking: normal;
}

6
libs/common/src/lib/helper.ts

@ -381,10 +381,10 @@ export function resolveFearAndGreedIndex(aValue: number) {
export function resolveMarketCondition(
aMarketCondition: Benchmark['marketCondition']
) {
if (aMarketCondition === 'BEAR_MARKET') {
if (aMarketCondition === 'ALL_TIME_HIGH') {
return { emoji: '🎉' };
} else if (aMarketCondition === 'BEAR_MARKET') {
return { emoji: '🐻' };
} else if (aMarketCondition === 'BULL_MARKET') {
return { emoji: '🐮' };
} else {
return { emoji: '⚪' };
}

2
libs/common/src/lib/interfaces/benchmark.interface.ts

@ -3,7 +3,7 @@ import { BenchmarkTrend } from '@ghostfolio/common/types/';
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
export interface Benchmark {
marketCondition: 'BEAR_MARKET' | 'BULL_MARKET' | 'NEUTRAL_MARKET';
marketCondition: 'ALL_TIME_HIGH' | 'BEAR_MARKET' | 'NEUTRAL_MARKET';
name: EnhancedSymbolProfile['name'];
performances: {
allTimeHigh: {

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

@ -0,0 +1,492 @@
<div *ngIf="hasPermissionToCreateActivity" class="d-flex justify-content-end">
<button
class="align-items-center d-flex"
mat-stroked-button
(click)="onImport()"
>
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<ng-container i18n>Import Activities</ng-container>...
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="mx-1 no-min-width px-2"
mat-stroked-button
[matMenuTriggerFor]="activitiesMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #activitiesMenu="matMenu" xPosition="before">
<button
mat-menu-item
[disabled]="dataSource?.data.length === 0"
(click)="onImportDividends()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
<ng-container i18n>Import Dividends</ng-container>...
</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="dataSource?.data.length === 0"
(click)="onExport()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export Activities</span>
</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="!hasDrafts"
(click)="onExportDrafts()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Drafts as ICS</span>
</span>
</button>
<button
class="align-items-center d-flex"
mat-menu-item
(click)="onDeleteAllActivities()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete all Activities</span>
</span>
</button>
</mat-menu>
</div>
<div class="activities">
<table
class="gf-table w-100"
mat-table
matSort
[dataSource]="dataSource"
[matSortActive]="sortColumn"
[matSortDirection]="sortDirection"
[matSortDisabled]="sortDisabled"
>
<ng-container matColumnDef="select" sticky>
<th *matHeaderCellDef class="px-1" mat-header-cell>
<mat-checkbox
color="primary"
[checked]="
areAllRowsSelected() && !hasErrors && selectedRows.hasValue()
"
[disabled]="hasErrors"
[indeterminate]="selectedRows.hasValue() && !areAllRowsSelected()"
(change)="$event ? toggleAllRows() : null"
></mat-checkbox>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<mat-checkbox
color="primary"
[checked]="element.error ? false : selectedRows.isSelected(element)"
[disabled]="element.error"
(change)="$event ? selectedRows.toggle(element) : null"
(click)="$event.stopPropagation()"
></mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="importStatus">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n></ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div
*ngIf="element.error"
class="d-flex"
matTooltipPosition="above"
[matTooltip]="element.error.message"
>
<ion-icon class="text-danger" name="alert-circle-outline"></ion-icon>
</div>
</td>
</ng-container>
<ng-container matColumnDef="icon">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-symbol-icon
[dataSource]="element.SymbolProfile?.dataSource"
[symbol]="element.SymbolProfile?.symbol"
[tooltip]="element.SymbolProfile?.name"
></gf-symbol-icon>
<div>{{ element.dataSource }}</div>
</td>
</ng-container>
<ng-container matColumnDef="nameWithSymbol">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
<div class="d-flex align-items-center">
<div>
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span>
<span
*ngIf="element.isDraft"
class="badge badge-secondary ml-1"
i18n
>Draft</span
>
</div>
</div>
<div *ngIf="!isUUID(element.SymbolProfile?.symbol)">
<small class="text-muted">{{
element.SymbolProfile?.symbol | gfSymbol
}}</small>
</div>
</td>
</ng-container>
<ng-container matColumnDef="type">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Type</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<gf-activity-type [activityType]="element.type"></gf-activity-type>
</td>
</ng-container>
<ng-container matColumnDef="date">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Date</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
{{ element.date | date: defaultDateFormat }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
mat-sort-header
>
<ng-container i18n>Quantity</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.quantity"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="unitPrice">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
mat-sort-header
>
<ng-container i18n>Unit Price</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.unitPrice"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="fee">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
mat-sort-header
>
<ng-container i18n>Fee</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.fee"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="value">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
>
<ng-container i18n>Value</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.value"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="currency">
<th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
<ng-container i18n>Currency</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
{{ element.SymbolProfile?.currency }}
</td>
</ng-container>
<ng-container matColumnDef="valueInBaseCurrency">
<th
*matHeaderCellDef
class="d-lg-none d-xl-none justify-content-end px-1"
mat-header-cell
>
<ng-container i18n>Value</ng-container>
</th>
<td *matCellDef="let element" class="d-lg-none d-xl-none px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.valueInBaseCurrency"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="account">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<span class="d-none d-lg-block" i18n>Account</span>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
<gf-symbol-icon
*ngIf="element.Account?.Platform?.url"
class="mr-1"
[tooltip]="element.Account?.Platform?.name"
[url]="element.Account?.Platform?.url"
></gf-symbol-icon>
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="comment">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1"
mat-header-cell
></th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<button
*ngIf="element.comment"
class="mx-1 no-min-width px-2"
mat-button
title="Note"
(click)="onOpenComment(element.comment); $event.stopPropagation()"
>
<ion-icon name="document-text-outline"></ion-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
<button
*ngIf="
!hasPermissionToCreateActivity && hasPermissionToExportActivities
"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="activitiesMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #activitiesMenu="matMenu" xPosition="before">
<button
*ngIf="hasPermissionToCreateActivity"
class="align-items-center d-flex"
mat-menu-item
(click)="onImport()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<ng-container i18n>Import Activities</ng-container>...
</span>
</button>
<button
*ngIf="hasPermissionToCreateActivity"
mat-menu-item
[disabled]="dataSource?.data.length === 0"
(click)="onImportDividends()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
<ng-container i18n>Import Dividends</ng-container>...
</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="dataSource?.data.length === 0"
(click)="onExport()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export Activities</span>
</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="!hasDrafts"
(click)="onExportDrafts()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Drafts as ICS</span>
</span>
</button>
</mat-menu>
</th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<button
*ngIf="showActions"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="activityMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal"></ion-icon>
</button>
<mat-menu #activityMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdateActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline"></ion-icon>
<span i18n>Edit</span>
</span>
</button>
<button mat-menu-item (click)="onCloneActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="copy-outline"></ion-icon>
<span i18n>Clone</span>
</span>
</button>
<button
mat-menu-item
[disabled]="!element.isDraft"
(click)="onExportDraft(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Draft as ICS</span>
</span>
</button>
<button mat-menu-item (click)="onDeleteActivity(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
mat-row
[ngClass]="{
'cursor-pointer':
hasPermissionToOpenDetails &&
!row.isDraft &&
row.type !== 'FEE' &&
row.type !== 'INTEREST' &&
row.type !== 'ITEM' &&
row.type !== 'LIABILITY'
}"
(click)="onClickActivity(row)"
></tr>
</table>
</div>
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
class="px-4 py-3"
[theme]="{
height: '1.5rem',
width: '100%'
}"
></ngx-skeleton-loader>
<mat-paginator
[length]="totalItems"
[ngClass]="{
'd-none': (isLoading && !totalItems) || totalItems <= pageSize
}"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[showFirstLastButtons]="true"
(page)="onChangePage($event)"
></mat-paginator>
<div
*ngIf="
dataSource?.data.length === 0 && hasPermissionToCreateActivity && !isLoading
"
class="p-3 text-center"
>
<gf-no-transactions-info-indicator
[hasBorder]="false"
></gf-no-transactions-info-indicator>
</div>

9
libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.scss

@ -0,0 +1,9 @@
@import 'apps/client/src/styles/ghostfolio-style';
:host {
display: block;
.activities {
overflow-x: auto;
}
}

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

@ -0,0 +1,250 @@
import { SelectionModel } from '@angular/cdk/collections';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort, SortDirection } 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 { 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 AfterViewInit, OnChanges, OnDestroy, OnInit
{
@Input() baseCurrency: string;
@Input() dataSource: MatTableDataSource<Activity>;
@Input() deviceType: string;
@Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToExportActivities: boolean;
@Input() hasPermissionToOpenDetails = true;
@Input() locale: string;
@Input() pageIndex: number;
@Input() pageSize = DEFAULT_PAGE_SIZE;
@Input() showActions = true;
@Input() showCheckbox = false;
@Input() showFooter = true;
@Input() showNameColumn = true;
@Input() sortColumn: string;
@Input() sortDirection: SortDirection;
@Input() sortDisabled = false;
@Input() totalItems = Number.MAX_SAFE_INTEGER;
@Output() activityDeleted = new EventEmitter<string>();
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@Output() deleteAllActivities = new EventEmitter<void>();
@Output() export = new EventEmitter<string[]>();
@Output() exportDrafts = new EventEmitter<string[]>();
@Output() import = new EventEmitter<void>();
@Output() importDividends = new EventEmitter<UniqueAsset>();
@Output() pageChanged = new EventEmitter<PageEvent>();
@Output() selectedActivities = new EventEmitter<Activity[]>();
@Output() sortChanged = new EventEmitter<Sort>();
@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 routeQueryParams: Subscription;
public searchKeywords: string[] = [];
public selectedRows = new SelectionModel<Activity>(true, []);
private unsubscribeSubject = new Subject<void>();
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 ngAfterViewInit() {
this.sort.sortChange.subscribe((value: Sort) => {
this.sortChanged.emit(value);
});
}
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',
'icon',
'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.isLoading = false;
}
}
public onChangePage(page: PageEvent) {
this.pageChanged.emit(page);
}
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();
}
}

42
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 {}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save