Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -18,6 +19,7 @@ export default function EnvVarsSection({
onRemove,
onChange,
submitAttempted,
onPendingInputChange,
}: EnvVarsSectionProps) {
const [newKey, setNewKey] = React.useState('');
const [newValue, setNewValue] = React.useState('');
Expand All @@ -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();
Expand Down
185 changes: 130 additions & 55 deletions ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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({
Expand Down Expand Up @@ -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"
>
Expand All @@ -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>
</>
);
}
Loading