feat: add takeUntil to manage subscriptions lifecycle and prevent memory leaks

This commit is contained in:
Chneemann 2025-04-08 10:51:08 +02:00
parent 86692177a0
commit ea1e14b1a8
15 changed files with 373 additions and 218 deletions

View file

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms'; import { FormsModule, NgForm } from '@angular/forms';
import { AssignedComponent } from './assigned/assigned.component'; import { AssignedComponent } from './assigned/assigned.component';
import { Subtask, Task } from '../../interfaces/task.interface'; import { Subtask, Task } from '../../interfaces/task.interface';
@ -8,7 +8,7 @@ import { FormBtnComponent } from '../../shared/components/buttons/form-btn/form-
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { firstValueFrom, map } from 'rxjs'; import { firstValueFrom, map, Subject, takeUntil } from 'rxjs';
import { TaskService } from '../../services/task.service'; import { TaskService } from '../../services/task.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { UpdateNotifierService } from '../../services/update-notifier.service'; import { UpdateNotifierService } from '../../services/update-notifier.service';
@ -27,7 +27,7 @@ import { ToastNotificationService } from '../../services/toast-notification.serv
templateUrl: './add-task.component.html', templateUrl: './add-task.component.html',
styleUrl: './add-task.component.scss', styleUrl: './add-task.component.scss',
}) })
export class AddTaskComponent implements OnInit { export class AddTaskComponent implements OnInit, OnDestroy {
@Input() overlayData: string = ''; @Input() overlayData: string = '';
@Input() overlayType: string = ''; @Input() overlayType: string = '';
@Input() overlayMobile: boolean = false; @Input() overlayMobile: boolean = false;
@ -36,6 +36,8 @@ export class AddTaskComponent implements OnInit {
dateInPast: boolean = false; dateInPast: boolean = false;
subtaskValue: string = ''; subtaskValue: string = '';
private destroy$ = new Subject<void>();
constructor( constructor(
private overlayService: OverlayService, private overlayService: OverlayService,
private taskService: TaskService, private taskService: TaskService,
@ -67,16 +69,24 @@ export class AddTaskComponent implements OnInit {
* - Sets up route parameters to determine task status. * - Sets up route parameters to determine task status.
* - Loads any existing task data from local storage. * - Loads any existing task data from local storage.
*/ */
ngOnInit() { ngOnInit(): void {
this.setCurrentUserId(); this.setCurrentUserId();
this.loadExistingTaskData(); this.loadExistingTaskData();
this.routeParams(); this.routeParams();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
setCurrentUserId() { setCurrentUserId() {
this.authService this.authService
.getCurrentUserId() .getCurrentUserId()
.pipe(map((userId) => userId ?? '')) .pipe(
takeUntil(this.destroy$),
map((userId) => userId ?? '')
)
.subscribe((userId) => { .subscribe((userId) => {
this.taskData.creator = userId; this.taskData.creator = userId;
}); });
@ -103,14 +113,12 @@ export class AddTaskComponent implements OnInit {
* @returns {void} * @returns {void}
*/ */
routeParams() { routeParams() {
if (this.route.params.subscribe()) { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.route.params.subscribe((params) => {
if (params['id']) { if (params['id']) {
this.taskData.status = params['id']; this.taskData.status = params['id'];
} }
}); });
} }
}
/** /**
* Loads any existing task data from local storage and assigns it to the taskData object. * Loads any existing task data from local storage and assigns it to the taskData object.
@ -270,7 +278,10 @@ export class AddTaskComponent implements OnInit {
} }
createTask(ngForm: NgForm) { createTask(ngForm: NgForm) {
this.apiService.saveNewTask(this.taskData).subscribe({ this.apiService
.saveNewTask(this.taskData)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (response) => { next: (response) => {
this.toastNotificationService.createTaskSuccessToast(); this.toastNotificationService.createTaskSuccessToast();
this.updateNotifierService.notifyUpdate('task'); this.updateNotifierService.notifyUpdate('task');
@ -286,7 +297,10 @@ export class AddTaskComponent implements OnInit {
updateTask(ngForm: NgForm) { updateTask(ngForm: NgForm) {
// OverlayData = Current TaskId // OverlayData = Current TaskId
this.apiService.updateTask(this.taskData, this.overlayData).subscribe({ this.apiService
.updateTask(this.taskData, this.overlayData)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (response) => { next: (response) => {
this.toastNotificationService.updateTaskSuccessToast(); this.toastNotificationService.updateTaskSuccessToast();
this.updateNotifierService.notifyUpdate('task'); this.updateNotifierService.notifyUpdate('task');

View file

@ -3,6 +3,8 @@ import {
EventEmitter, EventEmitter,
HostListener, HostListener,
Input, Input,
OnDestroy,
OnInit,
Output, Output,
} from '@angular/core'; } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -11,7 +13,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { User } from '../../../interfaces/user.interface'; import { User } from '../../../interfaces/user.interface';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '../../../services/api.service';
import { map, catchError, of } from 'rxjs'; import { map, catchError, of, Subject, takeUntil } from 'rxjs';
import { Assignee } from '../../../interfaces/task.interface'; import { Assignee } from '../../../interfaces/task.interface';
@Component({ @Component({
@ -21,7 +23,7 @@ import { Assignee } from '../../../interfaces/task.interface';
templateUrl: './assigned.component.html', templateUrl: './assigned.component.html',
styleUrl: './assigned.component.scss', styleUrl: './assigned.component.scss',
}) })
export class AssignedComponent { export class AssignedComponent implements OnInit, OnDestroy {
@Input() taskCreator: string = ''; @Input() taskCreator: string = '';
@Input() currentAssignees: Assignee[] = []; @Input() currentAssignees: Assignee[] = [];
@Output() assignedChange = new EventEmitter<string[]>(); @Output() assignedChange = new EventEmitter<string[]>();
@ -35,16 +37,24 @@ export class AssignedComponent {
dialogX: number = 0; dialogX: number = 0;
dialogY: number = 0; dialogY: number = 0;
private destroy$ = new Subject<void>();
constructor(private apiService: ApiService) {} constructor(private apiService: ApiService) {}
ngOnInit() { ngOnInit(): void {
this.loadUsers(); this.loadUsers();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
loadUsers(): void { loadUsers(): void {
this.apiService this.apiService
.getUsers() .getUsers()
.pipe( .pipe(
takeUntil(this.destroy$),
map((users) => users.filter((user) => user.id !== this.taskCreator)), map((users) => users.filter((user) => user.id !== this.taskCreator)),
catchError(() => of([])) catchError(() => of([]))
) )

View file

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { DragDropService } from '../../services/drag-drop.service'; import { DragDropService } from '../../services/drag-drop.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TaskComponent } from './task/task.component'; import { TaskComponent } from './task/task.component';
@ -32,14 +32,23 @@ import { ResizeService } from '../../services/resize.service';
templateUrl: './board.component.html', templateUrl: './board.component.html',
styleUrl: './board.component.scss', styleUrl: './board.component.scss',
}) })
export class BoardComponent { export class BoardComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
readonly TODO = 'todo'; readonly TODO = 'todo';
readonly IN_PROGRESS = 'inprogress'; readonly IN_PROGRESS = 'inprogress';
readonly AWAIT_FEEDBACK = 'awaitfeedback'; readonly AWAIT_FEEDBACK = 'awaitfeedback';
readonly DONE = 'done'; readonly DONE = 'done';
allTasks: Task[] = [];
filteredTasks: { [key: string]: Task[] } = {};
searchValue: string = '';
searchInput: boolean = false;
taskMovedTo: string = '';
taskMovedFrom: string = '';
isLoading = false;
private destroy$ = new Subject<void>();
constructor( constructor(
public dragDropService: DragDropService, public dragDropService: DragDropService,
public overlayService: OverlayService, public overlayService: OverlayService,
@ -51,27 +60,26 @@ export class BoardComponent {
private router: Router private router: Router
) {} ) {}
allTasks: Task[] = []; ngOnInit(): void {
filteredTasks: { [key: string]: Task[] } = {};
searchValue: string = '';
searchInput: boolean = false;
taskMovedTo: string = '';
taskMovedFrom: string = '';
isLoading = false;
ngOnInit() {
this.loadAllTasks(); this.loadAllTasks();
this.subscribeToTaskUpdates(); this.subscribeToTaskUpdates();
this.subscribeToDragDropEvents(); this.subscribeToDragDropEvents();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
loadAllTasks(): void { loadAllTasks(): void {
this.isLoading = true; this.isLoading = true;
this.taskService this.taskService
.getTasksWithUsers() .getTasksWithUsers()
.pipe(finalize(() => (this.isLoading = false))) .pipe(
takeUntil(this.destroy$),
finalize(() => (this.isLoading = false))
)
.subscribe({ .subscribe({
next: (response) => { next: (response) => {
this.allTasks = response.allTasks; this.allTasks = response.allTasks;
@ -111,15 +119,21 @@ export class BoardComponent {
* - `itemMovedFrom`: Sets `taskMovedFrom` to the status. * - `itemMovedFrom`: Sets `taskMovedFrom` to the status.
*/ */
private subscribeToDragDropEvents(): void { private subscribeToDragDropEvents(): void {
this.dragDropService.itemDropped.subscribe(({ task, status }) => { this.dragDropService.itemDropped
.pipe(takeUntil(this.destroy$))
.subscribe(({ task, status }) => {
this.handleItemDropped(task, status); this.handleItemDropped(task, status);
}); });
this.dragDropService.itemMovedTo.subscribe(({ status }) => { this.dragDropService.itemMovedTo
.pipe(takeUntil(this.destroy$))
.subscribe(({ status }) => {
this.taskMovedTo = status; this.taskMovedTo = status;
}); });
this.dragDropService.itemMovedFrom.subscribe(({ status }) => { this.dragDropService.itemMovedFrom
.pipe(takeUntil(this.destroy$))
.subscribe(({ status }) => {
this.taskMovedFrom = status; this.taskMovedFrom = status;
}); });
} }
@ -142,7 +156,10 @@ export class BoardComponent {
handleItemDropped(task: Task, status: string): void { handleItemDropped(task: Task, status: string): void {
if (!task || task.status === status) return; if (!task || task.status === status) return;
this.apiService.updateTaskStatus(task.id!, status).subscribe({ this.apiService
.updateTaskStatus(task.id!, status)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: () => { next: () => {
this.toastNotificationService.taskStatusUpdatedToast(); this.toastNotificationService.taskStatusUpdatedToast();
this.updateTaskStatus(task.id!, status); this.updateTaskStatus(task.id!, status);
@ -201,9 +218,4 @@ export class BoardComponent {
]) ])
); );
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
} }

View file

@ -2,13 +2,15 @@ import {
Component, Component,
EventEmitter, EventEmitter,
Input, Input,
OnChanges,
OnDestroy,
Output, Output,
SimpleChanges, SimpleChanges,
} from '@angular/core'; } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { finalize, lastValueFrom } from 'rxjs'; import { finalize, lastValueFrom, Subject, takeUntil } from 'rxjs';
import { User } from '../../../interfaces/user.interface'; import { User } from '../../../interfaces/user.interface';
import { OverlayService } from '../../../services/overlay.service'; import { OverlayService } from '../../../services/overlay.service';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '../../../services/api.service';
@ -23,7 +25,7 @@ import { ConfirmDialogComponent } from '../../../shared/components/confirm-dialo
templateUrl: './contact-detail.component.html', templateUrl: './contact-detail.component.html',
styleUrl: './contact-detail.component.scss', styleUrl: './contact-detail.component.scss',
}) })
export class ContactDetailComponent { export class ContactDetailComponent implements OnChanges, OnDestroy {
@Input() selectedUserId: string | null = null; @Input() selectedUserId: string | null = null;
@Input() currentUser: User | null = null; @Input() currentUser: User | null = null;
@Output() contactClosed = new EventEmitter<boolean>(); @Output() contactClosed = new EventEmitter<boolean>();
@ -33,6 +35,8 @@ export class ContactDetailComponent {
showMobileNavbar = false; showMobileNavbar = false;
showConfirmDialog = false; showConfirmDialog = false;
private destroy$ = new Subject<void>();
constructor( constructor(
private overlayService: OverlayService, private overlayService: OverlayService,
private userService: UserService, private userService: UserService,
@ -56,6 +60,38 @@ export class ContactDetailComponent {
} }
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Loads the user data with the given id and sets it to the `selectedUser` property.
*/
loadUser(): void {
this.isLoading = true;
if (!this.selectedUserId) {
this.isLoading = false;
return;
}
this.userService
.getUserById(this.selectedUserId)
.pipe(
takeUntil(this.destroy$),
finalize(() => (this.isLoading = false))
)
.subscribe({
next: (response) => {
this.selectedUser = response;
},
error: (err) => {
console.error('Error loading the tasks:', err);
},
});
}
/** /**
* Emits an event with the value false to trigger the contact list to close. * Emits an event with the value false to trigger the contact list to close.
*/ */
@ -104,28 +140,4 @@ export class ContactDetailComponent {
console.error(error); console.error(error);
} }
} }
/**
* Loads the user data with the given id and sets it to the `selectedUser` property.
*/
loadUser(): void {
this.isLoading = true;
if (!this.selectedUserId) {
this.isLoading = false;
return;
}
this.userService
.getUserById(this.selectedUserId)
.pipe(finalize(() => (this.isLoading = false)))
.subscribe({
next: (response) => {
this.selectedUser = response;
},
error: (err) => {
console.error('Error loading the tasks:', err);
},
});
}
} }

View file

@ -1,4 +1,4 @@
import { Component, HostListener } from '@angular/core'; import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { User } from '../../interfaces/user.interface'; import { User } from '../../interfaces/user.interface';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ContactDetailComponent } from './contact-detail/contact-detail.component'; import { ContactDetailComponent } from './contact-detail/contact-detail.component';
@ -15,15 +15,15 @@ import { UpdateNotifierService } from '../../services/update-notifier.service';
templateUrl: './contacts.component.html', templateUrl: './contacts.component.html',
styleUrl: './contacts.component.scss', styleUrl: './contacts.component.scss',
}) })
export class ContactsComponent { export class ContactsComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
allUsers: User[] = []; allUsers: User[] = [];
currentUser: User | null = null; currentUser: User | null = null;
selectedUserId: string | null = null; selectedUserId: string | null = null;
showAllUsers: boolean = false; showAllUsers: boolean = false;
isLoading: boolean = false; isLoading: boolean = false;
private destroy$ = new Subject<void>();
constructor( constructor(
private userService: UserService, private userService: UserService,
private overlayService: OverlayService, private overlayService: OverlayService,
@ -35,6 +35,11 @@ export class ContactsComponent {
this.initializeData(); this.initializeData();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private async initializeData(): Promise<void> { private async initializeData(): Promise<void> {
try { try {
await this.loadAllUsers(); await this.loadAllUsers();
@ -74,7 +79,10 @@ export class ContactsComponent {
} }
private loadCurrentUser(): void { private loadCurrentUser(): void {
this.userService.getCurrentUser().subscribe({ this.userService
.getCurrentUser()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (userData) => { next: (userData) => {
this.currentUser = userData; this.currentUser = userData;
}, },
@ -130,9 +138,4 @@ export class ContactsComponent {
this.showAllUsers = true; this.showAllUsers = true;
} }
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
} }

View file

@ -1,12 +1,12 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms'; import { FormsModule, NgForm } from '@angular/forms';
import { FormBtnComponent } from '../../../../shared/components/buttons/form-btn/form-btn.component'; import { FormBtnComponent } from '../../../../shared/components/buttons/form-btn/form-btn.component';
import { FooterComponent } from '../../footer/footer.component'; import { FooterComponent } from '../../footer/footer.component';
import { HeaderComponent } from '../../header/header.component'; import { HeaderComponent } from '../../header/header.component';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subject, Subscription, takeUntil } from 'rxjs';
import { ButtonStateService } from '../../../../services/button-state.service'; import { ButtonStateService } from '../../../../services/button-state.service';
import { AuthService } from '../../../../services/auth.service'; import { AuthService } from '../../../../services/auth.service';
import { PasswordVisibilityService } from '../../../../services/password-visibility.service'; import { PasswordVisibilityService } from '../../../../services/password-visibility.service';
@ -25,7 +25,7 @@ import { PasswordVisibilityService } from '../../../../services/password-visibil
templateUrl: './pw-reset.component.html', templateUrl: './pw-reset.component.html',
styleUrl: './pw-reset.component.scss', styleUrl: './pw-reset.component.scss',
}) })
export class PwResetComponent { export class PwResetComponent implements OnInit, OnDestroy {
private routeSubscription: Subscription = new Subscription(); private routeSubscription: Subscription = new Subscription();
uid: string = ''; uid: string = '';
@ -37,6 +37,8 @@ export class PwResetComponent {
passwordConfirm: '', passwordConfirm: '',
}; };
private destroy$ = new Subject<void>();
constructor( constructor(
public pwVisibility: PasswordVisibilityService, public pwVisibility: PasswordVisibilityService,
private authService: AuthService, private authService: AuthService,
@ -45,16 +47,19 @@ export class PwResetComponent {
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.routeSubscription = this.route.params.subscribe((params) => { this.routeParams();
this.uid = params['uid'];
this.token = params['token'];
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.routeSubscription) { this.destroy$.next();
this.routeSubscription.unsubscribe(); this.destroy$.complete();
} }
routeParams() {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.uid = params['uid'];
this.token = params['token'];
});
} }
isButtonDisabled() { isButtonDisabled() {

View file

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms'; import { FormsModule, NgForm } from '@angular/forms';
import { FormBtnComponent } from '../../shared/components/buttons/form-btn/form-btn.component'; import { FormBtnComponent } from '../../shared/components/buttons/form-btn/form-btn.component';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
@ -12,6 +12,7 @@ import { AuthService } from '../../services/auth.service';
import { TokenService } from '../../services/token.service'; import { TokenService } from '../../services/token.service';
import { ButtonStateService } from '../../services/button-state.service'; import { ButtonStateService } from '../../services/button-state.service';
import { PasswordVisibilityService } from '../../services/password-visibility.service'; import { PasswordVisibilityService } from '../../services/password-visibility.service';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@ -29,7 +30,7 @@ import { PasswordVisibilityService } from '../../services/password-visibility.se
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrl: './login.component.scss', styleUrl: './login.component.scss',
}) })
export class LoginComponent { export class LoginComponent implements OnInit, OnDestroy {
isPasswordIconVisible: boolean = true; isPasswordIconVisible: boolean = true;
checkboxRememberMe: boolean = false; checkboxRememberMe: boolean = false;
@ -38,6 +39,8 @@ export class LoginComponent {
password: '', password: '',
}; };
private destroy$ = new Subject<void>();
constructor( constructor(
public pwVisibility: PasswordVisibilityService, public pwVisibility: PasswordVisibilityService,
private authService: AuthService, private authService: AuthService,
@ -47,13 +50,18 @@ export class LoginComponent {
private route: ActivatedRoute private route: ActivatedRoute
) {} ) {}
ngOnInit() { ngOnInit(): void {
this.routeParams(); this.routeParams();
this.deleteTokens(); this.deleteTokens();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
routeParams() { routeParams() {
this.route.params.subscribe((params) => { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
if (params['id'] && params['id'] === 'pw-send') { if (params['id'] && params['id'] === 'pw-send') {
this.overlayService.setOverlayData('dialog', 'pw-send'); this.overlayService.setOverlayData('dialog', 'pw-send');
} }

View file

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { LanguageService } from '../../services/language.service'; import { LanguageService } from '../../services/language.service';
@ -8,6 +8,7 @@ import { HeaderComponent } from '../../shared/components/header/header.component
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { User } from '../../interfaces/user.interface'; import { User } from '../../interfaces/user.interface';
import { OverlayComponent } from '../../shared/components/overlay/overlay.component'; import { OverlayComponent } from '../../shared/components/overlay/overlay.component';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
selector: 'app-main-layout', selector: 'app-main-layout',
@ -23,9 +24,11 @@ import { OverlayComponent } from '../../shared/components/overlay/overlay.compon
templateUrl: './main-layout.component.html', templateUrl: './main-layout.component.html',
styleUrl: './main-layout.component.scss', styleUrl: './main-layout.component.scss',
}) })
export class MainLayoutComponent { export class MainLayoutComponent implements OnInit, OnDestroy {
currentUser: User | null = null; currentUser: User | null = null;
private destroy$ = new Subject<void>();
constructor( constructor(
public langService: LanguageService, public langService: LanguageService,
private userService: UserService private userService: UserService
@ -35,8 +38,16 @@ export class MainLayoutComponent {
this.loadCurrentUser(); this.loadCurrentUser();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
loadCurrentUser(): void { loadCurrentUser(): void {
this.userService.getCurrentUser().subscribe((userData) => { this.userService
.getCurrentUser()
.pipe(takeUntil(this.destroy$))
.subscribe((userData) => {
this.currentUser = userData; this.currentUser = userData;
}); });
} }

View file

@ -1,10 +1,10 @@
import { Component } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Task } from '../../interfaces/task.interface'; import { Task } from '../../interfaces/task.interface';
import { TaskService } from '../../services/task.service'; import { TaskService } from '../../services/task.service';
import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component'; import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component';
import { finalize } from 'rxjs'; import { finalize, Subject, takeUntil } from 'rxjs';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { User } from '../../interfaces/user.interface'; import { User } from '../../interfaces/user.interface';
@ -16,13 +16,14 @@ import { User } from '../../interfaces/user.interface';
templateUrl: './summary.component.html', templateUrl: './summary.component.html',
styleUrl: './summary.component.scss', styleUrl: './summary.component.scss',
}) })
export class SummaryComponent { export class SummaryComponent implements OnInit, OnDestroy {
nextUrgendTask: number[] = [];
allTasks: Task[] = []; allTasks: Task[] = [];
nextUrgendTask: number[] = [];
currentUser: User | null = null; currentUser: User | null = null;
isLoading = false; isLoading = false;
private destroy$ = new Subject<void>();
constructor( constructor(
private translateService: TranslateService, private translateService: TranslateService,
private userService: UserService, private userService: UserService,
@ -32,11 +33,16 @@ export class SummaryComponent {
/** /**
* This method loads all tasks from the TaskService. * This method loads all tasks from the TaskService.
*/ */
ngOnInit() { ngOnInit(): void {
this.loadAllTasks(); this.loadAllTasks();
this.loadCurrentUser(); this.loadCurrentUser();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/** /**
* Loads all tasks from the TaskService. * Loads all tasks from the TaskService.
*/ */
@ -45,7 +51,10 @@ export class SummaryComponent {
this.taskService this.taskService
.getTasks() .getTasks()
.pipe(finalize(() => (this.isLoading = false))) .pipe(
takeUntil(this.destroy$),
finalize(() => (this.isLoading = false))
)
.subscribe({ .subscribe({
next: (response) => { next: (response) => {
this.allTasks = response; this.allTasks = response;
@ -57,7 +66,10 @@ export class SummaryComponent {
} }
loadCurrentUser(): void { loadCurrentUser(): void {
this.userService.getCurrentUser().subscribe((userData) => { this.userService
.getCurrentUser()
.pipe(takeUntil(this.destroy$))
.subscribe((userData) => {
this.currentUser = userData; this.currentUser = userData;
}); });
} }

View file

@ -1,10 +1,11 @@
import { Component, HostListener } from '@angular/core'; import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { NavbarComponent } from './navbar/navbar.component'; import { NavbarComponent } from './navbar/navbar.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { User } from '../../../interfaces/user.interface'; import { User } from '../../../interfaces/user.interface';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
@ -13,25 +14,35 @@ import { User } from '../../../interfaces/user.interface';
templateUrl: './header.component.html', templateUrl: './header.component.html',
styleUrl: './header.component.scss', styleUrl: './header.component.scss',
}) })
export class HeaderComponent { export class HeaderComponent implements OnInit, OnDestroy {
showNavbar: boolean = false; showNavbar: boolean = false;
showLanguageNavbar: boolean = false; showLanguageNavbar: boolean = false;
currentUser: User | null = null; currentUser: User | null = null;
private destroy$ = new Subject<void>();
constructor(private userService: UserService) {} constructor(private userService: UserService) {}
/** /**
* Loads the current user data by calling the loadCurrentUser method. * Loads the current user data by calling the loadCurrentUser method.
*/ */
ngOnInit() { ngOnInit(): void {
this.loadCurrentUser(); this.loadCurrentUser();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/** /**
* Loads the current user data and sets it to the `currentUser` property. * Loads the current user data and sets it to the `currentUser` property.
*/ */
loadCurrentUser(): void { loadCurrentUser(): void {
this.userService.getCurrentUser().subscribe((userData) => { this.userService
.getCurrentUser()
.pipe(takeUntil(this.destroy$))
.subscribe((userData) => {
this.currentUser = userData; this.currentUser = userData;
}); });
} }

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ContactFormComponent } from './contact-form/contact-form.component'; import { ContactFormComponent } from './contact-form/contact-form.component';
import { BtnCloseComponent } from '../../buttons/btn-close/btn-close.component'; import { BtnCloseComponent } from '../../buttons/btn-close/btn-close.component';
@ -18,7 +18,7 @@ import { ColorPickerModule } from 'ngx-color-picker';
templateUrl: './contact-overlay.component.html', templateUrl: './contact-overlay.component.html',
styleUrl: './contact-overlay.component.scss', styleUrl: './contact-overlay.component.scss',
}) })
export class ContactOverlayComponent implements OnInit { export class ContactOverlayComponent {
@Input() overlayData: any = []; @Input() overlayData: any = [];
@Input() overlayType: string = ''; @Input() overlayType: string = '';
@Output() closeDialogEmitter = new EventEmitter<boolean>(); @Output() closeDialogEmitter = new EventEmitter<boolean>();
@ -27,10 +27,6 @@ export class ContactOverlayComponent implements OnInit {
userInitials: string = ''; userInitials: string = '';
newColor: string = ''; newColor: string = '';
constructor() {}
ngOnInit() {}
getRandomColor(): string { getRandomColor(): string {
const letters = '0123456789ABCDEF'; const letters = '0123456789ABCDEF';
let color = '#'; let color = '#';

View file

@ -1,4 +1,4 @@
import { Component, HostListener, OnInit } from '@angular/core'; import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { OverlayService } from '../../../services/overlay.service'; import { OverlayService } from '../../../services/overlay.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TaskOverlayComponent } from './task-overlay/task-overlay.component'; import { TaskOverlayComponent } from './task-overlay/task-overlay.component';
@ -6,6 +6,7 @@ import { TaskEditOverlayComponent } from './task-edit-overlay/task-edit-overlay.
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogOverlayComponent } from './dialog-overlay/dialog-overlay.component'; import { DialogOverlayComponent } from './dialog-overlay/dialog-overlay.component';
import { ContactOverlayComponent } from './contact-overlay/contact-overlay.component'; import { ContactOverlayComponent } from './contact-overlay/contact-overlay.component';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
selector: 'app-overlay', selector: 'app-overlay',
@ -20,10 +21,12 @@ import { ContactOverlayComponent } from './contact-overlay/contact-overlay.compo
templateUrl: './overlay.component.html', templateUrl: './overlay.component.html',
styleUrl: './overlay.component.scss', styleUrl: './overlay.component.scss',
}) })
export class OverlayComponent implements OnInit { export class OverlayComponent implements OnInit, OnDestroy {
overlayType: any; overlayType: any;
overlayData: any; overlayData: any;
private destroy$ = new Subject<void>();
constructor(private overlayService: OverlayService, private router: Router) {} constructor(private overlayService: OverlayService, private router: Router) {}
/** /**
@ -34,13 +37,20 @@ export class OverlayComponent implements OnInit {
this.checkOverlayData(); this.checkOverlayData();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/** /**
* Subscribes to the overlay data observable from the OverlayService. * Subscribes to the overlay data observable from the OverlayService.
* Updates the component's `overlayType` and `overlayData` properties * Updates the component's `overlayType` and `overlayData` properties
* whenever new overlay data is emitted. * whenever new overlay data is emitted.
*/ */
checkOverlayData() { checkOverlayData() {
this.overlayService.overlayData$.subscribe((data) => { this.overlayService.overlayData$
.pipe(takeUntil(this.destroy$))
.subscribe((data) => {
if (data) { if (data) {
this.overlayType = data.overlay; this.overlayType = data.overlay;
this.overlayData = data.data; this.overlayData = data.data;

View file

@ -1,10 +1,18 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { BtnCloseComponent } from '../../buttons/btn-close/btn-close.component'; import { BtnCloseComponent } from '../../buttons/btn-close/btn-close.component';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AddTaskComponent } from '../../../../components/add-task/add-task.component'; import { AddTaskComponent } from '../../../../components/add-task/add-task.component';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../../../../services/api.service'; import { ApiService } from '../../../../services/api.service';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
selector: 'app-task-edit-overlay', selector: 'app-task-edit-overlay',
@ -13,13 +21,15 @@ import { ApiService } from '../../../../services/api.service';
templateUrl: './task-edit-overlay.component.html', templateUrl: './task-edit-overlay.component.html',
styleUrl: './task-edit-overlay.component.scss', styleUrl: './task-edit-overlay.component.scss',
}) })
export class TaskEditOverlayComponent { export class TaskEditOverlayComponent implements OnInit, OnDestroy {
@Input() overlayData: string = ''; @Input() overlayData: string = '';
@Input() overlayType: string = ''; @Input() overlayType: string = '';
@Output() closeDialogEmitter = new EventEmitter<boolean>(); @Output() closeDialogEmitter = new EventEmitter<boolean>();
overlayMobile: boolean = false; overlayMobile: boolean = false;
private destroy$ = new Subject<void>();
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -33,17 +43,24 @@ export class TaskEditOverlayComponent {
* and sets the overlay data and type from the route parameters. It also sets * and sets the overlay data and type from the route parameters. It also sets
* the overlay mobile flag to true. * the overlay mobile flag to true.
*/ */
ngOnInit() { ngOnInit(): void {
if (this.overlayData == '') { this.loadOverlayData();
if (this.route.params.subscribe()) { }
this.route.params.subscribe((params) => {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private loadOverlayData(): void {
if (this.overlayData === '') {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.overlayData = params['id']; this.overlayData = params['id'];
this.overlayType = this.overlayType; this.overlayType = this.overlayType;
this.overlayMobile = true; this.overlayMobile = true;
}); });
} }
} }
}
/** /**
* Gets the task data from the Firebase service for the given task id. * Gets the task data from the Firebase service for the given task id.

View file

@ -1,4 +1,11 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { BtnCloseComponent } from '../../buttons/btn-close/btn-close.component'; import { BtnCloseComponent } from '../../buttons/btn-close/btn-close.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { OverlayService } from '../../../../services/overlay.service'; import { OverlayService } from '../../../../services/overlay.service';
@ -8,7 +15,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { Task } from '../../../../interfaces/task.interface'; import { Task } from '../../../../interfaces/task.interface';
import { TaskService } from '../../../../services/task.service'; import { TaskService } from '../../../../services/task.service';
import { AuthService } from '../../../../services/auth.service'; import { AuthService } from '../../../../services/auth.service';
import { map } from 'rxjs'; import { map, Subject, takeUntil } from 'rxjs';
import { ApiService } from '../../../../services/api.service'; import { ApiService } from '../../../../services/api.service';
import { UpdateNotifierService } from '../../../../services/update-notifier.service'; import { UpdateNotifierService } from '../../../../services/update-notifier.service';
import { ToastNotificationService } from '../../../../services/toast-notification.service'; import { ToastNotificationService } from '../../../../services/toast-notification.service';
@ -27,7 +34,7 @@ import { ConfirmDialogComponent } from '../../confirm-dialog/confirm-dialog.comp
templateUrl: './task-overlay.component.html', templateUrl: './task-overlay.component.html',
styleUrl: './task-overlay.component.scss', styleUrl: './task-overlay.component.scss',
}) })
export class TaskOverlayComponent implements OnInit { export class TaskOverlayComponent implements OnInit, OnDestroy {
@Input() overlayData: string = ''; @Input() overlayData: string = '';
@Output() closeDialogEmitter = new EventEmitter<boolean>(); @Output() closeDialogEmitter = new EventEmitter<boolean>();
@ -36,6 +43,8 @@ export class TaskOverlayComponent implements OnInit {
currentUserId: string = ''; currentUserId: string = '';
showConfirmDialog = false; showConfirmDialog = false;
private destroy$ = new Subject<void>();
constructor( constructor(
private overlayService: OverlayService, private overlayService: OverlayService,
private taskService: TaskService, private taskService: TaskService,
@ -52,16 +61,24 @@ export class TaskOverlayComponent implements OnInit {
['Technical Task', '#20d7c2'], ['Technical Task', '#20d7c2'],
]); ]);
ngOnInit() { ngOnInit(): void {
this.setCurrentUserId(); this.setCurrentUserId();
this.setOverlayDataFromRoute(); this.setOverlayDataFromRoute();
this.loadTask(this.overlayData); this.loadTask(this.overlayData);
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
setCurrentUserId() { setCurrentUserId() {
this.authService this.authService
.getCurrentUserId() .getCurrentUserId()
.pipe(map((userId) => userId ?? '')) .pipe(
takeUntil(this.destroy$),
map((userId) => userId ?? '')
)
.subscribe((userId) => { .subscribe((userId) => {
this.currentUserId = userId; this.currentUserId = userId;
}); });
@ -69,7 +86,7 @@ export class TaskOverlayComponent implements OnInit {
setOverlayDataFromRoute() { setOverlayDataFromRoute() {
if (this.overlayData === '') { if (this.overlayData === '') {
this.route.params.subscribe((params) => { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.overlayData = params['id']; this.overlayData = params['id'];
this.overlayMobile = true; this.overlayMobile = true;
}); });
@ -77,7 +94,10 @@ export class TaskOverlayComponent implements OnInit {
} }
loadTask(taskId: string) { loadTask(taskId: string) {
this.taskService.getTaskById(taskId).subscribe({ this.taskService
.getTaskById(taskId)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (task) => { next: (task) => {
this.task = task; this.task = task;
}, },
@ -117,7 +137,10 @@ export class TaskOverlayComponent implements OnInit {
} }
deleteTask(taskId: string) { deleteTask(taskId: string) {
this.apiService.deleteTaskById(taskId).subscribe({ this.apiService
.deleteTaskById(taskId)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (task) => { next: (task) => {
this.toastNotificationService.deleteTaskSuccessToast(); this.toastNotificationService.deleteTaskSuccessToast();
this.updateNotifierService.notifyUpdate('task'); this.updateNotifierService.notifyUpdate('task');
@ -140,7 +163,10 @@ export class TaskOverlayComponent implements OnInit {
subtask_title: subtaskTitle, subtask_title: subtaskTitle,
subtask_status: !currentStatus, subtask_status: !currentStatus,
}; };
this.apiService.updateSubtaskStatus(taskId, body).subscribe( this.apiService
.updateSubtaskStatus(taskId, body)
.pipe(takeUntil(this.destroy$))
.subscribe(
(response) => { (response) => {
this.task?.subtasks.forEach((subtask) => { this.task?.subtasks.forEach((subtask) => {
if (subtask.id === subtaskId) { if (subtask.id === subtaskId) {

View file

@ -1,9 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { LanguageService } from '../../../services/language.service'; import { LanguageService } from '../../../services/language.service';
import { AuthService } from '../../../services/auth.service'; import { AuthService } from '../../../services/auth.service';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
selector: 'app-sidebar', selector: 'app-sidebar',
@ -12,9 +13,11 @@ import { AuthService } from '../../../services/auth.service';
templateUrl: './sidebar.component.html', templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.scss', styleUrl: './sidebar.component.scss',
}) })
export class SidebarComponent implements OnInit { export class SidebarComponent implements OnInit, OnDestroy {
currentPath: string = ''; currentPath: string = '';
private destroy$ = new Subject<void>();
constructor( constructor(
private router: Router, private router: Router,
public languageService: LanguageService, public languageService: LanguageService,
@ -28,13 +31,18 @@ export class SidebarComponent implements OnInit {
this.getCurrentPath(); this.getCurrentPath();
} }
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/** /**
* Subscribes to router events and updates the currentPath property * Subscribes to router events and updates the currentPath property
* with the current route's URL, excluding the leading slash, whenever * with the current route's URL, excluding the leading slash, whenever
* a navigation ends. * a navigation ends.
*/ */
getCurrentPath() { getCurrentPath() {
this.router.events.subscribe((event) => { this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
this.currentPath = this.router.url.substring(1); this.currentPath = this.router.url.substring(1);
} }