From 2cc27124ef151adffaf3b39d5be5451e0f4a0284 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Thu, 14 Nov 2024 14:08:28 +1000 Subject: [PATCH] add canvas --- .../pages/project/[ref]/canvas/index.tsx | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 apps/studio/pages/project/[ref]/canvas/index.tsx diff --git a/apps/studio/pages/project/[ref]/canvas/index.tsx b/apps/studio/pages/project/[ref]/canvas/index.tsx new file mode 100644 index 00000000000..3e022189297 --- /dev/null +++ b/apps/studio/pages/project/[ref]/canvas/index.tsx @@ -0,0 +1,310 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react' +import { useRouter } from 'next/router' +import { useSearchParams } from 'next/navigation' +import { useParams } from 'common' +import { motion } from 'framer-motion' +import { Button } from 'ui' +import { ProjectLayoutWithAuth } from 'components/layouts/ProjectLayout/ProjectLayout' +import { SqlCard } from 'components/ui/AIAssistantPanel/SqlSnippet' +import type { NextPageWithLayout } from 'types' +import { GripVertical } from 'lucide-react' +import { + useSnippets, + useSqlEditorV2StateSnapshot, + getSqlEditorV2StateSnapshot, +} from 'state/sql-editor-v2' +import { useSQLSnippetFoldersQuery } from 'data/content/sql-folders-query' +import { useContentIdQuery } from 'data/content/content-id-query' + +const CanvasPage: NextPageWithLayout = () => { + return +} +CanvasPage.getLayout = (page) => {page} + +export default CanvasPage + +interface DraggedItem { + id: number + x: number + y: number + width: number + height: number + props: Record +} + +interface ItemWrapperProps { + item: DraggedItem + scale: number + onPositionUpdate: (id: number, x: number, y: number) => void + onSizeUpdate: (id: number, width: number, height: number) => void +} + +const ItemWrapper: React.FC = ({ + item, + scale, + onPositionUpdate, + onSizeUpdate, +}) => { + const { ref } = useParams() + const [isDragging, setIsDragging] = useState(false) + const [size, setSize] = useState({ width: item.width, height: item.height }) + const lastPosition = useRef({ x: 0, y: 0 }) + const [position, setPosition] = useState({ x: item.x, y: item.y }) + const isItemDragging = useRef(false) + + const { data: contentData } = useContentIdQuery({ + projectRef: ref!, + id: item.id.toString(), + }) + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault() // Prevent text selection + isItemDragging.current = true + lastPosition.current = { x: e.clientX, y: e.clientY } + } + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isItemDragging.current) return + e.preventDefault() // Prevent text selection + + const deltaX = (e.clientX - lastPosition.current.x) / scale + const deltaY = (e.clientY - lastPosition.current.y) / scale + + setPosition((prev) => ({ + x: prev.x + deltaX, + y: prev.y + deltaY, + })) + + onPositionUpdate(item.id, position.x + deltaX, position.y + deltaY) + lastPosition.current = { x: e.clientX, y: e.clientY } + } + + const handleMouseUp = () => { + isItemDragging.current = false + } + + if (!contentData) return null + + let formatted = contentData.content.sql + const propsMatch = formatted.match(/--\s*props:\s*(\{[^}]+\})/) + const props = propsMatch ? JSON.parse(propsMatch[1]) : {} + const title = props.title || 'SQL Query' + formatted = formatted.replace(/--\s*props:\s*\{[^}]+\}/, '').trim() + + const sqlCardProps = { + sql: formatted, + title, + ...props, + } + + return ( +
+
+ +
+ + { + const newWidth = Math.max(200, size.width + info.delta.x / scale) + const newHeight = Math.max(100, size.height + info.delta.y / scale) + setSize({ width: newWidth, height: newHeight }) + onSizeUpdate(item.id, newWidth, newHeight) + }} + onDragStart={(e) => { + e.preventDefault() + setIsDragging(true) + }} + onDragEnd={() => setIsDragging(false)} + > + + + + +
+ ) +} + +const Canvas: React.FC = () => { + const router = useRouter() + const searchParams = useSearchParams() + const [items, setItems] = useState([]) + const debounceTimer = useRef() + const [scale, setScale] = useState(1) + const [position, setPosition] = useState({ x: 0, y: 0 }) + const lastPosition = useRef({ x: 0, y: 0 }) + const isDragging = useRef(false) + + useEffect(() => { + const queryItems = searchParams.get('items') + if (queryItems) { + try { + const parsedItems = JSON.parse(decodeURIComponent(queryItems)) + const itemsWithDefaults = parsedItems.map((item: any) => ({ + ...item, + x: item.x ?? 0, + y: item.y ?? 0, + width: item.width ?? 400, + height: item.height ?? 150, + })) + setItems(itemsWithDefaults) + } catch (error) { + console.error('Failed to parse items from URL:', error) + } + } + }, [searchParams]) + + useEffect(() => { + if (items.length === 0) return + + const queryString = encodeURIComponent( + JSON.stringify( + items.map((item) => ({ + id: item.id, + x: item.x, + y: item.y, + width: item.width, + height: item.height, + props: item.props, + })) + ) + ) + + if (debounceTimer.current) { + clearTimeout(debounceTimer.current) + } + + debounceTimer.current = setTimeout(() => { + router.replace( + { + query: { ...router.query, items: queryString }, + }, + undefined, + { shallow: true } + ) + }, 500) + + return () => { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current) + } + } + }, [items, router]) + + const handlePositionUpdate = useCallback((id: number, x: number, y: number) => { + setItems((prev) => prev.map((item) => (item.id === id ? { ...item, x, y } : item))) + }, []) + + const handleSizeUpdate = useCallback((id: number, width: number, height: number) => { + setItems((prev) => prev.map((item) => (item.id === id ? { ...item, width, height } : item))) + }, []) + + const handleMouseDown = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).closest('.border')) return + e.preventDefault() + isDragging.current = true + lastPosition.current = { x: e.clientX, y: e.clientY } + } + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging.current) return + e.preventDefault() + + const deltaX = e.clientX - lastPosition.current.x + const deltaY = e.clientY - lastPosition.current.y + + setPosition((prev) => ({ + x: prev.x + deltaX, + y: prev.y + deltaY, + })) + + lastPosition.current = { x: e.clientX, y: e.clientY } + } + + const handleMouseUp = () => { + isDragging.current = false + } + + const handleZoomIn = () => { + setScale((prev) => Math.min(prev + 0.1, 4)) + } + + const handleZoomOut = () => { + setScale((prev) => Math.max(prev - 0.1, 0.1)) + } + + return ( +
+
+ + +
+ + {items.map((item) => ( + + ))} + +
+ ) +} + +function Component() { + return ( +
+ +
+ ) +}