diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d2970448..5cd20a1bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the type filter of the activities table on the activities page to only list the activity types in use (experimental) +- Improved the permission selector with icons in the create or update access dialog +- Extracted the access level icon to a reusable component +- Disabled the telemetry in the _Storybook_ setup - Improved the indexes of the order database table - Upgraded the `Node.js` engine from version `>=22.18.0` to `>=22.22.3` (`package.json`) +### Fixed + +- Fixed the _Storybook_ setup by loading the `@angular/localize` polyfill centrally +- Fixed an issue in the activities import where an unused custom asset profile was created if the related activities were not imported + ## 3.57.0 - 2026-08-21 ### Added diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 26162b9e45..47935c9a79 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -48,6 +48,7 @@ import { omit, uniqBy } from 'lodash'; import { randomUUID } from 'node:crypto'; import { ImportDataDto } from './import-data.dto'; +import { AssetProfileToCreate } from './interfaces/asset-profile-to-create.interface'; @Injectable() export class ImportService { @@ -534,6 +535,8 @@ export class ImportService { } } + const assetProfilesToCreate: AssetProfileToCreate[] = []; + if (assetProfilesWithMarketDataDto?.length) { const customAssetProfileNames = assetProfilesWithMarketDataDto .filter(({ dataSource, name }) => { @@ -557,6 +560,7 @@ export class ImportService { ]); for (const assetProfileWithMarketData of assetProfilesWithMarketDataDto) { + let assetProfileToCreate: Prisma.SymbolProfileCreateInput; let symbol = assetProfileWithMarketData.symbol; // Check if there is any existing asset profile @@ -605,13 +609,10 @@ export class ImportService { assetProfile.symbol = symbol; if (!isDryRun) { - // Create a new asset profile - const assetProfileObject: Prisma.SymbolProfileCreateInput = { + assetProfileToCreate = { ...assetProfile, user: { connect: { id: user.id } } }; - - await this.symbolProfileService.add(assetProfileObject); } } @@ -625,7 +626,6 @@ export class ImportService { } if (!isDryRun) { - // Insert or update market data const marketDataObjects = ( assetProfileWithMarketData.marketData ?? [] ).map((marketData) => { @@ -636,7 +636,40 @@ export class ImportService { } as Prisma.MarketDataUpdateInput; }); - await this.marketDataService.updateMany({ data: marketDataObjects }); + if (assetProfileToCreate) { + const assetProfileToCreateIdentifier = + getAssetProfileIdentifier(assetProfileToCreate); + + const duplicateAssetProfileToCreate = assetProfilesToCreate.find( + ({ assetProfile }) => { + return ( + getAssetProfileIdentifier(assetProfile) === + assetProfileToCreateIdentifier + ); + } + ); + + if (duplicateAssetProfileToCreate) { + // The import contains the same asset profile more than once, + // which would fail with a unique constraint violation. Keep the + // first asset profile and merge the market data into it. + duplicateAssetProfileToCreate.marketDataObjects.push( + ...marketDataObjects + ); + } else { + // Create the new asset profile and its market data later, once it + // is known which activities are imported + assetProfilesToCreate.push({ + marketDataObjects, + assetProfile: assetProfileToCreate + }); + } + } else { + // Insert or update market data + await this.marketDataService.updateMany({ + data: marketDataObjects + }); + } } } } @@ -719,6 +752,25 @@ export class ImportService { return id === TAG_ID_DRAFT; }) ?? { id: TAG_ID_DRAFT, name: 'DRAFT' }; + // Create the new asset profiles of the activities to import only, so that + // no unused asset profile remains, for example if no activity refers to + // the asset profile. An asset profile which is created before the + // validation of the activities would stay behind, because the import is + // not rolled back on an error. + if (!isDryRun) { + for (const { + assetProfile, + marketDataObjects + } of this.getAssetProfilesToCreate({ + activities: activitiesExtendedWithErrors, + assetProfiles: assetProfilesToCreate + })) { + await this.symbolProfileService.add(assetProfile); + + await this.marketDataService.updateMany({ data: marketDataObjects }); + } + } + const activities: Activity[] = []; for (const activity of activitiesExtendedWithErrors) { @@ -934,7 +986,7 @@ export class ImportService { activitiesDto: Partial[]; userCurrency: string; userId: string; - }): Promise[]> { + }): Promise<(Partial & Pick)[]> { const { activities: existingActivities } = await this.activitiesService.getActivities({ userCurrency, @@ -1055,6 +1107,30 @@ export class ImportService { return matchingAccountsOfUser[0]; } + private getAssetProfilesToCreate({ + activities, + assetProfiles + }: { + activities: Pick[]; + assetProfiles: AssetProfileToCreate[]; + }) { + const assetProfileIdentifiersToImport = new Set( + activities + .filter(({ error }) => { + return !error; + }) + .map(({ assetProfile }) => { + return getAssetProfileIdentifier(assetProfile); + }) + ); + + return assetProfiles.filter(({ assetProfile }) => { + return assetProfileIdentifiersToImport.has( + getAssetProfileIdentifier(assetProfile) + ); + }); + } + private isUniqueAccount(accounts: AccountWithValue[]) { const uniqueAccountIds = new Set(); diff --git a/apps/api/src/app/import/interfaces/asset-profile-to-create.interface.ts b/apps/api/src/app/import/interfaces/asset-profile-to-create.interface.ts new file mode 100644 index 0000000000..2787b7eb1e --- /dev/null +++ b/apps/api/src/app/import/interfaces/asset-profile-to-create.interface.ts @@ -0,0 +1,6 @@ +import { Prisma } from '@prisma/client'; + +export interface AssetProfileToCreate { + assetProfile: Prisma.SymbolProfileCreateInput; + marketDataObjects: Prisma.MarketDataUpdateInput[]; +} diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index e999bd5953..85b4a068f5 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -39,7 +39,7 @@ import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { eachDayOfInterval, format, isValid } from 'date-fns'; -import { groupBy, isEmpty, isNumber, uniqWith } from 'lodash'; +import { groupBy, isEmpty, isNumber, omit, uniqWith } from 'lodash'; import ms from 'ms'; import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error'; @@ -272,23 +272,31 @@ export class DataProviderService implements OnModuleInit { }); if (!assetProfiles[assetProfileIdentifier]) { + const assetProfileInImport = assetProfilesWithMarketDataDto?.find( + (assetProfileWithMarketData) => { + return ( + assetProfileWithMarketData.dataSource === dataSource && + assetProfileWithMarketData.symbol === symbol + ); + } + ); + + // A custom asset profile of the import is created after the + // validation, thus the data provider cannot resolve it yet if ( (dataSource === DataSource.MANUAL && type === 'BUY') || + assetProfileInImport?.dataSource === DataSource.MANUAL || NON_INVESTMENT_ACTIVITY_TYPES.includes(type) ) { - const assetProfileInImport = assetProfilesWithMarketDataDto?.find( - (assetProfile) => { - return ( - assetProfile.dataSource === dataSource && - assetProfile.symbol === symbol - ); - } - ); - assetProfiles[assetProfileIdentifier] = { - currency, + ...omit(assetProfileInImport ?? {}, [ + 'dataSource', + 'marketData', + 'symbol' + ]), dataSource, symbol, + currency: assetProfileInImport?.currency ?? currency, name: assetProfileInImport?.name ?? symbol }; @@ -308,20 +316,6 @@ export class DataProviderService implements OnModuleInit { )?.[assetProfileIdentifier]; } catch {} - if (!assetProfile?.name) { - const assetProfileInImport = assetProfilesWithMarketDataDto?.find( - (profile) => { - return ( - profile.dataSource === dataSource && profile.symbol === symbol - ); - } - ); - - if (assetProfileInImport) { - Object.assign(assetProfile, assetProfileInImport); - } - } - if (!assetProfile?.name) { throw new Error( `${activityPath}.symbol ("${symbol}") is not valid for the specified data source ("${maskedDataSource}")` diff --git a/apps/client/src/app/components/access-table/access-table.component.html b/apps/client/src/app/components/access-table/access-table.component.html index 38122c39c9..4edaed42d8 100644 --- a/apps/client/src/app/components/access-table/access-table.component.html +++ b/apps/client/src/app/components/access-table/access-table.component.html @@ -17,18 +17,7 @@ Permission -
- @if (hasScopesToWrite(element)) { - - View and manage - } @else if (hasScopeToReadValues(element)) { - - View - } @else { - - Restricted view - } -
+
diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index 58a9e80345..9001841101 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -1,11 +1,8 @@ import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { Access, User } from '@ghostfolio/common/interfaces'; import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { - hasAnyScopeOfWriteAccess, - hasScope, - scopes -} from '@ghostfolio/common/scopes'; +import { getAccessLevel } from '@ghostfolio/common/scopes'; +import { GfAccessLevelIconComponent } from '@ghostfolio/ui/access-level-icon'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'; @@ -31,8 +28,6 @@ import { createOutline, ellipsisHorizontal, linkOutline, - lockClosedOutline, - lockOpenOutline, removeCircleOutline } from 'ionicons/icons'; import ms from 'ms'; @@ -42,6 +37,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ClipboardModule, + GfAccessLevelIconComponent, IonIcon, MatButtonModule, MatMenuModule, @@ -75,6 +71,8 @@ export class GfAccessTableComponent { return columns; }); + protected readonly getAccessLevel = getAccessLevel; + protected readonly isLoading = computed(() => { return !this.accesses(); }); @@ -89,8 +87,6 @@ export class GfAccessTableComponent { createOutline, ellipsisHorizontal, linkOutline, - lockClosedOutline, - lockOpenOutline, removeCircleOutline }); @@ -105,14 +101,6 @@ export class GfAccessTableComponent { return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`; } - protected hasScopeToReadValues({ scopes: scopesOfAccess }: Access) { - return hasScope(scopesOfAccess, scopes.portfolioReadValues); - } - - protected hasScopesToWrite({ scopes: scopesOfAccess }: Access) { - return hasAnyScopeOfWriteAccess(scopesOfAccess); - } - protected onCopyUrlToClipboard(aId: string) { this.clipboard.copy(this.getPublicUrl(aId)); diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index 40c4fe703e..2f61e850a2 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -2,16 +2,15 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { - SCOPES_OF_READ_ACCESS, - SCOPES_OF_READ_RESTRICTED_ACCESS, - SCOPES_OF_WRITE_ACCESS, Scope, - hasAnyScopeOfWriteAccess, + getAccessLevel, + getScopesOfAccessLevel, hasScope, scopes } from '@ghostfolio/common/scopes'; -import { AccountWithPlatform } from '@ghostfolio/common/types'; +import { AccessLevel, AccountWithPlatform } from '@ghostfolio/common/types'; import { validateObjectForForm } from '@ghostfolio/common/utils'; +import { GfAccessLevelIconComponent } from '@ghostfolio/ui/access-level-icon'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfPortfolioFilterFormComponent, @@ -52,16 +51,14 @@ import { MatSelectModule } from '@angular/material/select'; import { StatusCodes } from 'http-status-codes'; import { EMPTY, catchError } from 'rxjs'; -import { - AccessLevel, - CreateOrUpdateAccessDialogParams -} from './interfaces/interfaces'; +import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'h-100' }, imports: [ FormsModule, + GfAccessLevelIconComponent, GfPortfolioFilterFormComponent, MatButtonModule, MatDialogModule, @@ -120,7 +117,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { const isPublic = access?.type === 'PUBLIC'; this.accessForm = this.formBuilder.group({ - accessLevel: this.getAccessLevel(access?.scopes), + accessLevel: getAccessLevel(access?.scopes), alias: [access?.alias ?? ''], filters: [null], granteeUserId: [ @@ -172,6 +169,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { this.loadHoldings(); } + protected get accessLevel(): AccessLevel { + return this.accessForm?.get('accessLevel')?.value as AccessLevel; + } + protected onCancel() { this.dialogRef.close(); } @@ -191,28 +192,18 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } private buildScopes(): Scope[] { - const accessLevel = this.accessForm.get('accessLevel') - ?.value as AccessLevel; - const scopesOfAccess = this.data.access?.scopes ?? []; if ( scopesOfAccess.length > 0 && - accessLevel === this.getAccessLevel(scopesOfAccess) + this.accessLevel === getAccessLevel(scopesOfAccess) ) { return Object.values(scopes).filter((scope) => { return hasScope(scopesOfAccess, scope); }); } - switch (accessLevel) { - case 'CREATE_READ_UPDATE_DELETE': - return [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS]; - case 'READ': - return [...SCOPES_OF_READ_ACCESS]; - default: - return [...SCOPES_OF_READ_RESTRICTED_ACCESS]; - } + return getScopesOfAccessLevel(this.accessLevel); } private async createAccess() { @@ -254,16 +245,6 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } } - private getAccessLevel(scopesOfAccess: string[] | undefined): AccessLevel { - if (hasAnyScopeOfWriteAccess(scopesOfAccess)) { - return 'CREATE_READ_UPDATE_DELETE'; - } - - return hasScope(scopesOfAccess, scopes.portfolioReadValues) - ? 'READ' - : 'READ_RESTRICTED'; - } - private loadHoldings() { this.dataService .fetchPortfolioHoldings() diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index 545dde2280..a01b21138f 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -37,15 +37,22 @@ Permission - Restricted view + + + + + + @if (accessForm.get('type')?.value === 'PRIVATE') { - View + + + View and manage + + } diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts index ca7cee25ab..8d1ac0ba94 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts @@ -3,6 +3,3 @@ import { Access } from '@ghostfolio/common/interfaces'; export interface CreateOrUpdateAccessDialogParams { access?: Access; } - -export type AccessLevel = - 'CREATE_READ_UPDATE_DELETE' | 'READ' | 'READ_RESTRICTED'; diff --git a/libs/common/src/lib/scopes.ts b/libs/common/src/lib/scopes.ts index fa05f42908..ea3c342fde 100644 --- a/libs/common/src/lib/scopes.ts +++ b/libs/common/src/lib/scopes.ts @@ -1,3 +1,5 @@ +import { AccessLevel } from '@ghostfolio/common/types'; + /** * Scopes describe what a grantee may do on behalf of the granting user. They * are a separate axis from the permissions, which describe the capabilities of @@ -57,6 +59,19 @@ export const SCOPES_OF_READ_RESTRICTED_ACCESS: readonly Scope[] = return scope !== scopes.portfolioReadValues; }); +/** + * Access level which the scopes of an access grant + */ +export function getAccessLevel(aScopes: string[] = []): AccessLevel { + if (hasAnyScopeOfWriteAccess(aScopes)) { + return 'CREATE_READ_UPDATE_DELETE'; + } + + return hasScope(aScopes, scopes.portfolioReadValues) + ? 'READ' + : 'READ_RESTRICTED'; +} + export function getScopesOfAccess({ granteeUserId, scopes: scopesOfAccess @@ -81,6 +96,20 @@ export function getScopesOfAccess({ }); } +/** + * Scopes which an access level grants + */ +export function getScopesOfAccessLevel(aAccessLevel: AccessLevel): Scope[] { + switch (aAccessLevel) { + case 'CREATE_READ_UPDATE_DELETE': + return [...SCOPES_OF_READ_ACCESS, ...SCOPES_OF_WRITE_ACCESS]; + case 'READ': + return [...SCOPES_OF_READ_ACCESS]; + default: + return [...SCOPES_OF_READ_RESTRICTED_ACCESS]; + } +} + /** * Scopes of a user acting on their own data, which is unrestricted. The * permissions of the role are evaluated separately. diff --git a/libs/common/src/lib/types/access-level.type.ts b/libs/common/src/lib/types/access-level.type.ts new file mode 100644 index 0000000000..33f0f11a21 --- /dev/null +++ b/libs/common/src/lib/types/access-level.type.ts @@ -0,0 +1,2 @@ +export type AccessLevel = + 'CREATE_READ_UPDATE_DELETE' | 'READ' | 'READ_RESTRICTED'; diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index c31d9079f3..7dea25ddca 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -1,3 +1,4 @@ +import type { AccessLevel } from './access-level.type'; import type { AccessType } from './access-type.type'; import type { AccessWithGranteeUser } from './access-with-grantee-user.type'; import type { AccountWithBalance } from './account-with-balance.type'; @@ -29,6 +30,7 @@ import type { UserWithSettings } from './user-with-settings.type'; import type { ViewMode } from './view-mode.type'; export type { + AccessLevel, AccessType, AccessWithGranteeUser, AccountWithBalance, diff --git a/libs/ui/.storybook/main.mjs b/libs/ui/.storybook/main.mjs index 28a7854e36..7242240263 100644 --- a/libs/ui/.storybook/main.mjs +++ b/libs/ui/.storybook/main.mjs @@ -9,6 +9,9 @@ const config = { getAbsolutePath('@storybook/addon-docs'), getAbsolutePath('@storybook/addon-themes') ], + core: { + disableTelemetry: true + }, framework: { name: getAbsolutePath('@storybook/angular'), options: {} diff --git a/libs/ui/.storybook/preview.js b/libs/ui/.storybook/preview.js index e8e2fe2827..c29b26fd61 100644 --- a/libs/ui/.storybook/preview.js +++ b/libs/ui/.storybook/preview.js @@ -1,3 +1,4 @@ +import '@angular/localize/init'; import { withThemeByClassName } from '@storybook/addon-themes'; const preview = { diff --git a/libs/ui/src/lib/access-level-icon/access-level-icon.component.html b/libs/ui/src/lib/access-level-icon/access-level-icon.component.html new file mode 100644 index 0000000000..c610d4b575 --- /dev/null +++ b/libs/ui/src/lib/access-level-icon/access-level-icon.component.html @@ -0,0 +1,16 @@ + + @switch (accessLevel()) { + @case ('CREATE_READ_UPDATE_DELETE') { + + View and manage + } + @case ('READ') { + + View + } + @case ('READ_RESTRICTED') { + + Restricted view + } + } + diff --git a/libs/ui/src/lib/access-level-icon/access-level-icon.component.stories.ts b/libs/ui/src/lib/access-level-icon/access-level-icon.component.stories.ts new file mode 100644 index 0000000000..a230f60de1 --- /dev/null +++ b/libs/ui/src/lib/access-level-icon/access-level-icon.component.stories.ts @@ -0,0 +1,42 @@ +import { CommonModule } from '@angular/common'; +import { IonIcon } from '@ionic/angular/standalone'; +import { moduleMetadata } from '@storybook/angular'; +import type { Meta, StoryObj } from '@storybook/angular'; + +import { GfAccessLevelIconComponent } from './access-level-icon.component'; + +export default { + title: 'Access Level Icon', + component: GfAccessLevelIconComponent, + decorators: [ + moduleMetadata({ + imports: [CommonModule, IonIcon] + }) + ], + argTypes: { + accessLevel: { + control: 'select', + options: ['CREATE_READ_UPDATE_DELETE', 'READ', 'READ_RESTRICTED'] + } + } +} as Meta; + +type Story = StoryObj; + +export const RestrictedView: Story = { + args: { + accessLevel: 'READ_RESTRICTED' + } +}; + +export const View: Story = { + args: { + accessLevel: 'READ' + } +}; + +export const ViewAndManage: Story = { + args: { + accessLevel: 'CREATE_READ_UPDATE_DELETE' + } +}; diff --git a/libs/ui/src/lib/access-level-icon/access-level-icon.component.ts b/libs/ui/src/lib/access-level-icon/access-level-icon.component.ts new file mode 100644 index 0000000000..3b5fc585d9 --- /dev/null +++ b/libs/ui/src/lib/access-level-icon/access-level-icon.component.ts @@ -0,0 +1,24 @@ +import { AccessLevel } from '@ghostfolio/common/types'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { IonIcon } from '@ionic/angular/standalone'; +import { addIcons } from 'ionicons'; +import { + createOutline, + lockClosedOutline, + lockOpenOutline +} from 'ionicons/icons'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [IonIcon], + selector: 'gf-access-level-icon', + templateUrl: './access-level-icon.component.html' +}) +export class GfAccessLevelIconComponent { + public readonly accessLevel = input.required(); + + public constructor() { + addIcons({ createOutline, lockClosedOutline, lockOpenOutline }); + } +} diff --git a/libs/ui/src/lib/access-level-icon/index.ts b/libs/ui/src/lib/access-level-icon/index.ts new file mode 100644 index 0000000000..8230daa025 --- /dev/null +++ b/libs/ui/src/lib/access-level-icon/index.ts @@ -0,0 +1 @@ +export * from './access-level-icon.component'; diff --git a/libs/ui/src/lib/account-selector/account-selector.component.stories.ts b/libs/ui/src/lib/account-selector/account-selector.component.stories.ts index 6908ef2576..d974145aae 100644 --- a/libs/ui/src/lib/account-selector/account-selector.component.stories.ts +++ b/libs/ui/src/lib/account-selector/account-selector.component.stories.ts @@ -3,7 +3,6 @@ import { AccountWithPlatform } from '@ghostfolio/common/types'; import { CommonModule } from '@angular/common'; import { ANIMATION_MODULE_TYPE, importProvidersFrom } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import '@angular/localize/init'; import { applicationConfig, Meta, diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts b/libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts index beb63e369e..53872ce31a 100644 --- a/libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts @@ -1,6 +1,5 @@ import { ANIMATION_MODULE_TYPE } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import '@angular/localize/init'; import { MatFormFieldModule } from '@angular/material/form-field'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; diff --git a/libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.stories.ts b/libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.stories.ts index b5b2074da9..52e716a821 100644 --- a/libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.stories.ts +++ b/libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.stories.ts @@ -1,4 +1,3 @@ -import '@angular/localize/init'; import { moduleMetadata } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts index f4528aac63..5d0304a7db 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts @@ -3,7 +3,6 @@ import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { CommonModule } from '@angular/common'; import { ANIMATION_MODULE_TYPE } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import '@angular/localize/init'; import { MatButtonModule } from '@angular/material/button'; import { provideNativeDateAdapter } from '@angular/material/core'; import { MatDatepickerModule } from '@angular/material/datepicker'; diff --git a/libs/ui/src/lib/membership-card/membership-card.component.stories.ts b/libs/ui/src/lib/membership-card/membership-card.component.stories.ts index 0d475bda70..234b3517ad 100644 --- a/libs/ui/src/lib/membership-card/membership-card.component.stories.ts +++ b/libs/ui/src/lib/membership-card/membership-card.component.stories.ts @@ -1,5 +1,4 @@ import { CommonModule } from '@angular/common'; -import '@angular/localize/init'; import { MatButtonModule } from '@angular/material/button'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts index 2b28d63a78..fc56158e94 100644 --- a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts @@ -1,6 +1,5 @@ import { AccountWithPlatform } from '@ghostfolio/common/types'; -import '@angular/localize/init'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { holdings } from '../mocks/holdings'; diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts index 8ee9a4c7f0..ccfdbb9f40 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts @@ -1,5 +1,4 @@ import { CommonModule } from '@angular/common'; -import '@angular/localize/init'; import { moduleMetadata } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.stories.ts b/libs/ui/src/lib/tags-selector/tags-selector.component.stories.ts index e8bdd1873c..29e85e393d 100644 --- a/libs/ui/src/lib/tags-selector/tags-selector.component.stories.ts +++ b/libs/ui/src/lib/tags-selector/tags-selector.component.stories.ts @@ -1,6 +1,5 @@ import { CommonModule } from '@angular/common'; import { ANIMATION_MODULE_TYPE } from '@angular/core'; -import '@angular/localize/init'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { GfTagsSelectorComponent } from './tags-selector.component'; diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts index e98b852524..04ea5c65f0 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts @@ -1,7 +1,6 @@ import { DEFAULT_COLOR_SCHEME } from '@ghostfolio/common/config'; import { CommonModule } from '@angular/common'; -import '@angular/localize/init'; import { moduleMetadata } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; diff --git a/libs/ui/src/lib/value/value.component.stories.ts b/libs/ui/src/lib/value/value.component.stories.ts index 19139e266e..eb8520a887 100644 --- a/libs/ui/src/lib/value/value.component.stories.ts +++ b/libs/ui/src/lib/value/value.component.stories.ts @@ -1,5 +1,4 @@ import { ANIMATION_MODULE_TYPE } from '@angular/core'; -import '@angular/localize/init'; import { applicationConfig, moduleMetadata } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';