Skip to content

Commit 4abbca5

Browse files
authored
Add unsubscribe handling in existing components (#14)
* component unsubscribe * takeuntil, move subscribe logic to tap * keep logic out of subscribe * index for errorservice
1 parent 78fc18e commit 4abbca5

File tree

11 files changed

+176
-103
lines changed

11 files changed

+176
-103
lines changed

src/@seed/api/cycle/cycle.service.ts

+21-20
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { HttpErrorResponse } from '@angular/common/http'
22
import { HttpClient } from '@angular/common/http'
33
import { inject, Injectable } from '@angular/core'
44
import type { Observable } from 'rxjs'
5-
import { BehaviorSubject, catchError, map, tap } from 'rxjs'
5+
import { BehaviorSubject, catchError, map, switchMap, tap } from 'rxjs'
66
import { OrganizationService } from '@seed/api/organization'
7-
import { ErrorService } from '@seed/services/error/error.service'
7+
import { ErrorService } from '@seed/services'
88
import { SnackbarService } from 'app/core/snackbar/snackbar.service'
99
import type { Cycle, CycleResponse, CyclesResponse } from './cycle.types'
1010

@@ -20,24 +20,25 @@ export class CycleService {
2020
cycles$ = this._cycles.asObservable()
2121

2222
get(): void {
23-
// fetch current organization
24-
this._organizationService.currentOrganization$.subscribe(({ org_id }) => {
25-
this.orgId = org_id
26-
const url = `/api/v3/cycles/?organization_id=${org_id}`
27-
// fetch cycles
28-
this._httpClient
29-
.get<CyclesResponse>(url)
30-
.pipe(
31-
map((response) => response.cycles),
32-
tap((cycles) => {
33-
this._cycles.next(cycles)
34-
}),
35-
catchError((error: HttpErrorResponse) => {
36-
return this._errorService.handleError(error, 'Error fetching cycles')
37-
}),
38-
)
39-
.subscribe()
40-
})
23+
this._organizationService.currentOrganization$
24+
.pipe(
25+
tap(({ org_id }) => { this.orgId = org_id }),
26+
switchMap(({ org_id }) => {
27+
const url = `/api/v3/cycles/?organization_id=${org_id}`
28+
// fetch cycles
29+
return this._httpClient
30+
.get<CyclesResponse>(url)
31+
.pipe(
32+
map((response) => response.cycles),
33+
tap((cycles) => {
34+
this._cycles.next(cycles)
35+
}),
36+
catchError((error: HttpErrorResponse) => {
37+
return this._errorService.handleError(error, 'Error fetching cycles')
38+
}),
39+
)
40+
}),
41+
).subscribe()
4142
}
4243

4344
post({ data, orgId }): Observable<CycleResponse | null> {

src/@seed/api/organization/organization.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http'
33
import { inject, Injectable } from '@angular/core'
44
import type { Observable } from 'rxjs'
55
import { catchError, map, of, ReplaySubject, Subject, takeUntil, tap } from 'rxjs'
6-
import { ErrorService } from '@seed/services/error/error.service'
6+
import { ErrorService } from '@seed/services'
77
import { SnackbarService } from 'app/core/snackbar/snackbar.service'
88
import { naturalSort } from '../../utils'
99
import { UserService } from '../user'

src/@seed/api/user/user.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
SetDefaultOrganizationResponse,
1212
UserUpdateRequest,
1313
} from '@seed/api/user'
14-
import { ErrorService } from '@seed/services/error/error.service'
14+
import { ErrorService } from '@seed/services'
1515

1616
@Injectable({ providedIn: 'root' })
1717
export class UserService {

src/@seed/services/error/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './error.service'

src/@seed/services/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './config'
22
export * from './confirmation'
3+
export * from './error'
34
export * from './loading'
45
export * from './media-watcher'
56
export * from './platform'

src/app/modules/organizations/cycles/cycles.component.ts

+33-16
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { CommonModule } from '@angular/common'
2-
import type { OnInit } from '@angular/core'
2+
import type { OnDestroy, OnInit } from '@angular/core'
33
import { Component, inject } from '@angular/core'
44
import { MatButtonModule } from '@angular/material/button'
55
import { MatDialog, MatDialogModule } from '@angular/material/dialog'
66
import { MatIconModule } from '@angular/material/icon'
77
import { MatTableDataSource, MatTableModule } from '@angular/material/table'
8+
import { Subject, takeUntil, tap } from 'rxjs'
89
import type { Cycle } from '@seed/api/cycle'
910
import { CycleService } from '@seed/api/cycle/cycle.service'
1011
import { PageComponent, TableContainerComponent } from '@seed/components'
@@ -26,11 +27,12 @@ import { FormModalComponent } from './modal/form-modal.component'
2627
TableContainerComponent,
2728
],
2829
})
29-
export class CyclesComponent implements OnInit {
30+
export class CyclesComponent implements OnDestroy, OnInit {
3031
private _cycleService = inject(CycleService)
3132
private _dialog = inject(MatDialog)
3233
private _orgId: number
3334
private _existingNames: string[]
35+
private readonly _unsubscribeAll$ = new Subject<void>()
3436

3537
cyclesDataSource = new MatTableDataSource<Cycle>([])
3638
cyclesColumns = ['id', 'name', 'start', 'end', 'actions']
@@ -42,11 +44,15 @@ export class CyclesComponent implements OnInit {
4244
refreshCycles(): void {
4345
this._cycleService.get()
4446

45-
this._cycleService.cycles$.subscribe((cycles) => {
46-
this.cyclesDataSource.data = cycles
47-
this._orgId = cycles[0]?.organization
48-
this._existingNames = cycles.map((cycle) => cycle.name)
49-
})
47+
this._cycleService.cycles$
48+
.pipe(
49+
takeUntil(this._unsubscribeAll$),
50+
tap((cycles) => {
51+
this.cyclesDataSource.data = cycles
52+
this._orgId = cycles[0]?.organization
53+
this._existingNames = cycles.map((cycle) => cycle.name)
54+
}),
55+
).subscribe()
5056
}
5157

5258
createCycle = () => {
@@ -55,9 +61,11 @@ export class CyclesComponent implements OnInit {
5561
data: { cycle: null, orgId: this._orgId, existingNames: this._existingNames },
5662
})
5763

58-
dialogRef.afterClosed().subscribe(() => {
59-
this.refreshCycles()
60-
})
64+
dialogRef.afterClosed()
65+
.pipe(
66+
takeUntil(this._unsubscribeAll$),
67+
tap(() => { this.refreshCycles() }),
68+
).subscribe()
6169
}
6270

6371
editCycle(cycle: Cycle): void {
@@ -66,9 +74,11 @@ export class CyclesComponent implements OnInit {
6674
data: { cycle, orgId: this._orgId, existingNames: this._existingNames },
6775
})
6876

69-
dialogRef.afterClosed().subscribe(() => {
70-
this.refreshCycles()
71-
})
77+
dialogRef.afterClosed()
78+
.pipe(
79+
takeUntil(this._unsubscribeAll$),
80+
tap(() => { this.refreshCycles() }),
81+
).subscribe()
7282
}
7383

7484
deleteCycle(cycle: Cycle): void {
@@ -77,12 +87,19 @@ export class CyclesComponent implements OnInit {
7787
data: { cycle, orgId: this._orgId },
7888
})
7989

80-
dialogRef.afterClosed().subscribe(() => {
81-
this.refreshCycles()
82-
})
90+
dialogRef.afterClosed()
91+
.pipe(
92+
takeUntil(this._unsubscribeAll$),
93+
tap(() => { this.refreshCycles() }),
94+
).subscribe()
8395
}
8496

