mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
This PR bumps `shadcn` and regenerates all blocks with the latest CLI. The blocks have no meaningful change (only a json property reorder). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated internal development tooling used by the UI library and Vue blocks. * **Tests** * Improved type consistency in registry-related test utilities, supporting more reliable validation without changing user-facing behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
43 lines
14 KiB
JSON
43 lines
14 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "realtime-flow-react-router",
|
|
"title": "Realtime Flow",
|
|
"description": "Real-time flow diagram editor for collaborative applications.",
|
|
"dependencies": [
|
|
"@xyflow/react@latest",
|
|
"@supabase-labs/y-supabase@latest",
|
|
"y-protocols@latest",
|
|
"yjs@latest",
|
|
"@supabase/ssr@latest",
|
|
"@supabase/supabase-js@latest"
|
|
],
|
|
"registryDependencies": [],
|
|
"files": [
|
|
{
|
|
"path": "registry/default/blocks/realtime-flow/hooks/use-realtime-flow.ts",
|
|
"content": "'use client'\n\nimport { type SupabasePersistenceOptions, SupabaseProvider } from '@supabase-labs/y-supabase'\nimport {\n type Edge,\n type Node,\n type NodeChange,\n type EdgeChange,\n type Connection,\n applyNodeChanges,\n applyEdgeChanges,\n addEdge,\n} from '@xyflow/react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Awareness } from 'y-protocols/awareness'\nimport * as Y from 'yjs'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\n\ntype UseRealtimeFlowOptions = {\n channel: string\n persistence?: boolean | SupabasePersistenceOptions\n awareness?: boolean | Awareness\n initialNodes?: Node[]\n initialEdges?: Edge[]\n}\n\ntype SetStateAction<T> = T | ((prev: T) => T)\n\nconst INITIAL_SYNC_MS = 350\n\nexport function useRealtimeFlow({\n channel,\n persistence,\n awareness = true,\n initialNodes = [],\n initialEdges = [],\n}: UseRealtimeFlowOptions) {\n const [nodes, setNodesState] = useState<Node[]>([])\n const [edges, setEdgesState] = useState<Edge[]>([])\n const [synced, setSynced] = useState(false)\n const [syncError, setSyncError] = useState<string | null>(null)\n\n const docRef = useRef<Y.Doc | null>(null)\n const providerRef = useRef<SupabaseProvider | null>(null)\n const syncedRef = useRef(false)\n const initialNodesRef = useRef(initialNodes)\n const initialEdgesRef = useRef(initialEdges)\n\n useEffect(() => {\n const doc = new Y.Doc()\n const yNodes = doc.getMap<string>('nodes')\n const yEdges = doc.getMap<string>('edges')\n const initialNodesForChannel = initialNodesRef.current\n const initialEdgesForChannel = initialEdgesRef.current\n let initialSyncTimer: ReturnType<typeof setTimeout> | null = null\n let hasRemoteActivity = false\n\n const supabase = createClient()\n const provider = new SupabaseProvider(channel, doc, supabase as any, {\n awareness,\n persistence,\n })\n\n // Sync Y.Map state to React state\n const syncNodesFromYjs = () => {\n const parsed: Node[] = []\n yNodes.forEach((value) => {\n try {\n parsed.push(JSON.parse(value))\n } catch {}\n })\n setNodesState(parsed)\n }\n\n const syncEdgesFromYjs = () => {\n const parsed: Edge[] = []\n yEdges.forEach((value) => {\n try {\n parsed.push(JSON.parse(value))\n } catch {}\n })\n setEdgesState(parsed)\n }\n\n const clearInitialSyncTimer = () => {\n if (initialSyncTimer) {\n clearTimeout(initialSyncTimer)\n initialSyncTimer = null\n }\n }\n\n const clearSyncError = () => {\n setSyncError(null)\n }\n\n const seedInitialState = () => {\n if (yNodes.size === 0 && initialNodesForChannel.length > 0) {\n doc.transact(() => {\n for (const node of initialNodesForChannel) {\n yNodes.set(node.id, JSON.stringify(node))\n }\n }, 'local')\n }\n\n if (yEdges.size === 0 && initialEdgesForChannel.length > 0) {\n doc.transact(() => {\n for (const edge of initialEdgesForChannel) {\n yEdges.set(edge.id, JSON.stringify(edge))\n }\n }, 'local')\n }\n }\n\n const nodesObserver = (event: Y.YMapEvent<string>) => {\n if (event.transaction.origin === 'local') return\n hasRemoteActivity = true\n syncNodesFromYjs()\n }\n\n const edgesObserver = (event: Y.YMapEvent<string>) => {\n if (event.transaction.origin === 'local') return\n hasRemoteActivity = true\n syncEdgesFromYjs()\n }\n\n yNodes.observe(nodesObserver)\n yEdges.observe(edgesObserver)\n\n const markSynced = (shouldSeed = false) => {\n if (syncedRef.current) return\n clearInitialSyncTimer()\n clearSyncError()\n syncedRef.current = true\n\n if (shouldSeed && !hasRemoteActivity) {\n seedInitialState()\n }\n\n // Sync React state from Yjs (includes seeded initial data if applied)\n syncNodesFromYjs()\n syncEdgesFromYjs()\n setSynced(true)\n }\n\n const markSyncError = (error: Error) => {\n clearInitialSyncTimer()\n if (syncedRef.current) return\n setSyncError(error.message || 'Failed to sync flow')\n }\n\n const persistenceInstance = provider.getPersistence()\n if (persistenceInstance) {\n // With persistence: wait for persisted state to load\n if (persistenceInstance.synced) {\n markSynced()\n } else {\n persistenceInstance.on('synced', () => {\n markSynced()\n })\n persistenceInstance.on('error', (error) => {\n markSyncError(error)\n })\n }\n } else {\n provider.on('connect', () => {\n clearSyncError()\n clearInitialSyncTimer()\n initialSyncTimer = setTimeout(() => {\n markSynced(true)\n }, INITIAL_SYNC_MS)\n })\n provider.on('message', () => {\n hasRemoteActivity = true\n markSynced()\n })\n provider.on('error', (error) => {\n markSyncError(error)\n })\n }\n\n docRef.current = doc\n providerRef.current = provider\n\n return () => {\n clearInitialSyncTimer()\n yNodes.unobserve(nodesObserver)\n yEdges.unobserve(edgesObserver)\n provider.destroy()\n doc.destroy()\n docRef.current = null\n providerRef.current = null\n syncedRef.current = false\n setSynced(false)\n setSyncError(null)\n }\n }, [channel, awareness, persistence])\n\n const syncNodesToYjs = useCallback((updated: Node[]) => {\n const doc = docRef.current\n if (!doc || !syncedRef.current) return\n\n const yNodes = doc.getMap<string>('nodes')\n doc.transact(() => {\n const newIds = new Set(updated.map((n) => n.id))\n for (const key of Array.from(yNodes.keys())) {\n if (!newIds.has(key)) yNodes.delete(key)\n }\n for (const node of updated) {\n yNodes.set(node.id, JSON.stringify(node))\n }\n }, 'local')\n }, [])\n\n const syncEdgesToYjs = useCallback((updated: Edge[]) => {\n const doc = docRef.current\n if (!doc || !syncedRef.current) return\n\n const yEdges = doc.getMap<string>('edges')\n doc.transact(() => {\n const newIds = new Set(updated.map((e) => e.id))\n for (const key of Array.from(yEdges.keys())) {\n if (!newIds.has(key)) yEdges.delete(key)\n }\n for (const edge of updated) {\n yEdges.set(edge.id, JSON.stringify(edge))\n }\n }, 'local')\n }, [])\n\n const setNodes = useCallback(\n (nodesOrUpdater: SetStateAction<Node[]>) => {\n setNodesState((current) => {\n const updated =\n typeof nodesOrUpdater === 'function' ? nodesOrUpdater(current) : nodesOrUpdater\n syncNodesToYjs(updated)\n return updated\n })\n },\n [syncNodesToYjs]\n )\n\n const setEdges = useCallback(\n (edgesOrUpdater: SetStateAction<Edge[]>) => {\n setEdgesState((current) => {\n const updated =\n typeof edgesOrUpdater === 'function' ? edgesOrUpdater(current) : edgesOrUpdater\n syncEdgesToYjs(updated)\n return updated\n })\n },\n [syncEdgesToYjs]\n )\n\n const onNodesChange = useCallback((changes: NodeChange[]) => {\n setNodesState((currentNodes) => {\n const updated = applyNodeChanges(changes, currentNodes)\n\n const doc = docRef.current\n if (!doc || !syncedRef.current) return updated\n\n const yNodes = doc.getMap<string>('nodes')\n\n doc.transact(() => {\n for (const change of changes) {\n if (change.type === 'remove') {\n yNodes.delete(change.id)\n } else if (change.type === 'add') {\n yNodes.set(change.item.id, JSON.stringify(change.item))\n } else {\n const node = updated.find((n) => n.id === change.id)\n if (node) {\n yNodes.set(node.id, JSON.stringify(node))\n }\n }\n }\n }, 'local')\n\n return updated\n })\n }, [])\n\n const onEdgesChange = useCallback((changes: EdgeChange[]) => {\n setEdgesState((currentEdges) => {\n const updated = applyEdgeChanges(changes, currentEdges)\n\n const doc = docRef.current\n if (!doc || !syncedRef.current) return updated\n\n const yEdges = doc.getMap<string>('edges')\n\n doc.transact(() => {\n for (const change of changes) {\n if (change.type === 'remove') {\n yEdges.delete(change.id)\n } else if (change.type === 'add') {\n yEdges.set(change.item.id, JSON.stringify(change.item))\n } else {\n const edge = updated.find((e) => e.id === change.id)\n if (edge) {\n yEdges.set(edge.id, JSON.stringify(edge))\n }\n }\n }\n }, 'local')\n\n return updated\n })\n }, [])\n\n const onConnect = useCallback((connection: Connection) => {\n setEdgesState((currentEdges) => {\n const updated = addEdge(connection, currentEdges)\n\n const doc = docRef.current\n if (!doc || !syncedRef.current) return updated\n\n const yEdges = doc.getMap<string>('edges')\n const newEdge = updated.find((e) => !currentEdges.some((ce) => ce.id === e.id))\n\n if (newEdge) {\n doc.transact(() => {\n yEdges.set(newEdge.id, JSON.stringify(newEdge))\n }, 'local')\n }\n\n return updated\n })\n }, [])\n\n return {\n nodes,\n edges,\n synced,\n syncError,\n onNodesChange,\n onEdgesChange,\n onConnect,\n setNodes,\n setEdges,\n }\n}\n",
|
|
"type": "registry:hook"
|
|
},
|
|
{
|
|
"path": "registry/default/blocks/realtime-flow/components/realtime-flow.tsx",
|
|
"content": "'use client'\n\nimport {\n ReactFlow,\n ReactFlowProvider,\n Background,\n Controls,\n type Node,\n type Edge,\n type NodeTypes,\n type EdgeTypes,\n} from '@xyflow/react'\nimport '@xyflow/react/dist/style.css'\nimport type { SupabasePersistenceOptions } from '@supabase-labs/y-supabase'\n\nimport { RealtimeFlowOverlay } from './realtime-flow-overlay'\nimport { useRealtimeFlow } from '../hooks/use-realtime-flow'\n\ntype RealtimeFlowProps = {\n channel: string\n className?: string\n style?: React.CSSProperties\n persistence?: boolean | SupabasePersistenceOptions\n initialNodes?: Node[]\n initialEdges?: Edge[]\n nodeTypes?: NodeTypes\n edgeTypes?: EdgeTypes\n height?: string | number\n}\n\nconst DEFAULT_HEIGHT = 550\n\nconst RealtimeFlowContent = ({\n channel,\n className,\n style,\n persistence,\n initialNodes,\n initialEdges,\n nodeTypes,\n edgeTypes,\n height = DEFAULT_HEIGHT,\n}: RealtimeFlowProps) => {\n const { nodes, edges, synced, syncError, onNodesChange, onEdgesChange, onConnect } =\n useRealtimeFlow({\n channel,\n persistence,\n initialNodes,\n initialEdges,\n })\n\n return (\n <div style={{ height, position: 'relative', ...style }} className={className}>\n <ReactFlow\n nodes={synced ? nodes : []}\n edges={synced ? edges : []}\n onNodesChange={synced ? onNodesChange : undefined}\n onEdgesChange={synced ? onEdgesChange : undefined}\n onConnect={synced ? onConnect : undefined}\n nodeTypes={nodeTypes}\n edgeTypes={edgeTypes}\n fitView\n >\n <Background />\n <Controls />\n </ReactFlow>\n {!synced && !syncError && <RealtimeFlowOverlay status=\"syncing\" />}\n {!synced && syncError && <RealtimeFlowOverlay status=\"error\" message={syncError} />}\n </div>\n )\n}\n\nconst RealtimeFlow = (props: RealtimeFlowProps) => (\n <ReactFlowProvider>\n <RealtimeFlowContent {...props} />\n </ReactFlowProvider>\n)\n\nexport { RealtimeFlow }\n",
|
|
"type": "registry:component"
|
|
},
|
|
{
|
|
"path": "registry/default/clients/react-router/lib/supabase/client.ts",
|
|
"content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n",
|
|
"type": "registry:lib"
|
|
},
|
|
{
|
|
"path": "registry/default/clients/react-router/lib/supabase/server.ts",
|
|
"content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n",
|
|
"type": "registry:lib"
|
|
}
|
|
],
|
|
"envVars": {
|
|
"VITE_SUPABASE_URL": "",
|
|
"VITE_SUPABASE_PUBLISHABLE_KEY": ""
|
|
},
|
|
"docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.",
|
|
"type": "registry:component"
|
|
} |