fix(editor): replace prompt/confirm with proper dialogs in BackendServicesPanel

Electron doesn't support browser prompt()/confirm() APIs.
Replaced with:
- Inline TextInput form for creating local backends
- useConfirmationDialog hook for delete confirmations

Fixes console error when clicking + button on Local Backends section.
This commit is contained in:
Richard Osborne
2026-01-15 18:43:01 +01:00
parent 1c40f45195
commit 9181d5d83c

View File

@@ -16,9 +16,11 @@ import { BackendServices, BackendServicesEvent } from '@noodl-models/BackendServ
import { ActivityIndicator } from '@noodl-core-ui/components/common/ActivityIndicator'; import { ActivityIndicator } from '@noodl-core-ui/components/common/ActivityIndicator';
import { IconName, IconSize } from '@noodl-core-ui/components/common/Icon'; import { IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
import { IconButton } from '@noodl-core-ui/components/inputs/IconButton'; import { IconButton } from '@noodl-core-ui/components/inputs/IconButton';
import { PrimaryButton, PrimaryButtonVariant, PrimaryButtonSize } from '@noodl-core-ui/components/inputs/PrimaryButton';
import { TextInput } from '@noodl-core-ui/components/inputs/TextInput';
import { Box } from '@noodl-core-ui/components/layout/Box'; import { Box } from '@noodl-core-ui/components/layout/Box';
import { Container } from '@noodl-core-ui/components/layout/Container'; import { Container } from '@noodl-core-ui/components/layout/Container';
import { VStack } from '@noodl-core-ui/components/layout/Stack'; import { HStack, VStack } from '@noodl-core-ui/components/layout/Stack';
import { useConfirmationDialog } from '@noodl-core-ui/components/popups/ConfirmationDialog/ConfirmationDialog.hooks'; import { useConfirmationDialog } from '@noodl-core-ui/components/popups/ConfirmationDialog/ConfirmationDialog.hooks';
import { BasePanel } from '@noodl-core-ui/components/sidebar/BasePanel'; import { BasePanel } from '@noodl-core-ui/components/sidebar/BasePanel';
import { Section, SectionVariant } from '@noodl-core-ui/components/sidebar/Section'; import { Section, SectionVariant } from '@noodl-core-ui/components/sidebar/Section';
@@ -36,6 +38,10 @@ export function BackendServicesPanel() {
const [isAddDialogVisible, setIsAddDialogVisible] = useState(false); const [isAddDialogVisible, setIsAddDialogVisible] = useState(false);
const [hasActivity, setHasActivity] = useState(false); const [hasActivity, setHasActivity] = useState(false);
// Local backend creation state
const [isAddLocalVisible, setIsAddLocalVisible] = useState(false);
const [newLocalBackendName, setNewLocalBackendName] = useState('');
// Local backends hook // Local backends hook
const { const {
backends: localBackends, backends: localBackends,
@@ -65,13 +71,42 @@ export function BackendServicesPanel() {
setActiveBackendId(id); setActiveBackendId(id);
}); });
// Delete confirmation dialog // Delete confirmation dialog for external backends
const [DeleteDialog, confirmDelete] = useConfirmationDialog({ const [DeleteDialog, confirmDelete] = useConfirmationDialog({
title: 'Delete Backend', title: 'Delete Backend',
message: 'Are you sure you want to delete this backend configuration? This cannot be undone.', message: 'Are you sure you want to delete this backend configuration? This cannot be undone.',
isDangerousAction: true isDangerousAction: true
}); });
// Delete confirmation dialog for local backends
const [DeleteLocalDialog, confirmDeleteLocal] = useConfirmationDialog({
title: 'Delete Local Backend',
message: 'Delete this local backend? All data will be permanently lost.',
isDangerousAction: true
});
// Handle delete local backend
const handleDeleteLocalBackend = useCallback(
async (id: string) => {
try {
await confirmDeleteLocal();
await deleteLocalBackend(id);
} catch {
// User cancelled
}
},
[confirmDeleteLocal, deleteLocalBackend]
);
// Handle create local backend
const handleCreateLocalBackend = useCallback(async () => {
if (newLocalBackendName.trim()) {
await createLocalBackend(newLocalBackendName.trim());
setNewLocalBackendName('');
setIsAddLocalVisible(false);
}
}, [newLocalBackendName, createLocalBackend]);
// Handle delete backend // Handle delete backend
const handleDeleteBackend = useCallback( const handleDeleteBackend = useCallback(
async (id: string) => { async (id: string) => {
@@ -124,6 +159,7 @@ export function BackendServicesPanel() {
return ( return (
<BasePanel title="Backend Services" hasActivityBlocker={hasActivity} hasContentScroll> <BasePanel title="Backend Services" hasActivityBlocker={hasActivity} hasContentScroll>
<DeleteDialog /> <DeleteDialog />
<DeleteLocalDialog />
{isLoading || isLoadingLocal ? ( {isLoading || isLoadingLocal ? (
<Container hasLeftSpacing hasTopSpacing> <Container hasLeftSpacing hasTopSpacing>
@@ -136,19 +172,59 @@ export function BackendServicesPanel() {
title="Local Backends" title="Local Backends"
variant={SectionVariant.Panel} variant={SectionVariant.Panel}
actions={ actions={
!isAddLocalVisible && (
<IconButton <IconButton
icon={IconName.Plus} icon={IconName.Plus}
size={IconSize.Small} size={IconSize.Small}
onClick={async () => { onClick={() => setIsAddLocalVisible(true)}
const name = prompt('Enter backend name:');
if (name) {
await createLocalBackend(name);
}
}}
testId="add-local-backend-button" testId="add-local-backend-button"
/> />
)
} }
> >
{/* Add Local Backend Form */}
{isAddLocalVisible && (
<Container hasLeftSpacing hasRightSpacing hasTopSpacing hasBottomSpacing>
<VStack>
<Text textType={TextType.DefaultContrast}>Create Local Backend</Text>
<Box hasTopSpacing>
<TextInput
value={newLocalBackendName}
onChange={(e) => setNewLocalBackendName(e.target.value)}
placeholder="Backend name"
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateLocalBackend();
if (e.key === 'Escape') {
setIsAddLocalVisible(false);
setNewLocalBackendName('');
}
}}
/>
</Box>
<Box hasTopSpacing>
<HStack hasSpacing>
<PrimaryButton
label="Create"
size={PrimaryButtonSize.Small}
variant={PrimaryButtonVariant.Muted}
onClick={handleCreateLocalBackend}
isDisabled={!newLocalBackendName.trim()}
/>
<PrimaryButton
label="Cancel"
size={PrimaryButtonSize.Small}
variant={PrimaryButtonVariant.Muted}
onClick={() => {
setIsAddLocalVisible(false);
setNewLocalBackendName('');
}}
/>
</HStack>
</Box>
</VStack>
</Container>
)}
{localBackends.length > 0 ? ( {localBackends.length > 0 ? (
<VStack> <VStack>
{localBackends.map((backend) => ( {localBackends.map((backend) => (
@@ -157,15 +233,12 @@ export function BackendServicesPanel() {
backend={backend} backend={backend}
onStart={async () => startLocalBackend(backend.id)} onStart={async () => startLocalBackend(backend.id)}
onStop={async () => stopLocalBackend(backend.id)} onStop={async () => stopLocalBackend(backend.id)}
onDelete={() => { onDelete={() => handleDeleteLocalBackend(backend.id)}
if (confirm('Delete this local backend? All data will be lost.')) {
deleteLocalBackend(backend.id);
}
}}
/> />
))} ))}
</VStack> </VStack>
) : ( ) : (
!isAddLocalVisible && (
<Container hasLeftSpacing hasTopSpacing hasBottomSpacing> <Container hasLeftSpacing hasTopSpacing hasBottomSpacing>
<Text>No local backends</Text> <Text>No local backends</Text>
<Box hasTopSpacing> <Box hasTopSpacing>
@@ -174,6 +247,7 @@ export function BackendServicesPanel() {
</Text> </Text>
</Box> </Box>
</Container> </Container>
)
)} )}
</Section> </Section>