Browse Source

Task/enable strict null checks in apps/client (#7309)

* fix(client): resolve type errors

* feat(ui): resolve type error in assistant component

* feat(client): enable strict null checks

* feat(client): enforce encapsulation

* feat(client): enforce immutability

* feat(client): replace constructor based DI with inject functions

* fix(client): prevent race condition in nested subscription
pull/6751/merge
Kenrick Tandrian 1 day ago
committed by GitHub
parent
commit
fd61a1e62b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
  2. 201
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  3. 8
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  4. 4
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts
  5. 7
      apps/client/src/app/pages/portfolio/activities/interfaces/interfaces.ts
  6. 1
      apps/client/tsconfig.json
  7. 2
      libs/ui/src/lib/assistant/assistant.component.ts

4
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html

@ -7,8 +7,8 @@
> >
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
@if ( @if (
data.rule.configuration.thresholdMin && data.rule.configuration?.thresholdMin &&
data.rule.configuration.thresholdMax data.rule.configuration?.thresholdMax
) { ) {
<div class="w-100"> <div class="w-100">
<h6 class="d-flex mb-0"> <h6 class="d-flex mb-0">

201
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -20,6 +20,7 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
inject,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -31,12 +32,14 @@ import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subscription } from 'rxjs'; import { of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { GfCreateOrUpdateActivityDialogComponent } from './create-or-update-activity-dialog/create-or-update-activity-dialog.component'; import { GfCreateOrUpdateActivityDialogComponent } from './create-or-update-activity-dialog/create-or-update-activity-dialog.component';
import { CreateOrUpdateActivityDialogParams } from './create-or-update-activity-dialog/interfaces/interfaces'; import { CreateOrUpdateActivityDialogParams } from './create-or-update-activity-dialog/interfaces/interfaces';
import { GfImportActivitiesDialogComponent } from './import-activities-dialog/import-activities-dialog.component'; import { GfImportActivitiesDialogComponent } from './import-activities-dialog/import-activities-dialog.component';
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces'; import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
import { ActivitiesPageParams } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -51,54 +54,53 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa
templateUrl: './activities-page.html' templateUrl: './activities-page.html'
}) })
export class GfActivitiesPageComponent implements OnInit { export class GfActivitiesPageComponent implements OnInit {
public activityTypesFilter: string[] = []; protected dataSource: MatTableDataSource<Activity> | undefined;
public dataSource: MatTableDataSource<Activity>; protected deviceType: string;
public deviceType: string; protected hasImpersonationId: boolean;
public hasImpersonationId: boolean; protected hasPermissionToCreateActivity: boolean;
public hasPermissionToCreateActivity: boolean; protected hasPermissionToDeleteActivity: boolean;
public hasPermissionToDeleteActivity: boolean; protected pageIndex = 0;
public pageIndex = 0; protected readonly pageSize = DEFAULT_PAGE_SIZE;
public pageSize = DEFAULT_PAGE_SIZE; protected sortColumn = 'date';
public routeQueryParams: Subscription; protected sortDirection: SortDirection = 'desc';
public sortColumn = 'date'; protected totalItems: number | undefined;
public sortDirection: SortDirection = 'desc'; protected user: User;
public totalItems: number | undefined;
public user: User; private activityTypesFilter: string[] = [];
public constructor( private readonly changeDetectorRef = inject(ChangeDetectorRef);
private changeDetectorRef: ChangeDetectorRef, private readonly dataService = inject(DataService);
private dataService: DataService, private readonly destroyRef = inject(DestroyRef);
private destroyRef: DestroyRef, private readonly deviceDetectorService = inject(DeviceDetectorService);
private deviceDetectorService: DeviceDetectorService, private readonly dialog = inject(MatDialog);
private dialog: MatDialog, private readonly icsService = inject(IcsService);
private icsService: IcsService, private readonly impersonationStorageService = inject(
private impersonationStorageService: ImpersonationStorageService, ImpersonationStorageService
private route: ActivatedRoute, );
private router: Router, private readonly route = inject(ActivatedRoute);
private userService: UserService private readonly router = inject(Router);
) { private readonly userService = inject(UserService);
this.routeQueryParams = route.queryParams
.pipe(takeUntilDestroyed(this.destroyRef)) public constructor() {
.subscribe((params) => { this.route.queryParams
if (params['createDialog']) { .pipe(
if (params['activityId']) { takeUntilDestroyed(this.destroyRef),
this.dataService switchMap((params: ActivitiesPageParams) => {
.fetchActivity(params['activityId']) if (params.activityId && (params.createDialog || params.editDialog)) {
.pipe(takeUntilDestroyed(this.destroyRef)) return this.dataService
.subscribe((activity) => { .fetchActivity(params.activityId)
.pipe(map((activity) => ({ activity, params })));
}
return of({ params, activity: undefined });
})
)
.subscribe(({ activity, params }) => {
if (params.createDialog) {
this.openCreateActivityDialog(activity); this.openCreateActivityDialog(activity);
}); } else if (params.editDialog) {
} else { if (activity) {
this.openCreateActivityDialog();
}
} else if (params['editDialog']) {
if (params['activityId']) {
this.dataService
.fetchActivity(params['activityId'])
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((activity) => {
this.openUpdateActivityDialog(activity); this.openUpdateActivityDialog(activity);
});
} else { } else {
this.router.navigate(['.'], { relativeTo: this.route }); this.router.navigate(['.'], { relativeTo: this.route });
} }
@ -131,49 +133,13 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
public fetchActivities() { protected onChangePage(page: PageEvent) {
// Reset dataSource and totalItems to show loading state
this.dataSource = undefined;
this.totalItems = undefined;
const dateRange = this.user?.settings?.dateRange;
const range = this.isCalendarYear(dateRange) ? dateRange : undefined;
this.dataService
.fetchActivities({
range,
activityTypes: this.activityTypesFilter.length
? this.activityTypesFilter
: undefined,
filters: this.userService.getFilters(),
skip: this.pageIndex * this.pageSize,
sortColumn: this.sortColumn,
sortDirection: this.sortDirection,
take: this.pageSize
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ activities, count }) => {
this.dataSource = new MatTableDataSource(activities);
this.totalItems = count;
if (
this.hasPermissionToCreateActivity &&
this.user?.activitiesCount === 0
) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
});
}
public onChangePage(page: PageEvent) {
this.pageIndex = page.pageIndex; this.pageIndex = page.pageIndex;
this.fetchActivities(); this.fetchActivities();
} }
public onClickActivity({ dataSource, symbol }: AssetProfileIdentifier) { protected onClickActivity({ dataSource, symbol }: AssetProfileIdentifier) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { queryParams: {
dataSource, dataSource,
@ -183,11 +149,11 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
public onCloneActivity(aActivity: Activity) { protected onCloneActivity(aActivity: Activity) {
this.openCreateActivityDialog(aActivity); this.openCreateActivityDialog(aActivity);
} }
public onDeleteActivities() { protected onDeleteActivities() {
this.dataService this.dataService
.deleteActivities({ .deleteActivities({
filters: this.userService.getFilters() filters: this.userService.getFilters()
@ -205,7 +171,7 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
public onDeleteActivity(aId: string) { protected onDeleteActivity(aId: string) {
this.dataService this.dataService
.deleteActivity(aId) .deleteActivity(aId)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -221,7 +187,7 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
public onExport(activityIds?: string[]) { protected onExport(activityIds?: string[]) {
let fetchExportParams: any = { activityIds }; let fetchExportParams: any = { activityIds };
if (!activityIds) { if (!activityIds) {
@ -238,7 +204,7 @@ export class GfActivitiesPageComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => { .subscribe((data) => {
for (const activity of data.activities) { for (const activity of data.activities) {
delete activity.id; delete (activity as Omit<typeof activity, 'id'> & { id?: string }).id;
} }
downloadAsFile({ downloadAsFile({
@ -252,7 +218,7 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
public onExportDrafts(activityIds?: string[]) { protected onExportDrafts(activityIds?: string[]) {
this.dataService this.dataService
.fetchExport({ activityIds }) .fetchExport({ activityIds })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -270,7 +236,7 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
public onImport() { protected onImport() {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfImportActivitiesDialogComponent, GfImportActivitiesDialogComponent,
ImportActivitiesDialogParams ImportActivitiesDialogParams
@ -298,7 +264,7 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
public onImportDividends() { protected onImportDividends() {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfImportActivitiesDialogComponent, GfImportActivitiesDialogComponent,
ImportActivitiesDialogParams ImportActivitiesDialogParams
@ -327,7 +293,7 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
public onSortChanged({ active, direction }: Sort) { protected onSortChanged({ active, direction }: Sort) {
this.pageIndex = 0; this.pageIndex = 0;
this.sortColumn = active; this.sortColumn = active;
this.sortDirection = direction; this.sortDirection = direction;
@ -335,20 +301,56 @@ export class GfActivitiesPageComponent implements OnInit {
this.fetchActivities(); this.fetchActivities();
} }
public onTypesFilterChanged(aTypes: string[]) { protected onTypesFilterChanged(aTypes: string[]) {
this.activityTypesFilter = aTypes; this.activityTypesFilter = aTypes;
this.pageIndex = 0; this.pageIndex = 0;
this.fetchActivities(); this.fetchActivities();
} }
public onUpdateActivity(aActivity: Activity) { protected onUpdateActivity(aActivity: Activity) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { activityId: aActivity.id, editDialog: true } queryParams: { activityId: aActivity.id, editDialog: true }
}); });
} }
public openUpdateActivityDialog(aActivity: Activity) { private fetchActivities() {
// Reset dataSource and totalItems to show loading state
this.dataSource = undefined;
this.totalItems = undefined;
const dateRange = this.user?.settings?.dateRange;
const range = this.isCalendarYear(dateRange) ? dateRange : undefined;
this.dataService
.fetchActivities({
range,
activityTypes: this.activityTypesFilter.length
? this.activityTypesFilter
: undefined,
filters: this.userService.getFilters(),
skip: this.pageIndex * this.pageSize,
sortColumn: this.sortColumn,
sortDirection: this.sortDirection,
take: this.pageSize
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ activities, count }) => {
this.dataSource = new MatTableDataSource(activities);
this.totalItems = count;
if (
this.hasPermissionToCreateActivity &&
this.user?.activitiesCount === 0
) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
});
}
private openUpdateActivityDialog(aActivity: Activity) {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfCreateOrUpdateActivityDialogComponent, GfCreateOrUpdateActivityDialogComponent,
CreateOrUpdateActivityDialogParams CreateOrUpdateActivityDialogParams
@ -383,7 +385,7 @@ export class GfActivitiesPageComponent implements OnInit {
}); });
} }
private isCalendarYear(dateRange: DateRange) { private isCalendarYear(dateRange?: DateRange) {
if (!dateRange) { if (!dateRange) {
return false; return false;
} }
@ -410,11 +412,12 @@ export class GfActivitiesPageComponent implements OnInit {
date: new Date(), date: new Date(),
id: null, id: null,
fee: 0, fee: 0,
SymbolProfile: null,
type: aActivity?.type ?? 'BUY', type: aActivity?.type ?? 'BUY',
unitPrice: null unitPrice: null
}, },
user: this.user user: this.user
}, } satisfies CreateOrUpdateActivityDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : '80vh', height: this.deviceType === 'mobile' ? '98vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });

8
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -536,7 +536,13 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.dialogRef.close(activity); this.dialogRef.close(activity);
} else { } else {
(activity as UpdateOrderDto).id = this.data.activity?.id; const activityId = this.data.activity?.id;
if (!activityId) {
throw new Error('Activity ID is required for update');
}
(activity as UpdateOrderDto).id = activityId;
await validateObjectForForm({ await validateObjectForForm({
classDto: UpdateOrderDto, classDto: UpdateOrderDto,

4
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts

@ -4,8 +4,10 @@ import { Account } from '@prisma/client';
export interface CreateOrUpdateActivityDialogParams { export interface CreateOrUpdateActivityDialogParams {
accounts: Account[]; accounts: Account[];
activity: Omit<Activity, 'SymbolProfile'> & { activity: Partial<Omit<Activity, 'id' | 'SymbolProfile' | 'unitPrice'>> & {
id: string | null;
SymbolProfile: Activity['SymbolProfile'] | null; SymbolProfile: Activity['SymbolProfile'] | null;
unitPrice: number | null;
}; };
user: User; user: User;
} }

7
apps/client/src/app/pages/portfolio/activities/interfaces/interfaces.ts

@ -0,0 +1,7 @@
import { Params } from '@angular/router';
export interface ActivitiesPageParams extends Params {
activityId?: string;
createDialog?: string;
editDialog?: string;
}

1
apps/client/tsconfig.json

@ -22,6 +22,7 @@
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "es2022"], "lib": ["dom", "es2022"],
"module": "preserve", "module": "preserve",
"strictNullChecks": true,
"target": "es2020" "target": "es2020"
} }
} }

2
libs/ui/src/lib/assistant/assistant.component.ts

@ -713,7 +713,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
return { return {
routerLink, routerLink,
mode: SearchMode.QUICK_LINK as const, mode: SearchMode.QUICK_LINK as const,
name: title name: title ?? ''
}; };
}); });
} }

Loading…
Cancel
Save