Browse Source

Merge branch 'main' into task/zh-localization

pull/7490/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
2ff6fe3333
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 20
      CHANGELOG.md
  2. 7
      apps/api/src/app/admin/admin.service.ts
  3. 7
      apps/api/src/app/auth/api-key.strategy.ts
  4. 7
      apps/api/src/app/auth/jwt.strategy.ts
  5. 4
      apps/api/src/app/health/health.service.ts
  6. 1
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  7. 208
      apps/api/src/app/portfolio/portfolio.service.ts
  8. 4
      apps/api/src/app/subscription/subscription.controller.ts
  9. 70
      apps/api/src/app/user/user.service.ts
  10. 6
      apps/api/src/services/benchmark/benchmark.service.ts
  11. 70
      apps/api/src/services/property/property.service.ts
  12. 1
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  13. 1
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  14. 1
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  15. 11
      apps/client/src/styles.scss
  16. 3
      libs/common/src/lib/config.ts
  17. 13
      libs/ui/src/lib/activities-table/activities-table.component.html
  18. 12
      libs/ui/src/lib/activities-table/activities-table.component.ts

20
CHANGELOG.md

@ -9,11 +9,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the style of the tabs in the account detail dialog on mobile
- Improved the style of the tabs in the holding detail dialog on mobile
- Improved the style of the tabs in the asset profile dialog of the admin control panel on mobile
- Improved the style of the empty state in the _Fear & Greed Index_ component - Improved the style of the empty state in the _Fear & Greed Index_ component
- Added the activity count to the delete menu item of the activities table
- Added the activity count to the deletion confirmation dialog of the activities table
- Improved the style of the type filter in the activities table component (experimental) - Improved the style of the type filter in the activities table component (experimental)
- Improved the performance of the property service by caching the properties in memory
- Improved the language localization for Chinese (`zh`) - Improved the language localization for Chinese (`zh`)
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
### Fixed
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Asset Class Cluster Risks_ (Equity)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Asset Class Cluster Risks_ (Fixed Income)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Currency Cluster Risks_ (Investment)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Currency Cluster Risks_ (Investment: Base Currency)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Economic Market Cluster Risks_ (Developed Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Economic Market Cluster Risks_ (Emerging Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Asia-Pacific)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Emerging Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Europe)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Japan)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (North America)
## 3.37.0 - 2026-07-30 ## 3.37.0 - 2026-07-30
### Added ### Added

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

