From 1631cd338382ac9596745d623056594f0d1fc5a2 Mon Sep 17 00:00:00 2001 From: Aditya Date: Tue, 12 May 2026 14:24:47 +0530 Subject: [PATCH] feat(dialogs): polished success + error modals replace bare snackbar/dialog Add reusable SuccessDialogComponent (green check, gradient hero, pop-in animation) and redesign ErrorDialogComponent to match (red exclamation, clear title, centered OK). Wire edit-user submit flow to the new dialogs: success defers navigation until the user confirms, errors surface the backend message in the styled modal instead of a grey snackbar. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../error-dialog/error-dialog.component.html | 18 ++-- .../error-dialog/error-dialog.component.scss | 84 +++++++++++++++++++ .../error-dialog/error-dialog.component.ts | 6 +- .../success-dialog.component.html | 23 +++++ .../success-dialog.component.scss | 76 +++++++++++++++++ .../success-dialog.component.ts | 41 +++++++++ .../users/edit-user/edit-user.component.ts | 41 +++++---- 7 files changed, 261 insertions(+), 28 deletions(-) create mode 100644 src/app/shared/success-dialog/success-dialog.component.html create mode 100644 src/app/shared/success-dialog/success-dialog.component.scss create mode 100644 src/app/shared/success-dialog/success-dialog.component.ts diff --git a/src/app/shared/error-dialog/error-dialog.component.html b/src/app/shared/error-dialog/error-dialog.component.html index e23d13a0d..892421df5 100644 --- a/src/app/shared/error-dialog/error-dialog.component.html +++ b/src/app/shared/error-dialog/error-dialog.component.html @@ -6,17 +6,19 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -

{{ 'Error Log' | translate }}

- -
+ +
+ +
+

We encountered an issue. Please try again.

