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) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-12 14:24:47 +05:30
co-authored by Claude Opus 4.7
parent 09fab88451
commit 1631cd3383
7 changed files with 261 additions and 28 deletions
@@ -6,17 +6,19 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/. file, You can obtain one at http://mozilla.org/MPL/2.0/.
--> -->
<h1 mat-dialog-title>{{ 'Error Log' | translate }}</h1> <mat-dialog-content class="ed-content">
<div class="ed-icon">
<div mat-dialog-content> <fa-icon icon="exclamation-circle"></fa-icon>
</div>
<h2 class="ed-title">We encountered an issue. Please try again.</h2>
@if (!showAsCode) { @if (!showAsCode) {
<p>{{ displayData }}</p> <p class="ed-message">{{ displayData }}</p>
} }
@if (showAsCode) { @if (showAsCode) {
<span [innerHTML]="sanitizedData"></span> <div class="ed-code" [innerHTML]="sanitizedData"></div>
} }
</div> </mat-dialog-content>
<mat-dialog-actions align="left"> <mat-dialog-actions align="center" class="ed-actions">
<button mat-raised-button mat-dialog-close>{{ 'Cancel' | translate }}</button> <button mat-flat-button color="warn" mat-dialog-close>OK</button>
</mat-dialog-actions> </mat-dialog-actions>
@@ -5,3 +5,87 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. * 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;
}
}
@@ -11,13 +11,13 @@ import { Component, OnInit, SecurityContext, inject } from '@angular/core';
import { import {
MatDialogRef, MatDialogRef,
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent, MatDialogContent,
MatDialogActions, MatDialogActions,
MatDialogClose MatDialogClose
} from '@angular/material/dialog'; } from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling'; import { CdkScrollable } from '@angular/cdk/scrolling';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({ @Component({
@@ -26,11 +26,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./error-dialog.component.scss'], styleUrls: ['./error-dialog.component.scss'],
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatDialogTitle,
CdkScrollable, CdkScrollable,
MatDialogContent, MatDialogContent,
MatDialogActions, MatDialogActions,
MatDialogClose MatDialogClose,
FaIconComponent
] ]
}) })
export class ErrorDialogComponent { export class ErrorDialogComponent {
@@ -0,0 +1,23 @@
<!--
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/.
-->
<mat-dialog-content class="sd-content">
<div class="sd-icon">
<fa-icon icon="check-circle"></fa-icon>
</div>
<h2 class="sd-title">{{ data.title || 'Success' }}</h2>
@if (data.message) {
<p class="sd-message">{{ data.message }}</p>
}
</mat-dialog-content>
<mat-dialog-actions align="center" class="sd-actions">
<button mat-flat-button color="primary" mat-dialog-close>
{{ data.buttonLabel || 'OK' }}
</button>
</mat-dialog-actions>
@@ -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;
}
}
@@ -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<SuccessDialogComponent>>(MatDialogRef);
data = inject<SuccessDialogData>(MAT_DIALOG_DATA);
}
+24 -17
View File
@@ -10,13 +10,15 @@
import { Component, OnInit, inject } from '@angular/core'; import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatDialog } from '@angular/material/dialog';
/** Custom Services */ /** Custom Services */
import { UsersService } from '../users.service'; import { UsersService } from '../users.service';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; 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. * Edit User Component.
@@ -28,8 +30,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatCheckbox, MatCheckbox,
MatIcon, MatIcon
MatSnackBarModule
] ]
}) })
export class EditUserComponent implements OnInit { export class EditUserComponent implements OnInit {
@@ -37,7 +38,7 @@ export class EditUserComponent implements OnInit {
private usersService = inject(UsersService); private usersService = inject(UsersService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private snackBar = inject(MatSnackBar); private dialog = inject(MatDialog);
/** User Data */ /** User Data */
userData: any; userData: any;
@@ -144,23 +145,29 @@ export class EditUserComponent implements OnInit {
// Fineract sometimes omits resourceId in PUT responses — fall back to // Fineract sometimes omits resourceId in PUT responses — fall back to
// the user id we're editing so navigation never silently no-ops. // the user id we're editing so navigation never silently no-ops.
const targetId = response?.resourceId ?? this.userData.id; const targetId = response?.resourceId ?? this.userData.id;
this.snackBar.open('User updated successfully', 'OK', { const dialogRef = this.dialog.open(SuccessDialogComponent, {
duration: 3000, data: {
panelClass: ['success-snackbar'] 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) => { error: (err) => {
console.error('[EditUser] update failed', err); console.error('[EditUser] update failed', err);
this.snackBar.open('Failed to update user', 'Close', { this.dialog.open(ErrorDialogComponent, {
duration: 4000, data: err?.error?.defaultUserMessage || err?.error?.message || 'Failed to update user.'
panelClass: ['error-snackbar']
}); });
} }
}); });