This commit is contained in:
sudacode 2025-01-19 22:27:06 -08:00
commit d1688242b0
25 changed files with 1922 additions and 2460 deletions

View File

@ -1,151 +0,0 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@nx/enforce-module-boundaries": [
"warn",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
],
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"]
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"]
},
{
"files": ["*.ts"],
"plugins": ["eslint-plugin-import", "@typescript-eslint"],
"extends": [
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked"
],
"rules": {
"@typescript-eslint/consistent-indexed-object-style": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/member-ordering": "warn",
"@typescript-eslint/naming-convention": [
"off",
{
"selector": "default",
"format": ["camelCase"],
"leadingUnderscore": "allow",
"trailingUnderscore": "allow"
},
{
"selector": ["variable", "classProperty", "typeProperty"],
"format": ["camelCase", "UPPER_CASE"],
"leadingUnderscore": "allow",
"trailingUnderscore": "allow"
},
{
"selector": "objectLiteralProperty",
"format": null
},
{
"selector": "enumMember",
"format": ["camelCase", "UPPER_CASE", "PascalCase"]
},
{
"selector": "typeLike",
"format": ["PascalCase"]
}
],
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/no-inferrable-types": [
"warn",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-shadow": [
"warn",
{
"hoist": "all"
}
],
"@typescript-eslint/unified-signatures": "error",
"@typescript-eslint/no-loss-of-precision": "warn",
"@typescript-eslint/no-var-requires": "warn",
"@typescript-eslint/ban-types": "warn",
"arrow-body-style": "off",
"constructor-super": "error",
"eqeqeq": ["error", "smart"],
"guard-for-in": "warn",
"id-blacklist": "off",
"id-match": "off",
"import/no-deprecated": "warn",
"no-bitwise": "error",
"no-caller": "error",
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-restricted-imports": ["error", "rxjs/Rx"],
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-var": "error",
"radix": "error",
"no-unsafe-optional-chaining": "warn",
"no-extra-boolean-cast": "warn",
"no-empty-pattern": "warn",
"no-useless-catch": "warn",
"no-unsafe-finally": "warn",
"no-prototype-builtins": "warn",
"no-async-promise-executor": "warn",
"no-constant-condition": "warn",
// The following rules are part of @typescript-eslint/recommended-type-checked
// and can be remove once solved
"@typescript-eslint/await-thenable": "warn",
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/no-base-to-string": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/no-redundant-type-constituents": "warn",
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-enum-comparison": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/require-await": "warn",
"@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/unbound-method": "warn",
// The following rules are part of @typescript-eslint/stylistic-type-checked
// and can be remove once solved
"@typescript-eslint/prefer-nullish-coalescing": "warn" // TODO: Requires strictNullChecks: true
}
}
],
"extends": ["plugin:storybook/recommended"]
}

View File

@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.135.0 - 2025-01-19
### Changed
- Moved the language localization for Polski (`pl`) from experimental to general availability
- Extended the _Financial Modeling Prep_ service
- Switched to _ESLint_s flat config format
- Upgraded `chart.js` from version `4.2.0` to `4.4.7`
- Upgraded `chartjs-chart-treemap` from version `2.3.1` to `3.1.0`
- Upgraded `chartjs-plugin-annotation` from version `2.1.2` to `3.1.0`
- Upgraded `eslint` dependencies
- Upgraded `nestjs` from version `10.1.3` to `10.4.15`
- Upgraded `Nx` from version `20.3.0` to `20.3.2`
- Upgraded `reflect-metadata` from version `0.1.13` to `0.2.2`
- Upgraded `uuid` from version `11.0.2` to `11.0.5`
## 2.134.0 - 2025-01-15
### Added

View File

@ -1,22 +0,0 @@
{
"extends": "../../.eslintrc.json",
"ignorePatterns": ["!**/*"],
"rules": {},
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/api/tsconfig.*?.json"]
},
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,31 @@
const baseConfig = require('../../eslint.config.cjs');
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
{
rules: {}
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['apps/api/tsconfig.*?.json']
}
}
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}
}
];

View File

