Skip to content

WEB-894 | Ng-busy lazy loader + no-data null record pipe#3443

Open
anshita-21 wants to merge 1 commit intoopenMF:devfrom
anshita-21:fix/WEB-894
Open

WEB-894 | Ng-busy lazy loader + no-data null record pipe#3443
anshita-21 wants to merge 1 commit intoopenMF:devfrom
anshita-21:fix/WEB-894

Conversation

@anshita-21
Copy link
Copy Markdown

@anshita-21 anshita-21 commented Mar 25, 2026

Add ng-busy directive and no-data pipe for improved form UX

#Problem

Two UX gaps were identified across the application:

No loading feedback on form submissions — Submit buttons had no loading indicator. Users received no visual feedback after clicking, making it unclear whether the action was processing or had failed silently.

Blank fields for missing data — Fields with no value were left empty, giving users no indication that data was simply unavailable.

Solution

ng-busy Directive Created a reusable directive that can be dropped onto any submit button to show a spinner and disable the element while a request is in flight — no component refactoring needed.

no-data Pipe Created a transform pipe that replaces empty/null/undefined values with a — placeholder, ensuring users always see a meaningful fallback instead of a blank.

#{https://mifosforge.jira.com/browse/WEB-894}

Screenshots, if any

image image

Checklist

Please make sure these boxes are checked before submitting your pull request - thanks!

  • If you have multiple commits please combine them into one commit by squashing them.

  • Read and understood the contribution guidelines at web-app/.github/CONTRIBUTING.md.

Summary by CodeRabbit

  • New Features

    • Show a loading/busy state on Create Center and Create Group submit buttons with a localized "Creating..." message and disabled interaction while busy.
    • Display a standardized "no data" placeholder for empty fields in the groups list.
  • Bug Fixes

    • Navigate only after successful creation and ensure the loading state is cleared on error.
  • Style

    • Add shared styles for loading/busy visuals.
  • Translations

    • Add "Creating..." and "no-data" translation entries.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'pre_merge_checks'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Adds a standalone busy/loading directive and styles, wires a per-component loading state into create-center/create-group flows, tightens group autocomplete behavior, shows a localized no-data fallback in group table cells, and adds translation keys for busy text and no-data.

Changes

Cohort / File(s) Summary
Busy Loading System
src/app/shared/directives/ng-busy.directive.ts, src/app/shared/_busy.scss
New standalone NgBusyDirective that toggles host into a busy state (injects global spinner styles, preserves/restores content and disabled state). New SCSS partial provides spinner styles and animations.
Create Center
src/app/centers/create-center/create-center.component.ts, src/app/centers/create-center/create-center.component.html
Added public loading: boolean, imported NgBusyDirective, set loading = true on submit and switched subscribe to object form (next/error). Template binds [mifosxNgBusy] and [busyText] on submit button.
Create Group
src/app/groups/create-group/create-group.component.ts, src/app/groups/create-group/create-group.component.html
Converted component to standalone, added NgBusyDirective import and loading state, tightened client autocomplete to clear results for null/short inputs, set loading lifecycle around createGroup with next/error handlers, and template binds busy directive.
Groups List Display
src/app/groups/groups.component.html
Updated table cells to render a translated no-data placeholder (labels.text.no-data) when group properties are falsy.
Translations (multiple locales)
src/assets/translations/en-US.json, src/assets/translations/cs-CS.json, src/assets/translations/de-DE.json, src/assets/translations/es-CL.json, src/assets/translations/es-MX.json, src/assets/translations/fr-FR.json, src/assets/translations/it-IT.json, src/assets/translations/ko-KO.json, src/assets/translations/lt-LT.json, src/assets/translations/lv-LV.json, src/assets/translations/ne-NE.json, src/assets/translations/pt-PT.json, src/assets/translations/sw-SW.json
Added labels.buttons.Creating... and labels.text.no-data (en-US) and added text.no-data"-" across many locale files for a consistent no-data placeholder and busy label.

Sequence Diagram

