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. 203
      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>
@if (
data.rule.configuration.thresholdMin &&
data.rule.configuration.thresholdMax
data.rule.configuration?.thresholdMin &&
data.rule.configuration?.thresholdMax
) {
<div class="w-100">
<h6 class="d-flex mb-0">

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

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

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

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

Loading…
Cancel
Save