@ -47,7 +47,7 @@ export class SymbolController {
try {
return this.symbolService.lookup({
includeIndices,
query: query.toLowerCase(),
query,
user: this.request.user
});
} catch {

View File

@ -504,12 +504,10 @@
<loc>https://ghostfol.io/pl/o-ghostfolio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<!--
<url>
<loc>https://ghostfol.io/pl/open</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
-->
<url>
<loc>https://ghostfol.io/pl/rynki</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -1,3 +1,5 @@
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common';
const cryptocurrencies = require('../../assets/cryptocurrencies/cryptocurrencies.json');
@ -9,7 +11,11 @@ export class CryptocurrencyService {
public isCryptocurrency(aSymbol = '') {
const cryptocurrencySymbol = aSymbol.substring(0, aSymbol.length - 3);
return this.getCryptocurrencies().includes(cryptocurrencySymbol);
return (
aSymbol.endsWith(DEFAULT_CURRENCY) &&
this.getCryptocurrencies().includes(cryptocurrencySymbol)
);
}
private getCryptocurrencies() {

View File

@ -1,4 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import {
DataProviderInterface,
GetDividendsParams,
@ -10,7 +11,6 @@ import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import {
DataProviderInfo,
@ -19,16 +19,31 @@ import {
} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import { format, isAfter, isBefore, isSameDay } from 'date-fns';
import {
AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
} from '@prisma/client';
import { isISIN } from 'class-validator';
import { countries } from 'countries-list';
import {
addDays,
format,
isAfter,
isBefore,
isSameDay,
parseISO
} from 'date-fns';
@Injectable()
export class FinancialModelingPrepService implements DataProviderInterface {
private apiKey: string;
private readonly URL = 'https://financialmodelingprep.com/api/v3';
private readonly URL = this.getUrl({ version: 3 });
public constructor(
private readonly configurationService: ConfigurationService
private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService
) {
this.apiKey = this.configurationService.get(
'API_KEY_FINANCIAL_MODELING_PREP'
@ -44,10 +59,152 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
return {
const response: Partial<SymbolProfile> = {
symbol,
dataSource: this.getName()
};
try {
if (this.cryptocurrencyService.isCryptocurrency(symbol)) {
const [quote] = await fetch(
`${this.URL}/quote/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.assetClass = AssetClass.LIQUIDITY;
response.assetSubClass = AssetSubClass.CRYPTOCURRENCY;
response.currency = symbol.substring(symbol.length - 3);
response.name = quote.name;
} else {
const [assetProfile] = await fetch(
`${this.URL}/profile/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
const { assetClass, assetSubClass } =
this.parseAssetClass(assetProfile);
response.assetClass = assetClass;
response.assetSubClass = assetSubClass;
if (assetSubClass === AssetSubClass.ETF) {
const etfCountryWeightings = await fetch(
`${this.URL}/etf-country-weightings/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.countries = etfCountryWeightings.map(
({ country: countryName, weightPercentage }) => {
let countryCode: string;
for (const [code, country] of Object.entries(countries)) {
if (country.name === countryName) {
countryCode = code;
break;
}
}
return {
code: countryCode,
weight: parseFloat(weightPercentage.slice(0, -1)) / 100
};
}
);
const [portfolioDate] = await fetch(
`${this.getUrl({ version: 4 })}/etf-holdings/portfolio-date?symbol=${symbol}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
if (portfolioDate) {
const etfHoldings = await fetch(
`${this.getUrl({ version: 4 })}/etf-holdings?date=${portfolioDate.date}&symbol=${symbol}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
const sortedTopHoldings = etfHoldings
.sort((a, b) => {
return b.pctVal - a.pctVal;
})
.slice(0, 10);
response.holdings = sortedTopHoldings.map(({ name, pctVal }) => {
return { name, weight: pctVal / 100 };
});
}
const etfSectorWeightings = await fetch(
`${this.URL}/etf-sector-weightings/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.sectors = etfSectorWeightings.map(
({ sector, weightPercentage }) => {
return {
name: sector,
weight: parseFloat(weightPercentage.slice(0, -1)) / 100
};
}
);
} else if (assetSubClass === AssetSubClass.STOCK) {
if (assetProfile.country) {
response.countries = [{ code: assetProfile.country, weight: 1 }];
}
if (assetProfile.sector) {
response.sectors = [{ name: assetProfile.sector, weight: 1 }];
}
}
response.currency = assetProfile.currency;
if (assetProfile.isin) {
response.isin = assetProfile.isin;
}
response.name = assetProfile.companyName;
if (assetProfile.website) {
response.url = assetProfile.website;
}
}
} catch (error) {
let message = error;
if (error?.name === 'AbortError') {
message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`;
}
Logger.error(message, 'FinancialModelingPrepService');
}
return response;
}
public getDataProviderInfo(): DataProviderInfo {
@ -58,9 +215,55 @@ export class FinancialModelingPrepService implements DataProviderInterface {
};
}
public async getDividends({}: GetDividendsParams) {
public async getDividends({
from,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol,
to
}: GetDividendsParams) {
if (isSameDay(from, to)) {
to = addDays(to, 1);
}
try {
const response: {
[date: string]: IDataProviderHistoricalResponse;
} = {};
const { historical } = await fetch(
`${this.URL}/historical-price-full/stock_dividend/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
historical
.filter(({ date }) => {
return (
(isSameDay(parseISO(date), from) ||
isAfter(parseISO(date), from)) &&
isBefore(parseISO(date), to)
);
})
.forEach(({ adjDividend, date }) => {
response[date] = {
marketPrice: adjDividend
};
});
return response;
} catch (error) {
Logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`,
'FinancialModelingPrepService'
);
return {};
}
}
public async getHistorical({
from,
@ -84,14 +287,14 @@ export class FinancialModelingPrepService implements DataProviderInterface {
[symbol]: {}
};
for (const { close, date } of historical) {
for (const { adjClose, date } of historical) {
if (
(isSameDay(parseDate(date), from) ||
isAfter(parseDate(date), from)) &&
isBefore(parseDate(date), to)
) {
result[symbol][date] = {
marketPrice: close
marketPrice: adjClose
};
}
}
@ -130,8 +333,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
).then((res) => res.json());
for (const { price, symbol } of quotes) {
const { currency } = await this.getAssetProfile({ symbol });
response[symbol] = {
currency: DEFAULT_CURRENCY,
currency,
dataProviderInfo: this.getDataProviderInfo(),
dataSource: DataSource.FINANCIAL_MODELING_PREP,
marketPrice: price,
@ -161,6 +366,28 @@ export class FinancialModelingPrepService implements DataProviderInterface {
let items: LookupItem[] = [];
try {
if (isISIN(query)) {
const result = await fetch(
`${this.getUrl({ version: 4 })}/search/isin?isin=${query}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
items = result.map(({ companyName, currency, symbol }) => {
return {
currency,
symbol,
assetClass: undefined, // TODO
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(),
name: companyName
};
});
} else {
const result = await fetch(
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
{
@ -172,14 +399,16 @@ export class FinancialModelingPrepService implements DataProviderInterface {
items = result.map(({ currency, name, symbol }) => {
return {
// TODO: Add assetClass
// TODO: Add assetSubClass
currency,
name,
symbol,
assetClass: undefined, // TODO
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName()
};
});
}
} catch (error) {
let message = error;
@ -194,4 +423,29 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return { items };
}
private getUrl({ version }: { version: number }) {
return `https://financialmodelingprep.com/api/v${version}`;
}
private parseAssetClass(profile: any): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {
let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
if (profile.isEtf) {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
} else if (profile.isFund) {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.MUTUALFUND;
} else {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
}
return { assetClass, assetSubClass };
}
}

View File

@ -1,46 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/client/tsconfig.*?.json"]
},
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts"],
"rules": {
"@angular-eslint/prefer-standalone": "off"
}
}
],
"plugins": ["@angular-eslint/eslint-plugin", "@typescript-eslint"],
"rules": {
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "gf",
"style": "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "gf",
"style": "camelCase"
}
]
}
}

View File

@ -0,0 +1,62 @@
const baseConfig = require('../../eslint.config.cjs');
const angularEslintPlugin = require('@angular-eslint/eslint-plugin');
const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
{
plugins: {
'@angular-eslint': angularEslintPlugin,
'@typescript-eslint': typescriptEslintPlugin
}
},
{
rules: {
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'gf',
style: 'kebab-case'
}
],
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'gf',
style: 'camelCase'
}
]
}
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['apps/client/tsconfig.*?.json']
}
}
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off'
}
}
];

View File

@ -12,7 +12,9 @@
[href]="pricingUrl"
>
@if (isGhostfolioApiKeyValid === false) {
<span class="badge badge-warning mr-1" i18n>NEW</span>
<span class="badge badge-warning mr-1" i18n
>Early Access</span
>
}
Ghostfolio Premium
<gf-premium-indicator

View File

@ -100,12 +100,10 @@
>Nederlands (<ng-container i18n>Community</ng-container
>)</mat-option
>
@if (user?.settings?.isExperimentalFeatures) {
<mat-option value="pl"
>Polski (<ng-container i18n>Community</ng-container
>)</mat-option
>
}
<mat-option value="pt"
>Português (<ng-container i18n>Community</ng-container
>)</mat-option

View File

@ -244,9 +244,8 @@
Use Ghostfolio in multiple languages: English,
<!-- Català, -->
<!-- Chinese, -->
Dutch, French, German, Italian,
<!-- Polish, -->
Portuguese, Spanish and Turkish
Dutch, French, German, Italian, Polish, Portuguese, Spanish
and Turkish
<!-- and Ukrainian -->
are currently supported.
</p>

View File

@ -1,20 +0,0 @@
{
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["apps/ui-e2e/tsconfig.json"]
},
"rules": {}
},
{
"files": ["src/plugins/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off"
}
}
]
}

