WEB-623 refactor: remove hard-coded back-routing for component reload (#3044)

This commit is contained in:
Shubham Kumar
2026-01-23 23:45:19 -06:00
committed by GitHub
parent 8d4f6636d0
commit 47951f241d
2 changed files with 146 additions and 56 deletions
@@ -7,25 +7,28 @@
*/ */
/** Angular Imports */ /** Angular Imports */
import { Component, OnInit, inject } from '@angular/core'; import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { ActivatedRoute, Router, RouterLinkActive, RouterOutlet } from '@angular/router';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
/** Dialog Imports */ /** Dialog Imports */
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
/** Custom Services */
import { CentersService } from '../centers.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
/** Services */
import { CentersService } from '../centers.service';
import { DataReloadService } from 'app/core/services/data-reload.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
/** UI Imports */
import { import {
MatCard,
MatCardHeader, MatCardHeader,
MatCardTitleGroup, MatCardTitleGroup,
MatCardMdImage, MatCardMdImage,
MatCardTitle, MatCardTitle,
MatCardSubtitle, MatCardSubtitle
MatCardContent
} from '@angular/material/card'; } from '@angular/material/card';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { NgClass, LowerCasePipe } from '@angular/common'; import { NgClass, LowerCasePipe } from '@angular/common';
@@ -38,8 +41,9 @@ import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { StatusLookupPipe } from '../../pipes/status-lookup.pipe'; import { StatusLookupPipe } from '../../pipes/status-lookup.pipe';
import { DateFormatPipe } from '../../pipes/date-format.pipe'; import { DateFormatPipe } from '../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** /**
* Create Center View * Centers View Component
*/ */
@Component({ @Component({
selector: 'mifosx-centers-view', selector: 'mifosx-centers-view',
@@ -71,57 +75,63 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
DateFormatPipe DateFormatPipe
] ]
}) })
export class CentersViewComponent implements OnInit { export class CentersViewComponent implements OnInit, OnDestroy {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
dialog = inject(MatDialog); private dialog = inject(MatDialog);
centersService = inject(CentersService); private centersService = inject(CentersService);
private translateService = inject(TranslateService); private translateService = inject(TranslateService);
private dataReloadService = inject(DataReloadService);
/** Stores Center View Data */
centerViewData: any; centerViewData: any;
/** Center datatable */
centerDatatables: any; centerDatatables: any;
/** Meeting data */ meetingData = false;
meetingData: boolean;
/** private reloadContext!: string;
* Retrieves the data for center private destroy$ = new Subject<void>();
* @param route route Activated Route.
*/ ngOnInit(): void {
constructor() { this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data: { centerViewData: any; centerDatatables: any }) => {
this.route.data.subscribe((data: { centerViewData: any; centerDatatables: any }) => {
this.centerViewData = data.centerViewData; this.centerViewData = data.centerViewData;
this.centerDatatables = data.centerDatatables; this.centerDatatables = data.centerDatatables;
this.meetingData = !!this.centerViewData?.collectionMeetingCalendar;
this.reloadContext = `center-${this.centerViewData.id}`;
// Subscribe to reload events after we have the center ID
this.dataReloadService
.getReloadObservable(this.reloadContext)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.refreshData();
});
}); });
} }
ngOnInit() { ngOnDestroy(): void {
if (this.centerViewData.collectionMeetingCalendar) { this.destroy$.next();
this.meetingData = true; this.destroy$.complete();
} else { if (this.reloadContext) {
this.meetingData = false; this.dataReloadService.cleanup(this.reloadContext);
} }
} }
/** /**
* Checks if meeting is editable. * Checks if meeting is editable.
*/ */
get editMeeting() { get editMeeting(): boolean {
if (this.centerViewData.collectionMeetingCalendar) { if (!this.centerViewData?.collectionMeetingCalendar) {
const entityType = this.centerViewData.collectionMeetingCalendar.entityType.value; return false;
if (entityType === 'CENTERS' && this.centerViewData.hierarchy === '.' + this.centerViewData.id + '.') {
return true;
}
} }
return false;
const entityType = this.centerViewData.collectionMeetingCalendar.entityType.value;
return entityType === 'CENTERS' && this.centerViewData.hierarchy === `.${this.centerViewData.id}.`;
} }
/** /**
* Performs action button/option action. * Performs action button/option action.
* @param {string} name action name. * @param {string} name action name.
*/ */
doAction(name: string) { doAction(name: string): void {
switch (name) { switch (name) {
case 'Activate': case 'Activate':
case 'Assign Staff': case 'Assign Staff':
@@ -132,38 +142,46 @@ export class CentersViewComponent implements OnInit {
case 'Staff Assignment History': case 'Staff Assignment History':
this.router.navigate([`actions/${name}`], { relativeTo: this.route }); this.router.navigate([`actions/${name}`], { relativeTo: this.route });
break; break;
case 'Edit Meeting': case 'Edit Meeting':
const queryParams: any = { calendarId: this.centerViewData.collectionMeetingCalendar.id }; this.router.navigate([`actions/${name}`], {
this.router.navigate([`actions/${name}`], { relativeTo: this.route, queryParams: queryParams }); relativeTo: this.route,
queryParams: {
calendarId: this.centerViewData.collectionMeetingCalendar.id
}
});
break; break;
case 'Unassign Staff': case 'Unassign Staff':
this.centersUnassignStaff(); this.unassignStaff();
break; break;
case 'Delete': case 'Delete':
this.deleteCenter(); this.deleteCenter();
break; break;
case 'Edit': case 'Edit':
this.router.navigate(['edit'], { relativeTo: this.route }); this.router.navigate(['edit'], { relativeTo: this.route });
break;
} }
} }
/** /**
* Unassign's the centers's staff. * Unassign's the centers's staff.
*/ */
private centersUnassignStaff() { private unassignStaff(): void {
const unAssignStaffDialogRef = this.dialog.open(ConfirmationDialogComponent, { const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: { data: {
heading: this.translateService.instant('labels.heading.Unassign Staff'), heading: this.translateService.instant('labels.heading.Unassign Staff'),
dialogContext: this.translateService.instant('labels.dialogContext.Are you sure you want Unassign Staff') dialogContext: this.translateService.instant('labels.dialogContext.Are you sure you want Unassign Staff')
} }
}); });
unAssignStaffDialogRef.afterClosed().subscribe((response: { confirm: any }) => {
if (response.confirm) { dialogRef.afterClosed().subscribe((response: { confirm: boolean }) => {
if (response?.confirm) {
this.centersService this.centersService
.executeGroupActionCommand(this.centerViewData.id, 'unassignStaff', { staffId: this.centerViewData.staffId }) .executeGroupActionCommand(this.centerViewData.id, 'unassignStaff', { staffId: this.centerViewData.staffId })
.subscribe(() => { .subscribe(() => this.reload());
this.reload();
});
} }
}); });
} }
@@ -171,25 +189,39 @@ export class CentersViewComponent implements OnInit {
/** /**
* Deletes the center * Deletes the center
*/ */
private deleteCenter() { private deleteCenter(): void {
const deleteGroupDialogRef = this.dialog.open(DeleteDialogComponent, { const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `center with id: ${this.centerViewData.id}` } data: {
deleteContext: `center with id: ${this.centerViewData.id}`
}
}); });
deleteGroupDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) { dialogRef.afterClosed().subscribe((response: any) => {
if (response?.delete) {
this.centersService.deleteCenter(this.centerViewData.id).subscribe(() => { this.centersService.deleteCenter(this.centerViewData.id).subscribe(() => {
this.router.navigate(['/centers'], { relativeTo: this.route }); this.router.navigate(['/centers']);
}); });
} }
}); });
} }
/** /**
* Refetches data for the component * Triggers a reload event for this center.
* TODO: Replace by a custom reload component instead of hard-coded back-routing.
*/ */
reload() { reload(): void {
const url: string = this.router.url; this.dataReloadService.triggerReload(this.reloadContext);
this.router.navigateByUrl(`/centers`, { skipLocationChange: true }).then(() => this.router.navigate([url])); }
/**
* Refreshes the center data when reload is triggered.
*/
private refreshData(): void {
this.centersService
.getCenterData(this.centerViewData.id)
.pipe(takeUntil(this.destroy$))
.subscribe((data: any) => {
this.centerViewData = data;
this.meetingData = !!data?.collectionMeetingCalendar;
});
} }
} }
@@ -0,0 +1,58 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Injectable } from '@angular/core';
import { ReplaySubject, Observable } from 'rxjs';
/**
* Service to handle component data reloading without navigation hacks.
* Provides a clean way to trigger data refreshes across the application.
* Uses ReplaySubject to handle late subscribers and race conditions.
*/
@Injectable({
providedIn: 'root'
})
export class DataReloadService {
private reloadSubjects = new Map<string, ReplaySubject<void>>();
/**
* Gets or creates a reload observable for a specific component/context.
* @param context Unique identifier for the reload context (e.g., 'savings-account', 'loan-view')
* @returns Observable that emits when a reload is triggered
*/
getReloadObservable(context: string): Observable<void> {
if (!this.reloadSubjects.has(context)) {
this.reloadSubjects.set(context, new ReplaySubject<void>(1));
}
return this.reloadSubjects.get(context)!.asObservable();
}
/**
* Triggers a reload for the specified context.
* All components subscribed to this context will be notified.
* Late subscribers will immediately receive the last reload notification.
* @param context Unique identifier for the reload context
*/
triggerReload(context: string): void {
if (!this.reloadSubjects.has(context)) {
this.reloadSubjects.set(context, new ReplaySubject<void>(1));
}
this.reloadSubjects.get(context)!.next();
}
/**
* Cleanup method to remove subjects when they're no longer needed.
* @param context Unique identifier for the reload context
*/
cleanup(context: string): void {
if (this.reloadSubjects.has(context)) {
this.reloadSubjects.get(context)!.complete();
this.reloadSubjects.delete(context);
}
}
}