feat(ui): Add confirmation dialog for unsaved changes in extension modal (#2971)
Co-authored-by: Alex Rouse <alex.rouse@block.xyz>
This commit is contained in:
@@ -10,6 +10,7 @@ interface EnvVarsSectionProps {
|
||||
onRemove: (index: number) => void;
|
||||
onChange: (index: number, field: 'key' | 'value', value: string) => void;
|
||||
submitAttempted: boolean;
|
||||
onPendingInputChange?: (hasPendingInput: boolean) => void;
|
||||
}
|
||||
|
||||
export default function EnvVarsSection({
|
||||
@@ -18,6 +19,7 @@ export default function EnvVarsSection({
|
||||
onRemove,
|
||||
onChange,
|
||||
submitAttempted,
|
||||
onPendingInputChange,
|
||||
}: EnvVarsSectionProps) {
|
||||
const [newKey, setNewKey] = React.useState('');
|
||||
const [newValue, setNewValue] = React.useState('');
|
||||
@@ -27,6 +29,12 @@ export default function EnvVarsSection({
|
||||
value: false,
|
||||
});
|
||||
|
||||
// Notify parent when pending input changes
|
||||
React.useEffect(() => {
|
||||
const hasPendingInput = newKey.trim() !== '' || newValue.trim() !== '';
|
||||
onPendingInputChange?.(hasPendingInput);
|
||||
}, [newKey, newValue, onPendingInputChange]);
|
||||
|
||||
const handleAdd = () => {
|
||||
const keyEmpty = !newKey.trim();
|
||||
const valueEmpty = !newValue.trim();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react';
|
||||
import ExtensionInfoFields from './ExtensionInfoFields';
|
||||
import ExtensionTimeoutField from './ExtensionTimeoutField';
|
||||
import { upsertConfig } from '../../../../api/sdk.gen';
|
||||
import { ConfirmationModal } from '../../../ui/ConfirmationModal';
|
||||
|
||||
interface ExtensionModalProps {
|
||||
title: string;
|
||||
@@ -31,6 +32,65 @@ export default function ExtensionModal({
|
||||
const [formData, setFormData] = useState<ExtensionFormData>(initialData);
|
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||
const [showCloseConfirmation, setShowCloseConfirmation] = useState(false);
|
||||
const [hasPendingEnvVars, setHasPendingEnvVars] = useState(false);
|
||||
|
||||
// Function to check if form has been modified
|
||||
const hasFormChanges = (): boolean => {
|
||||
// Check if command/endpoint has changed
|
||||
const commandChanged =
|
||||
(formData.type === 'stdio' && formData.cmd !== initialData.cmd) ||
|
||||
(formData.type === 'sse' && formData.endpoint !== initialData.endpoint);
|
||||
|
||||
// Check if any environment variables have been modified
|
||||
const envVarsChanged = formData.envVars.some((envVar) => envVar.isEdited === true);
|
||||
|
||||
// Check if new env vars have been added
|
||||
const envVarsAdded = formData.envVars.length > initialData.envVars.length;
|
||||
|
||||
// Check if env vars have been removed
|
||||
const envVarsRemoved = formData.envVars.length < initialData.envVars.length;
|
||||
|
||||
// Check if any environment variable fields have text entered (even if not marked as edited)
|
||||
const envVarsHaveText = formData.envVars.some(
|
||||
(envVar) =>
|
||||
(envVar.key.trim() !== '' || envVar.value.trim() !== '') &&
|
||||
// Don't count placeholder values for existing secrets
|
||||
envVar.value !== '••••••••'
|
||||
);
|
||||
|
||||
// Check if there are pending environment variables being typed
|
||||
const hasPendingInput = hasPendingEnvVars;
|
||||
|
||||
return (
|
||||
commandChanged ||
|
||||
envVarsChanged ||
|
||||
envVarsAdded ||
|
||||
envVarsRemoved ||
|
||||
envVarsHaveText ||
|
||||
hasPendingInput
|
||||
);
|
||||
};
|
||||
|
||||
// Handle backdrop close with confirmation if needed
|
||||
const handleBackdropClose = () => {
|
||||
if (hasFormChanges()) {
|
||||
setShowCloseConfirmation(true);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle confirmed close
|
||||
const handleConfirmClose = () => {
|
||||
setShowCloseConfirmation(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Handle cancel close confirmation
|
||||
const handleCancelClose = () => {
|
||||
setShowCloseConfirmation(false);
|
||||
};
|
||||
|
||||
const handleAddEnvVar = (key: string, value: string) => {
|
||||
setFormData({
|
||||
@@ -208,7 +268,7 @@ export default function ExtensionModal({
|
||||
{submitLabel}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
onClick={handleBackdropClose}
|
||||
variant="ghost"
|
||||
className="w-full h-[60px] rounded-none hover:bg-bgSubtle text-textSubtle hover:text-textStandard text-md font-regular"
|
||||
>
|
||||
@@ -221,69 +281,84 @@ export default function ExtensionModal({
|
||||
const modalTitle = showDeleteConfirmation ? `Delete Extension "${formData.name}"` : title;
|
||||
|
||||
return (
|
||||
<Modal footer={footerContent} onClose={onClose}>
|
||||
{/* Title and Icon */}
|
||||
<div className="flex flex-col mb-6">
|
||||
<div>{getModalIcon()}</div>
|
||||
<div className="mt-2">
|
||||
<h2 className="text-2xl font-regular text-textStandard">{modalTitle}</h2>
|
||||
<>
|
||||
<Modal footer={footerContent} onClose={handleBackdropClose}>
|
||||
{/* Title and Icon */}
|
||||
<div className="flex flex-col mb-6">
|
||||
<div>{getModalIcon()}</div>
|
||||
<div className="mt-2">
|
||||
<h2 className="text-2xl font-regular text-textStandard">{modalTitle}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showDeleteConfirmation ? (
|
||||
<div className="mb-6">
|
||||
<p className="text-textStandard">
|
||||
This will permanently remove this extension and all of its settings.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Form Fields */}
|
||||
{/* Name and Type */}
|
||||
<ExtensionInfoFields
|
||||
name={formData.name}
|
||||
type={formData.type}
|
||||
description={formData.description}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
/>
|
||||
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle mb-4" />
|
||||
|
||||
{/* Command */}
|
||||
{showDeleteConfirmation ? (
|
||||
<div className="mb-6">
|
||||
<ExtensionConfigFields
|
||||
<p className="text-textStandard">
|
||||
This will permanently remove this extension and all of its settings.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Form Fields */}
|
||||
{/* Name and Type */}
|
||||
<ExtensionInfoFields
|
||||
name={formData.name}
|
||||
type={formData.type}
|
||||
full_cmd={formData.cmd || ''}
|
||||
endpoint={formData.endpoint || ''}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
isValid={isConfigValid()}
|
||||
/>
|
||||
<div className="mb-4" />
|
||||
<ExtensionTimeoutField
|
||||
timeout={formData.timeout || 300}
|
||||
description={formData.description}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle mb-4" />
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle mb-4" />
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="mb-6">
|
||||
<EnvVarsSection
|
||||
envVars={formData.envVars}
|
||||
onAdd={handleAddEnvVar}
|
||||
onRemove={handleRemoveEnvVar}
|
||||
onChange={handleEnvVarChange}
|
||||
submitAttempted={submitAttempted}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
{/* Command */}
|
||||
<div className="mb-6">
|
||||
<ExtensionConfigFields
|
||||
type={formData.type}
|
||||
full_cmd={formData.cmd || ''}
|
||||
endpoint={formData.endpoint || ''}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
isValid={isConfigValid()}
|
||||
/>
|
||||
<div className="mb-4" />
|
||||
<ExtensionTimeoutField
|
||||
timeout={formData.timeout || 300}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle mb-4" />
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="mb-6">
|
||||
<EnvVarsSection
|
||||
envVars={formData.envVars}
|
||||
onAdd={handleAddEnvVar}
|
||||
onRemove={handleRemoveEnvVar}
|
||||
onChange={handleEnvVarChange}
|
||||
submitAttempted={submitAttempted}
|
||||
onPendingInputChange={setHasPendingEnvVars}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Close Confirmation Modal */}
|
||||
{showCloseConfirmation && (
|
||||
<ConfirmationModal
|
||||
isOpen={showCloseConfirmation}
|
||||
title="Unsaved Changes"
|
||||
message="You have unsaved changes to the extension configuration. Are you sure you want to close without saving?"
|
||||
confirmLabel="Close Without Saving"
|
||||
onConfirm={handleConfirmClose}
|
||||
onCancel={handleCancelClose}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user