feat(tasks): open a checker inbox entry in a popup instead of a page
Clicking a row navigated away to its own page, costing the checker the list and whatever search they had set up. It now opens in a dialog over the list, the same pattern the Disbursement and Closure Requests tabs already use. ViewCheckerInboxComponent gained the optional MatDialogRef/MAT_DIALOG_DATA pair rather than being duplicated, so the direct route still works for a bookmarked link. Two differences follow from being a dialog: - the list rows carry only summary fields, so the detail (which is what has commandAsJson) is fetched here; by route it stays resolver-provided. The template reads checkerInboxDetail.* unguarded, so it is now behind an @if with a loading state -- otherwise a dialog would render against undefined. - commandAsJson is absent on some entries (a DELETE carries no payload) and JSON.parse(undefined) throws. By route that broke the page; in a dialog it would have left an empty shell open. Parsing is now guarded. The list only reloads when something was actually actioned, since reload() re-navigates the route and would otherwise discard the checker's search on a plain close. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-6
@@ -155,12 +155,7 @@
|
||||
<td mat-cell *matCellDef="let makerChecker">{{ makerChecker.entityName }}</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[routerLink]="[row.id, 'view']"
|
||||
class="table-row"
|
||||
></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" (click)="viewEntry(row)" class="table-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { SettingsService } from 'app/settings/settings.service';
|
||||
|
||||
/** Dialog Components */
|
||||
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { ViewCheckerInboxComponent } from '../../view-checker-inbox/view-checker-inbox.component';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
@@ -288,6 +289,27 @@ export class CheckerInboxComponent implements OnInit {
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens one inbox entry in a popup rather than navigating to its own page, so the checker keeps
|
||||
* the list (and any search/filter they set up) behind the dialog. The direct route stays available
|
||||
* for a bookmarked link — ViewCheckerInboxComponent supports both entry points.
|
||||
*/
|
||||
viewEntry(row: any): void {
|
||||
this.dialog
|
||||
.open(ViewCheckerInboxComponent, {
|
||||
data: { checkerId: row.id },
|
||||
autoFocus: false
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((result: any) => {
|
||||
// Only reload when something was actually actioned — a plain close must not throw away the
|
||||
// checker's current search, which reload() does by re-navigating the route.
|
||||
if (result?.refreshed) {
|
||||
this.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refetches data for the component
|
||||
* TODO: Replace by a custom reload component instead of hard-coded back-routing.
|
||||
|
||||
@@ -6,14 +6,29 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
@if (isProductEditEntry) {
|
||||
<div [class.vci--dialog]="isDialog">
|
||||
@if (isDialog) {
|
||||
<div class="vci-dialog-bar">
|
||||
<span class="vci-dialog-bar__title">{{ 'labels.heading.View Checker Inbox' | translate }}</span>
|
||||
<span class="vci-dialog-bar__close" (click)="close()">✕ {{ 'labels.buttons.Close' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- As a dialog the detail is fetched rather than resolved, so nothing below can be rendered
|
||||
until it arrives — the template reads checkerInboxDetail.* unguarded. -->
|
||||
@if (loading) {
|
||||
<div class="vci-loading">{{ 'labels.heading.Loading' | translate }}…</div>
|
||||
}
|
||||
|
||||
@if (checkerInboxDetail) {
|
||||
@if (isProductEditEntry) {
|
||||
<div class="container m-b-20">
|
||||
<div class="mat-elevation-z1" style="padding: 12px 16px; border-left: 4px solid #f59e0b; background: #fffbeb">
|
||||
This is a product edit awaiting approval. Approve or reject it from
|
||||
<strong>Approvals > Pending Product Edits</strong> instead — actioning it here would not release its lock.
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
} @else {
|
||||
<div class="layout-row align-end gap-1percent layout-lt-md-column container m-b-20">
|
||||
<button mat-raised-button color="success" (click)="approveChecker()">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>{{ 'labels.buttons.Approve' | translate }}
|
||||
@@ -25,9 +40,9 @@
|
||||
<fa-icon icon="times" class="m-r-10"></fa-icon>{{ 'labels.buttons.Reject' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
<div class="container">
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="layout-row-wrap responsive-column">
|
||||
@@ -110,4 +125,6 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,60 @@
|
||||
margin: 1em 0 1.5em;
|
||||
}
|
||||
|
||||
/* Opened as a popup rather than a full page — sizes to its own content (capped so a long command
|
||||
payload still scrolls inside the dialog) instead of stretching to the viewport. */
|
||||
.vci--dialog {
|
||||
width: min(720px, 92vw);
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.container {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vci-dialog-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
&__close {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vci-loading {
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import * as _ from 'lodash';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
|
||||
/** Custom Services */
|
||||
import { TasksService } from '../tasks.service';
|
||||
@@ -33,7 +33,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
FaIconComponent,
|
||||
MatDivider,
|
||||
KeyValuePipe,
|
||||
DateFormatPipe
|
||||
DateFormatPipe,
|
||||
MatDialogModule
|
||||
]
|
||||
})
|
||||
export class ViewCheckerInboxComponent {
|
||||
@@ -42,6 +43,17 @@ export class ViewCheckerInboxComponent {
|
||||
private router = inject(Router);
|
||||
private translateService = inject(TranslateService);
|
||||
private tasksService = inject(TasksService);
|
||||
/** Present only when opened as a popup from the Checker Inbox list; absent when reached by direct
|
||||
* route (a bookmarked or shared link). Both entry points stay supported — this only changes where
|
||||
* the detail comes from and how "done" is signalled: closing the dialog vs. navigating back. */
|
||||
private dialogRef = inject(MatDialogRef<ViewCheckerInboxComponent>, { optional: true });
|
||||
private dialogData = inject<{ checkerId: number | string } | null>(MAT_DIALOG_DATA, { optional: true });
|
||||
|
||||
loading = false;
|
||||
|
||||
get isDialog(): boolean {
|
||||
return !!this.dialogRef;
|
||||
}
|
||||
|
||||
/** Checker Inbox Details Data */
|
||||
checkerInboxDetail: any;
|
||||
@@ -79,11 +91,49 @@ export class ViewCheckerInboxComponent {
|
||||
* @param {TasksService} tasksService Tasks Service.
|
||||
*/
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { checkerInboxDetail: any }) => {
|
||||
this.checkerInboxDetail = data.checkerInboxDetail;
|
||||
this.jsondata = JSON.parse(this.checkerInboxDetail.commandAsJson);
|
||||
this.displayJSONData = !_.isEmpty(this.jsondata);
|
||||
// As a popup the list only has summary rows, so the detail (which carries commandAsJson) is
|
||||
// fetched here; as a route it is already resolved by GetCheckerInboxDetailResolver.
|
||||
if (this.dialogData?.checkerId != null) {
|
||||
this.loading = true;
|
||||
this.tasksService.getCheckerInboxDetail(this.dialogData.checkerId).subscribe((detail: any) => {
|
||||
this.loading = false;
|
||||
this.applyDetail(detail);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.route.data.subscribe((data: { checkerInboxDetail: any }) => {
|
||||
this.applyDetail(data.checkerInboxDetail);
|
||||
});
|
||||
}
|
||||
|
||||
private applyDetail(detail: any): void {
|
||||
this.checkerInboxDetail = detail;
|
||||
// commandAsJson is absent on some entries (a DELETE carries no payload), and JSON.parse(undefined)
|
||||
// throws — which as a route only broke the page, but in a dialog would leave an empty shell open.
|
||||
try {
|
||||
this.jsondata = detail?.commandAsJson ? JSON.parse(detail.commandAsJson) : {};
|
||||
} catch {
|
||||
this.jsondata = {};
|
||||
}
|
||||
this.displayJSONData = !_.isEmpty(this.jsondata);
|
||||
}
|
||||
|
||||
/** "Done" for this screen — closes the popup (telling the list to refresh) when opened as a
|
||||
* dialog, or returns to the inbox route otherwise. */
|
||||
private finish(): void {
|
||||
if (this.dialogRef) {
|
||||
this.dialogRef.close({ refreshed: true });
|
||||
} else {
|
||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.dialogRef) {
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +149,7 @@ export class ViewCheckerInboxComponent {
|
||||
approveCheckerDialogRef.afterClosed().subscribe((response: { confirm: any }) => {
|
||||
if (response.confirm) {
|
||||
this.tasksService.executeMakerCheckerAction(this.checkerInboxDetail.id, 'approve').subscribe((res: any) => {
|
||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||
this.finish();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -118,7 +168,7 @@ export class ViewCheckerInboxComponent {
|
||||
rejectCheckerDialogRef.afterClosed().subscribe((response: { confirm: any }) => {
|
||||
if (response.confirm) {
|
||||
this.tasksService.executeMakerCheckerAction(this.checkerInboxDetail.id, 'reject').subscribe((res: any) => {
|
||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||
this.finish();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -137,7 +187,7 @@ export class ViewCheckerInboxComponent {
|
||||
deleteCheckerDialogRef.afterClosed().subscribe((response: { confirm: any }) => {
|
||||
if (response.confirm) {
|
||||
this.tasksService.deleteMakerChecker(this.checkerInboxDetail.id).subscribe((res: any) => {
|
||||
this.router.navigate(['../../'], { relativeTo: this.route });
|
||||
this.finish();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user