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:
co-authored by
Claude Opus 4.7
parent
09fab88451
commit
1631cd3383
@@ -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);
|
||||||
|
}
|
||||||
@@ -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']
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user