Browse Source

Merge branch 'main' into task/improve-type-filter-of-activities-table-component-to-only-list-activity-types-in-use

pull/7602/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
599aa6e18a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 90
      apps/api/src/app/import/import.service.ts
  3. 6
      apps/api/src/app/import/interfaces/asset-profile-to-create.interface.ts
  4. 44
      apps/api/src/services/data-provider/data-provider.service.ts
  5. 13
      apps/client/src/app/components/access-table/access-table.component.html
  6. 22
      apps/client/src/app/components/access-table/access-table.component.ts
  7. 45
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  8. 15
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  9. 3
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts
  10. 29
      libs/common/src/lib/scopes.ts
  11. 2
      libs/common/src/lib/types/access-level.type.ts
  12. 2
      libs/common/src/lib/types/index.ts
  13. 3
      libs/ui/.storybook/main.mjs
  14. 1
      libs/ui/.storybook/preview.js
  15. 16
      libs/ui/src/lib/access-level-icon/access-level-icon.component.html
  16. 42
      libs/ui/src/lib/access-level-icon/access-level-icon.component.stories.ts
  17. 24
      libs/ui/src/lib/access-level-icon/access-level-icon.component.ts
  18. 1
      libs/ui/src/lib/access-level-icon/index.ts
  19. 1
      libs/ui/src/lib/account-selector/account-selector.component.stories.ts
  20. 1
      libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts
  21. 1
      libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.stories.ts
  22. 1
      libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts
  23. 1
      libs/ui/src/lib/membership-card/membership-card.component.stories.ts
  24. 1
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts
  25. 1
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.stories.ts
  26. 1
      libs/ui/src/lib/tags-selector/tags-selector.component.stories.ts
  27. 1
      libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts
  28. 1
      libs/ui/src/lib/value/value.component.stories.ts

8
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

90
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<CreateOrderDto>[];
userCurrency: string;
userId: string;
}): Promise<Partial<Activity>[]> {
}): Promise<(Partial<Activity> & Pick<Activity, 'assetProfile'>)[]> {
const { activities: existingActivities } =
await this.activitiesService.getActivities({
userCurrency,
@ -1055,6 +1107,30 @@ export class ImportService {
return matchingAccountsOfUser[0];
}
private getAssetProfilesToCreate({
activities,
assetProfiles
}: {
activities: Pick<Activity, 'assetProfile' | 'error'>[];
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<string>();

6
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[];
}

44
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}")`

13
apps/client/src/app/components/access-table/access-table.component.html

@ -17,18 +17,7 @@
<ng-container matColumnDef="type">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<div class="align-items-center d-flex">
@if (hasScopesToWrite(element)) {
<ion-icon class="mr-1" name="create-outline" />
<ng-container i18n>View and manage</ng-container>
} @else if (hasScopeToReadValues(element)) {
<ion-icon class="mr-1" name="lock-open-outline" />
<ng-container i18n>View</ng-container>
} @else {
<ion-icon class="mr-1" name="lock-closed-outline" />
<ng-container i18n>Restricted view</ng-container>
}
</div>
<gf-access-level-icon [accessLevel]="getAccessLevel(element.scopes)" />
</td>
</ng-container>

22
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));

45
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()

15
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -37,15 +37,22 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Permission</mat-label>
<mat-select formControlName="accessLevel">
<mat-option i18n value="READ_RESTRICTED">Restricted view</mat-option>
<mat-select-trigger>
<gf-access-level-icon [accessLevel]="accessLevel" />
</mat-select-trigger>
<mat-option value="READ_RESTRICTED">
<gf-access-level-icon accessLevel="READ_RESTRICTED" />
</mat-option>
@if (accessForm.get('type')?.value === 'PRIVATE') {
<mat-option i18n value="READ">View</mat-option>
<mat-option value="READ">
<gf-access-level-icon accessLevel="READ" />
</mat-option>
<mat-option
i18n
value="CREATE_READ_UPDATE_DELETE"
[disabled]="!canGrantWriteAccess"
>View and manage</mat-option
>
<gf-access-level-icon accessLevel="CREATE_READ_UPDATE_DELETE" />
</mat-option>
}
</mat-select>
</mat-form-field>

3
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';

29
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.

2
libs/common/src/lib/types/access-level.type.ts

@ -0,0 +1,2 @@
export type AccessLevel =
'CREATE_READ_UPDATE_DELETE' | 'READ' | 'READ_RESTRICTED';

2
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,

3
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: {}

1
libs/ui/.storybook/preview.js

@ -1,3 +1,4 @@
import '@angular/localize/init';
import { withThemeByClassName } from '@storybook/addon-themes';
const preview = {

16
libs/ui/src/lib/access-level-icon/access-level-icon.component.html

@ -0,0 +1,16 @@
<span class="align-items-center d-flex">
@switch (accessLevel()) {
@case ('CREATE_READ_UPDATE_DELETE') {
<ion-icon class="mr-1" name="create-outline" />
<ng-container i18n>View and manage</ng-container>
}
@case ('READ') {
<ion-icon class="mr-1" name="lock-open-outline" />
<ng-container i18n>View</ng-container>
}
@case ('READ_RESTRICTED') {
<ion-icon class="mr-1" name="lock-closed-outline" />
<ng-container i18n>Restricted view</ng-container>
}
}
</span>

42
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<GfAccessLevelIconComponent>;
type Story = StoryObj<GfAccessLevelIconComponent>;
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'
}
};

24
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<AccessLevel>();
public constructor() {
addIcons({ createOutline, lockClosedOutline, lockOpenOutline });
}
}

1
libs/ui/src/lib/access-level-icon/index.ts

@ -0,0 +1 @@
export * from './access-level-icon.component';

1
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,

1
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';

1
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';

1
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';

1
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';

1
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';

1
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';

1
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';

1
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';

1
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';

Loading…
Cancel
Save