ui(deploy): router blocker + DS dialog for unsaved edits
- Add deployedConfigSnapshot field to Deployment interface (mirrors server shape) - Remove the Task 10.3 cast in handleRestore now that the type has the field - New useUnsavedChangesBlocker hook (react-router useBlocker, v7.13.1) - Wire AlertDialog into AppDeploymentPage for in-app navigation guard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,11 @@ export interface Deployment {
|
|||||||
deployedAt: string | null;
|
deployedAt: string | null;
|
||||||
stoppedAt: string | null;
|
stoppedAt: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
deployedConfigSnapshot?: {
|
||||||
|
jarVersionId: string;
|
||||||
|
agentConfig: Record<string, unknown> | null;
|
||||||
|
containerConfig: Record<string, unknown>;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useBlocker } from 'react-router';
|
||||||
|
|
||||||
|
export function useUnsavedChangesBlocker(hasUnsavedChanges: boolean) {
|
||||||
|
const blocker = useBlocker(({ currentLocation, nextLocation }) =>
|
||||||
|
hasUnsavedChanges && currentLocation.pathname !== nextLocation.pathname
|
||||||
|
);
|
||||||
|
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (blocker.state === 'blocked') setDialogOpen(true);
|
||||||
|
}, [blocker.state]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dialogOpen,
|
||||||
|
confirm: () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
blocker.proceed?.();
|
||||||
|
},
|
||||||
|
cancel: () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
blocker.reset?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import { DeploymentTab } from './DeploymentTab/DeploymentTab';
|
|||||||
import { PrimaryActionButton, computeMode } from './PrimaryActionButton';
|
import { PrimaryActionButton, computeMode } from './PrimaryActionButton';
|
||||||
import { useDeploymentPageState } from './hooks/useDeploymentPageState';
|
import { useDeploymentPageState } from './hooks/useDeploymentPageState';
|
||||||
import { useFormDirty } from './hooks/useFormDirty';
|
import { useFormDirty } from './hooks/useFormDirty';
|
||||||
|
import { useUnsavedChangesBlocker } from './hooks/useUnsavedChangesBlocker';
|
||||||
import { deriveAppName } from './utils/deriveAppName';
|
import { deriveAppName } from './utils/deriveAppName';
|
||||||
import styles from './AppDeploymentPage.module.css';
|
import styles from './AppDeploymentPage.module.css';
|
||||||
|
|
||||||
@@ -108,6 +109,8 @@ export default function AppDeploymentPage() {
|
|||||||
// Derived
|
// Derived
|
||||||
const mode = app ? 'deployed' : 'net-new';
|
const mode = app ? 'deployed' : 'net-new';
|
||||||
const dirty = useFormDirty(form, serverState, stagedJar);
|
const dirty = useFormDirty(form, serverState, stagedJar);
|
||||||
|
const { dialogOpen: blockerOpen, confirm: blockerConfirm, cancel: blockerCancel } =
|
||||||
|
useUnsavedChangesBlocker(dirty.anyLocalEdit);
|
||||||
const serverDirtyAgainstDeploy = dirtyState?.dirty ?? false;
|
const serverDirtyAgainstDeploy = dirtyState?.dirty ?? false;
|
||||||
const deploymentInProgress = !!activeDeployment;
|
const deploymentInProgress = !!activeDeployment;
|
||||||
const primaryMode = computeMode({
|
const primaryMode = computeMode({
|
||||||
@@ -302,9 +305,7 @@ export default function AppDeploymentPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRestore(deploymentId: string) {
|
function handleRestore(deploymentId: string) {
|
||||||
const deployment = deployments.find((d) => d.id === deploymentId) as
|
const deployment = deployments.find((d) => d.id === deploymentId);
|
||||||
| (Deployment & { deployedConfigSnapshot?: { agentConfig?: Record<string, unknown>; containerConfig?: Record<string, unknown> } })
|
|
||||||
| undefined;
|
|
||||||
if (!deployment) return;
|
if (!deployment) return;
|
||||||
const snap = deployment.deployedConfigSnapshot;
|
const snap = deployment.deployedConfigSnapshot;
|
||||||
if (!snap) return;
|
if (!snap) return;
|
||||||
@@ -519,6 +520,17 @@ export default function AppDeploymentPage() {
|
|||||||
confirmLabel="Delete"
|
confirmLabel="Delete"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* ── Unsaved changes navigation blocker ── */}
|
||||||
|
<AlertDialog
|
||||||
|
open={blockerOpen}
|
||||||
|
onClose={blockerCancel}
|
||||||
|
onConfirm={blockerConfirm}
|
||||||
|
title="Unsaved changes"
|
||||||
|
description="You have unsaved changes on this page. Discard and leave?"
|
||||||
|
confirmLabel="Discard & Leave"
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user