Browse Source

Merge remote-tracking branch 'origin/main' into task/services-type-safety

pull/6557/head
KenTandrian 2 weeks ago
parent
commit
0ffef5f54c
  1. 2
      .github/workflows/build-code.yml
  2. 6
      CHANGELOG.md
  3. 26
      apps/client/src/app/app.component.ts
  4. 45
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  5. 37
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  6. 18
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts
  7. 34
      apps/client/src/app/components/admin-platform/admin-platform.component.ts
  8. 17
      apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.component.ts
  9. 21
      apps/client/src/app/components/admin-settings/admin-settings.component.ts
  10. 34
      apps/client/src/app/components/admin-tag/admin-tag.component.ts
  11. 17
      apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts
  12. 26
      apps/client/src/app/components/admin-users/admin-users.component.ts
  13. 21
      apps/client/src/app/components/data-provider-status/data-provider-status.component.ts
  14. 30
      apps/client/src/app/components/header/header.component.ts
  15. 17
      apps/client/src/app/pages/auth/auth-page.component.ts
  16. 15
      apps/client/src/app/pages/public/public-page.component.ts
  17. 6
      libs/common/src/lib/helper.spec.ts
  18. 7
      libs/ui/src/lib/tags-selector/interfaces/interfaces.ts
  19. 2
      libs/ui/src/lib/tags-selector/tags-selector.component.html
  20. 61
      libs/ui/src/lib/tags-selector/tags-selector.component.ts
  21. 27
      package-lock.json
  22. 2
      package.json

2
.github/workflows/build-code.yml

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node_version: node_version:
- 22 - 22.22.1
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

6
CHANGELOG.md

@ -11,7 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Consolidated the sign-out logic within the user service to unify cookie, state and token clearance - Consolidated the sign-out logic within the user service to unify cookie, state and token clearance
- Improved the language localization for Polish (`pl`) - Improved the language localization for Polish (`pl`)
- Upgraded `@ionic/angular` from version `8.7.3` to `8.8.1`
- Upgraded `svgmap` from version `2.14.0` to `2.19.2` - Upgraded `svgmap` from version `2.14.0` to `2.19.2`
- Pinned the _Node.js_ version in the _Build code_ _GitHub Action_ to ensure environment consistency for tests
### Fixed
- Fixed an issue with the detection of the thousand separator for the `de-CH` locale
## 2.249.0 - 2026-03-10 ## 2.249.0 - 2026-03-10

26
apps/client/src/app/app.component.ts

