import { Editor, HTMLContainer, Rectangle2d, ShapeUtil, TLDefaultColorTheme, getDefaultColorTheme, createShapeId } from 'tldraw' import { AllNodeShapes } from './graph-shape-types' import { AllRelationshipShapes } from './graph-relationship-types' import { getNodeComponent } from './nodeComponents'; import axios from '../../../axiosConfig'; import graphState from './graphStateUtil'; export const nodeTypeConfig = { Developer: { shapeType: 'developer_node', color: 'light-blue' }, Teacher: { shapeType: 'teacher_node', color: 'light-green' }, User: { shapeType: 'user_node', color: 'light-green' }, TeacherTimetable: { shapeType: 'teacher_timetable_node', color: 'blue' }, TimetableLesson: { shapeType: 'timetable_lesson_node', color: 'light-blue' }, PlannedLesson: { shapeType: 'planned_lesson_node', color: 'light-green' }, School: { shapeType: 'school_node', color: 'grey' }, Calendar: { shapeType: 'calendar_node', color: 'violet' }, CalendarYear: { shapeType: 'calendar_year_node', color: 'red' }, CalendarMonth: { shapeType: 'calendar_month_node', color: 'light-violet' }, CalendarWeek: { shapeType: 'calendar_week_node', color: 'light-red' }, CalendarDay: { shapeType: 'calendar_day_node', color: 'light-blue' }, CalendarTimeChunk: { shapeType: 'calendar_time_chunk_node', color: 'blue' }, ScienceLab: { shapeType: 'science_lab_node', color: 'yellow' }, KeyStageSyllabus: { shapeType: 'key_stage_syllabus_node', color: 'grey' }, YearGroupSyllabus: { shapeType: 'year_group_syllabus_node', color: 'light-blue' }, CurriculumStructure: { shapeType: 'curriculum_structure_node', color: 'grey' }, Topic: { shapeType: 'topic_node', color: 'green' }, TopicLesson: { shapeType: 'topic_lesson_node', color: 'light-green' }, LearningStatement: { shapeType: 'learning_statement_node', color: 'light-blue' }, SchoolTimetable: { shapeType: 'school_timetable_node', color: 'grey' }, AcademicYear: { shapeType: 'academic_year_node', color: 'light-violet' }, AcademicTerm: { shapeType: 'academic_term_node', color: 'yellow' }, AcademicWeek: { shapeType: 'academic_week_node', color: 'orange' }, AcademicDay: { shapeType: 'academic_day_node', color: 'light-red' }, AcademicPeriod: { shapeType: 'academic_period_node', color: 'light-green' }, RegistrationPeriod: { shapeType: 'registration_period_node', color: 'light-green' }, PastoralStructure: { shapeType: 'pastoral_structure_node', color: 'grey' }, KeyStage: { shapeType: 'key_stage_node', color: 'blue' }, Department: { shapeType: 'department_node', color: 'light-blue' }, Room: { shapeType: 'room_node', color: 'violet' }, SubjectClass: { shapeType: 'subject_class_node', color: 'light-blue' }, }; const createNodeComponent = (shape: AllNodeShapes, theme: TLDefaultColorTheme, editor: Editor) => { let isDragging = false; let startX = 0; let startY = 0; const borderColor = theme.id === 'dark' ? 'white' : 'black' const handlePointerDown = (e: React.PointerEvent) => { e.preventDefault() e.stopPropagation() const rect = e.currentTarget.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // Define button areas const openFileButtonArea = { x: 10, y: shape.props.h - 60, width: shape.props.w - 20, height: 25 } const getConnectedNodesButtonArea = { x: 10, y: shape.props.h - 30, width: shape.props.w - 20, height: 25 } if (isPointInRect(x, y, openFileButtonArea)) { console.log('Clicked on Open File button') loadTldrawFile(shape.props.path, editor) } else if (isPointInRect(x, y, getConnectedNodesButtonArea)) { console.log('Clicked on Get Connected Nodes button') handleGetConnectedNodes() } else if (isPointInShape(x, y, shape) && !isPointInRect(x, y, openFileButtonArea) && !isPointInRect(x, y, getConnectedNodesButtonArea)) { console.log('Clicked on shape') isDragging = true; startX = e.clientX - shape.x; startY = e.clientY - shape.y; } } const handlePointerMove = (e: React.PointerEvent) => { if (isDragging) { const newX = e.clientX - startX; const newY = e.clientY - startY; editor.updateShape({ id: shape.id, type: shape.type, x: newX, y: newY, }); } } const handlePointerUp = (e: React.PointerEvent) => { isDragging = false; } const isPointInShape = (x: number, y: number, shape: AllNodeShapes) => { const bounds = editor.getShapeGeometry(shape).bounds return x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height } const isPointInRect = (x: number, y: number, rect: { x: number, y: number, width: number, height: number }) => { return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height } let isFetchingConnectedNodes = false; const handleGetConnectedNodes = async () => { if (isFetchingConnectedNodes) { console.log("WARNING! Already fetching connected nodes. Skipping..."); return; } isFetchingConnectedNodes = true; console.log("Getting connected nodes for:", shape.props.uuid_string); try { const response = await axios.get(`/api/database/tools/get-connected-nodes-and-edges?uuid_string=${shape.props.uuid_string}`); console.log("Connected nodes response:", response.data); if (response.data.status === "success") { const mainNode = response.data.main_node; const connectedNodes = response.data.connected_nodes; const relationships = response.data.relationships; // Add nodes to the graph [mainNode, ...connectedNodes].forEach((node: any) => { console.log("Node:", node); const newShapeId = createShapeId(node.node_data.uuid_string); const doesShapeExist = editor.getShape(newShapeId); if (!doesShapeExist) { console.log("Creating new shape with ID:", newShapeId); const nodeConfig = nodeTypeConfig[node.node_type as keyof typeof nodeTypeConfig]; if (nodeConfig) { const newShape = { id: newShapeId, type: nodeConfig.shapeType, x: 0, y: 0, props: { color: nodeConfig.color, ...node.node_data } }; console.log("New shape:", newShape); console.log("Creating shape:", newShape); editor.createShape(newShape); console.log("New shape created:", newShape); const bounds = editor.getShapeGeometry(newShapeId).bounds; console.log("Shape bounds:", bounds); console.log("Updating shape with width:", bounds.w, "and height:", bounds.h); newShape.props.w = bounds.w; newShape.props.h = bounds.h; console.log("Adding node to graphState:", newShape); const shapeWithWidthAndHeight = { ...newShape, w: bounds.w, h: bounds.h } graphState.addNode(shapeWithWidthAndHeight); console.log("Node added to graphState:", newShape); } else { console.log("WARNING! Node type not found:", node.node_type); } } }); console.log("Updating shapes with dagre..."); graphState.setEditor(editor); graphState.updateShapesWithDagre(); // Add edges to the graph relationships.forEach((relationship: any) => { graphState.addEdge(relationship.start_node.uuid_string, relationship.end_node.uuid_string); }); // Create edge shapes graphState.getEdges().forEach((edge: any) => { console.log("WARNING! Cancelling createEdgeComponent()..."); // console.log("handleGetConnectedNodes(): Creating edge component for:", edge.v, edge.w); // createEdgeComponent(edge.v, edge.w, editor); }); console.log("Done!"); } else { console.error('Error in response:', response.data.message); } } catch (error) { console.error('Error fetching connected nodes:', error); } finally { isFetchingConnectedNodes = false; } }; const loadTldrawFile = async (path: string, editor: any) => { console.log("Loading tldraw_file...") try { const response = await axios.get(`/api/database/tldraw_fs/get_tldraw_user_file${path}/tldraw_file.json`); const fileContent = response.data; console.log("File content:", fileContent); if (fileContent && fileContent.document && fileContent.document.store) { // Ensure the schema version is set if (!fileContent.document.schema) { console.log("!fileContent.document.schema") fileContent.document.schema = { schemaVersion: 1 }; } else if (!fileContent.document.schema.schemaVersion) { console.log("!fileContent.document.schema.schemaVersion") fileContent.document.schema.schemaVersion = 1; } // Load the new content console.log("Loading snapshot: ", fileContent) editor.loadSnapshot(fileContent); } else { console.error('Invalid file content structure:', fileContent); throw new Error('Invalid file content structure'); } } catch (error) { console.error('Error loading tldraw file:', error); } }; return ( {getNodeComponent(shape, theme)} loadTldrawFile(shape.props.path, editor)} > Open File Get Connected Nodes ) } const createNodeIndicator = (shape: AllNodeShapes, editor: any) => { const bounds = editor.getShapeGeometry(shape).bounds const theme = getDefaultColorTheme({ isDarkMode: editor.user.getIsDarkMode() }) return ( ) } const createEdgeComponent = (sourceId: string, targetId: string, editor: any) => { console.log("Creating edge component for:", sourceId, targetId) const edge = { type: 'general_relationship', props: { w: 200, h: 300, color: 'black', __relationshiptype__: '', source: sourceId, target: targetId, } }; editor.createShape(edge); graphState.addNode(edge); }; export abstract class BaseNodeShapeUtil extends ShapeUtil { static override type: string static override props: any static override migrations: any override isAspectRatioLocked = (_shape: T) => true override canResize = (_shape: T) => true abstract override getDefaultProps(): T['props'] getGeometry(shape: T) { return new Rectangle2d({ width: shape.props.w, height: shape.props.h, x: 0, y: 0, isFilled: true, }) } component(shape: T) { const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) return createNodeComponent(shape, theme, this.editor) } indicator(shape: T) { return createNodeIndicator(shape, this.editor) } onDrag = (shape: T, dx: number, dy: number) => { return { x: shape.x + dx, y: shape.y + dy, } } } export abstract class BaseRelationshipShapeUtil extends ShapeUtil { static override type: string static override props: any static override migrations: any override isAspectRatioLocked = (_shape: T) => true override canResize = (_shape: T) => true abstract override getDefaultProps(): T['props'] getGeometry(shape: T) { return new Rectangle2d({ width: shape.props.w, height: shape.props.h, x: 0, y: 0, isFilled: true, }); } component(shape: T) { // Define how the edge is rendered return ( ); } indicator(shape: T) { // Define the indicator for the edge return ( ); } }