8597
trackByFn(_index: number, { id }: Cycle) {
8698
return id
8799
}
100+
101+
ngOnDestroy(): void {
102+
this._unsubscribeAll$.next()
103+
this._unsubscribeAll$.complete()
104+
}
88105
}

src/app/modules/organizations/cycles/modal/delete-modal.component.ts

+21-22
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { CommonModule } from '@angular/common'
2-
import type { HttpErrorResponse } from '@angular/common/http'
2+
import type { OnDestroy } from '@angular/core'
33
import { Component, inject } from '@angular/core'
44
import { MatButtonModule } from '@angular/material/button'
55
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'
66
import { MatProgressBarModule } from '@angular/material/progress-bar'
7-
import { catchError, throwError } from 'rxjs'
7+
import { Subject, switchMap, takeUntil, tap } from 'rxjs'
88
import type { Cycle } from '@seed/api/cycle'
99
import { CycleService } from '@seed/api/cycle/cycle.service'
1010
import { AlertComponent } from '@seed/components'
@@ -23,11 +23,12 @@ import { SnackbarService } from 'app/core/snackbar/snackbar.service'
2323
MatProgressBarModule,
2424
],
2525
})
26-
export class DeleteModalComponent {
26+
export class DeleteModalComponent implements OnDestroy {
2727
private _cycleService = inject(CycleService)
2828
private _uploaderService = inject(UploaderService)
2929
private _dialogRef = inject(MatDialogRef<DeleteModalComponent>)
3030
private _snackBar = inject(SnackbarService)
31+
private readonly _unsubscribeAll$ = new Subject<void>()
3132
errorMessage: string
3233
inProgress = false
3334
progressBarObj: ProgressBarObj = {
@@ -55,31 +56,24 @@ export class DeleteModalComponent {
5556
}
5657

5758
// initiate delete cycle task
58-
this._cycleService.delete(this.data.cycle.id, this.data.orgId).subscribe({
59-
next: (response: { progress_key: string; value: number }) => {
60-
this.progressBarObj.progress = response.value
61-
// monitor delete cycle task
62-
this._uploaderService
63-
.checkProgressLoop({
64-
progressKey: response.progress_key,
59+
this._cycleService.delete(this.data.cycle.id, this.data.orgId)
60+
.pipe(
61+
takeUntil(this._unsubscribeAll$),
62+
tap((response: { progress_key: string; value: number }) => {
63+
this.progressBarObj.progress = response.value
64+
}),
65+
switchMap(({ progress_key }) => {
66+
return this._uploaderService.checkProgressLoop({
67+
progressKey: progress_key,
6568
offset: 0,
6669
multiplier: 1,
6770
successFn,
6871
failureFn,
6972
progressBarObj: this.progressBarObj,
7073
})
71-
.pipe(
72-
catchError(({ error }: { error: HttpErrorResponse }) => {
73-
return throwError(() => new Error(error?.message || 'Error checking progress'))
74-
}),
75-
)
76-
.subscribe()
77-
},
78-
error: (error: string) => {
79-
this.inProgress = false
80-
this.errorMessage = error
81-
},
82-
})
74+
}),
75+
)
76+
.subscribe()
8377
}
8478

8579
close() {
@@ -89,4 +83,9 @@ export class DeleteModalComponent {
8983
dismiss() {
9084
this._dialogRef.close()
9185
}
86+
87+
ngOnDestroy(): void {
88+
this._unsubscribeAll$.next()
89+
this._unsubscribeAll$.complete()
90+
}
9291
}

src/app/modules/organizations/cycles/modal/form-modal.component.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CommonModule, DatePipe } from '@angular/common'
2-
import type { OnInit } from '@angular/core'
2+
import type { OnDestroy, OnInit } from '@angular/core'
33
import { Component, inject } from '@angular/core'
44
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'
55
import { MatButtonModule } from '@angular/material/button'
@@ -8,6 +8,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker'
88
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'
99
import { MatFormFieldModule } from '@angular/material/form-field'
1010
import { MatInputModule } from '@angular/material/input'
11+
import { Subject, takeUntil, tap } from 'rxjs'
1112
import type { Cycle } from '@seed/api/cycle'
1213
import { CycleService } from '@seed/api/cycle/cycle.service'
1314
import { SEEDValidators } from '@seed/validators'
@@ -41,10 +42,11 @@ export const MY_DATE_FORMATS = {
4142
MatNativeDateModule,
4243
],
4344
})
44-
export class FormModalComponent implements OnInit {
45+
export class FormModalComponent implements OnDestroy, OnInit {
4546
private _cycleService = inject(CycleService)
4647
private _datePipe = inject(DatePipe)
4748
private _dialogRef = inject(MatDialogRef<FormModalComponent>)
49+
private readonly _unsubscribeAll$ = new Subject<void>()
4850

4951
create = true
5052
data = inject(MAT_DIALOG_DATA) as { cycle: Cycle | null; orgId: number; existingNames: string[] }
@@ -62,9 +64,11 @@ export class FormModalComponent implements OnInit {
6264
this.create = false
6365
this.form.patchValue(this.data.cycle)
6466
}
65-
this.form.get('start')?.valueChanges.subscribe(() => {
66-
this.form.get('end')?.updateValueAndValidity()
67-
})
67+
this.form.get('start')?.valueChanges
68+
.pipe(
69+
takeUntil(this._unsubscribeAll$),
70+
tap(() => { this.form.get('end')?.updateValueAndValidity() }),
71+
).subscribe()
6872
}
6973

7074
onSubmit() {
@@ -86,6 +90,11 @@ export class FormModalComponent implements OnInit {
8690
this._dialogRef.close()
8791
}
8892

93+
ngOnDestroy(): void {
94+
this._unsubscribeAll$.next()
95+
this._unsubscribeAll$.complete()
96+
}
97+
8998
private _formatDates() {
9099
this.form.value.start = this._datePipe.transform(this.form.value.start, 'yyyy-MM-dd')
91100
this.form.value.end = this._datePipe.transform(this.form.value.end, 'yyyy-MM-dd')

0 commit comments

Comments
 (0)