sequenceDiagram
    actor User
    participant Component as Create Component
    participant Directive as NgBusyDirective
    participant DOM as Browser DOM
    participant Service as Backend Service
    participant API as Backend API

    User->>Component: Click Submit
    Component->>Component: set loading = true
    Component->>Directive: [mifosxNgBusy] = true
    Directive->>DOM: inject spinner styles (once) and replace content with spinner + busyText
    Component->>Service: create request(data)
    Service->>API: HTTP POST
    API-->>Service: response / error
    alt Success
        Service-->>Component: next handler
        Component->>Component: navigate to list
        Component->>Component: set loading = false
        Component->>Directive: [mifosxNgBusy] = false
        Directive->>DOM: restore original content/state
    else Error
        Service-->>Component: error handler
        Component->>Component: set loading = false
        Component->>Directive: [mifosxNgBusy] = false
        Directive->>DOM: restore original content/state
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • IOhacker
  • alberto-art3ch
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main changes: addition of an ng-busy directive for loading states and no-data handling for null records, matching the core objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@anshita-21 anshita-21 changed the title WEB-894 | Ng-busy lazy loader + no-data null pipe8 WEB-894 | Ng-busy lazy loader + no-data null pipe Mar 25, 2026
@anshita-21 anshita-21 changed the title WEB-894 | Ng-busy lazy loader + no-data null pipe WEB-894 | Ng-busy lazy loader + no-data null record pipe Mar 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/app/shared/directives/ng-busy.directive.ts (2)

55-57: Consider using Renderer2 consistently for SSR compatibility.

Direct innerHTML manipulation (lines 56, 73) bypasses Angular's platform abstraction, which can cause issues with server-side rendering. While the current usage is safe (no user input), using Renderer2 methods consistently would improve SSR compatibility.

The static analysis XSS warnings are false positives here since originalContent comes from the component's own template, not user input.

Also applies to: 71-74

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` around lines 55 - 57, The
directive currently manipulates the host element DOM directly
(this.el.nativeElement.innerHTML = '' and appendChild(busyContent)); replace
those with Renderer2 calls for SSR safety: use
renderer.setProperty(this.el.nativeElement, 'innerHTML', '') to clear or iterate
children and call renderer.removeChild for each, and use
renderer.appendChild(this.el.nativeElement, busyContent) to add the busy node;
update NgBusyDirective where busyContent is created to use
renderer.createElement/createText if needed so the same Renderer2 instance is
used for all DOM operations.

91-95: Hardcoded white spinner may not be visible on light buttons.

The spinner uses hardcoded white colors (#ffffff), which won't be visible on light-themed or outline-style buttons. Consider using currentColor or CSS custom properties to inherit from the button's text color.

💡 Alternative using currentColor
-    this.renderer.setStyle(this.spinnerElement, 'border', '2px solid rgba(255, 255, 255, 0.3)');
-    this.renderer.setStyle(this.spinnerElement, 'border-top', '2px solid `#ffffff`');
+    this.renderer.setStyle(this.spinnerElement, 'border', '2px solid currentColor');
+    this.renderer.setStyle(this.spinnerElement, 'border-top-color', 'transparent');
+    this.renderer.setStyle(this.spinnerElement, 'opacity', '0.8');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` around lines 91 - 95, The
spinner currently sets hardcoded white colors via renderer.setStyle on
this.spinnerElement ('border' and 'border-top'), which fails on light
backgrounds; update the styles in ng-busy.directive to use an inheritable color
(e.g., use 'currentColor' for the spinner border or a CSS custom property like
'--spinner-color' with a sensible fallback) instead of '#ffffff', and ensure
both the general 'border' and the 'border-top' assignments reference the chosen
inherited value so the spinner color follows the button's text color or theme
variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/shared/directives/ng-busy.directive.ts`:
- Around line 71-74: The restoration logic in ng-busy.directive.ts uses a truthy
check on this.originalContent which skips restoring empty strings; change the
conditional in the restore block to explicitly check for null/undefined (e.g.,
this.originalContent !== null && this.originalContent !== undefined) so that
empty string content is correctly restored to this.el.nativeElement.innerHTML;
update the block around the existing comment "// Restore original content" and
the references to this.originalContent and this.el.nativeElement.innerHTML
accordingly.
- Around line 108-130: The addGlobalStyles function currently appends a style
tag per directive instance and never removes it; update the ng-busy.directive to
implement OnDestroy and make global styles a singleton: add a static flag (e.g.,
NgBusyDirective.stylesInjected) and a static reference for the shared style
element so addGlobalStyles only creates/appends the <style> once, and in
ngOnDestroy remove the style element only if this instance is the owner or
decrement a simple instance counter; ensure you still use this.renderer to
create/append/remove the element and keep the unique symbols styleElement,
addGlobalStyles, and ngOnDestroy/OnDestroy in the directive for locating the
changes.

---