View File

@ -0,0 +1,33 @@
const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../../eslint.config.cjs');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended
});
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
...compat.extends('plugin:cypress/recommended'),
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['apps/ui-e2e/tsconfig.json']
}
}
},
{
files: ['src/plugins/index.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'no-undef': 'off'
}
}
];

196
eslint.config.cjs Normal file
View File

@ -0,0 +1,196 @@
const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
const storybook = require('eslint-plugin-storybook');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended
});
module.exports = [
{
ignores: ['**/dist']
},
...storybook.configs['flat/recommended'],
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'warn',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*']
}
]
}
],
'@typescript-eslint/no-extra-semi': 'error',
'no-extra-semi': 'off'
}
},
...compat
.config({
extends: ['plugin:@nx/typescript']
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
rules: {
...config.rules
}
})),
...compat
.config({
extends: ['plugin:@nx/javascript']
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules
}
})),
...compat
.config({
plugins: ['eslint-plugin-import', '@typescript-eslint'],
extends: [
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:@typescript-eslint/stylistic-type-checked'
]
})
.map((config) => ({
...config,
files: ['**/*.ts'],
rules: {
...config.rules,
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/dot-notation': 'off',
'@typescript-eslint/explicit-member-accessibility': [
'off',
{
accessibility: 'explicit'
}
],
'@typescript-eslint/member-ordering': 'warn',
'@typescript-eslint/naming-convention': [
'off',
{
selector: 'default',
format: ['camelCase'],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: ['variable', 'classProperty', 'typeProperty'],
format: ['camelCase', 'UPPER_CASE'],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: 'objectLiteralProperty',
format: null
},
{
selector: 'enumMember',
format: ['camelCase', 'UPPER_CASE', 'PascalCase']
},
{
selector: 'typeLike',
format: ['PascalCase']
}
],
'@typescript-eslint/no-empty-interface': 'warn',
'@typescript-eslint/no-inferrable-types': [
'warn',
{
ignoreParameters: true
}
],
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-shadow': [
'warn',
{
hoist: 'all'
}
],
'@typescript-eslint/unified-signatures': 'error',
'@typescript-eslint/no-loss-of-precision': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
'arrow-body-style': 'off',
'constructor-super': 'error',
eqeqeq: ['error', 'smart'],
'guard-for-in': 'warn',
'id-blacklist': 'off',
'id-match': 'off',
'import/no-deprecated': 'warn',
'no-bitwise': 'error',
'no-caller': 'error',
'no-debugger': 'error',
'no-empty': 'off',
'no-eval': 'error',
'no-fallthrough': 'error',
'no-new-wrappers': 'error',
'no-restricted-imports': ['error', 'rxjs/Rx'],
'no-undef-init': 'error',
'no-underscore-dangle': 'off',
'no-var': 'error',
radix: 'error',
'no-unsafe-optional-chaining': 'warn',
'no-extra-boolean-cast': 'warn',
'no-empty-pattern': 'warn',
'no-useless-catch': 'warn',
'no-unsafe-finally': 'warn',
'no-prototype-builtins': 'warn',
'no-async-promise-executor': 'warn',
'no-constant-condition': 'warn',
// The following rules are part of eslint:recommended
// and can be remove once solved
'no-constant-binary-expression': 'warn',
'no-loss-of-precision': 'warn',
// The following rules are part of @typescript-eslint/recommended-type-checked
// and can be remove once solved
'@typescript-eslint/await-thenable': 'warn',
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-base-to-string': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-misused-promises': 'warn',
'@typescript-eslint/no-redundant-type-constituents': 'warn',
'@typescript-eslint/no-require-imports': 'warn',
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
'@typescript-eslint/no-unsafe-function-type': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
caughtErrors: 'none'
}
],
'@typescript-eslint/no-wrapper-object-types': 'warn',
'@typescript-eslint/only-throw-error': 'warn',
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
'@typescript-eslint/require-await': 'warn',
'@typescript-eslint/restrict-template-expressions': 'warn',
'@typescript-eslint/unbound-method': 'warn',
// The following rules are part of @typescript-eslint/stylistic-type-checked
// and can be remove once solved
'@typescript-eslint/prefer-nullish-coalescing': 'warn', // TODO: Requires strictNullChecks: true
'@typescript-eslint/prefer-regexp-exec': 'warn'
}
}))
];

