Browse Source

Merge remote-tracking branch 'origin/main' into task/enable-no-implicit-override

pull/7355/head
KenTandrian 2 days ago
parent
commit
1e7075991f
  1. 13
      CHANGELOG.md
  2. 2
      apps/api/src/app/subscription/subscription.service.ts
  3. 6
      apps/api/src/app/symbol/symbol.service.ts
  4. 7
      apps/api/src/app/user/user.service.ts
  5. 31
      apps/api/src/services/data-provider/data-provider.service.ts
  6. 62
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  7. 13
      apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
  8. 7
      apps/api/src/services/queues/statistics-gathering/interfaces/interfaces.ts
  9. 46
      apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts
  10. 2
      apps/api/src/services/tag/tag.service.ts
  11. 10
      apps/client/src/locales/messages.ca.xlf
  12. 10
      apps/client/src/locales/messages.de.xlf
  13. 10
      apps/client/src/locales/messages.es.xlf
  14. 10
      apps/client/src/locales/messages.fr.xlf
  15. 10
      apps/client/src/locales/messages.it.xlf
  16. 10
      apps/client/src/locales/messages.ja.xlf
  17. 10
      apps/client/src/locales/messages.ko.xlf
  18. 10
      apps/client/src/locales/messages.nl.xlf
  19. 10
      apps/client/src/locales/messages.pl.xlf
  20. 10
      apps/client/src/locales/messages.pt.xlf
  21. 10
      apps/client/src/locales/messages.tr.xlf
  22. 10
      apps/client/src/locales/messages.uk.xlf
  23. 10
      apps/client/src/locales/messages.xlf
  24. 10
      apps/client/src/locales/messages.zh.xlf
  25. 10
      libs/ui/src/lib/tags-selector/tags-selector.component.html
  26. 9
      libs/ui/src/lib/value/value.component.html
  27. 15
      libs/ui/src/lib/value/value.component.stories.ts
  28. 35
      libs/ui/src/lib/value/value.component.ts
  29. 12
      package-lock.json
  30. 4
      package.json

13
CHANGELOG.md

@ -5,11 +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).
## Unreleased
## 3.29.0 - 2026-07-18
### Added
- Added support for the _Fear & Greed Index_ (market mood) via the `GHOSTFOLIO` data provider in self-hosted environments
- Added a _Storybook_ story for the copy-to-clipboard functionality in the value component
### Changed
- Improved the copy-to-clipboard functionality in the value component by providing a visual confirmation
- Improved the language localization for German (`de`)
- Upgraded `stripe` from version `22.2.3` to `22.3.2`
### Fixed
- Fixed an issue with the delete button in the tags selector component
## 3.28.0 - 2026-07-17

2
apps/api/src/app/subscription/subscription.service.ts

@ -38,7 +38,7 @@ export class SubscriptionService {
this.stripe = new Stripe(
this.configurationService.get('STRIPE_SECRET_KEY'),
{
apiVersion: '2026-05-27.dahlia'
apiVersion: '2026-06-24.dahlia'
}
);
}

6
apps/api/src/app/symbol/symbol.service.ts