@if (!showAsCode) { -

{{ displayData }}

+

{{ displayData }}

} @if (showAsCode) { - +
} -
+ - - + + diff --git a/src/app/shared/error-dialog/error-dialog.component.scss b/src/app/shared/error-dialog/error-dialog.component.scss index af9f4c075..6a59c437a 100644 --- a/src/app/shared/error-dialog/error-dialog.component.scss +++ b/src/app/shared/error-dialog/error-dialog.component.scss @@ -5,3 +5,87 @@ * 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/. */ + +:host { + display: block; + min-width: 380px; + max-width: 480px; + font-family: inherit; + color: #0f172a; +} + +.ed-content { + display: flex !important; + flex-direction: column; + align-items: center; + text-align: center; + gap: 14px; + padding: 32px 28px 8px !important; + background: linear-gradient(180deg, #fef2f2 0%, #fff 70%); +} + +.ed-icon { + width: 76px; + height: 76px; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 38px; + box-shadow: 0 0 0 6px rgb(220 38 38 / 10%); + animation: ed-pop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes ed-pop { + 0% { + transform: scale(0.6); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +.ed-title { + margin: 4px 0 0; + font-size: 20px; + font-weight: 700; + color: #0f172a; +} + +.ed-message { + margin: 0; + color: #475569; + font-size: 14px; + line-height: 1.55; + max-width: 380px; + word-break: break-word; +} + +.ed-code { + width: 100%; + max-height: 320px; + overflow: auto; + text-align: left; + background: #0f172a; + color: #e2e8f0; + border-radius: 8px; + padding: 12px 14px; + font-family: 'SF Mono', Menlo, Consolas, monospace; + font-size: 12.5px; + line-height: 1.5; +} + +.ed-actions { + padding: 16px 24px 22px !important; + background: #fff; + justify-content: center !important; + + button { + min-width: 120px; + } +} diff --git a/src/app/shared/error-dialog/error-dialog.component.ts b/src/app/shared/error-dialog/error-dialog.component.ts index e09675f03..fb0d96e49 100644 --- a/src/app/shared/error-dialog/error-dialog.component.ts +++ b/src/app/shared/error-dialog/error-dialog.component.ts @@ -11,13 +11,13 @@ import { Component, OnInit, SecurityContext, inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA, - MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose } from '@angular/material/dialog'; import { CdkScrollable } from '@angular/cdk/scrolling'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -26,11 +26,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./error-dialog.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatDialogTitle, CdkScrollable, MatDialogContent, MatDialogActions, - MatDialogClose + MatDialogClose, + FaIconComponent ] }) export class ErrorDialogComponent { diff --git a/src/app/shared/success-dialog/success-dialog.component.html b/src/app/shared/success-dialog/success-dialog.component.html new file mode 100644 index 000000000..db81a3b74 --- /dev/null +++ b/src/app/shared/success-dialog/success-dialog.component.html @@ -0,0 +1,23 @@ + + + +
+ +
+

{{ data.title || 'Success' }}

+ @if (data.message) { +

{{ data.message }}

+ } +
+ + + + diff --git a/src/app/shared/success-dialog/success-dialog.component.scss b/src/app/shared/success-dialog/success-dialog.component.scss new file mode 100644 index 000000000..2e5c3c9ab --- /dev/null +++ b/src/app/shared/success-dialog/success-dialog.component.scss @@ -0,0 +1,76 @@ +/** + * 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/. + */ + +:host { + display: block; + min-width: 380px; + max-width: 460px; + font-family: inherit; + color: #0f172a; +} + +.sd-content { + display: flex !important; + flex-direction: column; + align-items: center; + text-align: center; + gap: 14px; + padding: 32px 28px 8px !important; + background: linear-gradient(180deg, #f0fdf4 0%, #fff 70%); +} + +.sd-icon { + width: 76px; + height: 76px; + border-radius: 50%; + background: #dcfce7; + color: #16a34a; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 38px; + box-shadow: 0 0 0 6px rgb(22 163 74 / 10%); + animation: sd-pop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes sd-pop { + 0% { + transform: scale(0.6); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +.sd-title { + margin: 4px 0 0; + font-size: 20px; + font-weight: 700; + color: #0f172a; +} + +.sd-message { + margin: 0; + color: #475569; + font-size: 14px; + line-height: 1.5; + max-width: 360px; +} + +.sd-actions { + padding: 16px 24px 22px !important; + background: #fff; + justify-content: center !important; + + button { + min-width: 120px; + } +} diff --git a/src/app/shared/success-dialog/success-dialog.component.ts b/src/app/shared/success-dialog/success-dialog.component.ts new file mode 100644 index 000000000..eca68acfc --- /dev/null +++ b/src/app/shared/success-dialog/success-dialog.component.ts @@ -0,0 +1,41 @@ +/** + * 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 { Component, inject } from '@angular/core'; +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialogContent, + MatDialogActions, + MatDialogClose +} from '@angular/material/dialog'; +import { MatButton } from '@angular/material/button'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +export interface SuccessDialogData { + title?: string; + message?: string; + buttonLabel?: string; +} + +@Component({ + selector: 'mifosx-success-dialog', + templateUrl: './success-dialog.component.html', + styleUrls: ['./success-dialog.component.scss'], + imports: [ + MatDialogContent, + MatDialogActions, + MatDialogClose, + MatButton, + FaIconComponent + ] +}) +export class SuccessDialogComponent { + dialogRef = inject>(MatDialogRef); + data = inject(MAT_DIALOG_DATA); +} diff --git a/src/app/users/edit-user/edit-user.component.ts b/src/app/users/edit-user/edit-user.component.ts index f26465e06..30908290a 100644 --- a/src/app/users/edit-user/edit-user.component.ts +++ b/src/app/users/edit-user/edit-user.component.ts @@ -10,13 +10,15 @@ import { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; -import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatDialog } from '@angular/material/dialog'; /** Custom Services */ import { UsersService } from '../users.service'; import { MatCheckbox } from '@angular/material/checkbox'; import { MatIcon } from '@angular/material/icon'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component'; +import { ErrorDialogComponent } from 'app/shared/error-dialog/error-dialog.component'; /** * Edit User Component. @@ -28,8 +30,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; imports: [ ...STANDALONE_SHARED_IMPORTS, MatCheckbox, - MatIcon, - MatSnackBarModule + MatIcon ] }) export class EditUserComponent implements OnInit { @@ -37,7 +38,7 @@ export class EditUserComponent implements OnInit { private usersService = inject(UsersService); private route = inject(ActivatedRoute); private router = inject(Router); - private snackBar = inject(MatSnackBar); + private dialog = inject(MatDialog); /** User Data */ userData: any; @@ -144,23 +145,29 @@ export class EditUserComponent implements OnInit { // Fineract sometimes omits resourceId in PUT responses — fall back to // the user id we're editing so navigation never silently no-ops. const targetId = response?.resourceId ?? this.userData.id; - this.snackBar.open('User updated successfully', 'OK', { - duration: 3000, - panelClass: ['success-snackbar'] + const dialogRef = this.dialog.open(SuccessDialogComponent, { + data: { + title: 'User updated', + message: `Changes to ${this.userData.username} have been saved successfully.`, + buttonLabel: 'View user' + }, + panelClass: 'sd-panel', + disableClose: true + }); + dialogRef.afterClosed().subscribe(() => { + this.router.navigate( + [ + '../../', + targetId + ], + { relativeTo: this.route } + ); }); - this.router.navigate( - [ - '../../', - targetId - ], - { relativeTo: this.route } - ); }, error: (err) => { console.error('[EditUser] update failed', err); - this.snackBar.open('Failed to update user', 'Close', { - duration: 4000, - panelClass: ['error-snackbar'] + this.dialog.open(ErrorDialogComponent, { + data: err?.error?.defaultUserMessage || err?.error?.message || 'Failed to update user.' }); } });