View File

@ -1,21 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["libs/common/tsconfig.*?.json"]
},
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,28 @@
const baseConfig = require('../../eslint.config.cjs');
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
languageOptions: {
parserOptions: {
project: ['libs/common/tsconfig.*?.json']
}
}
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {}
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {}
}
];

View File

@ -1,40 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "**/*.stories.ts"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"parserOptions": {
"project": ["libs/ui/tsconfig.*?.json"]
},
"extends": [
"plugin:@nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "gf",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "gf",
"style": "kebab-case"
}
],
"@angular-eslint/prefer-standalone": "off"
}
},
{
"files": ["*.html"],
"extends": ["plugin:@nx/angular-template"],
"rules": {}
}
]
}

65
libs/ui/eslint.config.cjs Normal file
View File

@ -0,0 +1,65 @@
const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../../eslint.config.cjs');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended
});
module.exports = [
{
ignores: ['**/dist']
},
...baseConfig,
...compat
.config({
extends: [
'plugin:@nx/angular',
'plugin:@angular-eslint/template/process-inline-templates'
]
})
.map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
...config.rules,
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'gf',
style: 'camelCase'
}
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'gf',
style: 'kebab-case'
}
],
'@angular-eslint/prefer-standalone': 'off'
},
languageOptions: {
parserOptions: {
project: ['libs/ui/tsconfig.*?.json']
}
}
})),
...compat
.config({
extends: ['plugin:@nx/angular-template']
})
.map((config) => ({
...config,
files: ['**/*.html'],
rules: {
...config.rules
}
})),
{
ignores: ['**/*.stories.ts']
}
];