Nitpick comments:
In `@src/app/shared/directives/ng-busy.directive.ts`:
- Around line 55-57: The directive currently manipulates the host element DOM
directly (this.el.nativeElement.innerHTML = '' and appendChild(busyContent));
replace those with Renderer2 calls for SSR safety: use
renderer.setProperty(this.el.nativeElement, 'innerHTML', '') to clear or iterate
children and call renderer.removeChild for each, and use
renderer.appendChild(this.el.nativeElement, busyContent) to add the busy node;
update NgBusyDirective where busyContent is created to use
renderer.createElement/createText if needed so the same Renderer2 instance is
used for all DOM operations.
- Around line 91-95: The spinner currently sets hardcoded white colors via
renderer.setStyle on this.spinnerElement ('border' and 'border-top'), which
fails on light backgrounds; update the styles in ng-busy.directive to use an
inheritable color (e.g., use 'currentColor' for the spinner border or a CSS
custom property like '--spinner-color' with a sensible fallback) instead of
'#ffffff', and ensure both the general 'border' and the 'border-top' assignments
reference the chosen inherited value so the spinner color follows the button's
text color or theme variable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a7608f7-7ea8-491b-ab24-256b4d8be33e

📥 Commits

Reviewing files that changed from the base of the PR and between f82ad09 and 9601331.

📒 Files selected for processing (8)
  • src/app/centers/create-center/create-center.component.html
  • src/app/centers/create-center/create-center.component.ts
  • src/app/groups/create-group/create-group.component.html
  • src/app/groups/create-group/create-group.component.ts
  • src/app/groups/groups.component.html
  • src/app/shared/_busy.scss
  • src/app/shared/directives/ng-busy.directive.ts
  • src/assets/translations/en-US.json
👮 Files not reviewed due to content moderation or server errors (6)
  • src/assets/translations/en-US.json
  • src/app/centers/create-center/create-center.component.html
  • src/app/groups/groups.component.html
  • src/app/groups/create-group/create-group.component.html
  • src/app/shared/_busy.scss
  • src/app/centers/create-center/create-center.component.ts

@IOhacker
Copy link
Copy Markdown
Contributor

@anshita-21 please review the error

Copy link
Copy Markdown
Contributor

@IOhacker IOhacker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kindly review my comments

@anshita-21
Copy link
Copy Markdown
Author

Kindly review my comments

Thank you for the review, have updated the PR

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/app/shared/directives/ng-busy.directive.ts (1)

15-17: ⚠️ Potential issue | 🟠 Major

Don't remove the shared style tag while other busy directives are still mounted.

globalStylesAdded is static, but styleElement is instance-local. The first directive instance that injected the shared styles will remove them in ngOnDestroy(), even if other [mifosxNgBusy] instances are still alive, so those survivors lose the spinner animation rules. Keep the shared element in a static field and reference-count instances, or leave the global style tag in place for the app lifetime.