@ -107,8 +107,11 @@ export class AdminService {
await this.marketDataService.deleteMany({ dataSource, symbol }); await this.marketDataService.deleteMany({ dataSource, symbol });
const currency = getCurrencyFromSymbol(symbol); const currency = getCurrencyFromSymbol(symbol);
const customCurrencies =
await this.propertyService.getByKey<string[]>(PROPERTY_CURRENCIES); const customCurrencies = await this.propertyService.getByKey<string[]>(
PROPERTY_CURRENCIES,
{ skipCache: true }
);
if (customCurrencies.includes(currency)) { if (customCurrencies.includes(currency)) {
const updatedCustomCurrencies = customCurrencies.filter( const updatedCustomCurrencies = customCurrencies.filter(

7
apps/api/src/app/auth/api-key.strategy.ts

@ -35,6 +35,13 @@ export class ApiKeyStrategy extends PassportStrategy(
); );
} }
if (await this.userService.isDailyRequestLimitExceeded({ user })) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
await this.prismaService.analytics.upsert({ await this.prismaService.analytics.upsert({
create: { user: { connect: { id: user.id } } }, create: { user: { connect: { id: user.id } } },
update: { update: {

7
apps/api/src/app/auth/jwt.strategy.ts

@ -42,6 +42,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
); );
} }
if (await this.userService.isDailyRequestLimitExceeded({ user })) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
const country = const country =
countriesAndTimezones.getCountryForTimezone(timezone)?.id; countriesAndTimezones.getCountryForTimezone(timezone)?.id;

4
apps/api/src/app/health/health.service.ts

@ -26,7 +26,9 @@ export class HealthService {
public async isDatabaseHealthy() { public async isDatabaseHealthy() {
try { try {
await this.propertyService.getByKey(PROPERTY_CURRENCIES); await this.propertyService.getByKey(PROPERTY_CURRENCIES, {
skipCache: true
});
return true; return true;
} catch { } catch {

1
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -92,6 +92,7 @@ describe('PortfolioService', () => {
null, null,
null, null,
null, null,
null,
null null
); );

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

@ -1126,6 +1126,8 @@ export class PortfolioService {
withSummary: true withSummary: true
}); });
const hasOpenHoldings = Object.keys(holdings).length > 0;
const marketsAdvancedTotalInBaseCurrency = getSum( const marketsAdvancedTotalInBaseCurrency = getSum(
Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => { Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency); return new Big(valueInBaseCurrency);
@ -1185,26 +1187,25 @@ export class PortfolioService {
id: 'rule.currencyClusterRisk.category', id: 'rule.currencyClusterRisk.category',
languageCode: userSettings.language languageCode: userSettings.language
}), }),
rules: rules: hasOpenHoldings
summary.activityCount > 0 ? await this.rulesService.evaluate(
? await this.rulesService.evaluate( [
[ new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, Object.values(holdings),
Object.values(holdings), userSettings.language
userSettings.language ),
), new CurrencyClusterRiskCurrentInvestment(
new CurrencyClusterRiskCurrentInvestment( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, Object.values(holdings),
Object.values(holdings), userSettings.language
userSettings.language )
) ],
], userSettings
userSettings )
) : undefined
: undefined
}, },
{ {
key: 'assetClassClusterRisk', key: 'assetClassClusterRisk',
@ -1212,26 +1213,25 @@ export class PortfolioService {
id: 'rule.assetClassClusterRisk.category', id: 'rule.assetClassClusterRisk.category',
languageCode: userSettings.language languageCode: userSettings.language
}), }),
rules: rules: hasOpenHoldings
summary.activityCount > 0 ? await this.rulesService.evaluate(
? await this.rulesService.evaluate( [
[ new AssetClassClusterRiskEquity(
new AssetClassClusterRiskEquity( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, Object.values(holdings)
Object.values(holdings) ),
), new AssetClassClusterRiskFixedIncome(
new AssetClassClusterRiskFixedIncome( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, Object.values(holdings)
Object.values(holdings) )
) ],
], userSettings
userSettings )
) : undefined
: undefined
}, },
{ {
key: 'accountClusterRisk', key: 'accountClusterRisk',
@ -1266,28 +1266,27 @@ export class PortfolioService {
id: 'rule.economicMarketClusterRisk.category', id: 'rule.economicMarketClusterRisk.category',
languageCode: userSettings.language languageCode: userSettings.language
}), }),
rules: rules: hasOpenHoldings
summary.activityCount > 0 ? await this.rulesService.evaluate(
? await this.rulesService.evaluate( [
[ new EconomicMarketClusterRiskDevelopedMarkets(
new EconomicMarketClusterRiskDevelopedMarkets( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, marketsTotalInBaseCurrency,
marketsTotalInBaseCurrency, markets.developedMarkets.valueInBaseCurrency,
markets.developedMarkets.valueInBaseCurrency, userSettings.language
userSettings.language ),
), new EconomicMarketClusterRiskEmergingMarkets(
new EconomicMarketClusterRiskEmergingMarkets( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, marketsTotalInBaseCurrency,
marketsTotalInBaseCurrency, markets.emergingMarkets.valueInBaseCurrency,
markets.emergingMarkets.valueInBaseCurrency, userSettings.language
userSettings.language )
) ],
], userSettings
userSettings )
) : undefined
: undefined
}, },
{ {
key: 'regionalMarketClusterRisk', key: 'regionalMarketClusterRisk',
@ -1295,49 +1294,48 @@ export class PortfolioService {
id: 'rule.regionalMarketClusterRisk.category', id: 'rule.regionalMarketClusterRisk.category',
languageCode: userSettings.language languageCode: userSettings.language
}), }),
rules: rules: hasOpenHoldings
summary.activityCount > 0 ? await this.rulesService.evaluate(
? await this.rulesService.evaluate( [
[ new RegionalMarketClusterRiskAsiaPacific(
new RegionalMarketClusterRiskAsiaPacific( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.asiaPacific.valueInBaseCurrency
marketsAdvanced.asiaPacific.valueInBaseCurrency ),
), new RegionalMarketClusterRiskEmergingMarkets(
new RegionalMarketClusterRiskEmergingMarkets( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.emergingMarkets.valueInBaseCurrency
marketsAdvanced.emergingMarkets.valueInBaseCurrency ),
), new RegionalMarketClusterRiskEurope(
new RegionalMarketClusterRiskEurope( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.europe.valueInBaseCurrency
marketsAdvanced.europe.valueInBaseCurrency ),
), new RegionalMarketClusterRiskJapan(
new RegionalMarketClusterRiskJapan( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.japan.valueInBaseCurrency
marketsAdvanced.japan.valueInBaseCurrency ),
), new RegionalMarketClusterRiskNorthAmerica(
new RegionalMarketClusterRiskNorthAmerica( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.northAmerica.valueInBaseCurrency
marketsAdvanced.northAmerica.valueInBaseCurrency )
) ],
], userSettings
userSettings )
) : undefined
: undefined
}, },
{ {
key: 'fees', key: 'fees',

4
apps/api/src/app/subscription/subscription.controller.ts

@ -54,7 +54,9 @@ export class SubscriptionController {
} }
let coupons = let coupons =
(await this.propertyService.getByKey<Coupon[]>(PROPERTY_COUPONS)) ?? []; (await this.propertyService.getByKey<Coupon[]>(PROPERTY_COUPONS, {
skipCache: true
})) ?? [];
const coupon = coupons.find((currentCoupon) => { const coupon = coupons.find((currentCoupon) => {
return currentCoupon.code === couponCode; return currentCoupon.code === couponCode;

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

@ -31,9 +31,12 @@ import {
DEFAULT_LOCALE, DEFAULT_LOCALE,
PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_MAX_DAILY_REQUESTS,
PROPERTY_REFERRAL_PARTNERS, PROPERTY_REFERRAL_PARTNERS,
PROPERTY_SYSTEM_MESSAGE, PROPERTY_SYSTEM_MESSAGE,
TAG_ID_EXCLUDE_FROM_ANALYSIS TAG_ID_EXCLUDE_FROM_ANALYSIS,
THROTTLE_DAILY_KEY,
THROTTLE_DAILY_TTL
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums'; import { SubscriptionType } from '@ghostfolio/common/enums';
import { import {
@ -50,15 +53,18 @@ import {
import { UserWithSettings } from '@ghostfolio/common/types'; import { UserWithSettings } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler';
import { Prisma, Role, Settings, User } from '@prisma/client'; import { Prisma, Role, Settings, User } from '@prisma/client';
import { differenceInDays, subDays } from 'date-fns'; import { differenceInDays, subDays } from 'date-fns';
import { without } from 'lodash'; import { isNil, without } from 'lodash';
import { createHmac } from 'node:crypto'; import { createHmac } from 'node:crypto';
@Injectable() @Injectable()
export class UserService { export class UserService {
private readonly logger = new Logger(UserService.name);
public constructor( public constructor(
private readonly activitiesService: ActivitiesService, private readonly activitiesService: ActivitiesService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
@ -67,7 +73,9 @@ export class UserService {
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService, private readonly subscriptionService: SubscriptionService,
private readonly tagService: TagService private readonly tagService: TagService,
@InjectThrottlerStorage()
private readonly throttlerStorage: ThrottlerStorage
) {} ) {}
public async count(args?: Prisma.UserCountArgs) { public async count(args?: Prisma.UserCountArgs) {
@ -228,6 +236,38 @@ export class UserService {
return usersWithAdminRole.length > 0; return usersWithAdminRole.length > 0;
} }
public async isDailyRequestLimitExceeded({
user
}: {
user: UserWithSettings;
}) {
if (user.subscription?.type === SubscriptionType.Premium) {
return false;
}
const maxDailyRequests = await this.getMaxDailyRequests();
if (maxDailyRequests === undefined) {
return false;
}
try {
const { isBlocked } = await this.throttlerStorage.increment(
`${THROTTLE_DAILY_KEY}-${user.id}`,
THROTTLE_DAILY_TTL,
maxDailyRequests,
THROTTLE_DAILY_TTL,
THROTTLE_DAILY_KEY
);
return isBlocked;
} catch (error) {
this.logger.error(error);
return false;
}
}
public async user( public async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> { ): Promise<UserWithSettings | null> {
@ -782,4 +822,26 @@ export class UserService {
return settings; return settings;
} }
private async getMaxDailyRequests() {
const value = await this.propertyService.getByKey<string>(
PROPERTY_MAX_DAILY_REQUESTS
);
if (isNil(value) || value === '') {
return undefined;
}
const maxDailyRequests = Number(value);
if (!Number.isInteger(maxDailyRequests) || maxDailyRequests < 0) {
this.logger.warn(
`The property ${PROPERTY_MAX_DAILY_REQUESTS} is not a non-negative integer ("${value}"), the daily request limit is not applied`
);
return undefined;
}
return maxDailyRequests;
}
} }

6
apps/api/src/services/benchmark/benchmark.service.ts

@ -159,7 +159,8 @@ export class BenchmarkService {
let benchmarks = let benchmarks =
(await this.propertyService.getByKey<BenchmarkProperty[]>( (await this.propertyService.getByKey<BenchmarkProperty[]>(
PROPERTY_BENCHMARKS PROPERTY_BENCHMARKS,
{ skipCache: true }
)) ?? []; )) ?? [];
benchmarks.push({ symbolProfileId: assetProfile.id }); benchmarks.push({ symbolProfileId: assetProfile.id });
@ -196,7 +197,8 @@ export class BenchmarkService {
let benchmarks = let benchmarks =
(await this.propertyService.getByKey<BenchmarkProperty[]>( (await this.propertyService.getByKey<BenchmarkProperty[]>(
PROPERTY_BENCHMARKS PROPERTY_BENCHMARKS,
{ skipCache: true }
)) ?? []; )) ?? [];
benchmarks = benchmarks.filter(({ symbolProfileId }) => { benchmarks = benchmarks.filter(({ symbolProfileId }) => {

70
apps/api/src/services/property/property.service.ts

@ -6,27 +6,39 @@ import {
import { PropertyKey } from '@ghostfolio/common/types'; import { PropertyKey } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Property } from '@prisma/client';
import { addMilliseconds, isBefore } from 'date-fns';
import ms from 'ms';
import { PropertyValue } from './interfaces/interfaces'; import { PropertyValue } from './interfaces/interfaces';
@Injectable() @Injectable()
export class PropertyService { export class PropertyService {
private static readonly CACHE_TTL = ms('1 minute');
private cachedProperties: Promise<Property[]>;
private cachedPropertiesExpiresAt: Date;
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async delete({ key }: { key: PropertyKey }) { public async delete({ key }: { key: PropertyKey }) {
return this.prismaService.property.delete({ const property = await this.prismaService.property.delete({
where: { key } where: { key }
}); });
this.invalidateCache();
return property;
} }
public async get() { public async get({ skipCache = false } = {}) {
const response: { const response: {
[key: string]: PropertyValue; [key: string]: PropertyValue;
} = { } = {
[PROPERTY_CURRENCIES]: [] [PROPERTY_CURRENCIES]: []
}; };
const properties = await this.prismaService.property.findMany(); const properties = await this.getProperties({ skipCache });
for (const property of properties) { for (const property of properties) {
let value = property.value; let value = property.value;
@ -41,8 +53,11 @@ export class PropertyService {
return response; return response;
} }
public async getByKey<TValue extends PropertyValue>(aKey: PropertyKey) { public async getByKey<TValue extends PropertyValue>(
const properties = await this.get(); aKey: PropertyKey,
{ skipCache = false } = {}
) {
const properties = await this.get({ skipCache });
return properties[aKey] as TValue; return properties[aKey] as TValue;
} }
@ -53,10 +68,53 @@ export class PropertyService {
} }
public async put({ key, value }: { key: PropertyKey; value: string }) { public async put({ key, value }: { key: PropertyKey; value: string }) {
return this.prismaService.property.upsert({ const property = await this.prismaService.property.upsert({
create: { key, value }, create: { key, value },
update: { value }, update: { value },
where: { key } where: { key }
}); });
this.invalidateCache();
return property;
}
/**
* Returns the properties from the in-memory cache, falling back to the
* database. Callers which write back a modified property must set
* skipCache to avoid basing the write on a stale read.
*/
private async getProperties({ skipCache = false } = {}) {
if (skipCache) {
return this.prismaService.property.findMany();
}
if (
this.cachedProperties &&
isBefore(new Date(), this.cachedPropertiesExpiresAt)
) {
return this.cachedProperties;
}
const properties = this.prismaService.property.findMany().catch((error) => {
if (this.cachedProperties === properties) {
this.invalidateCache();
}
throw error;
});
this.cachedProperties = properties;
this.cachedPropertiesExpiresAt = addMilliseconds(
new Date(),
PropertyService.CACHE_TTL
);
return this.cachedProperties;
}
private invalidateCache() {
this.cachedProperties = undefined;
this.cachedPropertiesExpiresAt = undefined;
} }
} }

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

@ -33,6 +33,7 @@
<mat-tab-group <mat-tab-group
animationDuration="0ms" animationDuration="0ms"
class="mb-4" class="mb-4"
[disablePagination]="true"
[mat-stretch-tabs]="false" [mat-stretch-tabs]="false"
> >
<mat-tab> <mat-tab>

1
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -105,6 +105,7 @@
<mat-tab-group <mat-tab-group
animationDuration="0ms" animationDuration="0ms"
[disablePagination]="true"
[dynamicHeight]="true" [dynamicHeight]="true"
[mat-stretch-tabs]="false" [mat-stretch-tabs]="false"
> >

1
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -39,6 +39,7 @@
<mat-tab-group <mat-tab-group
animationDuration="0ms" animationDuration="0ms"
class="mb-4" class="mb-4"
[disablePagination]="true"
[mat-stretch-tabs]="false" [mat-stretch-tabs]="false"
> >
<mat-tab> <mat-tab>

11
apps/client/src/styles.scss

@ -411,6 +411,17 @@ ngx-skeleton-loader {
.mdc-dialog__content { .mdc-dialog__content {
--mat-dialog-supporting-text-color: rgba(var(--dark-primary-text)); --mat-dialog-supporting-text-color: rgba(var(--dark-primary-text));
} }
@media (max-width: 575.98px) {
// Tabs fill the available width on mobile
.mat-mdc-tab-group {
.mat-mdc-tab {
flex-grow: 1;
min-width: unset;
padding: 0 0.5rem;
}
}
}
} }
.mat-mdc-fab, .mat-mdc-fab,

3
libs/common/src/lib/config.ts

@ -253,6 +253,7 @@ export const PROPERTY_DEMO_USER_ID = 'DEMO_USER_ID';
export const PROPERTY_IS_DATA_GATHERING_ENABLED = 'IS_DATA_GATHERING_ENABLED'; export const PROPERTY_IS_DATA_GATHERING_ENABLED = 'IS_DATA_GATHERING_ENABLED';
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE'; export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED'; export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED';
export const PROPERTY_MAX_DAILY_REQUESTS = 'MAX_DAILY_REQUESTS';
export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL'; export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL';
export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH'; export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH';
export const PROPERTY_PROXY_ROUTES = 'PROXY_ROUTES'; export const PROPERTY_PROXY_ROUTES = 'PROXY_ROUTES';
@ -326,6 +327,8 @@ export const TAG_ID_EXCLUDE_FROM_ANALYSIS =
'f2e868af-8333-459f-b161-cbc6544c24bd'; 'f2e868af-8333-459f-b161-cbc6544c24bd';
export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874'; export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874';
export const THROTTLE_DAILY_KEY = 'daily';
export const THROTTLE_DAILY_TTL = ms('1 day');
export const THROTTLE_DEFAULT_LIMIT = 10; export const THROTTLE_DEFAULT_LIMIT = 10;
export const THROTTLE_DEFAULT_TTL = ms('1 minute'); export const THROTTLE_DEFAULT_TTL = ms('1 minute');
export const THROTTLE_SIGNUP_LIMIT = 5; export const THROTTLE_SIGNUP_LIMIT = 5;

13
libs/ui/src/lib/activities-table/activities-table.component.html

@ -84,14 +84,19 @@
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"
mat-menu-item mat-menu-item
[disabled]=" [disabled]="!canDeleteActivities()"
dataSource()?.data.length === 0 || !hasPermissionToDeleteActivity
"
(click)="onDeleteActivities()" (click)="onDeleteActivities()"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" /> <ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete Activities</span> @if (canDeleteActivities()) {
<span i18n
>Delete {{ totalItems > 1 ? totalItems : '' }}
{totalItems, plural, =1 {Activity} other {Activities}}</span
>
} @else {
<span i18n>Delete Activities</span>
}
</span> </span>
</button> </button>
</mat-menu> </mat-menu>

12
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -282,6 +282,13 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
); );
} }
public canDeleteActivities() {
return (
(this.dataSource()?.data.length ?? 0) > 0 &&
this.hasPermissionToDeleteActivity
);
}
public isExcludedFromAnalysis(activity: Activity) { public isExcludedFromAnalysis(activity: Activity) {
return ( return (
(activity.account && isAccountExcluded(activity.account)) ?? (activity.account && isAccountExcluded(activity.account)) ??
@ -314,7 +321,10 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
this.activitiesDeleted.emit(); this.activitiesDeleted.emit();
}, },
confirmType: ConfirmationDialogType.Warn, confirmType: ConfirmationDialogType.Warn,
title: $localize`Do you really want to delete these activities?` title:
this.totalItems === 1
? $localize`Do you really want to delete this activity?`
: $localize`Do you really want to delete these ${this.totalItems}:count: activities?`
}); });
} }

Loading…
Cancel
Save