View File

@ -77,7 +77,7 @@ export class GfPortfolioProportionChartComponent
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'pie'>;
public chart: Chart<'doughnut'>;
public isLoading = true;
private readonly OTHER_KEY = 'OTHER';
@ -257,7 +257,7 @@ export class GfPortfolioProportionChartComponent
});
});
const datasets: ChartConfiguration['data']['datasets'] = [
const datasets: ChartConfiguration<'doughnut'>['data']['datasets'] = [
{
backgroundColor: chartDataSorted.map(([, item]) => {
return item.color;
@ -295,7 +295,7 @@ export class GfPortfolioProportionChartComponent
datasets[1].data[1] = Number.MAX_SAFE_INTEGER;
}
const data: ChartConfiguration['data'] = {
const data: ChartConfiguration<'doughnut'>['data'] = {
datasets,
labels
};
@ -308,7 +308,7 @@ export class GfPortfolioProportionChartComponent
) as unknown;
this.chart.update();
} else {
this.chart = new Chart(this.chartCanvas.nativeElement, {
this.chart = new Chart<'doughnut'>(this.chartCanvas.nativeElement, {
data,
options: {
animation: false,

View File

@ -196,7 +196,7 @@ export class GfTreemapChartComponent
min: Math.min(...negativeNetPerformancePercents)
};
const data: ChartConfiguration['data'] = {
const data: ChartConfiguration<'treemap'>['data'] = {
datasets: [
{
backgroundColor: (ctx) => {

View File

@ -63,6 +63,7 @@
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)",
"!{projectRoot}/.storybook/**/*",
"!{projectRoot}/eslint.config.cjs",
"!{projectRoot}/jest.config.[jt]s",
"!{projectRoot}/src/test-setup.[jt]s",
"!{projectRoot}/tsconfig.storybook.json",

3209
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.134.0",
"version": "2.135.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -75,17 +75,17 @@
"@dfinity/principal": "0.15.7",
"@dinero.js/currencies": "2.0.0-alpha.8",
"@internationalized/number": "3.6.0",
"@nestjs/bull": "10.0.1",
"@nestjs/cache-manager": "2.2.2",
"@nestjs/common": "10.1.3",
"@nestjs/config": "3.0.0",
"@nestjs/core": "10.1.3",
"@nestjs/event-emitter": "2.0.4",
"@nestjs/jwt": "10.1.0",
"@nestjs/bull": "10.2.3",
"@nestjs/cache-manager": "2.3.0",
"@nestjs/common": "10.4.15",
"@nestjs/config": "3.3.0",
"@nestjs/core": "10.4.15",
"@nestjs/event-emitter": "2.1.1",
"@nestjs/jwt": "10.2.0",
"@nestjs/passport": "10.0.3",
"@nestjs/platform-express": "10.1.3",
"@nestjs/schedule": "3.0.2",
"@nestjs/serve-static": "4.0.0",
"@nestjs/platform-express": "10.4.15",
"@nestjs/schedule": "4.1.2",
"@nestjs/serve-static": "4.0.2",
"@prisma/client": "6.2.1",
"@simplewebauthn/browser": "9.0.1",
"@simplewebauthn/server": "9.0.3",
@ -93,13 +93,13 @@
"alphavantage": "2.2.0",
"big.js": "6.2.2",
"bootstrap": "4.6.0",
"bull": "4.16.2",
"bull": "4.16.4",
"cache-manager": "5.7.6",
"cache-manager-redis-yet": "5.1.4",
"chart.js": "4.2.0",
"chart.js": "4.4.7",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-treemap": "2.3.1",
"chartjs-plugin-annotation": "2.1.2",
"chartjs-chart-treemap": "3.1.0",
"chartjs-plugin-annotation": "3.1.0",
"chartjs-plugin-datalabels": "2.2.0",
"cheerio": "1.0.0",
"class-transformer": "0.5.1",
@ -129,12 +129,12 @@
"passport-google-oauth20": "2.0.0",
"passport-headerapikey": "1.2.2",
"passport-jwt": "4.0.1",
"reflect-metadata": "0.1.13",
"reflect-metadata": "0.2.2",
"rxjs": "7.5.6",
"stripe": "17.3.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.14.2",
"uuid": "11.0.2",
"uuid": "11.0.5",
"yahoo-finance2": "2.11.3",
"zone.js": "0.15.0"
},
@ -150,19 +150,21 @@
"@angular/language-service": "19.0.5",
"@angular/localize": "19.0.5",
"@angular/pwa": "19.0.6",
"@nestjs/schematics": "10.0.1",
"@nestjs/testing": "10.1.3",
"@nx/angular": "20.3.0",
"@nx/cypress": "20.3.0",
"@nx/eslint-plugin": "20.3.0",
"@nx/jest": "20.3.0",
"@nx/js": "20.3.0",
"@nx/module-federation": "20.3.0",
"@nx/nest": "20.3.0",
"@nx/node": "20.3.0",
"@nx/storybook": "20.3.0",
"@nx/web": "20.3.0",
"@nx/workspace": "20.3.0",
"@eslint/eslintrc": "3.2.0",
"@eslint/js": "9.18.0",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.15",
"@nx/angular": "20.3.2",
"@nx/cypress": "20.3.2",
"@nx/eslint-plugin": "20.3.2",
"@nx/jest": "20.3.2",
"@nx/js": "20.3.2",
"@nx/module-federation": "20.3.2",
"@nx/nest": "20.3.2",
"@nx/node": "20.3.2",
"@nx/storybook": "20.3.2",
"@nx/web": "20.3.2",
"@nx/workspace": "20.3.2",
"@schematics/angular": "19.0.6",
"@simplewebauthn/types": "9.0.1",
"@storybook/addon-essentials": "8.4.7",
@ -179,20 +181,20 @@
"@types/node": "20.14.10",
"@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "8.20.0",
"codelyzer": "6.0.1",
"cypress": "6.2.1",
"eslint": "8.57.0",
"eslint": "9.18.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-cypress": "2.15.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-storybook": "0.6.15",
"eslint-plugin-cypress": "3.2.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-storybook": "0.10.2",
"husky": "9.1.7",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.4.2",
"nx": "20.3.0",
"nx": "20.3.2",
"prettier": "3.4.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.2.1",
@ -203,7 +205,7 @@
"storybook": "8.4.7",
"ts-jest": "29.1.0",
"ts-node": "10.9.2",
"tslib": "2.6.0",
"tslib": "2.8.1",
"typescript": "5.6.3",
"webpack-bundle-analyzer": "4.10.2"
},