diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ebe5ce2..7947be561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the sorting to be case-insensitive in the account selector component +- Refactored the services to use the `@Service()` decorator of _Angular_ ## 3.55.0 - 2026-08-19 diff --git a/apps/client/src/app/adapter/custom-date-adapter.ts b/apps/client/src/app/adapter/custom-date-adapter.ts index a2700f4ed..5383d0ca2 100644 --- a/apps/client/src/app/adapter/custom-date-adapter.ts +++ b/apps/client/src/app/adapter/custom-date-adapter.ts @@ -1,9 +1,10 @@ import { getDateFormatString } from '@ghostfolio/common/helper'; -import { inject } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { MAT_DATE_LOCALE, NativeDateAdapter } from '@angular/material/core'; import { addYears, format, getYear, parse } from 'date-fns'; +@Service({ autoProvided: false }) export class CustomDateAdapter extends NativeDateAdapter { public override locale = inject(MAT_DATE_LOCALE); diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts index c4d45b7f0..73ea1e86f 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts @@ -3,15 +3,13 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { AdminService } from '@ghostfolio/ui/services'; -import { Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { EMPTY, Subject, catchError, finalize, forkJoin } from 'rxjs'; -@Injectable() +@Service({ autoProvided: false }) export class AdminMarketDataService { - public constructor( - private adminService: AdminService, - private notificationService: NotificationService - ) {} + private readonly adminService = inject(AdminService); + private readonly notificationService = inject(NotificationService); public deleteAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) { const assetProfileDeleted = new Subject(); diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index 6ac3417db..91a14d135 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -3,7 +3,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes'; import { DataService } from '@ghostfolio/ui/services'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { ActivatedRouteSnapshot, Router, @@ -12,7 +12,7 @@ import { import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; -@Injectable({ providedIn: 'root' }) +@Service() export class AuthGuard { private readonly dataService = inject(DataService); private readonly router = inject(Router); diff --git a/apps/client/src/app/core/auth.interceptor.ts b/apps/client/src/app/core/auth.interceptor.ts index 9c06a11d5..04be15478 100644 --- a/apps/client/src/app/core/auth.interceptor.ts +++ b/apps/client/src/app/core/auth.interceptor.ts @@ -13,10 +13,10 @@ import { HttpInterceptor, HttpRequest } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { Observable } from 'rxjs'; -@Injectable() +@Service({ autoProvided: false }) export class AuthInterceptor implements HttpInterceptor { private readonly impersonationStorageService = inject( ImpersonationStorageService diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index 42c5b3ffc..f5735fc11 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -14,7 +14,7 @@ import { HttpInterceptor, HttpRequest } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { MatSnackBar, MatSnackBarRef, @@ -26,7 +26,7 @@ import ms from 'ms'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; -@Injectable() +@Service({ autoProvided: false }) export class HttpResponseInterceptor implements HttpInterceptor { private readonly info: InfoItem; private snackBarRef: MatSnackBarRef | undefined; diff --git a/apps/client/src/app/core/language.service.ts b/apps/client/src/app/core/language.service.ts deleted file mode 100644 index ebd0e21dc..000000000 --- a/apps/client/src/app/core/language.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable() -export class LanguageService {} diff --git a/apps/client/src/app/core/layout.service.ts b/apps/client/src/app/core/layout.service.ts index 2624c4dd6..afb7161d6 100644 --- a/apps/client/src/app/core/layout.service.ts +++ b/apps/client/src/app/core/layout.service.ts @@ -1,24 +1,22 @@ import { NotificationService } from '@ghostfolio/ui/notifications'; -import { Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Observable, Subject } from 'rxjs'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class LayoutService { public static readonly DEFAULT_NOTIFICATION_MAX_WIDTH = '50rem'; public static readonly DEFAULT_NOTIFICATION_WIDTH = '75vw'; - public shouldReloadContent$: Observable; + public readonly shouldReloadContent$: Observable; - private shouldReloadSubject = new Subject(); + private readonly shouldReloadSubject = new Subject(); - public constructor( - private deviceDetectorService: DeviceDetectorService, - private notificationService: NotificationService - ) { + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly notificationService = inject(NotificationService); + + public constructor() { this.shouldReloadContent$ = this.shouldReloadSubject.asObservable(); const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; diff --git a/apps/client/src/app/core/module-preload.service.ts b/apps/client/src/app/core/module-preload.service.ts index 85d9c5e33..4f9290ede 100644 --- a/apps/client/src/app/core/module-preload.service.ts +++ b/apps/client/src/app/core/module-preload.service.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; import { PreloadingStrategy, Route } from '@angular/router'; import { Observable, of } from 'rxjs'; -@Injectable() +@Service({ autoProvided: false }) export class ModulePreloadService implements PreloadingStrategy { /** * Preloads all lazy loading modules with the attribute 'preload' set to true diff --git a/apps/client/src/app/services/cache.service.ts b/apps/client/src/app/services/cache.service.ts index 69a85f926..5472f9a5c 100644 --- a/apps/client/src/app/services/cache.service.ts +++ b/apps/client/src/app/services/cache.service.ts @@ -1,11 +1,9 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class CacheService { - public constructor(private http: HttpClient) {} + private readonly http = inject(HttpClient); public flush() { return this.http.post(`/api/v1/cache/flush`, {}); diff --git a/apps/client/src/app/services/ics/ics.service.ts b/apps/client/src/app/services/ics/ics.service.ts index a3235380e..d97b427c9 100644 --- a/apps/client/src/app/services/ics/ics.service.ts +++ b/apps/client/src/app/services/ics/ics.service.ts @@ -1,13 +1,11 @@ import { capitalize } from '@ghostfolio/common/helper'; import { ExportResponse } from '@ghostfolio/common/interfaces'; -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; import { Type } from '@prisma/client'; import { format, parseISO } from 'date-fns'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class IcsService { private readonly ICS_DATE_FORMAT = 'yyyyMMdd'; private readonly ICS_LINE_BREAK = '\r\n'; diff --git a/apps/client/src/app/services/impersonation-storage.service.ts b/apps/client/src/app/services/impersonation-storage.service.ts index fb101e220..1cbba6444 100644 --- a/apps/client/src/app/services/impersonation-storage.service.ts +++ b/apps/client/src/app/services/impersonation-storage.service.ts @@ -1,11 +1,9 @@ -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; export const IMPERSONATION_KEY = 'impersonationId'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class ImpersonationStorageService { private hasImpersonationChangeSubject = new BehaviorSubject( this.getId() diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index b73676252..5c06b0e13 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -13,16 +13,14 @@ import { import { Activity } from '@ghostfolio/common/interfaces'; import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { Account, DataSource, Type as ActivityType } from '@prisma/client'; import { isFinite, isNumber, isString } from 'lodash'; import { parse as csvToJson } from 'papaparse'; import { firstValueFrom } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class ImportActivitiesService { private static ACCOUNT_KEYS = ['account', 'accountid']; private static COMMENT_KEYS = ['comment', 'note']; diff --git a/apps/client/src/app/services/page-title.strategy.ts b/apps/client/src/app/services/page-title.strategy.ts index ce04a0af8..ad4a5bdce 100644 --- a/apps/client/src/app/services/page-title.strategy.ts +++ b/apps/client/src/app/services/page-title.strategy.ts @@ -1,16 +1,14 @@ -import { Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; -@Injectable() +@Service({ autoProvided: false }) export class PageTitleStrategy extends TitleStrategy { private static readonly DEFAULT_TITLE = 'Ghostfolio – Open Source Wealth Management Software'; private static readonly DEFAULT_TITLE_SHORT = 'Ghostfolio'; - public constructor(private readonly title: Title) { - super(); - } + private readonly title = inject(Title); public override updateTitle(routerState: RouterStateSnapshot) { const title = this.buildTitle(routerState); diff --git a/apps/client/src/app/services/settings-storage.service.ts b/apps/client/src/app/services/settings-storage.service.ts index 681552cf7..095c11587 100644 --- a/apps/client/src/app/services/settings-storage.service.ts +++ b/apps/client/src/app/services/settings-storage.service.ts @@ -1,12 +1,10 @@ -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; export const KEY_RANGE = 'range'; export const KEY_STAY_SIGNED_IN = 'staySignedIn'; export const KEY_TOKEN = 'auth-token'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class SettingsStorageService { public getSetting(aKey: string): string | null { return window.localStorage.getItem(aKey); diff --git a/apps/client/src/app/services/token-storage.service.ts b/apps/client/src/app/services/token-storage.service.ts index b29fd207b..fe36043d3 100644 --- a/apps/client/src/app/services/token-storage.service.ts +++ b/apps/client/src/app/services/token-storage.service.ts @@ -1,10 +1,8 @@ -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; import { KEY_TOKEN } from './settings-storage.service'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class TokenStorageService { public getToken(): string | null { return ( diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index d58f77c3e..38e9bebc9 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -3,7 +3,7 @@ import { Filter, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HttpClient } from '@angular/common/http'; -import { computed, DestroyRef, inject, Injectable } from '@angular/core'; +import { computed, DestroyRef, inject, Service } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatDialog } from '@angular/material/dialog'; import { ObservableStore } from '@codewithdan/observable-store'; @@ -18,9 +18,7 @@ import { GfSubscriptionInterstitialDialogComponent } from '../../components/subs import { UserStoreActions } from './user-store.actions'; import { UserStoreState } from './user-store.state'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class UserService extends ObservableStore { private readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType diff --git a/apps/client/src/app/services/web-authn.service.ts b/apps/client/src/app/services/web-authn.service.ts index 24eee5dc1..b769e8506 100644 --- a/apps/client/src/app/services/web-authn.service.ts +++ b/apps/client/src/app/services/web-authn.service.ts @@ -6,7 +6,7 @@ import { } from '@ghostfolio/common/interfaces'; import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { startAuthentication, startRegistration @@ -14,9 +14,7 @@ import { import { of } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class WebAuthnService { private static readonly WEB_AUTH_N_DEVICE_ID = 'WEB_AUTH_N_DEVICE_ID'; diff --git a/apps/client/src/main.ts b/apps/client/src/main.ts index 3e80df9c7..159c2a7c9 100644 --- a/apps/client/src/main.ts +++ b/apps/client/src/main.ts @@ -4,7 +4,6 @@ import { registerChartConfiguration } from '@ghostfolio/ui/chart'; import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment'; import { GfNotificationModule } from '@ghostfolio/ui/notifications'; -import { Platform } from '@angular/cdk/platform'; import { provideHttpClient, withInterceptorsFromDi @@ -17,7 +16,6 @@ import { import { DateAdapter, MAT_DATE_FORMATS, - MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core'; import { MatSnackBarModule } from '@angular/material/snack-bar'; @@ -35,7 +33,6 @@ import { GfAppComponent } from './app/app.component'; import { routes } from './app/app.routes'; import { authInterceptorProviders } from './app/core/auth.interceptor'; import { httpResponseInterceptorProviders } from './app/core/http-response.interceptor'; -import { LanguageService } from './app/core/language.service'; import { ModulePreloadService } from './app/core/module-preload.service'; import { PageTitleStrategy } from './app/services/page-title.strategy'; import { environment } from './environments/environment'; @@ -44,8 +41,7 @@ import { environment } from './environments/environment'; const response = await fetch('/api/v1/info'); const info: InfoResponse = await response.json(); const utmSource = window.localStorage.getItem('utm_source') as - | 'ios' - | 'trusted-web-activity'; + 'ios' | 'trusted-web-activity'; info.globalPermissions = filterGlobalPermissions( info.globalPermissions, @@ -79,7 +75,6 @@ import { environment } from './environments/environment'; registrationStrategy: 'registerImmediately' }) ), - LanguageService, ModulePreloadService, provideHttpClient(withInterceptorsFromDi()), provideIonicAngular(), @@ -87,7 +82,6 @@ import { environment } from './environments/environment'; provideNgxSkeletonLoader(), provideZoneChangeDetection(), { - deps: [LanguageService, MAT_DATE_LOCALE, Platform], provide: DateAdapter, useClass: CustomDateAdapter }, diff --git a/libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts b/libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts index db916a34f..dc937e6b9 100644 --- a/libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts +++ b/libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts @@ -1,11 +1,10 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; -@Injectable({ - // Required to allow mocking in Storybook - providedIn: 'root' -}) +// Must stay auto-provided: several table stories render gf-entity-logo +// without providing an override and resolve this from the root injector +@Service() export class EntityLogoImageSourceService { public getLogoUrlByAssetProfileIdentifier({ dataSource, diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts index 848a9efa4..482aee2d5 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.service.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; import { Big } from 'big.js'; -@Injectable() +@Service({ autoProvided: false }) export class FireCalculatorService { private readonly COMPOUND_PERIOD = 12; diff --git a/libs/ui/src/lib/notifications/notification.service.ts b/libs/ui/src/lib/notifications/notification.service.ts index 389f52180..b0758b576 100644 --- a/libs/ui/src/lib/notifications/notification.service.ts +++ b/libs/ui/src/lib/notifications/notification.service.ts @@ -1,7 +1,7 @@ import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { translate } from '@ghostfolio/ui/i18n'; -import { inject, Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { isFunction } from 'lodash'; @@ -14,7 +14,7 @@ import { } from './interfaces/interfaces'; import { GfPromptDialogComponent } from './prompt-dialog/prompt-dialog.component'; -@Injectable() +@Service({ autoProvided: false }) export class NotificationService { private dialogMaxWidth: string; private dialogWidth: string; diff --git a/libs/ui/src/lib/services/admin.service.ts b/libs/ui/src/lib/services/admin.service.ts index a1735f9de..aa048faaf 100644 --- a/libs/ui/src/lib/services/admin.service.ts +++ b/libs/ui/src/lib/services/admin.service.ts @@ -24,14 +24,12 @@ import { DateRange } from '@ghostfolio/common/types'; import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { AssetProfileSplit, MarketData, Platform } from '@prisma/client'; import { JobStatus } from 'bull'; import { isNumber } from 'lodash'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class AdminService { private readonly environment = inject(GF_ENVIRONMENT); private readonly http = inject(HttpClient); diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 7081f9b2c..fb9376041 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -66,7 +66,7 @@ import type { import { translate } from '@ghostfolio/ui/i18n'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { utc } from '@date-fns/utc'; import { @@ -85,9 +85,7 @@ import { cloneDeep, groupBy, isNumber } from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -@Injectable({ - providedIn: 'root' -}) +@Service() export class DataService { private readonly http = inject(HttpClient);