🛠️ Suggested change
 export class NgBusyDirective implements OnInit, OnChanges, OnDestroy {
+  private static instanceCount = 0;
+  private static sharedStyleElement: HTMLStyleElement | null = null;
   `@Input`() mifosxNgBusy = false;
@@
-  private styleElement: HTMLStyleElement | null = null;
   private static globalStylesAdded = false;
@@
   ngOnInit() {
+    NgBusyDirective.instanceCount++;
     // Add global styles for animation
     this.addGlobalStyles();
   }
@@
   private addGlobalStyles() {
     // Add proper rotation animation only once globally
     if (!NgBusyDirective.globalStylesAdded) {
-      this.styleElement = this.renderer.createElement('style');
-      this.renderer.setProperty(this.styleElement, 'innerHTML', `
+      NgBusyDirective.sharedStyleElement = this.renderer.createElement('style');
+      this.renderer.setProperty(NgBusyDirective.sharedStyleElement, 'innerHTML', `
         `@keyframes` spin {
           0% { transform: rotate(0deg); }
           100% { transform: rotate(360deg); }
@@
-      this.renderer.appendChild(document.head, this.styleElement);
+      this.renderer.appendChild(document.head, NgBusyDirective.sharedStyleElement);
       NgBusyDirective.globalStylesAdded = true;
     }
   }
 
   ngOnDestroy() {
-    // Clean up style element if this is the last instance
-    if (this.styleElement && NgBusyDirective.globalStylesAdded) {
-      this.renderer.removeChild(document.head, this.styleElement);
+    NgBusyDirective.instanceCount--;
+    if (NgBusyDirective.instanceCount === 0 && NgBusyDirective.sharedStyleElement) {
+      this.renderer.removeChild(document.head, NgBusyDirective.sharedStyleElement);
+      NgBusyDirective.sharedStyleElement = null;
       NgBusyDirective.globalStylesAdded = false;
     }
   }

Also applies to: 107-138

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` around lines 15 - 17, The
shared style tag is removed by an instance-local styleElement in ngOnDestroy
even when other mifosxNgBusy instances still exist; change the implementation so
the shared style element is stored in a static field and add a static reference
counter (e.g., static sharedStyleElement: HTMLStyleElement | null and static
instanceCount = 0) that is incremented when an instance mounts
(constructor/ngOnInit) and decremented in ngOnDestroy, and only remove the style
element when instanceCount reaches 0 (or alternatively never remove it for app
lifetime) while keeping globalStylesAdded logic consistent with the static
sharedStyleElement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/shared/directives/ng-busy.directive.ts`:
- Around line 21-24: The snapshot of the host's DOM/state is taken once in
ngOnInit (originalContent, originalDisabled) which causes stale restores;
instead capture and store originalContent and originalDisabled immediately
before mutating the element inside showBusyState(), use those per-cycle
snapshots in hideBusyState() to restore, and clear them after restore; update
showBusyState() to set this.originalContent/this.originalDisabled (or per-cycle
vars) right before changing innerHTML/disabled, and update hideBusyState() to
read and restore from those variables rather than the init values.
- Around line 97-98: The fallback "Loading..." must be translated instead of
hardcoded; update the ng-busy directive to use `@ngx-translate/core` (inject
TranslateService into the directive) and replace the fallback assignment in the
busy text code (currently using this.busyText and renderer.createText) with a
translated lookup (e.g., translate.instant('BUSY.LOADING') or translate.get with
subscribe) so that if this.busyText is falsy you call TranslateService for the
i18n key, then pass the resulting string into renderer.createText;
alternatively, make the busyText input required and validate it in the directive
(e.g., throw or console.warn) if missing.
- Line 1: This new source file (ng-busy.directive.ts) is missing the standard
MPL license header; add the repository's canonical MPL header comment banner to
the top of the file (above the import line) so it matches other files, then run
the repo tooling to ensure correctness: execute npm run headers:add (or npm run
headers:check) to apply/verify the header; update the file containing the
Directive class/exports (ng-busy.directive.ts) only—no functional changes to
imports or symbols like Directive, Input, ElementRef, Renderer2, OnInit,
OnChanges, SimpleChanges, OnDestroy, inject.

---

Duplicate comments:
In `@src/app/shared/directives/ng-busy.directive.ts`:
- Around line 15-17: The shared style tag is removed by an instance-local
styleElement in ngOnDestroy even when other mifosxNgBusy instances still exist;
change the implementation so the shared style element is stored in a static
field and add a static reference counter (e.g., static sharedStyleElement:
HTMLStyleElement | null and static instanceCount = 0) that is incremented when
an instance mounts (constructor/ngOnInit) and decremented in ngOnDestroy, and
only remove the style element when instanceCount reaches 0 (or alternatively
never remove it for app lifetime) while keeping globalStylesAdded logic
consistent with the static sharedStyleElement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 101c7a50-597d-4869-bf69-10484ed64175

📥 Commits

Reviewing files that changed from the base of the PR and between 9601331 and c07a973.

📒 Files selected for processing (8)
  • src/app/centers/create-center/create-center.component.html
  • src/app/centers/create-center/create-center.component.ts
  • src/app/groups/create-group/create-group.component.html
  • src/app/groups/create-group/create-group.component.ts
  • src/app/groups/groups.component.html
  • src/app/shared/_busy.scss
  • src/app/shared/directives/ng-busy.directive.ts
  • src/assets/translations/en-US.json
✅ Files skipped from review due to trivial changes (3)
  • src/app/groups/groups.component.html
  • src/app/shared/_busy.scss
  • src/assets/translations/en-US.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/centers/create-center/create-center.component.html
  • src/app/groups/create-group/create-group.component.html
  • src/app/groups/create-group/create-group.component.ts

Copy link
Copy Markdown
Contributor

@IOhacker IOhacker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing translation files for the other Mifos WebApp supported languages, "no-data:-" key/value is very generic

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/app/shared/directives/ng-busy.directive.ts (2)

44-57: 🛠️ Refactor suggestion | 🟠 Major

Capture state before mutation, not at init.

Move the state capture from ngOnInit() to the start of showBusyState() to ensure the most recent content and disabled state are preserved:

♻️ Proposed refactor
   private showBusyState() {
+    // Capture current state before mutating
+    this.originalContent = this.el.nativeElement.innerHTML;
+    this.originalDisabled = this.el.nativeElement.disabled;
+
     // Add busy class
     this.renderer.addClass(this.el.nativeElement, this.busyClass);

And remove from ngOnInit():

   ngOnInit() {
-    // Store original content and state
-    this.originalContent = this.el.nativeElement.innerHTML;
-    this.originalDisabled = this.el.nativeElement.disabled;
-    
     // Add global styles for animation
     this.addGlobalStyles();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` around lines 44 - 57, The
current implementation captures the element state in ngOnInit; move that capture
to the start of showBusyState so you snapshot the latest innerHTML and disabled
state right before mutating the element. Update showBusyState to read and store
the current content and disabled flag (used by createBusyContent / busyClass
logic) before clearing/appending, and remove the redundant capture logic from
ngOnInit; ensure references to createBusyContent, busyClass, and any saved state
variables remain consistent.

132-138: ⚠️ Potential issue | 🟠 Major

ngOnDestroy cleanup logic is broken due to instance vs static mismatch.

Only the first directive instance sets this.styleElement (line 110). Subsequent instances have this.styleElement = null because addGlobalStyles() returns early when globalStylesAdded is true. This causes two problems:

  1. If a non-first instance is destroyed first, cleanup is skipped (harmless but wasteful check).
  2. If the first instance is destroyed while other instances still exist, the global styles are removed, breaking the spinner animation for remaining busy buttons.

The recommended fix from the prior review (static styleElement + instance counter) was not fully applied.

🛠️ Proposed fix using static reference and instance counting
   private originalContent: string | null = null;
   private originalDisabled: boolean | null = null;
   private spinnerElement: HTMLElement | null = null;
-  private styleElement: HTMLStyleElement | null = null;
-  private static globalStylesAdded = false;
+  private static styleElement: HTMLStyleElement | null = null;
+  private static instanceCount = 0;

   private el = inject(ElementRef);
   private renderer = inject(Renderer2);

   ngOnInit() {
     // Store original content and state
     this.originalContent = this.el.nativeElement.innerHTML;
     this.originalDisabled = this.el.nativeElement.disabled;
     
+    NgBusyDirective.instanceCount++;
     // Add global styles for animation
     this.addGlobalStyles();
   }

   // ... other methods ...

   private addGlobalStyles() {
     // Add proper rotation animation only once globally
-    if (!NgBusyDirective.globalStylesAdded) {
-      this.styleElement = this.renderer.createElement('style');
-      this.renderer.setProperty(this.styleElement, 'innerHTML', `
+    if (!NgBusyDirective.styleElement) {
+      NgBusyDirective.styleElement = this.renderer.createElement('style');
+      this.renderer.setProperty(NgBusyDirective.styleElement, 'innerHTML', `
         `@keyframes` spin {
           0% { transform: rotate(0deg); }
           100% { transform: rotate(360deg); }
         }
         
         .css-spinner {
           border: 2px solid rgba(255, 255, 255, 0.3) !important;
           border-top: 2px solid `#ffffff` !important;
         }
         
         button .css-spinner {
           border: 2px solid rgba(255, 255, 255, 0.3) !important;
           border-top: 2px solid `#ffffff` !important;
         }
       `);
-      this.renderer.appendChild(document.head, this.styleElement);
-      NgBusyDirective.globalStylesAdded = true;
+      this.renderer.appendChild(document.head, NgBusyDirective.styleElement);
     }
   }

   ngOnDestroy() {
-    // Clean up style element if this is the last instance
-    if (this.styleElement && NgBusyDirective.globalStylesAdded) {
-      this.renderer.removeChild(document.head, this.styleElement);
-      NgBusyDirective.globalStylesAdded = false;
+    NgBusyDirective.instanceCount--;
+    // Clean up style element only when last instance is destroyed
+    if (NgBusyDirective.instanceCount === 0 && NgBusyDirective.styleElement) {
+      this.renderer.removeChild(document.head, NgBusyDirective.styleElement);
+      NgBusyDirective.styleElement = null;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` around lines 132 - 138,
NgBusyDirective's cleanup is broken because styleElement is per-instance while
globalStylesAdded is static; change to a static shared styleElement and an
instance counter so addGlobalStyles() only creates and assigns
NgBusyDirective.styleElement once and increments a static instancesCount (in
constructor or ngOnInit), and ngOnDestroy decrements instancesCount and only
removes the static styleElement from document.head and sets globalStylesAdded =
false when instancesCount reaches 0; update references to this.styleElement to
NgBusyDirective.styleElement and ensure addGlobalStyles() returns early without
nulling instance properties for non-first instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/app/shared/directives/ng-busy.directive.ts`:
- Around line 44-57: The current implementation captures the element state in
ngOnInit; move that capture to the start of showBusyState so you snapshot the
latest innerHTML and disabled state right before mutating the element. Update
showBusyState to read and store the current content and disabled flag (used by
createBusyContent / busyClass logic) before clearing/appending, and remove the
redundant capture logic from ngOnInit; ensure references to createBusyContent,
busyClass, and any saved state variables remain consistent.
- Around line 132-138: NgBusyDirective's cleanup is broken because styleElement
is per-instance while globalStylesAdded is static; change to a static shared
styleElement and an instance counter so addGlobalStyles() only creates and
assigns NgBusyDirective.styleElement once and increments a static instancesCount
(in constructor or ngOnInit), and ngOnDestroy decrements instancesCount and only
removes the static styleElement from document.head and sets globalStylesAdded =
false when instancesCount reaches 0; update references to this.styleElement to
NgBusyDirective.styleElement and ensure addGlobalStyles() returns early without
nulling instance properties for non-first instances.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1abf1494-d219-4157-983a-522d4479694d

📥 Commits

Reviewing files that changed from the base of the PR and between c07a973 and cac3941.

📒 Files selected for processing (20)
  • src/app/centers/create-center/create-center.component.html
  • src/app/centers/create-center/create-center.component.ts
  • src/app/groups/create-group/create-group.component.html
  • src/app/groups/create-group/create-group.component.ts
  • src/app/groups/groups.component.html
  • src/app/shared/_busy.scss
  • src/app/shared/directives/ng-busy.directive.ts
  • src/assets/translations/cs-CS.json
  • src/assets/translations/de-DE.json
  • src/assets/translations/en-US.json
  • src/assets/translations/es-CL.json
  • src/assets/translations/es-MX.json
  • src/assets/translations/fr-FR.json
  • src/assets/translations/it-IT.json
  • src/assets/translations/ko-KO.json
  • src/assets/translations/lt-LT.json
  • src/assets/translations/lv-LV.json
  • src/assets/translations/ne-NE.json
  • src/assets/translations/pt-PT.json
  • src/assets/translations/sw-SW.json
✅ Files skipped from review due to trivial changes (14)
  • src/app/groups/groups.component.html
  • src/app/groups/create-group/create-group.component.html
  • src/assets/translations/de-DE.json
  • src/assets/translations/es-CL.json
  • src/assets/translations/fr-FR.json
  • src/assets/translations/en-US.json
  • src/assets/translations/cs-CS.json
  • src/assets/translations/ne-NE.json
  • src/assets/translations/lv-LV.json
  • src/assets/translations/es-MX.json
  • src/assets/translations/it-IT.json
  • src/app/shared/_busy.scss
  • src/assets/translations/lt-LT.json
  • src/assets/translations/sw-SW.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/groups/create-group/create-group.component.ts

@anshita-21
Copy link
Copy Markdown
Author

Missing translation files for the other Mifos WebApp supported languages, "no-data:-" key/value is very generic

Thank you @IOhacker, I've added the key across all lang files. However, ngx-translate falls back to en-US.json by default and the webapp continues to work fine across all languages.
The key-value is intentionally kept generic to serve as a neutral placeholder across all tables/lists, rather than leaving a blank display.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
src/app/shared/directives/ng-busy.directive.ts (4)

107-138: ⚠️ Potential issue | 🟠 Major

Make the injected stylesheet truly shared.

globalStylesAdded is static, but styleElement is owned by the single directive instance that first created it. If another [mifosxNgBusy] instance is still mounted during a route change, destroying the first one removes the shared stylesheet and the surviving spinner loses its animation. Store the style element and an instance count statically, and only remove the node when the last directive is destroyed.

🛠️ Suggested change
 export class NgBusyDirective implements OnInit, OnChanges, OnDestroy {
-  private styleElement: HTMLStyleElement | null = null;
   private static globalStylesAdded = false;
+  private static styleElement: HTMLStyleElement | null = null;
+  private static instanceCount = 0;
@@
   ngOnInit() {
+    NgBusyDirective.instanceCount++;
     // Add global styles for animation
     this.addGlobalStyles();
   }
@@
   private addGlobalStyles() {
     // Add proper rotation animation only once globally
     if (!NgBusyDirective.globalStylesAdded) {
-      this.styleElement = this.renderer.createElement('style');
-      this.renderer.setProperty(this.styleElement, 'innerHTML', `
+      NgBusyDirective.styleElement = this.renderer.createElement('style');
+      this.renderer.setProperty(NgBusyDirective.styleElement, 'innerHTML', `
         `@keyframes` spin {
           0% { transform: rotate(0deg); }
           100% { transform: rotate(360deg); }
@@
-      this.renderer.appendChild(document.head, this.styleElement);
+      this.renderer.appendChild(document.head, NgBusyDirective.styleElement);
       NgBusyDirective.globalStylesAdded = true;
     }
   }
 
   ngOnDestroy() {
-    // Clean up style element if this is the last instance
-    if (this.styleElement && NgBusyDirective.globalStylesAdded) {
-      this.renderer.removeChild(document.head, this.styleElement);
+    NgBusyDirective.instanceCount--;
+    if (NgBusyDirective.instanceCount === 0 && NgBusyDirective.styleElement) {
+      this.renderer.removeChild(document.head, NgBusyDirective.styleElement);
+      NgBusyDirective.styleElement = null;
       NgBusyDirective.globalStylesAdded = false;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` around lines 107 - 138, The
stylesheet is treated as global via NgBusyDirective.globalStylesAdded but the
style node is instance-owned (styleElement), causing removal while other
directives still exist; make the style element and an instance counter static on
NgBusyDirective (e.g., NgBusyDirective.styleElement and
NgBusyDirective.instanceCount), increment the counter when a directive is
initialized (or in addGlobalStyles) and only create/append the static
styleElement once (when instanceCount goes from 0 to 1), and in ngOnDestroy
decrement the static counter and remove the static styleElement from
document.head only when the counter reaches 0; update references in
addGlobalStyles() and ngOnDestroy() to use the static symbols.

97-98: ⚠️ Potential issue | 🟡 Minor

Translate the default busy label.

Any caller that omits [busyText] will render hardcoded English via Loading.... Resolve the fallback through @ngx-translate/core or make busyText required.

As per coding guidelines "Use proper i18n variables from @ngx-translate/core for all user-facing strings instead of hardcoded text".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` around lines 97 - 98, The
default hardcoded label "Loading..." must be replaced with a translated
fallback; inject TranslateService into the NgBusyDirective constructor and
replace the fallback expression so text = this.busyText ||
this.translate.instant('BUSY.LOADING') (or use this.translate.get(...) async)
before calling renderer.createText(text); ensure you add the TranslateService
import and constructor parameter, and create a translation key (e.g.,
BUSY.LOADING) in the i18n files rather than leaving a hardcoded string.

21-24: ⚠️ Potential issue | 🟠 Major

Only restore host state after a real busy cycle.

ngOnChanges() currently runs hideBusyState() on the initial false binding, before the directive has ever shown a spinner. On buttons that start disabled until the form is valid, that first pass can clobber the current disabled state; later busy cycles also restore the stale init-time snapshot. Capture innerHTML / disabled inside showBusyState() and gate hideBusyState() behind a flag that is set only after busy UI was applied.

🛠️ Suggested change
 export class NgBusyDirective implements OnInit, OnChanges, OnDestroy {
+  private busyStateApplied = false;
   private originalContent: string | null = null;
   private originalDisabled: boolean | null = null;
@@
   ngOnInit() {
-    // Store original content and state
-    this.originalContent = this.el.nativeElement.innerHTML;
-    this.originalDisabled = this.el.nativeElement.disabled;
-
     // Add global styles for animation
     this.addGlobalStyles();
   }
@@
   private updateBusyState() {
-    if (this.mifosxNgBusy) {
+    if (this.mifosxNgBusy && !this.busyStateApplied) {
       this.showBusyState();
-    } else {
+      this.busyStateApplied = true;
+    } else if (!this.mifosxNgBusy && this.busyStateApplied) {
       this.hideBusyState();
+      this.busyStateApplied = false;
     }
   }
 
   private showBusyState() {
+    this.originalContent = this.el.nativeElement.innerHTML;
+    this.originalDisabled = this.el.nativeElement.disabled;
+
     // Add busy class
     this.renderer.addClass(this.el.nativeElement, this.busyClass);
@@
   private hideBusyState() {
@@
     if (this.originalContent !== null && this.originalContent !== undefined) {
       this.el.nativeElement.innerHTML = this.originalContent;
     }
+    this.originalContent = null;
+    this.originalDisabled = null;
   }

Also applies to: 30-40, 44-57, 59-73

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` around lines 21 - 24,
ngOnChanges is calling hideBusyState on the initial false input and restoring a
stale snapshot; fix by capturing the host snapshot (originalContent,
originalDisabled) inside showBusyState instead of ngOnInit, add a boolean flag
(e.g. busyApplied) set to true when showBusyState has injected the busy UI, and
guard hideBusyState to only restore state if busyApplied is true (then reset
busyApplied to false after restoring); update references in ngOnChanges,
showBusyState and hideBusyState to use this flag and move snapshot logic out of
ngOnInit.

1-1: ⚠️ Potential issue | 🟠 Major

Add the standard MPL header before merging.

This new src/ file still lacks the repository banner, so headers:check will keep failing until it is added.

Based on learnings: "Applies to src/**/*.{ts,tsx,js,jsx,html,scss,css} : Always run npm run headers:check and npm run headers:add to ensure new files have the correct open source license file headers".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/shared/directives/ng-busy.directive.ts` at line 1, Add the
repository's standard MPL file header banner to the top of this new TypeScript
file (above the import line in ng-busy.directive.ts) so headers:check passes;
run the project's tooling (npm run headers:add) to insert the correct MPL header
and verify with npm run headers:check before merging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/app/shared/directives/ng-busy.directive.ts`:
- Around line 107-138: The stylesheet is treated as global via
NgBusyDirective.globalStylesAdded but the style node is instance-owned
(styleElement), causing removal while other directives still exist; make the
style element and an instance counter static on NgBusyDirective (e.g.,
NgBusyDirective.styleElement and NgBusyDirective.instanceCount), increment the
counter when a directive is initialized (or in addGlobalStyles) and only
create/append the static styleElement once (when instanceCount goes from 0 to
1), and in ngOnDestroy decrement the static counter and remove the static
styleElement from document.head only when the counter reaches 0; update
references in addGlobalStyles() and ngOnDestroy() to use the static symbols.
- Around line 97-98: The default hardcoded label "Loading..." must be replaced
with a translated fallback; inject TranslateService into the NgBusyDirective
constructor and replace the fallback expression so text = this.busyText ||
this.translate.instant('BUSY.LOADING') (or use this.translate.get(...) async)
before calling renderer.createText(text); ensure you add the TranslateService
import and constructor parameter, and create a translation key (e.g.,
BUSY.LOADING) in the i18n files rather than leaving a hardcoded string.
- Around line 21-24: ngOnChanges is calling hideBusyState on the initial false
input and restoring a stale snapshot; fix by capturing the host snapshot
(originalContent, originalDisabled) inside showBusyState instead of ngOnInit,
add a boolean flag (e.g. busyApplied) set to true when showBusyState has
injected the busy UI, and guard hideBusyState to only restore state if
busyApplied is true (then reset busyApplied to false after restoring); update
references in ngOnChanges, showBusyState and hideBusyState to use this flag and
move snapshot logic out of ngOnInit.
- Line 1: Add the repository's standard MPL file header banner to the top of
this new TypeScript file (above the import line in ng-busy.directive.ts) so
headers:check passes; run the project's tooling (npm run headers:add) to insert
the correct MPL header and verify with npm run headers:check before merging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 69fbaa46-97d5-4c77-8cf2-2ea05c7605af

📥 Commits

Reviewing files that changed from the base of the PR and between cac3941 and 4326e71.

📒 Files selected for processing (20)
  • src/app/centers/create-center/create-center.component.html
  • src/app/centers/create-center/create-center.component.ts
  • src/app/groups/create-group/create-group.component.html
  • src/app/groups/create-group/create-group.component.ts
  • src/app/groups/groups.component.html
  • src/app/shared/_busy.scss
  • src/app/shared/directives/ng-busy.directive.ts
  • src/assets/translations/cs-CS.json
  • src/assets/translations/de-DE.json
  • src/assets/translations/en-US.json
  • src/assets/translations/es-CL.json
  • src/assets/translations/es-MX.json
  • src/assets/translations/fr-FR.json
  • src/assets/translations/it-IT.json
  • src/assets/translations/ko-KO.json
  • src/assets/translations/lt-LT.json
  • src/assets/translations/lv-LV.json
  • src/assets/translations/ne-NE.json
  • src/assets/translations/pt-PT.json
  • src/assets/translations/sw-SW.json
✅ Files skipped from review due to trivial changes (14)
  • src/app/groups/groups.component.html
  • src/assets/translations/pt-PT.json
  • src/assets/translations/es-CL.json
  • src/assets/translations/it-IT.json
  • src/assets/translations/de-DE.json
  • src/app/shared/_busy.scss
  • src/assets/translations/ko-KO.json
  • src/assets/translations/ne-NE.json
  • src/assets/translations/cs-CS.json
  • src/assets/translations/en-US.json
  • src/assets/translations/es-MX.json
  • src/assets/translations/fr-FR.json
  • src/assets/translations/sw-SW.json
  • src/assets/translations/lt-LT.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/app/centers/create-center/create-center.component.html
  • src/app/groups/create-group/create-group.component.html
  • src/assets/translations/lv-LV.json
  • src/app/groups/create-group/create-group.component.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants