feat: Implement node deletion with shared modal and fix SurrealDB RecordId handling

Implements complete node deletion functionality for both galaxy view and debug panel:

**Core Changes:**
- Created shared DeleteNodeModal component used by both ThoughtGalaxy and UserMenu
- Modal provides consistent UX with proper confirmation messaging
- Deletion follows write-through cache pattern: ATproto first, then SurrealDB

**SurrealDB RecordId Fixes:**
- Fixed SELECT query to use type::thing($table, $recordId) for UUID-based RecordIds
- Fixed DELETE query to use type::thing() instead of db.delete() to handle dashes in UUIDs
- Without type::thing(), SurrealDB interprets dashes as subtraction operators

**Testing & Documentation:**
- Added comprehensive Magnitude tests for delete functionality (galaxy view and debug panel)
- Updated CLAUDE.md with complete testing workflow documentation
- Added pre-commit checklist requiring database verification and test execution
- Documented PlaywrightMCP manual testing process before Magnitude test writing

**Database Setup:**
- Configured docker-compose.yml to use environment variables for credentials
- Updated namespace/database to match .env configuration (ponderants/main)

**File Changes:**
- app/api/nodes/[id]/route.ts: Fixed RecordId query patterns (SELECT and DELETE)
- components/DeleteNodeModal.tsx: New shared modal component
- components/ThoughtGalaxy.tsx: Uses shared DeleteNodeModal
- components/UserMenu.tsx: Replaced browser confirm() with shared DeleteNodeModal
- tests/magnitude/03-delete-node.mag.ts: Added debug panel delete test
- AGENTS.md: Added testing workflow and pre-commit checklist documentation
- docker-compose.yml: Environment variable configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-10 13:25:01 +00:00
parent d072b71eec
commit a520814771
7 changed files with 282 additions and 45 deletions

View File

@@ -0,0 +1,58 @@
'use client';
import { Modal, Stack, Text, Group, Button } from '@mantine/core';
import { IconTrash } from '@tabler/icons-react';
interface DeleteNodeModalProps {
opened: boolean;
onClose: () => void;
onConfirm: () => void;
nodeTitle: string | null;
isDeleting: boolean;
}
export function DeleteNodeModal({
opened,
onClose,
onConfirm,
nodeTitle,
isDeleting,
}: DeleteNodeModalProps) {
return (
<Modal
opened={opened}
onClose={onClose}
title="Delete Node"
centered
zIndex={1001}
>
<Stack gap="md">
<Text>
Are you sure you want to delete &quot;{nodeTitle}&quot;? This will:
</Text>
<Stack gap="xs" ml="md">
<Text size="sm"> Remove the post from Bluesky</Text>
<Text size="sm"> Delete the node from your galaxy</Text>
<Text size="sm" fw={600} c="red">This action cannot be undone.</Text>
</Stack>
<Group justify="flex-end" gap="sm">
<Button
variant="subtle"
onClick={onClose}
disabled={isDeleting}
>
Cancel
</Button>
<Button
color="red"
onClick={onConfirm}
loading={isDeleting}
leftSection={<IconTrash size={16} />}
>
Delete Permanently
</Button>
</Group>
</Stack>
</Modal>
);
}

View File

@@ -7,9 +7,10 @@ import {
Text,
} from '@react-three/drei';
import { Suspense, useEffect, useRef, useState } from 'react';
import { Stack, Text as MantineText, Paper, Title, Box, CloseButton, Group, Anchor, useComputedColorScheme, Button, Modal } from '@mantine/core';
import { Stack, Text as MantineText, Paper, Title, Box, CloseButton, Group, Anchor, useComputedColorScheme, Button } from '@mantine/core';
import { IconTrash } from '@tabler/icons-react';
import { notifications } from '@mantine/notifications';
import { DeleteNodeModal } from './DeleteNodeModal';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import * as THREE from 'three';
@@ -482,41 +483,13 @@ export function ThoughtGalaxy() {
)}
{/* Delete confirmation modal */}
<Modal
<DeleteNodeModal
opened={deleteConfirmOpen}
onClose={() => setDeleteConfirmOpen(false)}
title="Delete Node"
centered
zIndex={1001}
>
<Stack gap="md">
<MantineText>
Are you sure you want to delete this node? This will:
</MantineText>
<Stack gap="xs" ml="md">
<MantineText size="sm"> Remove the post from Bluesky</MantineText>
<MantineText size="sm"> Delete the node from your galaxy</MantineText>
<MantineText size="sm" fw={600} c="red">This action cannot be undone.</MantineText>
</Stack>
<Group justify="flex-end" gap="sm">
<Button
variant="subtle"
onClick={() => setDeleteConfirmOpen(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
color="red"
onClick={handleDeleteNode}
loading={isDeleting}
leftSection={<IconTrash size={16} />}
>
Delete Permanently
</Button>
</Group>
</Stack>
</Modal>
onConfirm={handleDeleteNode}
nodeTitle={selectedNode?.title || null}
isDeleting={isDeleting}
/>
<Canvas
camera={{ position: [0, 5, 10], fov: 60 }}

View File

@@ -6,6 +6,7 @@ import { useMantineColorScheme } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { DeleteNodeModal } from './DeleteNodeModal';
interface UserProfile {
did: string;
@@ -28,6 +29,8 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
const [nodes, setNodes] = useState<Node[]>([]);
const [nodesLoading, setNodesLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [nodeToDelete, setNodeToDelete] = useState<Node | null>(null);
useEffect(() => {
// Fetch user profile on mount
@@ -65,12 +68,15 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
};
// Delete a node (debug) - Matches ThoughtGalaxy delete pattern
const handleDebugDelete = async (nodeId: string) => {
const handleDebugDelete = async () => {
if (!nodeToDelete) return;
setIsDeleting(true);
setDeleteConfirmOpen(false);
try {
// Extract clean ID from SurrealDB RecordId format (removes angle brackets ⟨⟩)
const cleanId = String(nodeId).replace(/[⟨⟩]/g, '');
const cleanId = String(nodeToDelete.id).replace(/[⟨⟩]/g, '');
const response = await fetch(`/api/nodes/${cleanId}`, {
method: 'DELETE',
@@ -89,7 +95,8 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
});
// Update local state to remove the deleted node
setNodes((prevNodes) => prevNodes.filter((n) => n.id !== nodeId));
setNodes((prevNodes) => prevNodes.filter((n) => n.id !== nodeToDelete.id));
setNodeToDelete(null);
} catch (error) {
console.error('[UserMenu Debug] Delete error:', error);
notifications.show({
@@ -269,9 +276,8 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
<Text size="xs" fw={600}>{node.title}</Text>
<button
onClick={() => {
if (confirm(`Delete "${node.title}"?`)) {
handleDebugDelete(node.id);
}
setNodeToDelete(node);
setDeleteConfirmOpen(true);
}}
style={{
background: '#fa5252',
@@ -282,6 +288,7 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
cursor: 'pointer',
fontSize: '0.65rem',
}}
disabled={isDeleting}
>
Delete
</button>
@@ -306,6 +313,15 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
Log out
</Menu.Item>
</Menu.Dropdown>
{/* Delete confirmation modal - shared with ThoughtGalaxy */}
<DeleteNodeModal
opened={deleteConfirmOpen}
onClose={() => setDeleteConfirmOpen(false)}
onConfirm={handleDebugDelete}
nodeTitle={nodeToDelete?.title || null}
isDeleting={isDeleting}
/>
</Menu>
);
}