Compare commits
4 Commits
e91886a1ce
...
d072b71eec
| Author | SHA1 | Date | |
|---|---|---|---|
| d072b71eec | |||
| 63c955c848 | |||
| a4739bddc1 | |||
| 57d5405c41 |
132
app/api/nodes/[id]/route.ts
Normal file
132
app/api/nodes/[id]/route.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { Agent } from '@atproto/api';
|
||||||
|
import { connectToDB } from '@/lib/db';
|
||||||
|
import { verifySurrealJwt } from '@/lib/auth/jwt';
|
||||||
|
import { getOAuthClient } from '@/lib/auth/oauth-client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/nodes/[id]
|
||||||
|
*
|
||||||
|
* Deletes a node from both ATproto (source of truth) and SurrealDB (cache).
|
||||||
|
*
|
||||||
|
* Process:
|
||||||
|
* 1. Verify user authentication and ownership
|
||||||
|
* 2. Fetch node from SurrealDB to get atp_uri
|
||||||
|
* 3. Delete post(s) from ATproto/Bluesky
|
||||||
|
* 4. Delete node from SurrealDB cache
|
||||||
|
*
|
||||||
|
* Note: ATproto is the source of truth. If ATproto deletion fails, we don't delete from cache.
|
||||||
|
*/
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
|
||||||
|
|
||||||
|
console.log('[DELETE /api/nodes/[id]] Auth check:', {
|
||||||
|
hasSurrealJwt: !!surrealJwt,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!surrealJwt) {
|
||||||
|
console.error('[DELETE /api/nodes/[id]] Missing auth cookie');
|
||||||
|
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the JWT and extract user info
|
||||||
|
const userSession = verifySurrealJwt(surrealJwt);
|
||||||
|
if (!userSession) {
|
||||||
|
console.error('[DELETE /api/nodes/[id]] Invalid JWT');
|
||||||
|
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { did: userDid } = userSession;
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
console.log('[DELETE /api/nodes/[id]] Deleting node:', { nodeId: id, userDid });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Fetch the node from SurrealDB to verify ownership and get atp_uri
|
||||||
|
const db = await connectToDB();
|
||||||
|
|
||||||
|
const nodeResult = await db.query<[Array<{ id: string; user_did: string; atp_uri: string }>]>(
|
||||||
|
'SELECT id, user_did, atp_uri FROM node WHERE id = $nodeId',
|
||||||
|
{ nodeId: id }
|
||||||
|
);
|
||||||
|
|
||||||
|
const node = nodeResult[0]?.[0];
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
console.error('[DELETE /api/nodes/[id]] Node not found:', id);
|
||||||
|
return NextResponse.json({ error: 'Node not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Verify ownership
|
||||||
|
if (node.user_did !== userDid) {
|
||||||
|
console.error('[DELETE /api/nodes/[id]] Unauthorized: user does not own node');
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'You do not have permission to delete this node' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Delete from ATproto (source of truth)
|
||||||
|
try {
|
||||||
|
const client = await getOAuthClient();
|
||||||
|
console.log('[DELETE /api/nodes/[id]] Got OAuth client, restoring session for DID:', userDid);
|
||||||
|
|
||||||
|
const session = await client.restore(userDid);
|
||||||
|
const agent = new Agent(session);
|
||||||
|
|
||||||
|
console.log('[DELETE /api/nodes/[id]] Successfully restored OAuth session');
|
||||||
|
|
||||||
|
// Parse the atp_uri to get repo and rkey
|
||||||
|
// Format: at://did:plc:xxx/app.bsky.feed.post/xxxxx
|
||||||
|
const atUriMatch = node.atp_uri.match(/at:\/\/([^/]+)\/([^/]+)\/(.+)/);
|
||||||
|
if (!atUriMatch) {
|
||||||
|
throw new Error(`Invalid atp_uri format: ${node.atp_uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, repo, collection, rkey] = atUriMatch;
|
||||||
|
|
||||||
|
console.log('[DELETE /api/nodes/[id]] Deleting ATproto record:', {
|
||||||
|
repo,
|
||||||
|
collection,
|
||||||
|
rkey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete the post from ATproto
|
||||||
|
await agent.api.com.atproto.repo.deleteRecord({
|
||||||
|
repo,
|
||||||
|
collection,
|
||||||
|
rkey,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DELETE /api/nodes/[id]] ✓ Deleted post from ATproto');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DELETE /api/nodes/[id]] ATproto deletion error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to delete post from Bluesky. Node not deleted.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Delete from SurrealDB cache (only after successful ATproto deletion)
|
||||||
|
try {
|
||||||
|
await db.delete(id);
|
||||||
|
console.log('[DELETE /api/nodes/[id]] ✓ Deleted node from SurrealDB cache');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[DELETE /api/nodes/[id]] ⚠ SurrealDB cache deletion failed (non-critical):', error);
|
||||||
|
// This is non-critical since ATproto (source of truth) was successfully deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, nodeId: id });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DELETE /api/nodes/[id]] Error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to delete node' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/api/nodes/debug/route.ts
Normal file
66
app/api/nodes/debug/route.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { connectToDB } from '@/lib/db';
|
||||||
|
import { verifySurrealJwt } from '@/lib/auth/jwt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/nodes/debug
|
||||||
|
*
|
||||||
|
* Debug endpoint to list all nodes for the current user in SurrealDB.
|
||||||
|
* Only available in development mode.
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
// Only allow in development
|
||||||
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
|
return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
|
||||||
|
|
||||||
|
console.log('[DEBUG /api/nodes/debug] Auth check:', {
|
||||||
|
hasSurrealJwt: !!surrealJwt,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!surrealJwt) {
|
||||||
|
console.error('[DEBUG /api/nodes/debug] Missing auth cookie');
|
||||||
|
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the JWT and extract user info
|
||||||
|
const userSession = verifySurrealJwt(surrealJwt);
|
||||||
|
if (!userSession) {
|
||||||
|
console.error('[DEBUG /api/nodes/debug] Invalid JWT');
|
||||||
|
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { did: userDid } = userSession;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await connectToDB();
|
||||||
|
|
||||||
|
// Fetch ALL nodes for this user (no filters)
|
||||||
|
const nodesResult = await db.query<
|
||||||
|
[Array<{ id: string; title: string; body: string; user_did: string; atp_uri: string }>]
|
||||||
|
>('SELECT id, title, body, user_did, atp_uri FROM node WHERE user_did = $userDid', {
|
||||||
|
userDid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes = nodesResult[0] || [];
|
||||||
|
|
||||||
|
console.log('[DEBUG /api/nodes/debug] Found nodes:', {
|
||||||
|
count: nodes.length,
|
||||||
|
userDid,
|
||||||
|
nodeIds: nodes.map((n) => n.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
nodes,
|
||||||
|
userDid,
|
||||||
|
count: nodes.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DEBUG /api/nodes/debug] Error:', error);
|
||||||
|
return NextResponse.json({ error: 'Failed to fetch nodes' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ import { IconMessageCircle, IconEdit, IconChartBubbleFilled } from '@tabler/icon
|
|||||||
import { useSelector } from '@xstate/react';
|
import { useSelector } from '@xstate/react';
|
||||||
import { useAppMachine } from '@/hooks/useAppMachine';
|
import { useAppMachine } from '@/hooks/useAppMachine';
|
||||||
import { UserMenu } from '@/components/UserMenu';
|
import { UserMenu } from '@/components/UserMenu';
|
||||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
|
||||||
import styles from './DesktopSidebar.module.css';
|
import styles from './DesktopSidebar.module.css';
|
||||||
|
|
||||||
export function DesktopSidebar() {
|
export function DesktopSidebar() {
|
||||||
@@ -106,10 +105,7 @@ export function DesktopSidebar() {
|
|||||||
|
|
||||||
<Divider my="md" />
|
<Divider my="md" />
|
||||||
|
|
||||||
{/* Theme Toggle */}
|
{/* User Menu - styled like other nav items, now includes theme toggle */}
|
||||||
<ThemeToggle />
|
|
||||||
|
|
||||||
{/* User Menu - styled like other nav items */}
|
|
||||||
<UserMenu showLabel={true} />
|
<UserMenu showLabel={true} />
|
||||||
|
|
||||||
{/* Development state panel */}
|
{/* Development state panel */}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@react-three/drei';
|
} from '@react-three/drei';
|
||||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||||
import { Stack, Text as MantineText, Paper, Title, Box, CloseButton, Group, Anchor, useComputedColorScheme } from '@mantine/core';
|
import { Stack, Text as MantineText, Paper, Title, Box, CloseButton, Group, Anchor, useComputedColorScheme, Button, Modal } from '@mantine/core';
|
||||||
|
import { IconTrash } from '@tabler/icons-react';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
@@ -96,6 +98,9 @@ export function ThoughtGalaxy() {
|
|||||||
const [links, setLinks] = useState<LinkData[]>([]);
|
const [links, setLinks] = useState<LinkData[]>([]);
|
||||||
const [selectedNode, setSelectedNode] = useState<NodeData | null>(null);
|
const [selectedNode, setSelectedNode] = useState<NodeData | null>(null);
|
||||||
const [emptyMessage, setEmptyMessage] = useState<string | null>(null);
|
const [emptyMessage, setEmptyMessage] = useState<string | null>(null);
|
||||||
|
const [currentUserDid, setCurrentUserDid] = useState<string | null>(null);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||||
const cameraControlsRef = useRef<CameraControls>(null);
|
const cameraControlsRef = useRef<CameraControls>(null);
|
||||||
const hasFitCamera = useRef(false);
|
const hasFitCamera = useRef(false);
|
||||||
const hasFocusedNode = useRef<string | null>(null);
|
const hasFocusedNode = useRef<string | null>(null);
|
||||||
@@ -104,6 +109,28 @@ export function ThoughtGalaxy() {
|
|||||||
const selectedNodeId = searchParams.get('node');
|
const selectedNodeId = searchParams.get('node');
|
||||||
const targetUserDid = searchParams.get('user'); // For viewing someone else's galaxy
|
const targetUserDid = searchParams.get('user'); // For viewing someone else's galaxy
|
||||||
|
|
||||||
|
// Fetch current user's profile to get their DID
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchCurrentUser() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user/profile', {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setCurrentUserDid(data.did);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ThoughtGalaxy] Error fetching current user:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only fetch current user if we're viewing our own galaxy
|
||||||
|
if (!targetUserDid) {
|
||||||
|
fetchCurrentUser();
|
||||||
|
}
|
||||||
|
}, [targetUserDid]);
|
||||||
|
|
||||||
// Fetch data from API on mount and poll for updates
|
// Fetch data from API on mount and poll for updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
@@ -290,6 +317,51 @@ export function ThoughtGalaxy() {
|
|||||||
router.replace(`${pathname}${newSearch ? `?${newSearch}` : ''}`, { scroll: false });
|
router.replace(`${pathname}${newSearch ? `?${newSearch}` : ''}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle deleting a node
|
||||||
|
const handleDeleteNode = async () => {
|
||||||
|
if (!selectedNode) return;
|
||||||
|
|
||||||
|
setIsDeleting(true);
|
||||||
|
setDeleteConfirmOpen(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract clean ID from SurrealDB RecordId format (removes angle brackets ⟨⟩)
|
||||||
|
const cleanId = String(selectedNode.id).replace(/[⟨⟩]/g, '');
|
||||||
|
|
||||||
|
const response = await fetch(`/api/nodes/${cleanId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'Failed to delete node');
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
title: 'Node deleted',
|
||||||
|
message: 'Node has been deleted from Bluesky and your galaxy',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove the node from local state
|
||||||
|
setNodes((prevNodes) => prevNodes.filter((n) => n.id !== selectedNode.id));
|
||||||
|
setLinks((prevLinks) => prevLinks.filter((l) => l.in !== selectedNode.id && l.out !== selectedNode.id));
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
handleCloseModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ThoughtGalaxy] Delete error:', error);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Delete failed',
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to delete node',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
console.log('[ThoughtGalaxy] Rendering with', nodes.length, 'nodes and', linkLines.length, 'link lines');
|
console.log('[ThoughtGalaxy] Rendering with', nodes.length, 'nodes and', linkLines.length, 'link lines');
|
||||||
|
|
||||||
// Show message if no nodes are ready yet
|
// Show message if no nodes are ready yet
|
||||||
@@ -360,6 +432,7 @@ export function ThoughtGalaxy() {
|
|||||||
<Title order={2} style={{ margin: 0, marginBottom: '0.25rem' }}>
|
<Title order={2} style={{ margin: 0, marginBottom: '0.25rem' }}>
|
||||||
{selectedNode.title}
|
{selectedNode.title}
|
||||||
</Title>
|
</Title>
|
||||||
|
<Group gap="sm" mt="xs">
|
||||||
<Anchor
|
<Anchor
|
||||||
href={`https://bsky.app/profile/${selectedNode.user_did}/post/${selectedNode.atp_uri.split('/').pop()}`}
|
href={`https://bsky.app/profile/${selectedNode.user_did}/post/${selectedNode.atp_uri.split('/').pop()}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -369,6 +442,20 @@ export function ThoughtGalaxy() {
|
|||||||
>
|
>
|
||||||
View on Bluesky
|
View on Bluesky
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
{/* Show delete button only for user's own nodes */}
|
||||||
|
{currentUserDid && selectedNode.user_did === currentUserDid && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
leftSection={<IconTrash size={14} />}
|
||||||
|
onClick={() => setDeleteConfirmOpen(true)}
|
||||||
|
loading={isDeleting}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
<CloseButton
|
<CloseButton
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -394,6 +481,43 @@ export function ThoughtGalaxy() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete confirmation modal */}
|
||||||
|
<Modal
|
||||||
|
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>
|
||||||
|
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{ position: [0, 5, 10], fov: 60 }}
|
camera={{ position: [0, 5, 10], fov: 60 }}
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Menu, Avatar, NavLink, ActionIcon } from '@mantine/core';
|
import { Menu, Avatar, NavLink, ActionIcon, SegmentedControl, Text, Stack, ScrollArea, Code } from '@mantine/core';
|
||||||
|
import { useMantineColorScheme } from '@mantine/core';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
@@ -11,10 +14,20 @@ interface UserProfile {
|
|||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
user_did: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
|
export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [nodes, setNodes] = useState<Node[]>([]);
|
||||||
|
const [nodesLoading, setNodesLoading] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch user profile on mount
|
// Fetch user profile on mount
|
||||||
@@ -33,6 +46,62 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Fetch user's nodes for debugging
|
||||||
|
const fetchNodes = async () => {
|
||||||
|
setNodesLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/nodes/debug', {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.error) {
|
||||||
|
setNodes(data.nodes || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch nodes:', error);
|
||||||
|
} finally {
|
||||||
|
setNodesLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete a node (debug) - Matches ThoughtGalaxy delete pattern
|
||||||
|
const handleDebugDelete = async (nodeId: string) => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract clean ID from SurrealDB RecordId format (removes angle brackets ⟨⟩)
|
||||||
|
const cleanId = String(nodeId).replace(/[⟨⟩]/g, '');
|
||||||
|
|
||||||
|
const response = await fetch(`/api/nodes/${cleanId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'Failed to delete node');
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
title: 'Node deleted',
|
||||||
|
message: 'Node has been deleted from Bluesky and your galaxy',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local state to remove the deleted node
|
||||||
|
setNodes((prevNodes) => prevNodes.filter((n) => n.id !== nodeId));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserMenu Debug] Delete error:', error);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Delete failed',
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to delete node',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/auth/logout', { method: 'POST' });
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
@@ -129,6 +198,109 @@ export function UserMenu({ showLabel = false }: { showLabel?: boolean } = {}) {
|
|||||||
@{profile.handle}
|
@{profile.handle}
|
||||||
</span>
|
</span>
|
||||||
</Menu.Label>
|
</Menu.Label>
|
||||||
|
<Menu.Divider />
|
||||||
|
|
||||||
|
{/* Theme Selection */}
|
||||||
|
<div style={{ padding: '8px 12px' }}>
|
||||||
|
<Text size="xs" fw={500} c="dimmed" mb={8}>
|
||||||
|
Theme
|
||||||
|
</Text>
|
||||||
|
<SegmentedControl
|
||||||
|
value={colorScheme}
|
||||||
|
onChange={(value) => setColorScheme(value as 'light' | 'dark' | 'auto')}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
value: 'light',
|
||||||
|
label: (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<IconSun size={16} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'dark',
|
||||||
|
label: (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<IconMoon size={16} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'auto',
|
||||||
|
label: (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<IconDeviceDesktop size={16} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
fullWidth
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Debug: Show all nodes */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Label>Debug: SurrealDB Nodes</Menu.Label>
|
||||||
|
<div style={{ padding: '8px 12px' }}>
|
||||||
|
<Text size="xs" fw={500} c="dimmed" mb={8}>
|
||||||
|
<button
|
||||||
|
onClick={fetchNodes}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nodesLoading ? 'Loading...' : `Fetch Nodes (${nodes.length})`}
|
||||||
|
</button>
|
||||||
|
</Text>
|
||||||
|
{nodes.length > 0 && (
|
||||||
|
<ScrollArea h={200}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<div key={node.id} style={{ fontSize: '0.7rem', marginBottom: '8px' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
|
||||||
|
<Text size="xs" fw={600}>{node.title}</Text>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Delete "${node.title}"?`)) {
|
||||||
|
handleDebugDelete(node.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
background: '#fa5252',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '2px 6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.65rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Code block style={{ fontSize: '0.65rem' }}>{node.id}</Code>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
{nodes.length === 0 && !nodesLoading && (
|
||||||
|
<Text size="xs" c="red">
|
||||||
|
No nodes found in SurrealDB
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
<Menu.Item onClick={handleLogout} c="red">
|
<Menu.Item onClick={handleLogout} c="red">
|
||||||
Log out
|
Log out
|
||||||
|
|||||||
@@ -1,38 +1,86 @@
|
|||||||
import { test } from 'magnitude-test';
|
import { test } from 'magnitude-test';
|
||||||
|
|
||||||
test('Theme toggle switches between light and dark modes', async (agent) => {
|
const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE;
|
||||||
// Act: Navigate to the homepage
|
const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD;
|
||||||
await agent.act('Navigate to the homepage');
|
|
||||||
|
|
||||||
// Check: Verify the page loads with a theme
|
if (!TEST_HANDLE || !TEST_PASSWORD) {
|
||||||
await agent.check('The page has either a light or dark background');
|
throw new Error('TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD must be set in .env');
|
||||||
|
}
|
||||||
|
|
||||||
// Act: Click the theme toggle button
|
test('Theme selector in profile dropdown has three options', async (agent) => {
|
||||||
await agent.act('Click the theme toggle button');
|
// Act: Log in first (theme selector is in authenticated area)
|
||||||
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
// Wait for theme transition
|
// Act: Open the profile dropdown menu
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click on the profile avatar or "Profile" button');
|
||||||
|
|
||||||
// Check: Verify the theme has changed
|
// Check: Verify the theme selector is visible with three options
|
||||||
await agent.check('The background color has changed to the opposite theme');
|
await agent.check('A "Theme" label is visible in the dropdown menu');
|
||||||
|
await agent.check('A segmented control with three icon buttons is visible');
|
||||||
|
await agent.check('A sun icon button is visible (for light mode)');
|
||||||
|
await agent.check('A moon icon button is visible (for dark mode)');
|
||||||
|
await agent.check('A desktop/monitor icon button is visible (for system/auto mode)');
|
||||||
|
});
|
||||||
|
|
||||||
// Act: Click the theme toggle button again
|
test('Theme can be changed between light, dark, and auto modes', async (agent) => {
|
||||||
await agent.act('Click the theme toggle button');
|
// Act: Log in
|
||||||
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
// Wait for theme transition
|
// Act: Open profile dropdown and select light mode
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click on the profile avatar');
|
||||||
|
await agent.act('Click the sun icon in the theme selector');
|
||||||
|
|
||||||
// Check: Verify the theme has changed back
|
// Check: Verify light mode is active
|
||||||
await agent.check('The background color has changed back to the original theme');
|
await agent.check('The page has a light background color');
|
||||||
|
await agent.check('The sun icon button appears selected/highlighted');
|
||||||
|
|
||||||
|
// Act: Switch to dark mode
|
||||||
|
await agent.act('Click on the profile avatar');
|
||||||
|
await agent.act('Click the moon icon in the theme selector');
|
||||||
|
await agent.act('Wait for 500 milliseconds');
|
||||||
|
|
||||||
|
// Check: Verify dark mode is active
|
||||||
|
await agent.check('The page has a dark background color');
|
||||||
|
await agent.check('The moon icon button appears selected/highlighted');
|
||||||
|
|
||||||
|
// Act: Switch to auto/system mode
|
||||||
|
await agent.act('Click on the profile avatar');
|
||||||
|
await agent.act('Click the desktop icon in the theme selector');
|
||||||
|
await agent.act('Wait for 500 milliseconds');
|
||||||
|
|
||||||
|
// Check: Verify auto mode is selected
|
||||||
|
await agent.check('The desktop icon button appears selected/highlighted');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Light mode displays correct colors', async (agent) => {
|
test('Light mode displays correct colors', async (agent) => {
|
||||||
// Act: Navigate to the homepage
|
// Act: Log in
|
||||||
await agent.act('Navigate to the homepage');
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
// Act: Ensure we're in light mode by toggling if needed
|
// Act: Set to light mode via profile dropdown
|
||||||
await agent.act('If the background is dark, click the theme toggle button');
|
await agent.act('Click on the profile avatar');
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click the sun icon in the theme selector');
|
||||||
|
await agent.act('Wait for 500 milliseconds');
|
||||||
|
|
||||||
// Check: Verify light mode colors
|
// Check: Verify light mode colors
|
||||||
await agent.check('The page has a light background color');
|
await agent.check('The page has a light background color');
|
||||||
@@ -42,12 +90,20 @@ test('Light mode displays correct colors', async (agent) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Dark mode displays correct colors', async (agent) => {
|
test('Dark mode displays correct colors', async (agent) => {
|
||||||
// Act: Navigate to the homepage
|
// Act: Log in
|
||||||
await agent.act('Navigate to the homepage');
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
// Act: Ensure we're in dark mode by toggling if needed
|
// Act: Set to dark mode via profile dropdown
|
||||||
await agent.act('If the background is light, click the theme toggle button');
|
await agent.act('Click on the profile avatar');
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click the moon icon in the theme selector');
|
||||||
|
await agent.act('Wait for 500 milliseconds');
|
||||||
|
|
||||||
// Check: Verify dark mode colors
|
// Check: Verify dark mode colors
|
||||||
await agent.check('The page has a dark background color');
|
await agent.check('The page has a dark background color');
|
||||||
@@ -57,12 +113,20 @@ test('Dark mode displays correct colors', async (agent) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Theme persists across page refreshes', async (agent) => {
|
test('Theme persists across page refreshes', async (agent) => {
|
||||||
// Act: Navigate to the homepage
|
// Act: Log in
|
||||||
await agent.act('Navigate to the homepage');
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
// Act: Set to light mode
|
// Act: Set to light mode
|
||||||
await agent.act('If the background is dark, click the theme toggle button');
|
await agent.act('Click on the profile avatar');
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click the sun icon in the theme selector');
|
||||||
|
await agent.act('Wait for 500 milliseconds');
|
||||||
|
|
||||||
// Act: Refresh the page
|
// Act: Refresh the page
|
||||||
await agent.act('Refresh the page');
|
await agent.act('Refresh the page');
|
||||||
@@ -72,8 +136,9 @@ test('Theme persists across page refreshes', async (agent) => {
|
|||||||
await agent.check('The page still has a light background color');
|
await agent.check('The page still has a light background color');
|
||||||
|
|
||||||
// Act: Switch to dark mode
|
// Act: Switch to dark mode
|
||||||
await agent.act('Click the theme toggle button');
|
await agent.act('Click on the profile avatar');
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click the moon icon in the theme selector');
|
||||||
|
await agent.act('Wait for 500 milliseconds');
|
||||||
|
|
||||||
// Act: Refresh the page again
|
// Act: Refresh the page again
|
||||||
await agent.act('Refresh the page');
|
await agent.act('Refresh the page');
|
||||||
@@ -84,12 +149,20 @@ test('Theme persists across page refreshes', async (agent) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Theme affects all UI components', async (agent) => {
|
test('Theme affects all UI components', async (agent) => {
|
||||||
// Act: Navigate to the homepage
|
// Act: Log in
|
||||||
await agent.act('Navigate to the homepage');
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
// Act: Ensure light mode
|
// Act: Set to light mode
|
||||||
await agent.act('If the background is dark, click the theme toggle button');
|
await agent.act('Click on the profile avatar');
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click the sun icon in the theme selector');
|
||||||
|
await agent.act('Wait for 500 milliseconds');
|
||||||
|
|
||||||
// Check: Verify all components use light theme
|
// Check: Verify all components use light theme
|
||||||
await agent.check('The navigation sidebar uses light colors');
|
await agent.check('The navigation sidebar uses light colors');
|
||||||
@@ -97,8 +170,9 @@ test('Theme affects all UI components', async (agent) => {
|
|||||||
await agent.check('All buttons and inputs use light theme styling');
|
await agent.check('All buttons and inputs use light theme styling');
|
||||||
|
|
||||||
// Act: Switch to dark mode
|
// Act: Switch to dark mode
|
||||||
await agent.act('Click the theme toggle button');
|
await agent.act('Click on the profile avatar');
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click the moon icon in the theme selector');
|
||||||
|
await agent.act('Wait for 500 milliseconds');
|
||||||
|
|
||||||
// Check: Verify all components use dark theme
|
// Check: Verify all components use dark theme
|
||||||
await agent.check('The navigation sidebar uses dark colors');
|
await agent.check('The navigation sidebar uses dark colors');
|
||||||
@@ -106,21 +180,35 @@ test('Theme affects all UI components', async (agent) => {
|
|||||||
await agent.check('All buttons and inputs use dark theme styling');
|
await agent.check('All buttons and inputs use dark theme styling');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Theme toggle icon changes based on current theme', async (agent) => {
|
test('Theme selector correctly indicates selected theme', async (agent) => {
|
||||||
// Act: Navigate to the homepage
|
// Act: Log in
|
||||||
await agent.act('Navigate to the homepage');
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
// Act: Ensure light mode
|
// Act: Set to light mode
|
||||||
await agent.act('If the background is dark, click the theme toggle button');
|
await agent.act('Click on the profile avatar');
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click the sun icon in the theme selector');
|
||||||
|
|
||||||
// Check: Verify icon shows moon (indicating can switch to dark)
|
// Check: Verify sun icon is highlighted/selected
|
||||||
await agent.check('The theme toggle button shows a moon icon');
|
await agent.check('The sun icon button appears selected or highlighted');
|
||||||
|
|
||||||
// Act: Switch to dark mode
|
// Act: Switch to dark mode
|
||||||
await agent.act('Click the theme toggle button');
|
await agent.act('Click on the profile avatar');
|
||||||
await agent.act('Wait for 1 second');
|
await agent.act('Click the moon icon in the theme selector');
|
||||||
|
|
||||||
// Check: Verify icon shows sun (indicating can switch to light)
|
// Check: Verify moon icon is highlighted/selected
|
||||||
await agent.check('The theme toggle button shows a sun icon');
|
await agent.check('The moon icon button appears selected or highlighted');
|
||||||
|
|
||||||
|
// Act: Switch to auto mode
|
||||||
|
await agent.act('Click on the profile avatar');
|
||||||
|
await agent.act('Click the desktop icon in the theme selector');
|
||||||
|
|
||||||
|
// Check: Verify desktop icon is highlighted/selected
|
||||||
|
await agent.check('The desktop icon button appears selected or highlighted');
|
||||||
});
|
});
|
||||||
|
|||||||
137
tests/magnitude/03-delete-node.mag.ts
Normal file
137
tests/magnitude/03-delete-node.mag.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { test } from 'magnitude-test';
|
||||||
|
|
||||||
|
const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE;
|
||||||
|
const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD;
|
||||||
|
|
||||||
|
if (!TEST_HANDLE || !TEST_PASSWORD) {
|
||||||
|
throw new Error('TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD must be set in .env');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('User can delete their own node from galaxy view', async (agent) => {
|
||||||
|
// Act: Log in
|
||||||
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
|
// Act: Create a test node via chat
|
||||||
|
await agent.act('Type "This is a test node for deletion" into the chat input');
|
||||||
|
await agent.act('Press Enter or click send');
|
||||||
|
await agent.check('AI responds with a message');
|
||||||
|
|
||||||
|
// Act: Trigger node creation
|
||||||
|
await agent.act('Wait for the AI to suggest creating a node or manually trigger node creation');
|
||||||
|
await agent.check('A node draft is created in the editor');
|
||||||
|
|
||||||
|
// Act: Publish the node
|
||||||
|
await agent.act('Click the "Publish" button');
|
||||||
|
await agent.check('A success notification appears');
|
||||||
|
await agent.check('The node is published to Bluesky');
|
||||||
|
|
||||||
|
// Act: Navigate to Galaxy view
|
||||||
|
await agent.act('Click the "Galaxy" navigation link');
|
||||||
|
await agent.check('The galaxy visualization loads');
|
||||||
|
await agent.check('At least one node is visible in the 3D galaxy view');
|
||||||
|
|
||||||
|
// Act: Click on the newly created node
|
||||||
|
await agent.act('Click on the test node in the galaxy view');
|
||||||
|
await agent.check('A node detail panel opens showing the node title and body');
|
||||||
|
await agent.check('The node detail panel shows "This is a test node for deletion"');
|
||||||
|
|
||||||
|
// Check: Verify delete button is visible (only for user\'s own nodes)
|
||||||
|
await agent.check('A "Delete" button is visible in the node detail panel');
|
||||||
|
|
||||||
|
// Act: Click the delete button
|
||||||
|
await agent.act('Click the "Delete" button');
|
||||||
|
|
||||||
|
// Check: Verify delete confirmation modal appears
|
||||||
|
await agent.check('A delete confirmation modal appears');
|
||||||
|
await agent.check('The modal is displayed above the node detail panel');
|
||||||
|
await agent.check('The modal shows "Are you sure you want to delete this node?"');
|
||||||
|
await agent.check('The modal explains this will remove the post from Bluesky');
|
||||||
|
await agent.check('The modal has a "Delete Permanently" button');
|
||||||
|
await agent.check('The modal has a "Cancel" button');
|
||||||
|
|
||||||
|
// Act: Confirm deletion
|
||||||
|
await agent.act('Click the "Delete Permanently" button');
|
||||||
|
|
||||||
|
// Check: Verify deletion succeeded
|
||||||
|
await agent.check('A success notification appears saying "Node deleted"');
|
||||||
|
await agent.check('The node detail panel closes');
|
||||||
|
await agent.check('The node is no longer visible in the galaxy view');
|
||||||
|
|
||||||
|
// Act: Verify node is deleted from Bluesky
|
||||||
|
await agent.act('Navigate to the user\'s Bluesky profile');
|
||||||
|
await agent.check('The test node "This is a test node for deletion" is not visible on Bluesky');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete button is not shown for other users\' nodes', async (agent) => {
|
||||||
|
// This test would require viewing another user's public galaxy
|
||||||
|
// Skipping for now as it requires a second test account
|
||||||
|
await agent.act('Skip this test - requires second test account');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cancel button closes delete confirmation without deleting', async (agent) => {
|
||||||
|
// Act: Log in
|
||||||
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
|
// Act: Navigate to Galaxy view
|
||||||
|
await agent.act('Click the "Galaxy" navigation link');
|
||||||
|
await agent.check('The galaxy visualization loads');
|
||||||
|
|
||||||
|
// Act: Click on any existing node
|
||||||
|
await agent.act('Click on any node in the galaxy view');
|
||||||
|
await agent.check('A node detail panel opens');
|
||||||
|
|
||||||
|
// Act: Click the delete button
|
||||||
|
await agent.act('Click the "Delete" button');
|
||||||
|
await agent.check('A delete confirmation modal appears');
|
||||||
|
|
||||||
|
// Act: Click cancel
|
||||||
|
await agent.act('Click the "Cancel" button');
|
||||||
|
|
||||||
|
// Check: Verify modal closes and node is still there
|
||||||
|
await agent.check('The delete confirmation modal closes');
|
||||||
|
await agent.check('The node detail panel is still open');
|
||||||
|
await agent.check('The node is still visible in the galaxy view');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Node deletion removes associated links', async (agent) => {
|
||||||
|
// Act: Log in
|
||||||
|
await agent.act('Navigate to /login');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`);
|
||||||
|
await agent.act('Click the "Log in with Bluesky" button');
|
||||||
|
await agent.check('The page URL contains "bsky.social"');
|
||||||
|
await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`);
|
||||||
|
await agent.act(`Type "${TEST_PASSWORD}" into the password field`);
|
||||||
|
await agent.act('Click the submit/authorize button');
|
||||||
|
await agent.check('The page URL contains "/chat"');
|
||||||
|
|
||||||
|
// Act: Create two linked nodes
|
||||||
|
await agent.act('Create a first test node via chat');
|
||||||
|
await agent.act('Create a second test node that links to the first');
|
||||||
|
|
||||||
|
// Act: Navigate to Galaxy view
|
||||||
|
await agent.act('Click the "Galaxy" navigation link');
|
||||||
|
await agent.check('The galaxy visualization shows two nodes with a link between them');
|
||||||
|
|
||||||
|
// Act: Delete one of the nodes
|
||||||
|
await agent.act('Click on the first test node');
|
||||||
|
await agent.act('Click the "Delete" button');
|
||||||
|
await agent.act('Click "Delete Permanently"');
|
||||||
|
|
||||||
|
// Check: Verify the link is also removed
|
||||||
|
await agent.check('The link between the nodes is no longer visible');
|
||||||
|
await agent.check('Only one node remains in the galaxy');
|
||||||
|
});
|
||||||
14
todo.md
14
todo.md
@@ -6,11 +6,15 @@ Upcoming items that should be implemented (time-permitting):
|
|||||||
playwright mcp testing as well as that of magnitude
|
playwright mcp testing as well as that of magnitude
|
||||||
- ADD MAGNITUDE TESTS FOR EVERYTHING, both existing and new additions
|
- ADD MAGNITUDE TESTS FOR EVERYTHING, both existing and new additions
|
||||||
- stream the AI output to deepgram for faster synthesis
|
- stream the AI output to deepgram for faster synthesis
|
||||||
- fix the freaking galaxy node clicking -- when going directly to a node ID
|
- dark mode/light mode favicon
|
||||||
link, it redirects to /chat; when clicking on a node in /galaxy (either
|
|
||||||
general or on a specific node ID url there), it closes the modal automatically
|
|
||||||
- dark mode/light mode favicon and overall app theme
|
|
||||||
- fix the double border on desktop between sidebar and conversation actions UI
|
- fix the double border on desktop between sidebar and conversation actions UI
|
||||||
- delete "backup"/"old" page.tsx files
|
- delete "backup"/"old" page.tsx files
|
||||||
- allow ai to transition to edit in chat
|
- allow ai to transition to edit in chat
|
||||||
- why wait for three nodes before umap?
|
- fix creation/display of node links
|
||||||
|
- render markdown
|
||||||
|
- fix the "new tables being created instead of adding to the proper table"
|
||||||
|
issues we're having with the other tables like we were having with the node
|
||||||
|
table and we're now having with at least the oauth session, oauth state, and
|
||||||
|
user tabless; it's probably happening with the link_to table as well but that
|
||||||
|
one doesn't have data because it seems like link creation is broken (see task
|
||||||
|
above to fix)
|
||||||
|
|||||||
Reference in New Issue
Block a user