@ -10,12 +10,13 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef,
DOCUMENT, DOCUMENT,
HostBinding, HostBinding,
Inject, Inject,
OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { import {
@ -30,8 +31,7 @@ import { DataSource } from '@prisma/client';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { openOutline } from 'ionicons/icons'; import { openOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { filter } from 'rxjs/operators';
import { filter, takeUntil } from 'rxjs/operators';
import { GfFooterComponent } from './components/footer/footer.component'; import { GfFooterComponent } from './components/footer/footer.component';
import { GfHeaderComponent } from './components/header/header.component'; import { GfHeaderComponent } from './components/header/header.component';
@ -47,7 +47,7 @@ import { UserService } from './services/user/user.service';
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
templateUrl: './app.component.html' templateUrl: './app.component.html'
}) })
export class GfAppComponent implements OnDestroy, OnInit { export class GfAppComponent implements OnInit {
@HostBinding('class.has-info-message') get getHasMessage() { @HostBinding('class.has-info-message') get getHasMessage() {
return this.hasInfoMessage; return this.hasInfoMessage;
} }
@ -68,11 +68,10 @@ export class GfAppComponent implements OnDestroy, OnInit {
public showFooter = false; public showFooter = false;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
@ -87,7 +86,7 @@ export class GfAppComponent implements OnDestroy, OnInit {
this.user = undefined; this.user = undefined;
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
if ( if (
params['dataSource'] && params['dataSource'] &&
@ -110,7 +109,7 @@ export class GfAppComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
@ -199,7 +198,7 @@ export class GfAppComponent implements OnDestroy, OnInit {
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
this.user = state.user; this.user = state.user;
@ -243,11 +242,6 @@ export class GfAppComponent implements OnDestroy, OnInit {
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private initializeTheme(userPreferredColorScheme?: ColorScheme) { private initializeTheme(userPreferredColorScheme?: ColorScheme) {
const isDarkTheme = userPreferredColorScheme const isDarkTheme = userPreferredColorScheme
? userPreferredColorScheme === 'DARK' ? userPreferredColorScheme === 'DARK'
@ -271,7 +265,7 @@ export class GfAppComponent implements OnDestroy, OnInit {
}) { }) {
this.userService this.userService
.get() .get()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -317,7 +311,7 @@ export class GfAppComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.router.navigate([], { this.router.navigate([], {
queryParams: { queryParams: {

45
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -26,10 +26,11 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
OnDestroy, DestroyRef,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -63,7 +64,7 @@ import {
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { distinctUntilChanged } from 'rxjs/operators';
import { AdminMarketDataService } from './admin-market-data.service'; import { AdminMarketDataService } from './admin-market-data.service';
import { GfAssetProfileDialogComponent } from './asset-profile-dialog/asset-profile-dialog.component'; import { GfAssetProfileDialogComponent } from './asset-profile-dialog/asset-profile-dialog.component';
@ -95,9 +96,7 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in
styleUrls: ['./admin-market-data.scss'], styleUrls: ['./admin-market-data.scss'],
templateUrl: './admin-market-data.html' templateUrl: './admin-market-data.html'
}) })
export class GfAdminMarketDataComponent export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
implements AfterViewInit, OnDestroy, OnInit
{
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ -166,13 +165,12 @@ export class GfAdminMarketDataComponent
public totalItems = 0; public totalItems = 0;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
public adminMarketDataService: AdminMarketDataService, public adminMarketDataService: AdminMarketDataService,
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -209,7 +207,7 @@ export class GfAdminMarketDataComponent
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
if ( if (
params['assetProfileDialog'] && params['assetProfileDialog'] &&
@ -226,7 +224,7 @@ export class GfAdminMarketDataComponent
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -238,7 +236,7 @@ export class GfAdminMarketDataComponent
}); });
this.filters$ this.filters$
.pipe(distinctUntilChanged(), takeUntil(this.unsubscribeSubject)) .pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
.subscribe((filters) => { .subscribe((filters) => {
this.activeFilters = filters; this.activeFilters = filters;
@ -302,7 +300,7 @@ export class GfAdminMarketDataComponent
public onGather7Days() { public onGather7Days() {
this.adminService this.adminService
.gather7Days() .gather7Days()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
@ -313,7 +311,7 @@ export class GfAdminMarketDataComponent
public onGatherMax() { public onGatherMax() {
this.adminService this.adminService
.gatherMax() .gatherMax()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
@ -324,7 +322,7 @@ export class GfAdminMarketDataComponent
public onGatherProfileData() { public onGatherProfileData() {
this.adminService this.adminService
.gatherProfileData() .gatherProfileData()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
} }
@ -334,14 +332,14 @@ export class GfAdminMarketDataComponent
}: AssetProfileIdentifier) { }: AssetProfileIdentifier) {
this.adminService this.adminService
.gatherProfileDataBySymbol({ dataSource, symbol }) .gatherProfileDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
} }
public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) { public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
this.adminService this.adminService
.gatherSymbol({ dataSource, symbol }) .gatherSymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
} }
@ -358,11 +356,6 @@ export class GfAdminMarketDataComponent
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private loadData( private loadData(
{ {
pageIndex, pageIndex,
@ -399,7 +392,7 @@ export class GfAdminMarketDataComponent
skip: pageIndex * this.pageSize, skip: pageIndex * this.pageSize,
take: this.pageSize take: this.pageSize
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ count, marketData }) => { .subscribe(({ count, marketData }) => {
this.totalItems = count; this.totalItems = count;
@ -430,7 +423,7 @@ export class GfAdminMarketDataComponent
}) { }) {
this.userService this.userService
.get() .get()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -452,7 +445,7 @@ export class GfAdminMarketDataComponent
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe( .subscribe(
(newAssetProfileIdentifier: AssetProfileIdentifier | undefined) => { (newAssetProfileIdentifier: AssetProfileIdentifier | undefined) => {
if (newAssetProfileIdentifier) { if (newAssetProfileIdentifier) {
@ -468,7 +461,7 @@ export class GfAdminMarketDataComponent
private openCreateAssetProfileDialog() { private openCreateAssetProfileDialog() {
this.userService this.userService
.get() .get()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -486,7 +479,7 @@ export class GfAdminMarketDataComponent
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((result) => { .subscribe((result) => {
if (!result) { if (!result) {
this.router.navigate(['.'], { relativeTo: this.route }); this.router.navigate(['.'], { relativeTo: this.route });
@ -499,7 +492,7 @@ export class GfAdminMarketDataComponent
if (addAssetProfile && dataSource && symbol) { if (addAssetProfile && dataSource && symbol) {
this.adminService this.adminService
.addAssetProfile({ dataSource, symbol }) .addAssetProfile({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.loadData(); this.loadData();
}); });

37
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -37,12 +37,13 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef,
ElementRef, ElementRef,
Inject, Inject,
OnDestroy,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
AbstractControl, AbstractControl,
FormBuilder, FormBuilder,
@ -87,8 +88,8 @@ import {
serverOutline serverOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import ms from 'ms'; import ms from 'ms';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { AssetProfileDialogParams } from './interfaces/interfaces'; import { AssetProfileDialogParams } from './interfaces/interfaces';
@ -121,7 +122,7 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
styleUrls: ['./asset-profile-dialog.component.scss'], styleUrls: ['./asset-profile-dialog.component.scss'],
templateUrl: 'asset-profile-dialog.html' templateUrl: 'asset-profile-dialog.html'
}) })
export class GfAssetProfileDialogComponent implements OnDestroy, OnInit { export class GfAssetProfileDialogComponent implements OnInit {
private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format( private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format(
new Date(), new Date(),
DATE_FORMAT DATE_FORMAT
@ -241,14 +242,13 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
public adminMarketDataService: AdminMarketDataService, public adminMarketDataService: AdminMarketDataService,
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams, @Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfAssetProfileDialogComponent>, public dialogRef: MatDialogRef<GfAssetProfileDialogComponent>,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private notificationService: NotificationService, private notificationService: NotificationService,
@ -282,7 +282,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
this.adminService this.adminService
.fetchAdminData() .fetchAdminData()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ settings }) => { .subscribe(({ settings }) => {
this.isDataGatheringEnabled = this.isDataGatheringEnabled =
settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true; settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true;
@ -291,7 +291,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -300,7 +300,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
this.assetProfileForm this.assetProfileForm
.get('assetClass') .get('assetClass')
.valueChanges.pipe(takeUntil(this.unsubscribeSubject)) .valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((assetClass) => { .subscribe((assetClass) => {
const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? []; const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? [];
@ -323,7 +323,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
dataSource: this.data.dataSource, dataSource: this.data.dataSource,
symbol: this.data.symbol symbol: this.data.symbol
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ assetProfile, marketData }) => { .subscribe(({ assetProfile, marketData }) => {
this.assetProfile = assetProfile; this.assetProfile = assetProfile;
@ -436,7 +436,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
}: AssetProfileIdentifier) { }: AssetProfileIdentifier) {
this.adminService this.adminService
.gatherProfileDataBySymbol({ dataSource, symbol }) .gatherProfileDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
} }
@ -449,7 +449,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
} & AssetProfileIdentifier) { } & AssetProfileIdentifier) {
this.adminService this.adminService
.gatherSymbol({ dataSource, range, symbol }) .gatherSymbol({ dataSource, range, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
} }
@ -462,7 +462,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
public onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) { public onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService this.dataService
.postBenchmark({ dataSource, symbol }) .postBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.dataService.updateInfo(); this.dataService.updateInfo();
@ -664,7 +664,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
return EMPTY; return EMPTY;
}), }),
takeUntil(this.unsubscribeSubject) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {
const newAssetProfileIdentifier = { const newAssetProfileIdentifier = {
@ -714,7 +714,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
}); });
return EMPTY; return EMPTY;
}), }),
takeUntil(this.unsubscribeSubject) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(({ price }) => { .subscribe(({ price }) => {
this.notificationService.alert({ this.notificationService.alert({
@ -745,7 +745,7 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
public onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) { public onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService this.dataService
.deleteBenchmark({ dataSource, symbol }) .deleteBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.dataService.updateInfo(); this.dataService.updateInfo();
@ -755,11 +755,6 @@ export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
public onTriggerSubmitAssetProfileForm() { public onTriggerSubmitAssetProfileForm() {
if (this.assetProfileForm.valid) { if (this.assetProfileForm.valid) {
this.onSubmitAssetProfileForm(); this.onSubmitAssetProfileForm();

18
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts

@ -10,9 +10,10 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
AbstractControl, AbstractControl,
FormBuilder, FormBuilder,
@ -31,7 +32,7 @@ import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { isISO4217CurrencyCode } from 'class-validator'; import { isISO4217CurrencyCode } from 'class-validator';
import { Subject, switchMap, takeUntil } from 'rxjs'; import { switchMap } from 'rxjs';
import { CreateAssetProfileDialogMode } from './interfaces/interfaces'; import { CreateAssetProfileDialogMode } from './interfaces/interfaces';
@ -52,19 +53,19 @@ import { CreateAssetProfileDialogMode } from './interfaces/interfaces';
styleUrls: ['./create-asset-profile-dialog.component.scss'], styleUrls: ['./create-asset-profile-dialog.component.scss'],
templateUrl: 'create-asset-profile-dialog.html' templateUrl: 'create-asset-profile-dialog.html'
}) })
export class GfCreateAssetProfileDialogComponent implements OnDestroy, OnInit { export class GfCreateAssetProfileDialogComponent implements OnInit {
public createAssetProfileForm: FormGroup; public createAssetProfileForm: FormGroup;
public ghostfolioPrefix = `${ghostfolioPrefix}_`; public ghostfolioPrefix = `${ghostfolioPrefix}_`;
public mode: CreateAssetProfileDialogMode; public mode: CreateAssetProfileDialogMode;
private customCurrencies: string[]; private customCurrencies: string[];
private dataSourceForExchangeRates: DataSource; private dataSourceForExchangeRates: DataSource;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
public readonly adminService: AdminService, public readonly adminService: AdminService,
private readonly changeDetectorRef: ChangeDetectorRef, private readonly changeDetectorRef: ChangeDetectorRef,
private readonly dataService: DataService, private readonly dataService: DataService,
private readonly destroyRef: DestroyRef,
public readonly dialogRef: MatDialogRef<GfCreateAssetProfileDialogComponent>, public readonly dialogRef: MatDialogRef<GfCreateAssetProfileDialogComponent>,
public readonly formBuilder: FormBuilder public readonly formBuilder: FormBuilder
) {} ) {}
@ -125,7 +126,7 @@ export class GfCreateAssetProfileDialogComponent implements OnDestroy, OnInit {
symbol: `${DEFAULT_CURRENCY}${currency}` symbol: `${DEFAULT_CURRENCY}${currency}`
}); });
}), }),
takeUntil(this.unsubscribeSubject) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {
this.dialogRef.close({ this.dialogRef.close({
@ -154,11 +155,6 @@ export class GfCreateAssetProfileDialogComponent implements OnDestroy, OnInit {
return false; return false;
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private atLeastOneValid(control: AbstractControl): ValidationErrors { private atLeastOneValid(control: AbstractControl): ValidationErrors {
const addCurrencyControl = control.get('addCurrency'); const addCurrencyControl = control.get('addCurrency');
const addSymbolControl = control.get('addSymbol'); const addSymbolControl = control.get('addSymbol');
@ -189,7 +185,7 @@ export class GfCreateAssetProfileDialogComponent implements OnDestroy, OnInit {
private initialize() { private initialize() {
this.adminService this.adminService
.fetchAdminData() .fetchAdminData()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ dataProviders, settings }) => { .subscribe(({ dataProviders, settings }) => {
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[]; this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];

34
apps/client/src/app/components/admin-platform/admin-platform.component.ts

@ -11,11 +11,12 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef,
Input, Input,
OnDestroy,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -32,7 +33,6 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
import { get } from 'lodash'; import { get } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, takeUntil } from 'rxjs';
import { GfCreateOrUpdatePlatformDialogComponent } from './create-or-update-platform-dialog/create-or-update-platform-dialog.component'; import { GfCreateOrUpdatePlatformDialogComponent } from './create-or-update-platform-dialog/create-or-update-platform-dialog.component';
import { CreateOrUpdatePlatformDialogParams } from './create-or-update-platform-dialog/interfaces/interfaces'; import { CreateOrUpdatePlatformDialogParams } from './create-or-update-platform-dialog/interfaces/interfaces';
@ -53,7 +53,7 @@ import { CreateOrUpdatePlatformDialogParams } from './create-or-update-platform-
styleUrls: ['./admin-platform.component.scss'], styleUrls: ['./admin-platform.component.scss'],
templateUrl: './admin-platform.component.html' templateUrl: './admin-platform.component.html'
}) })
export class GfAdminPlatformComponent implements OnDestroy, OnInit { export class GfAdminPlatformComponent implements OnInit {
@Input() locale = getLocale(); @Input() locale = getLocale();
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ -63,12 +63,11 @@ export class GfAdminPlatformComponent implements OnDestroy, OnInit {
public displayedColumns = ['name', 'url', 'accounts', 'actions']; public displayedColumns = ['name', 'url', 'accounts', 'actions'];
public platforms: Platform[]; public platforms: Platform[];
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private notificationService: NotificationService, private notificationService: NotificationService,
@ -77,7 +76,7 @@ export class GfAdminPlatformComponent implements OnDestroy, OnInit {
private userService: UserService private userService: UserService
) { ) {
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
if (params['createPlatformDialog']) { if (params['createPlatformDialog']) {
this.openCreatePlatformDialog(); this.openCreatePlatformDialog();
@ -119,20 +118,15 @@ export class GfAdminPlatformComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private deletePlatform(aId: string) { private deletePlatform(aId: string) {
this.adminService this.adminService
.deletePlatform(aId) .deletePlatform(aId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchPlatforms(); this.fetchPlatforms();
@ -143,7 +137,7 @@ export class GfAdminPlatformComponent implements OnDestroy, OnInit {
private fetchPlatforms() { private fetchPlatforms() {
this.adminService this.adminService
.fetchPlatforms() .fetchPlatforms()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((platforms) => { .subscribe((platforms) => {
this.platforms = platforms; this.platforms = platforms;
@ -175,17 +169,17 @@ export class GfAdminPlatformComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((platform: CreatePlatformDto | null) => { .subscribe((platform: CreatePlatformDto | null) => {
if (platform) { if (platform) {
this.adminService this.adminService
.postPlatform(platform) .postPlatform(platform)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchPlatforms(); this.fetchPlatforms();
@ -223,17 +217,17 @@ export class GfAdminPlatformComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((platform: UpdatePlatformDto | null) => { .subscribe((platform: UpdatePlatformDto | null) => {
if (platform) { if (platform) {
this.adminService this.adminService
.putPlatform(platform) .putPlatform(platform)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchPlatforms(); this.fetchPlatforms();

17
apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.component.ts

@ -2,12 +2,7 @@ import { CreatePlatformDto, UpdatePlatformDto } from '@ghostfolio/common/dtos';
import { validateObjectForForm } from '@ghostfolio/common/utils'; import { validateObjectForForm } from '@ghostfolio/common/utils';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
ChangeDetectionStrategy,
Component,
Inject,
OnDestroy
} from '@angular/core';
import { import {
FormBuilder, FormBuilder,
FormGroup, FormGroup,
@ -23,7 +18,6 @@ import {
} from '@angular/material/dialog'; } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { Subject } from 'rxjs';
import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces'; import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces';
@ -43,11 +37,9 @@ import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces';
styleUrls: ['./create-or-update-platform-dialog.scss'], styleUrls: ['./create-or-update-platform-dialog.scss'],
templateUrl: 'create-or-update-platform-dialog.html' templateUrl: 'create-or-update-platform-dialog.html'
}) })
export class GfCreateOrUpdatePlatformDialogComponent implements OnDestroy { export class GfCreateOrUpdatePlatformDialogComponent {
public platformForm: FormGroup; public platformForm: FormGroup;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams, @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams,
public dialogRef: MatDialogRef<GfCreateOrUpdatePlatformDialogComponent>, public dialogRef: MatDialogRef<GfCreateOrUpdatePlatformDialogComponent>,
@ -90,9 +82,4 @@ export class GfCreateOrUpdatePlatformDialogComponent implements OnDestroy {
console.error(error); console.error(error);
} }
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

21
apps/client/src/app/components/admin-settings/admin-settings.component.ts

@ -22,10 +22,11 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
OnDestroy, DestroyRef,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -38,7 +39,7 @@ import { addIcons } from 'ionicons';
import { ellipsisHorizontal, trashOutline } from 'ionicons/icons'; import { ellipsisHorizontal, trashOutline } from 'ionicons/icons';
import { get } from 'lodash'; import { get } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { catchError, filter, of, Subject, takeUntil } from 'rxjs'; import { catchError, filter, of } from 'rxjs';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -64,7 +65,7 @@ import { catchError, filter, of, Subject, takeUntil } from 'rxjs';
styleUrls: ['./admin-settings.component.scss'], styleUrls: ['./admin-settings.component.scss'],
templateUrl: './admin-settings.component.html' templateUrl: './admin-settings.component.html'
}) })
export class GfAdminSettingsComponent implements OnDestroy, OnInit { export class GfAdminSettingsComponent implements OnInit {
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public dataSource = new MatTableDataSource<DataProviderInfo>(); public dataSource = new MatTableDataSource<DataProviderInfo>();
@ -82,12 +83,11 @@ export class GfAdminSettingsComponent implements OnDestroy, OnInit {
public pricingUrl: string; public pricingUrl: string;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private notificationService: NotificationService, private notificationService: NotificationService,
private userService: UserService private userService: UserService
) { ) {
@ -96,7 +96,7 @@ export class GfAdminSettingsComponent implements OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -153,11 +153,6 @@ export class GfAdminSettingsComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private initialize() { private initialize() {
this.isLoading = true; this.isLoading = true;
@ -165,7 +160,7 @@ export class GfAdminSettingsComponent implements OnDestroy, OnInit {
this.adminService this.adminService
.fetchAdminData() .fetchAdminData()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ dataProviders, settings }) => { .subscribe(({ dataProviders, settings }) => {
const filteredProviders = dataProviders.filter(({ dataSource }) => { const filteredProviders = dataProviders.filter(({ dataSource }) => {
return dataSource !== 'MANUAL'; return dataSource !== 'MANUAL';
@ -193,7 +188,7 @@ export class GfAdminSettingsComponent implements OnDestroy, OnInit {
filter((status) => { filter((status) => {
return status !== null; return status !== null;
}), }),
takeUntil(this.unsubscribeSubject) takeUntilDestroyed(this.destroyRef)
) )
.subscribe((status) => { .subscribe((status) => {
this.ghostfolioApiStatus = status; this.ghostfolioApiStatus = status;

34
apps/client/src/app/components/admin-tag/admin-tag.component.ts

@ -10,11 +10,12 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef,
Input, Input,
OnDestroy,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -31,7 +32,6 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
import { get } from 'lodash'; import { get } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, takeUntil } from 'rxjs';
import { GfCreateOrUpdateTagDialogComponent } from './create-or-update-tag-dialog/create-or-update-tag-dialog.component'; import { GfCreateOrUpdateTagDialogComponent } from './create-or-update-tag-dialog/create-or-update-tag-dialog.component';
import { CreateOrUpdateTagDialogParams } from './create-or-update-tag-dialog/interfaces/interfaces'; import { CreateOrUpdateTagDialogParams } from './create-or-update-tag-dialog/interfaces/interfaces';
@ -51,7 +51,7 @@ import { CreateOrUpdateTagDialogParams } from './create-or-update-tag-dialog/int
styleUrls: ['./admin-tag.component.scss'], styleUrls: ['./admin-tag.component.scss'],
templateUrl: './admin-tag.component.html' templateUrl: './admin-tag.component.html'
}) })
export class GfAdminTagComponent implements OnDestroy, OnInit { export class GfAdminTagComponent implements OnInit {
@Input() locale = getLocale(); @Input() locale = getLocale();
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ -61,11 +61,10 @@ export class GfAdminTagComponent implements OnDestroy, OnInit {
public displayedColumns = ['name', 'userId', 'activities', 'actions']; public displayedColumns = ['name', 'userId', 'activities', 'actions'];
public tags: Tag[]; public tags: Tag[];
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private notificationService: NotificationService, private notificationService: NotificationService,
@ -74,7 +73,7 @@ export class GfAdminTagComponent implements OnDestroy, OnInit {
private userService: UserService private userService: UserService
) { ) {
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
if (params['createTagDialog']) { if (params['createTagDialog']) {
this.openCreateTagDialog(); this.openCreateTagDialog();
@ -116,20 +115,15 @@ export class GfAdminTagComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private deleteTag(aId: string) { private deleteTag(aId: string) {
this.dataService this.dataService
.deleteTag(aId) .deleteTag(aId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchTags(); this.fetchTags();
@ -140,7 +134,7 @@ export class GfAdminTagComponent implements OnDestroy, OnInit {
private fetchTags() { private fetchTags() {
this.dataService this.dataService
.fetchTags() .fetchTags()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((tags) => { .subscribe((tags) => {
this.tags = tags; this.tags = tags;
@ -171,17 +165,17 @@ export class GfAdminTagComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((tag: CreateTagDto | null) => { .subscribe((tag: CreateTagDto | null) => {
if (tag) { if (tag) {
this.dataService this.dataService
.postTag(tag) .postTag(tag)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchTags(); this.fetchTags();
@ -210,17 +204,17 @@ export class GfAdminTagComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((tag: UpdateTagDto | null) => { .subscribe((tag: UpdateTagDto | null) => {
if (tag) { if (tag) {
this.dataService this.dataService
.putTag(tag) .putTag(tag)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchTags(); this.fetchTags();

17
apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.component.ts

@ -1,12 +1,7 @@
import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos'; import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos';
import { validateObjectForForm } from '@ghostfolio/common/utils'; import { validateObjectForForm } from '@ghostfolio/common/utils';
import { import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
ChangeDetectionStrategy,
Component,
Inject,
OnDestroy
} from '@angular/core';
import { import {
FormBuilder, FormBuilder,
FormGroup, FormGroup,
@ -21,7 +16,6 @@ import {
} from '@angular/material/dialog'; } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { Subject } from 'rxjs';
import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces'; import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces';
@ -40,11 +34,9 @@ import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces';
styleUrls: ['./create-or-update-tag-dialog.scss'], styleUrls: ['./create-or-update-tag-dialog.scss'],
templateUrl: 'create-or-update-tag-dialog.html' templateUrl: 'create-or-update-tag-dialog.html'
}) })
export class GfCreateOrUpdateTagDialogComponent implements OnDestroy { export class GfCreateOrUpdateTagDialogComponent {
public tagForm: FormGroup; public tagForm: FormGroup;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams, @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams,
public dialogRef: MatDialogRef<GfCreateOrUpdateTagDialogComponent>, public dialogRef: MatDialogRef<GfCreateOrUpdateTagDialogComponent>,
@ -85,9 +77,4 @@ export class GfCreateOrUpdateTagDialogComponent implements OnDestroy {
console.error(error); console.error(error);
} }
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

26
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -25,10 +25,11 @@ import { CommonModule } from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
OnDestroy, DestroyRef,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -55,8 +56,7 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs'; import { switchMap, tap } from 'rxjs/operators';
import { switchMap, takeUntil, tap } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -75,7 +75,7 @@ import { switchMap, takeUntil, tap } from 'rxjs/operators';
styleUrls: ['./admin-users.scss'], styleUrls: ['./admin-users.scss'],
templateUrl: './admin-users.html' templateUrl: './admin-users.html'
}) })
export class GfAdminUsersComponent implements OnDestroy, OnInit { export class GfAdminUsersComponent implements OnInit {
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
public dataSource = new MatTableDataSource<AdminUsersResponse['users'][0]>(); public dataSource = new MatTableDataSource<AdminUsersResponse['users'][0]>();
@ -93,12 +93,11 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
public totalItems = 0; public totalItems = 0;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
@ -139,7 +138,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
this.userService.stateChanged this.userService.stateChanged
.pipe( .pipe(
takeUntil(this.unsubscribeSubject), takeUntilDestroyed(this.destroyRef),
tap((state) => { tap((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -204,7 +203,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
confirmFn: () => { confirmFn: () => {
this.dataService this.dataService
.deleteUser(aId) .deleteUser(aId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.router.navigate(['..'], { relativeTo: this.route }); this.router.navigate(['..'], { relativeTo: this.route });
}); });
@ -222,7 +221,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
confirmFn: () => { confirmFn: () => {
this.dataService this.dataService
.updateUserAccessToken(aUserId) .updateUserAccessToken(aUserId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ accessToken }) => { .subscribe(({ accessToken }) => {
this.notificationService.alert({ this.notificationService.alert({
discardFn: () => { discardFn: () => {
@ -258,11 +257,6 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
); );
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private fetchUsers({ pageIndex }: { pageIndex: number } = { pageIndex: 0 }) { private fetchUsers({ pageIndex }: { pageIndex: number } = { pageIndex: 0 }) {
this.isLoading = true; this.isLoading = true;
@ -275,7 +269,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
skip: pageIndex * this.pageSize, skip: pageIndex * this.pageSize,
take: this.pageSize take: this.pageSize
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ count, users }) => { .subscribe(({ count, users }) => {
this.dataSource = new MatTableDataSource(users); this.dataSource = new MatTableDataSource(users);
this.totalItems = count; this.totalItems = count;
@ -305,7 +299,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => { .subscribe((data) => {
if (data?.action === 'delete' && data?.userId) { if (data?.action === 'delete' && data?.userId) {
this.onDeleteUser(data.userId); this.onDeleteUser(data.userId);

21
apps/client/src/app/components/data-provider-status/data-provider-status.component.ts

@ -4,13 +4,14 @@ import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
DestroyRef,
Input, Input,
OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import type { DataSource } from '@prisma/client'; import type { DataSource } from '@prisma/client';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { catchError, map, type Observable, of, Subject, takeUntil } from 'rxjs'; import { catchError, map, type Observable, of } from 'rxjs';
import { DataProviderStatus } from './interfaces/interfaces'; import { DataProviderStatus } from './interfaces/interfaces';
@ -20,14 +21,15 @@ import { DataProviderStatus } from './interfaces/interfaces';
selector: 'gf-data-provider-status', selector: 'gf-data-provider-status',
templateUrl: './data-provider-status.component.html' templateUrl: './data-provider-status.component.html'
}) })
export class GfDataProviderStatusComponent implements OnDestroy, OnInit { export class GfDataProviderStatusComponent implements OnInit {
@Input() dataSource: DataSource; @Input() dataSource: DataSource;
public status$: Observable<DataProviderStatus>; public status$: Observable<DataProviderStatus>;
private unsubscribeSubject = new Subject<void>(); public constructor(
private dataService: DataService,
public constructor(private dataService: DataService) {} private destroyRef: DestroyRef
) {}
public ngOnInit() { public ngOnInit() {
this.status$ = this.dataService this.status$ = this.dataService
@ -39,12 +41,7 @@ export class GfDataProviderStatusComponent implements OnDestroy, OnInit {
catchError(() => { catchError(() => {
return of({ isHealthy: false }); return of({ isHealthy: false });
}), }),
takeUntil(this.unsubscribeSubject) takeUntilDestroyed(this.destroyRef)
); );
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

30
apps/client/src/app/components/header/header.component.ts

@ -24,6 +24,7 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
DestroyRef,
EventEmitter, EventEmitter,
HostListener, HostListener,
Input, Input,
@ -31,6 +32,7 @@ import {
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatBadgeModule } from '@angular/material/badge'; import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -48,8 +50,8 @@ import {
radioButtonOffOutline, radioButtonOffOutline,
radioButtonOnOutline radioButtonOnOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -131,10 +133,9 @@ export class GfHeaderComponent implements OnChanges {
public routerLinkRegister = publicRoutes.register.routerLink; public routerLinkRegister = publicRoutes.register.routerLink;
public routerLinkResources = publicRoutes.resources.routerLink; public routerLinkResources = publicRoutes.resources.routerLink;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private dialog: MatDialog, private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService, private layoutService: LayoutService,
@ -146,7 +147,7 @@ export class GfHeaderComponent implements OnChanges {
) { ) {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
this.impersonationId = impersonationId; this.impersonationId = impersonationId;
@ -224,11 +225,11 @@ export class GfHeaderComponent implements OnChanges {
public onDateRangeChange(dateRange: DateRange) { public onDateRangeChange(dateRange: DateRange) {
this.dataService this.dataService
.putUserSetting({ dateRange }) .putUserSetting({ dateRange })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
}); });
} }
@ -252,11 +253,11 @@ export class GfHeaderComponent implements OnChanges {
this.dataService this.dataService
.putUserSetting(userSetting) .putUserSetting(userSetting)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
}); });
} }
@ -301,7 +302,7 @@ export class GfHeaderComponent implements OnChanges {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => { .subscribe((data) => {
if (data?.accessToken) { if (data?.accessToken) {
this.dataService this.dataService
@ -314,7 +315,7 @@ export class GfHeaderComponent implements OnChanges {
return EMPTY; return EMPTY;
}), }),
takeUntil(this.unsubscribeSubject) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(({ authToken }) => { .subscribe(({ authToken }) => {
this.setToken(authToken); this.setToken(authToken);
@ -331,7 +332,7 @@ export class GfHeaderComponent implements OnChanges {
this.userService this.userService
.get() .get()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
const userLanguage = user?.settings?.language; const userLanguage = user?.settings?.language;
@ -342,9 +343,4 @@ export class GfHeaderComponent implements OnChanges {
} }
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

17
apps/client/src/app/pages/auth/auth-page.component.ts

@ -4,20 +4,18 @@ import {
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, DestroyRef, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'gf-auth-page', selector: 'gf-auth-page',
styleUrls: ['./auth-page.scss'], styleUrls: ['./auth-page.scss'],
templateUrl: './auth-page.html' templateUrl: './auth-page.html'
}) })
export class GfAuthPageComponent implements OnDestroy, OnInit { export class GfAuthPageComponent implements OnInit {
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private destroyRef: DestroyRef,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private settingsStorageService: SettingsStorageService, private settingsStorageService: SettingsStorageService,
@ -26,7 +24,7 @@ export class GfAuthPageComponent implements OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.route.params this.route.params
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
const jwt = params['jwt']; const jwt = params['jwt'];
@ -38,9 +36,4 @@ export class GfAuthPageComponent implements OnDestroy, OnInit {
this.router.navigate(['/']); this.router.navigate(['/']);
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

15
apps/client/src/app/pages/public/public-page.component.ts

@ -19,8 +19,10 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
@ -29,8 +31,8 @@ import { AssetClass } from '@prisma/client';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -83,12 +85,12 @@ export class GfPublicPageComponent implements OnInit {
public UNKNOWN_KEY = UNKNOWN_KEY; public UNKNOWN_KEY = UNKNOWN_KEY;
private accessId: string; private accessId: string;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private router: Router private router: Router
) { ) {
@ -110,7 +112,7 @@ export class GfPublicPageComponent implements OnInit {
this.dataService this.dataService
.fetchPublicPortfolio(this.accessId) .fetchPublicPortfolio(this.accessId)
.pipe( .pipe(
takeUntil(this.unsubscribeSubject), takeUntilDestroyed(this.destroyRef),
catchError((error) => { catchError((error) => {
if (error.status === StatusCodes.NOT_FOUND) { if (error.status === StatusCodes.NOT_FOUND) {
console.error(error); console.error(error);
@ -246,9 +248,4 @@ export class GfPublicPageComponent implements OnInit {
}; };
} }
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

6
libs/common/src/lib/helper.spec.ts

@ -25,7 +25,7 @@ describe('Helper', () => {
it('Get decimal number with group (dot notation)', () => { it('Get decimal number with group (dot notation)', () => {
expect( expect(
extractNumberFromString({ locale: 'de-CH', value: '99’999.99' }) extractNumberFromString({ locale: 'de-CH', value: `99'999.99` })
).toEqual(99999.99); ).toEqual(99999.99);
}); });
@ -54,12 +54,12 @@ describe('Helper', () => {
}); });
it('Get de-CH number format group', () => { it('Get de-CH number format group', () => {
expect(getNumberFormatGroup('de-CH')).toEqual('’'); expect(getNumberFormatGroup('de-CH')).toEqual(`'`);
}); });
it('Get de-CH number format group when it is default', () => { it('Get de-CH number format group when it is default', () => {
languageGetter.mockReturnValue('de-CH'); languageGetter.mockReturnValue('de-CH');
expect(getNumberFormatGroup()).toEqual('’'); expect(getNumberFormatGroup()).toEqual(`'`);
}); });
it('Get de-DE number format group', () => { it('Get de-DE number format group', () => {

7
libs/ui/src/lib/tags-selector/interfaces/interfaces.ts

@ -0,0 +1,7 @@
import { Tag } from '@prisma/client';
export interface NewTag extends Omit<Tag, 'id'> {
id: undefined;
}
export type SelectedTag = NewTag | Tag;

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

@ -2,7 +2,7 @@
<div class="col"> <div class="col">
@if (readonly) { @if (readonly) {
<div class="h5" i18n>Tags</div> <div class="h5" i18n>Tags</div>
@if (tags?.length > 0) { @if (tags && tags.length > 0) {
<mat-chip-listbox> <mat-chip-listbox>
@for (tag of tags; track tag) { @for (tag of tags; track tag) {
<mat-chip-option disabled>{{ tag.name }}</mat-chip-option> <mat-chip-option disabled>{{ tag.name }}</mat-chip-option>

61
libs/ui/src/lib/tags-selector/tags-selector.component.ts

@ -7,11 +7,11 @@ import {
ElementRef, ElementRef,
Input, Input,
OnChanges, OnChanges,
OnDestroy,
OnInit, OnInit,
signal, signal,
ViewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormControl, FormControl,
@ -30,7 +30,9 @@ import { IonIcon } from '@ionic/angular/standalone';
import { Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { addCircleOutline, closeOutline } from 'ionicons/icons'; import { addCircleOutline, closeOutline } from 'ionicons/icons';
import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs';
import { SelectedTag } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -57,27 +59,28 @@ import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
templateUrl: 'tags-selector.component.html' templateUrl: 'tags-selector.component.html'
}) })
export class GfTagsSelectorComponent export class GfTagsSelectorComponent
implements ControlValueAccessor, OnChanges, OnDestroy, OnInit implements ControlValueAccessor, OnChanges, OnInit
{ {
@Input() hasPermissionToCreateTag = false; @Input() hasPermissionToCreateTag = false;
@Input() readonly = false; @Input() readonly = false;
@Input() tags: Tag[]; @Input() tags: SelectedTag[];
@Input() tagsAvailable: Tag[]; @Input() tagsAvailable: SelectedTag[];
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
public filteredOptions: Subject<Tag[]> = new BehaviorSubject([]); public readonly filteredOptions: Subject<SelectedTag[]> = new BehaviorSubject(
[]
);
public readonly separatorKeysCodes: number[] = [COMMA, ENTER]; public readonly separatorKeysCodes: number[] = [COMMA, ENTER];
public readonly tagInputControl = new FormControl(''); public readonly tagInputControl = new FormControl('');
public readonly tagsSelected = signal<Tag[]>([]); public readonly tagsSelected = signal<SelectedTag[]>([]);
private unsubscribeSubject = new Subject<void>(); private readonly tagInput =
viewChild.required<ElementRef<HTMLInputElement>>('tagInput');
public constructor() { public constructor() {
this.tagInputControl.valueChanges this.tagInputControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed())
.subscribe((value) => { .subscribe((value) => {
this.filteredOptions.next(this.filterTags(value)); this.filteredOptions.next(this.filterTags(value ?? ''));
}); });
addIcons({ addCircleOutline, closeOutline }); addIcons({ addCircleOutline, closeOutline });
@ -106,15 +109,18 @@ export class GfTagsSelectorComponent
}; };
} }
this.tagsSelected.update((tags) => { if (tag) {
return [...(tags ?? []), tag]; this.tagsSelected.update((tags) => {
}); return [...(tags ?? []), tag];
});
const newTags = this.tagsSelected(); const newTags = this.tagsSelected();
this.onChange(newTags); this.onChange(newTags);
this.onTouched(); this.onTouched();
this.tagInput.nativeElement.value = ''; }
this.tagInputControl.setValue(undefined);
this.tagInput().nativeElement.value = '';
this.tagInputControl.setValue(null);
} }
public onRemoveTag(tag: Tag) { public onRemoveTag(tag: Tag) {
@ -130,7 +136,7 @@ export class GfTagsSelectorComponent
this.updateFilters(); this.updateFilters();
} }
public registerOnChange(fn: (value: Tag[]) => void) { public registerOnChange(fn: (value: SelectedTag[]) => void) {
this.onChange = fn; this.onChange = fn;
} }
@ -146,17 +152,12 @@ export class GfTagsSelectorComponent
} }
} }
public writeValue(value: Tag[]) { public writeValue(value: SelectedTag[]) {
this.tagsSelected.set(value || []); this.tagsSelected.set(value || []);
this.updateFilters(); this.updateFilters();
} }
public ngOnDestroy() { private filterTags(query: string = ''): SelectedTag[] {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private filterTags(query: string = ''): Tag[] {
const tags = this.tagsSelected() ?? []; const tags = this.tagsSelected() ?? [];
const tagIds = tags.map(({ id }) => { const tagIds = tags.map(({ id }) => {
return id; return id;
@ -170,7 +171,7 @@ export class GfTagsSelectorComponent
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
private onChange = (_value: Tag[]): void => { private onChange = (_value: SelectedTag[]): void => {
// ControlValueAccessor onChange callback // ControlValueAccessor onChange callback
}; };

27
package-lock.json

@ -27,7 +27,7 @@
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1", "@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.5", "@internationalized/number": "3.6.5",
"@ionic/angular": "8.7.8", "@ionic/angular": "8.8.1",
"@keyv/redis": "4.4.0", "@keyv/redis": "4.4.0",
"@nestjs/bull": "11.0.4", "@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.1.0", "@nestjs/cache-manager": "3.1.0",
@ -5070,12 +5070,12 @@
} }
}, },
"node_modules/@ionic/angular": { "node_modules/@ionic/angular": {
"version": "8.7.8", "version": "8.8.1",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.7.8.tgz", "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.8.1.tgz",
"integrity": "sha512-IBN5h3nIOwbuglLit48S7wNeg7NHtl/vaKAHDggICyzI92cSg5yYL07Fz59pszhkBlZQUB5SQnml990Zj2bZUg==", "integrity": "sha512-Jp7LbouSHAnR00Dsa8qE1CSOZNqAfBCO0XKXScJNz8NKVoZe5fPGy/CboehGtAQ1xgzh2eDa15zMmyetXjAkYA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ionic/core": "8.7.8", "@ionic/core": "8.8.1",
"ionicons": "^8.0.13", "ionicons": "^8.0.13",
"jsonc-parser": "^3.0.0", "jsonc-parser": "^3.0.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"
@ -5089,14 +5089,17 @@
} }
}, },
"node_modules/@ionic/core": { "node_modules/@ionic/core": {
"version": "8.7.8", "version": "8.8.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.8.tgz", "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.1.tgz",
"integrity": "sha512-GLWb/lz3kocpzTZTeQQ5xxoWz4CKHD6zpnbwJknTKsncebohAaw2KTe7uOw5toKQEDdohTseFuSGoDDBoRQ1Ug==", "integrity": "sha512-ksOUHyOEqoyUIVWcwCNSFZVGwNfP1DKrUVeN/Cdk/Xl9Rdd/5MLHGsrOQpWQfoCf3Csdnw+KHHPrXz/2fzMkMA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@stencil/core": "4.38.0", "@stencil/core": "4.43.0",
"ionicons": "^8.0.13", "ionicons": "^8.0.13",
"tslib": "^2.1.0" "tslib": "^2.1.0"
},
"engines": {
"node": ">= 16"
} }
}, },
"node_modules/@ioredis/commands": { "node_modules/@ioredis/commands": {
@ -11930,9 +11933,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@stencil/core": { "node_modules/@stencil/core": {
"version": "4.38.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.43.0.tgz",
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", "integrity": "sha512-6Uj2Z3lzLuufYAE7asZ6NLKgSwsB9uxl84Eh34PASnUjfj32GkrP4DtKK7fNeh1WFGGyffsTDka3gwtl+4reUg==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"stencil": "bin/stencil" "stencil": "bin/stencil"

2
package.json

@ -72,7 +72,7 @@
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1", "@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.5", "@internationalized/number": "3.6.5",
"@ionic/angular": "8.7.8", "@ionic/angular": "8.8.1",
"@keyv/redis": "4.4.0", "@keyv/redis": "4.4.0",
"@nestjs/bull": "11.0.4", "@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.1.0", "@nestjs/cache-manager": "3.1.0",

Loading…
Cancel
Save