@ -133,6 +133,12 @@ export class SymbolService {
}: {
includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> {
if (await this.dataProviderService.isDataProviderGhostfolioConfigured()) {
return this.dataProviderService.getMarketDataOfMarkets({
includeHistoricalData
});
}
const [
marketDataFearAndGreedIndexCryptocurrencies,
marketDataFearAndGreedIndexStocks

7
apps/api/src/app/user/user.service.ts

@ -29,6 +29,7 @@ import {
DEFAULT_DATE_RANGE,
DEFAULT_LANGUAGE_CODE,
DEFAULT_LOCALE,
PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_REFERRAL_PARTNERS,
PROPERTY_SYSTEM_MESSAGE,
@ -546,6 +547,12 @@ export class UserService {
if (hasRole(user, Role.ADMIN)) {
currentPermissions.push(permissions.syncDemoUserAccount);
}
} else {
if (
await this.propertyService.getByKey<string>(PROPERTY_API_KEY_GHOSTFOLIO)
) {
currentPermissions.push(permissions.readMarketDataOfMarkets);
}
}
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {

31
apps/api/src/services/data-provider/data-provider.service.ts

@ -27,7 +27,8 @@ import {
DataProviderHistoricalResponse,
DataProviderResponse,
LookupItem,
LookupResponse
LookupResponse,
MarketDataOfMarketsResponse
} from '@ghostfolio/common/interfaces';
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
@ -194,11 +195,7 @@ export class DataProviderService implements OnModuleInit {
return DataSource[dataSource];
});
const ghostfolioApiKey = await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_GHOSTFOLIO
);
if (ghostfolioApiKey) {
if (await this.isDataProviderGhostfolioConfigured()) {
dataSources.push('GHOSTFOLIO');
}
@ -551,6 +548,22 @@ export class DataProviderService implements OnModuleInit {
return result;
}
public async getMarketDataOfMarkets({
includeHistoricalData
}: {
includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> {
const dataProvider = this.getDataProvider(DataSource.GHOSTFOLIO);
if (!dataProvider.getMarketDataOfMarkets) {
throw new Error(
`The data provider (${DataSource.GHOSTFOLIO}) does not support the market data of markets`
);
}
return dataProvider.getMarketDataOfMarkets({ includeHistoricalData });
}
public async getQuotes({
items,
requestTimeout,
@ -807,6 +820,12 @@ export class DataProviderService implements OnModuleInit {
return response;
}
public async isDataProviderGhostfolioConfigured(): Promise<boolean> {
return !!(await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_GHOSTFOLIO
));
}
public async search({
includeIndices = false,
query,

62
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -5,6 +5,7 @@ import {
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetMarketDataOfMarketsParams,
GetQuotesParams,
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
@ -23,7 +24,9 @@ import {
DividendsResponse,
HistoricalResponse,
LookupResponse,
QuotesResponse
MarketDataOfMarketsResponse,
QuotesResponse,
SymbolItem
} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
@ -224,6 +227,63 @@ export class GhostfolioService implements DataProviderInterface {
}
}
public async getMarketDataOfMarkets({
includeHistoricalData = 0,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetMarketDataOfMarketsParams): Promise<MarketDataOfMarketsResponse> {
let marketDataOfMarkets: MarketDataOfMarketsResponse = {
fearAndGreedIndex: {
CRYPTOCURRENCIES: {} as SymbolItem,
STOCKS: {} as SymbolItem
}
};
try {
const queryParams = new URLSearchParams({
includeHistoricalData: includeHistoricalData.toString()
});
const response = await this.fetchService.fetch(
`${this.URL}/v1/data-providers/ghostfolio/markets?${queryParams.toString()}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(requestTimeout)
}
);
if (!response.ok) {
throw new Response(await response.text(), {
status: response.status,
statusText: response.statusText
});
}
marketDataOfMarkets =
(await response.json()) as MarketDataOfMarketsResponse;
} catch (error) {
let message = error;
if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the market data of markets was aborted because the request to the data provider took more than ${(
requestTimeout / 1000
).toFixed(3)} seconds`;
} else if (error?.status === StatusCodes.TOO_MANY_REQUESTS) {
message = 'RequestError: The daily request limit has been exceeded';
} else if (
[StatusCodes.FORBIDDEN, StatusCodes.UNAUTHORIZED].includes(
error?.status
)
) {
message =
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
}
this.logger.error(message);
}
return marketDataOfMarkets;
}
public getMaxNumberOfSymbolsPerRequest() {
return 20;
}

13
apps/api/src/services/data-provider/interfaces/data-provider.interface.ts

@ -2,7 +2,8 @@ import {
DataProviderHistoricalResponse,
DataProviderInfo,
DataProviderResponse,
LookupResponse
LookupResponse,
MarketDataOfMarketsResponse
} from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types';
@ -37,6 +38,11 @@ export interface DataProviderInterface {
[symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>; // TODO: Return only one symbol
getMarketDataOfMarkets?({
includeHistoricalData,
requestTimeout
}: GetMarketDataOfMarketsParams): Promise<MarketDataOfMarketsResponse>;
getMaxNumberOfSymbolsPerRequest?(): number;
getName(): DataSource;
@ -72,6 +78,11 @@ export interface GetHistoricalParams {
to: Date;
}
export interface GetMarketDataOfMarketsParams {
includeHistoricalData?: number;
requestTimeout?: number;
}
export interface GetQuotesParams {
requestTimeout?: number;
symbols: string[];

7
apps/api/src/services/queues/statistics-gathering/interfaces/interfaces.ts

@ -0,0 +1,7 @@
export interface BetterStackUptimeSlaResponse {
data: {
attributes: {
availability: number;
};
};
}

46
apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts

@ -25,6 +25,14 @@ import { Injectable, Logger } from '@nestjs/common';
import * as cheerio from 'cheerio';
import { format, subDays } from 'date-fns';
import { BetterStackUptimeSlaResponse } from './interfaces/interfaces';
const GATHER_STATISTICS_CONCURRENCY = parseInt(
process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ??
DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(),
10
);
@Injectable()
@Processor(STATISTICS_GATHERING_QUEUE)
export class StatisticsGatheringProcessor {
@ -37,11 +45,7 @@ export class StatisticsGatheringProcessor {
) {}
@Process({
concurrency: parseInt(
process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ??
DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(),
10
),
concurrency: GATHER_STATISTICS_CONCURRENCY,
name: GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME
})
public async gatherDockerHubPullsStatistics() {
@ -58,11 +62,7 @@ export class StatisticsGatheringProcessor {
}
@Process({
concurrency: parseInt(
process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ??
DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(),
10
),
concurrency: GATHER_STATISTICS_CONCURRENCY,
name: GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME
})
public async gatherGitHubContributorsStatistics() {
@ -83,11 +83,7 @@ export class StatisticsGatheringProcessor {
}
@Process({
concurrency: parseInt(
process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ??
DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(),
10
),
concurrency: GATHER_STATISTICS_CONCURRENCY,
name: GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME
})
public async gatherGitHubStargazersStatistics() {
@ -106,11 +102,7 @@ export class StatisticsGatheringProcessor {
}
@Process({
concurrency: parseInt(
process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ??
DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(),
10
),
concurrency: GATHER_STATISTICS_CONCURRENCY,
name: GATHER_STATISTICS_UPTIME_PROCESS_JOB_NAME
})
public async gatherUptimeStatistics() {
@ -140,14 +132,14 @@ export class StatisticsGatheringProcessor {
private async countDockerHubPulls(): Promise<number> {
try {
const { pull_count } = (await this.fetchService
const { pull_count } = await this.fetchService
.fetch('https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio', {
headers: { 'User-Agent': 'request' },
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
})
.then((res) => res.json())) as { pull_count: number };
.then<{ pull_count: number }>((res) => res.json());
return pull_count;
} catch (error) {
@ -157,7 +149,7 @@ export class StatisticsGatheringProcessor {
}
}
private async countGitHubContributors(): Promise<number> {
private async countGitHubContributors(): Promise<number | undefined> {
try {
const body = await this.fetchService
.fetch('https://github.com/ghostfolio/ghostfolio', {
@ -189,14 +181,14 @@ export class StatisticsGatheringProcessor {
private async countGitHubStargazers(): Promise<number> {
try {
const { stargazers_count } = (await this.fetchService
const { stargazers_count } = await this.fetchService
.fetch('https://api.github.com/repos/ghostfolio/ghostfolio', {
headers: { 'User-Agent': 'request' },
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
})
.then((res) => res.json())) as { stargazers_count: number };
.then<{ stargazers_count: number }>((res) => res.json());
return stargazers_count;
} catch (error) {
@ -213,7 +205,7 @@ export class StatisticsGatheringProcessor {
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90),
DATE_FORMAT
)}&to${format(new Date(), DATE_FORMAT)}`,
)}&to=${format(new Date(), DATE_FORMAT)}`,
{
headers: {
[HEADER_KEY_TOKEN]: `Bearer ${this.configurationService.get(
@ -225,7 +217,7 @@ export class StatisticsGatheringProcessor {
)
}
)
.then((res) => res.json());
.then<BetterStackUptimeSlaResponse>((res) => res.json());
return data.attributes.availability / 100;
} catch (error) {

2
apps/api/src/services/tag/tag.service.ts

@ -20,7 +20,7 @@ export class TagService {
public async getTag(
tagWhereUniqueInput: Prisma.TagWhereUniqueInput
): Promise<Tag> {
): Promise<Tag | null> {
return this.prismaService.tag.findUnique({
where: tagWhereUniqueInput
});

10
apps/client/src/locales/messages.ca.xlf

@ -2545,6 +2545,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -5263,8 +5267,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="1914201149277662818" datatype="html">
@ -7201,7 +7205,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
apps/client/src/locales/messages.de.xlf

@ -1432,6 +1432,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -2353,8 +2357,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="2446117790692479672" datatype="html">
@ -7225,7 +7229,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
apps/client/src/locales/messages.es.xlf

@ -1417,6 +1417,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -2338,8 +2342,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="2446117790692479672" datatype="html">
@ -7202,7 +7206,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
apps/client/src/locales/messages.fr.xlf

@ -1800,6 +1800,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -2857,8 +2861,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="2446117790692479672" datatype="html">
@ -7201,7 +7205,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
apps/client/src/locales/messages.it.xlf

@ -1417,6 +1417,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -2338,8 +2342,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="2446117790692479672" datatype="html">
@ -7202,7 +7206,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
apps/client/src/locales/messages.ja.xlf

@ -2342,6 +2342,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -4863,8 +4867,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="1914201149277662818" datatype="html">
@ -7242,7 +7246,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="7305671611654052345" datatype="html">

10
apps/client/src/locales/messages.ko.xlf

@ -2342,6 +2342,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -4855,8 +4859,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="1914201149277662818" datatype="html">
@ -7242,7 +7246,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="7305671611654052345" datatype="html">

10
apps/client/src/locales/messages.nl.xlf

@ -1416,6 +1416,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -2337,8 +2341,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="2446117790692479672" datatype="html">
@ -7201,7 +7205,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
apps/client/src/locales/messages.pl.xlf

@ -2309,6 +2309,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -4822,8 +4826,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="1914201149277662818" datatype="html">
@ -7201,7 +7205,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
apps/client/src/locales/messages.pt.xlf

@ -1800,6 +1800,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -2785,8 +2789,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="2446117790692479672" datatype="html">
@ -7201,7 +7205,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

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

@ -4246,8 +4246,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="1914201149277662818" datatype="html">
@ -4574,6 +4574,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -7201,7 +7205,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
apps/client/src/locales/messages.uk.xlf

@ -2773,6 +2773,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -5600,7 +5604,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">
@ -5679,8 +5683,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="5020357869062357338" datatype="html">

10
apps/client/src/locales/messages.xlf

@ -2146,6 +2146,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -4429,8 +4433,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="1914201149277662818" datatype="html">
@ -6577,7 +6581,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="7305671611654052345" datatype="html">

10
apps/client/src/locales/messages.zh.xlf

@ -2318,6 +2318,10 @@
<context context-type="sourcefile">libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="4039692315328513907" datatype="html">
<source>Grant access</source>
@ -4839,8 +4843,8 @@
<context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="1914201149277662818" datatype="html">
@ -7202,7 +7206,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/value/value.component.ts</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="6351408992301482473" datatype="html">

10
libs/ui/src/lib/tags-selector/tags-selector.component.html

@ -32,13 +32,11 @@
}
}
@for (tag of tagsSelected(); track tag.id) {
<mat-chip-row
matChipRemove
[removable]="true"
(removed)="onRemoveTag(tag)"
>
<mat-chip-row [removable]="true" (removed)="onRemoveTag(tag)">
{{ tag.name }}
<ion-icon matChipTrailingIcon name="close-outline" />
<button matChipRemove type="button">
<ion-icon name="close-outline" />
</button>
</mat-chip-row>
}
<input

9
libs/ui/src/lib/value/value.component.html

@ -13,13 +13,16 @@
@if (enableCopyToClipboardButton) {
<button
class="ml-1 no-height no-min-width p-1"
i18n-title
mat-button
title="Copy to clipboard"
type="button"
[title]="isCopied ? copiedTitle : copyToClipboardTitle"
(click)="onCopyValueToClipboard()"
(mousedown)="$event.preventDefault()"
>
<ion-icon class="text-muted" name="copy-outline" />
<ion-icon
class="text-muted"
[name]="isCopied ? 'checkmark-outline' : 'copy-outline'"
/>
</button>
}
</ng-template>

15
libs/ui/src/lib/value/value.component.stories.ts

@ -1,5 +1,6 @@
import '@angular/localize/init';
import { moduleMetadata } from '@storybook/angular';
import { provideNoopAnimations } from '@angular/platform-browser/animations';
import { applicationConfig, moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -9,6 +10,9 @@ export default {
title: 'Value',
component: GfValueComponent,
decorators: [
applicationConfig({
providers: [provideNoopAnimations()]
}),
moduleMetadata({
imports: [NgxSkeletonLoaderModule]
})
@ -103,3 +107,12 @@ export const Precision: Story = {
value: 7.2534802394809285309
}
};
export const WithCopyButton: Story = {
args: {
enableCopyToClipboardButton: true,
locale: 'en-US',
value: 1234.56
},
name: 'With Copy Button'
};

35
libs/ui/src/lib/value/value.component.ts

@ -13,13 +13,14 @@ import {
input,
Input,
OnChanges,
OnDestroy,
ViewChild
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBar } from '@angular/material/snack-bar';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { copyOutline } from 'ionicons/icons';
import { checkmarkOutline, copyOutline } from 'ionicons/icons';
import { isNumber } from 'lodash';
import ms from 'ms';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -32,7 +33,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
styleUrls: ['./value.component.scss'],
templateUrl: './value.component.html'
})
export class GfValueComponent implements AfterViewInit, OnChanges {
export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() colorizeSign = false;
@Input() deviceType: string;
@Input() enableCopyToClipboardButton = false;
@ -54,22 +55,29 @@ export class GfValueComponent implements AfterViewInit, OnChanges {
public absoluteValue = 0;
public formattedValue = '';
public hasLabel = false;
public isCopied = false;
public isNumber = false;
public isString = false;
public useAbsoluteValue = false;
public readonly copiedTitle = $localize`The value has been copied to the clipboard`;
public readonly copyToClipboardTitle = $localize`Copy to clipboard`;
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private clipboard: Clipboard,
private snackBar: MatSnackBar
) {
addIcons({
checkmarkOutline,
copyOutline
});
}
public readonly precision = input<number>();
private copyToClipboardTimeout: ReturnType<typeof setTimeout>;
private readonly formatOptions = computed<Intl.NumberFormatOptions>(() => {
const digits = this.hasPrecision ? this.precision() : 2;
@ -178,6 +186,18 @@ export class GfValueComponent implements AfterViewInit, OnChanges {
public onCopyValueToClipboard() {
this.clipboard.copy(String(this.value));
this.isCopied = true;
if (this.copyToClipboardTimeout) {
clearTimeout(this.copyToClipboardTimeout);
}
this.copyToClipboardTimeout = setTimeout(() => {
this.isCopied = false;
this.changeDetectorRef.markForCheck();
}, ms('3 seconds'));
this.snackBar.open(
'✅ ' + $localize`${this.value} has been copied to the clipboard`,
undefined,
@ -187,12 +207,23 @@ export class GfValueComponent implements AfterViewInit, OnChanges {
);
}
public ngOnDestroy() {
if (this.copyToClipboardTimeout) {
clearTimeout(this.copyToClipboardTimeout);
}
}
private initializeVariables() {
this.absoluteValue = 0;
this.formattedValue = '';
this.isCopied = false;
this.isNumber = false;
this.isString = false;
this.locale = this.locale || getLocale();
this.useAbsoluteValue = false;
if (this.copyToClipboardTimeout) {
clearTimeout(this.copyToClipboardTimeout);
}
}
}

12
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "3.28.0",
"version": "3.29.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "3.28.0",
"version": "3.29.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -92,7 +92,7 @@
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "22.2.3",
"stripe": "22.3.2",
"svgmap": "2.21.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.0",
@ -32428,9 +32428,9 @@
}
},
"node_modules/stripe": {
"version": "22.2.3",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.2.3.tgz",
"integrity": "sha512-9lrggvLtjO4Ef5WXH4t50ckHJcJgTD72xFB+KJgZCSszAH9zMV1BqU6PwmWbRLdcdX7LK6PbErBapiGK7pIb+g==",
"version": "22.3.2",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.3.2.tgz",
"integrity": "sha512-O13QOvgEIQvDlTy6Ubb5kB980wpbhmoZNsgCXKILjCMZS67f+bW+6w99k3gnSi/N1lkryoj1WYdpGT5Wc5edjg==",
"license": "MIT",
"engines": {
"node": ">=18"

4
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "3.28.0",
"version": "3.29.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -136,7 +136,7 @@
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "22.2.3",
"stripe": "22.3.2",
"svgmap": "2.21.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.0",

Loading…
Cancel
Save