From d006fd4fb155041891ec85507d150f33671e10bf Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 10 Nov 2025 19:42:06 -0800 Subject: [PATCH] fix shape rendering in prod --- src/automerge/AutomergeToTLStore.ts | 3 +- src/automerge/useAutomergeStoreV2.ts | 1863 +++----------------------- src/routes/Board.tsx | 8 +- 3 files changed, 174 insertions(+), 1700 deletions(-) diff --git a/src/automerge/AutomergeToTLStore.ts b/src/automerge/AutomergeToTLStore.ts index 78ff84f..bcd7246 100644 --- a/src/automerge/AutomergeToTLStore.ts +++ b/src/automerge/AutomergeToTLStore.ts @@ -310,7 +310,8 @@ function cleanRichTextNaN(richText: any): any { } // Minimal sanitization - only fix critical issues that break TLDraw -function sanitizeRecord(record: any): TLRecord { +// EXPORTED: Use this same sanitization in production bulk loading to match dev mode behavior +export function sanitizeRecord(record: any): TLRecord { const sanitized = { ...record } // CRITICAL FIXES ONLY - preserve all other properties diff --git a/src/automerge/useAutomergeStoreV2.ts b/src/automerge/useAutomergeStoreV2.ts index 038e3e8..db1da65 100644 --- a/src/automerge/useAutomergeStoreV2.ts +++ b/src/automerge/useAutomergeStoreV2.ts @@ -12,7 +12,7 @@ import { useRemoteAwareness, } from "@automerge/automerge-repo-react-hooks" -import { applyAutomergePatchesToTLStore } from "./AutomergeToTLStore.js" +import { applyAutomergePatchesToTLStore, sanitizeRecord } from "./AutomergeToTLStore.js" import { applyTLStoreChangesToAutomerge } from "./TLStoreToAutomerge.js" // Import custom shape utilities @@ -365,62 +365,62 @@ export function useAutomergeStoreV2({ // We'll handle validation issues in the record processing loop instead // Force cache refresh - pre-sanitization code has been removed - // Initialize store with existing records from Automerge - // NOTE: JSON sync might have already loaded data into the store - // Check if store is already populated before loading from Automerge - const existingStoreRecords = store.allRecords() - const existingStoreShapes = existingStoreRecords.filter((r: any) => r.typeName === 'shape') - - if (doc.store) { - const storeKeys = Object.keys(doc.store) - const docShapes = Object.values(doc.store).filter((r: any) => r?.typeName === 'shape').length - console.log(`📊 Automerge store initialization: doc has ${storeKeys.length} records (${docShapes} shapes), store already has ${existingStoreRecords.length} records (${existingStoreShapes.length} shapes)`) - - // If store already has shapes (from JSON sync), skip Automerge initialization - // JSON sync happened first and loaded the data - if (existingStoreShapes.length > 0 && docShapes === 0) { - console.log(`â„šī¸ Store already populated from JSON sync (${existingStoreShapes.length} shapes). Skipping Automerge initialization to prevent overwriting.`) - setStoreWithStatus({ - store, - status: "synced-remote", - connectionStatus: "online", - }) - return // Skip Automerge initialization - } - - console.log(`📊 Store keys count: ${storeKeys.length}`, storeKeys.slice(0, 10)) - - // Get all store values - Automerge should handle this correctly - const allStoreValues = Object.values(doc.store) - - // Debug: Log first few records in detail to see their structure - console.log("📊 Sample store values (first 3):", allStoreValues.slice(0, 3).map((v: any) => { - try { - return { - hasTypeName: !!v?.typeName, - hasId: !!v?.id, - typeName: v?.typeName, - id: v?.id, - type: v?.type, - keys: v ? Object.keys(v).slice(0, 10) : [], - // Try to stringify a sample to see structure - sample: JSON.stringify(v).substring(0, 200) - } - } catch (e) { - return { error: String(e), value: v } - } - })) - - // Debug: Count record types before filtering - const typeCountBefore = allStoreValues.reduce((acc: any, v: any) => { - const type = v?.typeName || 'unknown' - acc[type] = (acc[type] || 0) + 1 - return acc - }, {}) - console.log(`📊 Store values before filtering:`, { - total: allStoreValues.length, - typeCounts: typeCountBefore - }) + // Initialize store with existing records from Automerge + // NOTE: JSON sync might have already loaded data into the store + // Check if store is already populated before loading from Automerge + const existingStoreRecords = store.allRecords() + const existingStoreShapes = existingStoreRecords.filter((r: any) => r.typeName === 'shape') + + if (doc.store) { + const storeKeys = Object.keys(doc.store) + const docShapes = Object.values(doc.store).filter((r: any) => r?.typeName === 'shape').length + console.log(`📊 Automerge store initialization: doc has ${storeKeys.length} records (${docShapes} shapes), store already has ${existingStoreRecords.length} records (${existingStoreShapes.length} shapes)`) + + // If store already has shapes (from JSON sync), skip Automerge initialization + // JSON sync happened first and loaded the data + if (existingStoreShapes.length > 0 && docShapes === 0) { + console.log(`â„šī¸ Store already populated from JSON sync (${existingStoreShapes.length} shapes). Skipping Automerge initialization to prevent overwriting.`) + setStoreWithStatus({ + store, + status: "synced-remote", + connectionStatus: "online", + }) + return // Skip Automerge initialization + } + + console.log(`📊 Store keys count: ${storeKeys.length}`, storeKeys.slice(0, 10)) + + // Get all store values - Automerge should handle this correctly + const allStoreValues = Object.values(doc.store) + + // Debug: Log first few records in detail to see their structure + console.log("📊 Sample store values (first 3):", allStoreValues.slice(0, 3).map((v: any) => { + try { + return { + hasTypeName: !!v?.typeName, + hasId: !!v?.id, + typeName: v?.typeName, + id: v?.id, + type: v?.type, + keys: v ? Object.keys(v).slice(0, 10) : [], + // Try to stringify a sample to see structure + sample: JSON.stringify(v).substring(0, 200) + } + } catch (e) { + return { error: String(e), value: v } + } + })) + + // Debug: Count record types before filtering + const typeCountBefore = allStoreValues.reduce((acc: any, v: any) => { + const type = v?.typeName || 'unknown' + acc[type] = (acc[type] || 0) + 1 + return acc + }, {}) + console.log(`📊 Store values before filtering:`, { + total: allStoreValues.length, + typeCounts: typeCountBefore + }) // Simple filtering - only keep valid TLDraw records // Skip custom record types like obsidian_vault - they're not TLDraw records @@ -454,11 +454,10 @@ export function useAutomergeStoreV2({ console.log(`Found ${records.length} valid records in Automerge document`) } - // CRITICAL FIXES ONLY - preserve all other properties - // Note: obsidian_vault records are filtered out above - they're not TLDraw records + // CRITICAL: Use the same sanitization as dev mode (patch-based loading) + // This ensures production works exactly like dev mode const processedRecords = records.map((record: any) => { // Create a deep copy to avoid modifying immutable Automerge objects - // Use a more robust serialization that handles Automerge proxies let processedRecord: any try { // First try JSON serialization (works for most cases) @@ -503,934 +502,21 @@ export function useAutomergeStoreV2({ } } - // CRITICAL FIXES ONLY - preserve all other properties - if (processedRecord.typeName === 'shape') { - // Ensure basic required properties exist - if (typeof processedRecord.x !== 'number') processedRecord.x = 0 - if (typeof processedRecord.y !== 'number') processedRecord.y = 0 - if (typeof processedRecord.rotation !== 'number') processedRecord.rotation = 0 - if (typeof processedRecord.isLocked !== 'boolean') processedRecord.isLocked = false - if (typeof processedRecord.opacity !== 'number') processedRecord.opacity = 1 - if (!processedRecord.meta || typeof processedRecord.meta !== 'object') processedRecord.meta = {} - if (!processedRecord.index) processedRecord.index = 'a1' - if (!processedRecord.parentId) { - // Find all page records - const pageRecords = records.filter((r: any) => r.typeName === 'page') as any[] - // Prefer 'page:page' if it exists, otherwise use the first page found - const pageRecord = pageRecords.find((p: any) => p.id === 'page:page') || pageRecords[0] - if (pageRecord && pageRecord.id) { - processedRecord.parentId = pageRecord.id - } else { - // Default to 'page:page' - TLDraw will create it if needed - processedRecord.parentId = 'page:page' - } - } else { - // Validate that the parentId points to an existing page - const parentPage = records.find((r: any) => r.typeName === 'page' && r.id === processedRecord.parentId) - if (!parentPage) { - // Parent page doesn't exist, assign to first available page or default - const pageRecords = records.filter((r: any) => r.typeName === 'page') as any[] - const pageRecord = pageRecords.find((p: any) => p.id === 'page:page') || pageRecords[0] - if (pageRecord && pageRecord.id) { - console.log(`🔧 Shape ${processedRecord.id} has invalid parentId ${processedRecord.parentId}, reassigning to ${pageRecord.id}`) - processedRecord.parentId = pageRecord.id - } else { - processedRecord.parentId = 'page:page' - } - } - } - if (!processedRecord.props || typeof processedRecord.props !== 'object') processedRecord.props = {} - - // CRITICAL: Infer type from properties BEFORE defaulting to 'geo' - // This ensures arrows and other shapes are properly recognized - if (!processedRecord.type || typeof processedRecord.type !== 'string') { - // Check for arrow-specific properties first - if (processedRecord.props?.start !== undefined || - processedRecord.props?.end !== undefined || - processedRecord.props?.arrowheadStart !== undefined || - processedRecord.props?.arrowheadEnd !== undefined || - processedRecord.props?.kind === 'line' || - processedRecord.props?.kind === 'curved' || - processedRecord.props?.kind === 'straight') { - processedRecord.type = 'arrow' - } - // Check for line-specific properties - else if (processedRecord.props?.points !== undefined) { - processedRecord.type = 'line' - } - // Check for geo-specific properties (w/h/geo) - else if (processedRecord.props?.geo !== undefined || - ('w' in processedRecord && 'h' in processedRecord) || - ('w' in processedRecord.props && 'h' in processedRecord.props)) { - processedRecord.type = 'geo' - } - // Check for note-specific properties - else if (processedRecord.props?.growY !== undefined || - processedRecord.props?.verticalAlign !== undefined) { - processedRecord.type = 'note' - } - // Check for text-specific properties - else if (processedRecord.props?.textAlign !== undefined || - processedRecord.props?.autoSize !== undefined) { - processedRecord.type = 'text' - } - // Check for draw-specific properties - else if (processedRecord.props?.segments !== undefined) { - processedRecord.type = 'draw' - } - // Default to geo only if no other indicators found - else { - processedRecord.type = 'geo' - } - } - - // CRITICAL: For geo shapes, move w/h/geo from top-level to props (required by TLDraw schema) - if (processedRecord.type === 'geo' || ('w' in processedRecord && 'h' in processedRecord && processedRecord.type !== 'arrow')) { - if (!processedRecord.type || processedRecord.type === 'geo') { - processedRecord.type = 'geo' - } - - // Move w from top-level to props - if ('w' in processedRecord && processedRecord.w !== undefined) { - if (!('w' in processedRecord.props) || processedRecord.props.w === undefined) { - processedRecord.props.w = processedRecord.w - } - delete (processedRecord as any).w - } - - // Move h from top-level to props - if ('h' in processedRecord && processedRecord.h !== undefined) { - if (!('h' in processedRecord.props) || processedRecord.props.h === undefined) { - processedRecord.props.h = processedRecord.h - } - delete (processedRecord as any).h - } - - // Move geo from top-level to props - if ('geo' in processedRecord && processedRecord.geo !== undefined) { - if (!('geo' in processedRecord.props) || processedRecord.props.geo === undefined) { - processedRecord.props.geo = processedRecord.geo - } - delete (processedRecord as any).geo - } - - // Fix richText structure if it exists (preserve content) - if (processedRecord.props.richText) { - if (Array.isArray(processedRecord.props.richText)) { - processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } - } else if (typeof processedRecord.props.richText === 'object' && processedRecord.props.richText !== null) { - if (!processedRecord.props.richText.type) { - processedRecord.props.richText = { ...processedRecord.props.richText, type: 'doc' } - } - if (!processedRecord.props.richText.content) { - processedRecord.props.richText = { ...processedRecord.props.richText, content: [] } - } - } - } - } - - // CRITICAL: For arrow shapes, preserve text property - if (processedRecord.type === 'arrow') { - if ((processedRecord.props as any).text === undefined || (processedRecord.props as any).text === null) { - (processedRecord.props as any).text = '' - } - } - - // CRITICAL: For line shapes, ensure points structure exists (required by schema) - if (processedRecord.type === 'line') { - if ('w' in processedRecord.props) delete (processedRecord.props as any).w - if ('h' in processedRecord.props) delete (processedRecord.props as any).h - if (!processedRecord.props.points || typeof processedRecord.props.points !== 'object' || Array.isArray(processedRecord.props.points)) { - processedRecord.props.points = { - 'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 }, - 'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 } - } - } - } - - // CRITICAL: For group shapes, remove w/h from props (they cause validation errors) - if (processedRecord.type === 'group') { - if ('w' in processedRecord.props) delete (processedRecord.props as any).w - if ('h' in processedRecord.props) delete (processedRecord.props as any).h - } - - // CRITICAL: For image/video shapes, fix crop structure if it exists - if (processedRecord.type === 'image' || processedRecord.type === 'video') { - if (processedRecord.props.crop !== null && processedRecord.props.crop !== undefined) { - if (!processedRecord.props.crop.topLeft || !processedRecord.props.crop.bottomRight) { - if (processedRecord.props.crop.x !== undefined && processedRecord.props.crop.y !== undefined) { - processedRecord.props.crop = { - topLeft: { x: processedRecord.props.crop.x || 0, y: processedRecord.props.crop.y || 0 }, - bottomRight: { - x: (processedRecord.props.crop.x || 0) + (processedRecord.props.crop.w || 1), - y: (processedRecord.props.crop.y || 0) + (processedRecord.props.crop.h || 1) - } - } - } else { - processedRecord.props.crop = { - topLeft: { x: 0, y: 0 }, - bottomRight: { x: 1, y: 1 } - } - } - } - } - } - - // CRITICAL: Fix richText structure for note shapes if it exists - if (processedRecord.type === 'note' && processedRecord.props.richText) { - if (Array.isArray(processedRecord.props.richText)) { - processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } - } else if (typeof processedRecord.props.richText === 'object' && processedRecord.props.richText !== null) { - if (!processedRecord.props.richText.type) { - processedRecord.props.richText = { ...processedRecord.props.richText, type: 'doc' } - } - if (!processedRecord.props.richText.content) { - processedRecord.props.richText = { ...processedRecord.props.richText, content: [] } - } - } - } - - // Ensure props object exists for all shapes - if (!processedRecord.props) processedRecord.props = {} - - // Preserve original data structure - only move properties when TLDraw validation requires it - // Arrow shapes don't have w/h properties, so remove them if present - if (processedRecord.type === 'arrow') { - if ('w' in processedRecord) { - console.log(`Removing invalid w property from arrow shape ${processedRecord.id}`) - delete (processedRecord as any).w - } - if ('h' in processedRecord) { - console.log(`Removing invalid h property from arrow shape ${processedRecord.id}`) - delete (processedRecord as any).h - } - } - // For other shapes, preserve the original structure - don't move w/h unless validation fails - - // Handle arrow shapes specially - ensure they have required properties - if (processedRecord.type === 'arrow') { - // Ensure required arrow properties exist - if (!processedRecord.props.kind) processedRecord.props.kind = 'line' - if (!processedRecord.props.labelColor) processedRecord.props.labelColor = 'black' - if (!processedRecord.props.color) processedRecord.props.color = 'black' - if (!processedRecord.props.fill) processedRecord.props.fill = 'none' - if (!processedRecord.props.dash) processedRecord.props.dash = 'draw' - if (!processedRecord.props.size) processedRecord.props.size = 'm' - if (!processedRecord.props.arrowheadStart) processedRecord.props.arrowheadStart = 'none' - if (!processedRecord.props.arrowheadEnd) processedRecord.props.arrowheadEnd = 'arrow' - if (!processedRecord.props.font) processedRecord.props.font = 'draw' - if (!processedRecord.props.start) processedRecord.props.start = { x: 0, y: 0 } - if (!processedRecord.props.end) processedRecord.props.end = { x: 100, y: 0 } - if (processedRecord.props.bend === undefined) processedRecord.props.bend = 0 - if (!processedRecord.props.text) processedRecord.props.text = '' - if (processedRecord.props.labelPosition === undefined) processedRecord.props.labelPosition = 0.5 - if (processedRecord.props.scale === undefined) processedRecord.props.scale = 1 - if (processedRecord.props.elbowMidPoint === undefined) processedRecord.props.elbowMidPoint = 0.5 - - // Remove any invalid properties - const invalidArrowProps = ['w', 'h', 'geo', 'insets', 'scribbles'] - invalidArrowProps.forEach(prop => { - if (prop in processedRecord.props) { - console.log(`Removing invalid prop '${prop}' from arrow shape ${processedRecord.id}`) - delete (processedRecord.props as any)[prop] - } - }) - } - - // Handle note shapes specially - ensure they have required properties - if (processedRecord.type === 'note') { - // Ensure required note properties exist - if (!processedRecord.props.color) processedRecord.props.color = 'black' - if (!processedRecord.props.labelColor) processedRecord.props.labelColor = 'black' - if (!processedRecord.props.size) processedRecord.props.size = 'm' - if (!processedRecord.props.font) processedRecord.props.font = 'draw' - if (processedRecord.props.fontSizeAdjustment === undefined) processedRecord.props.fontSizeAdjustment = 0 - if (!processedRecord.props.align) processedRecord.props.align = 'start' - if (!processedRecord.props.verticalAlign) processedRecord.props.verticalAlign = 'start' - if (processedRecord.props.growY === undefined) processedRecord.props.growY = 0 - if (!processedRecord.props.url) processedRecord.props.url = '' - // Note: richText is not required for note shapes - if (processedRecord.props.scale === undefined) processedRecord.props.scale = 1 - - // Remove any invalid properties - const invalidNoteProps = ['w', 'h', 'geo', 'insets', 'scribbles'] - invalidNoteProps.forEach(prop => { - if (prop in processedRecord.props) { - console.log(`Removing invalid prop '${prop}' from note shape ${processedRecord.id}`) - delete (processedRecord.props as any)[prop] - } - }) - } - - // Handle text shapes specially - ensure they have required properties - if (processedRecord.type === 'text') { - // Ensure required text properties exist (matching default tldraw text shape schema) - if (!processedRecord.props.color) processedRecord.props.color = 'black' - if (!processedRecord.props.size) processedRecord.props.size = 'm' - if (!processedRecord.props.font) processedRecord.props.font = 'draw' - if (!processedRecord.props.textAlign) processedRecord.props.textAlign = 'start' - if (processedRecord.props.w === undefined || processedRecord.props.w === null) { - processedRecord.props.w = 100 - } - if (processedRecord.props.scale === undefined) processedRecord.props.scale = 1 - if (processedRecord.props.autoSize === undefined) processedRecord.props.autoSize = false - - // Ensure richText property exists for text shapes - if (!processedRecord.props.richText) { - console.log(`🔧 Creating default richText object for text shape ${processedRecord.id}`) - processedRecord.props.richText = { content: [], type: 'doc' } - } - - // Remove any invalid properties (including 'text' property which is not in default schema) - // Note: richText is actually required for text shapes, so don't remove it - const invalidTextProps = ['text', 'h', 'geo', 'insets', 'scribbles', 'isMinimized', 'roomUrl', 'roomId', 'align', 'verticalAlign', 'growY', 'url'] - invalidTextProps.forEach(prop => { - if (prop in processedRecord.props) { - console.log(`Removing invalid prop '${prop}' from text shape ${processedRecord.id}`) - delete (processedRecord.props as any)[prop] - } - }) - } - - // Handle draw shapes specially - ensure they have required properties - if (processedRecord.type === 'draw') { - // Ensure required draw properties exist - if (!processedRecord.props.color) processedRecord.props.color = 'black' - if (!processedRecord.props.fill) processedRecord.props.fill = 'none' - if (!processedRecord.props.dash) processedRecord.props.dash = 'draw' - if (!processedRecord.props.size) processedRecord.props.size = 'm' - - // Validate and fix segments array - this is critical for preventing Polyline2d errors - if (!processedRecord.props.segments || !Array.isArray(processedRecord.props.segments)) { - console.log(`🔧 Fixing missing/invalid segments for draw shape ${processedRecord.id}`) - processedRecord.props.segments = [ - { - type: "free", - points: [ - { x: 0, y: 0, z: 0.5 }, - { x: 10, y: 10, z: 0.5 } - ] - } - ] - } else { - // Validate each segment in the array - // Polyline2d requires at least 2 points per segment - const validSegments = [] - for (let i = 0; i < processedRecord.props.segments.length; i++) { - const segment = processedRecord.props.segments[i] - if (segment && typeof segment === 'object' && - segment.type && - Array.isArray(segment.points) && - segment.points.length >= 2) { - // Validate points in the segment - const validPoints = segment.points.filter((point: any) => - point && - typeof point === 'object' && - typeof point.x === 'number' && - typeof point.y === 'number' && - !isNaN(point.x) && !isNaN(point.y) - ) - // Polyline2d requires at least 2 points - if (validPoints.length >= 2) { - validSegments.push({ - type: segment.type, - points: validPoints - }) - } else if (validPoints.length === 1) { - // If only 1 point, duplicate it to create a valid 2-point segment - console.log(`🔧 Draw shape ${processedRecord.id} segment ${i} has only 1 point, duplicating to create valid segment`) - validSegments.push({ - type: segment.type, - points: [validPoints[0], { ...validPoints[0] }] - }) - } - } - } - - if (validSegments.length === 0) { - console.log(`🔧 All segments invalid for draw shape ${processedRecord.id}, creating default segment`) - processedRecord.props.segments = [ - { - type: "free", - points: [ - { x: 0, y: 0, z: 0.5 }, - { x: 10, y: 10, z: 0.5 } - ] - } - ] - } else { - processedRecord.props.segments = validSegments - } - } - - if (processedRecord.props.isComplete === undefined) processedRecord.props.isComplete = true - if (processedRecord.props.isClosed === undefined) processedRecord.props.isClosed = false - if (processedRecord.props.isPen === undefined) processedRecord.props.isPen = false - if (processedRecord.props.scale === undefined) processedRecord.props.scale = 1 - - // Remove any invalid properties - const invalidDrawProps = ['w', 'h', 'geo', 'insets', 'scribbles', 'richText'] - invalidDrawProps.forEach(prop => { - if (prop in processedRecord.props) { - console.log(`Removing invalid prop '${prop}' from draw shape ${processedRecord.id}`) - delete (processedRecord.props as any)[prop] - } - }) - } - - // Handle geo shapes specially - ensure geo property is in props where TLDraw expects it - if (processedRecord.type === 'geo') { - // Ensure props exists - if (!processedRecord.props) processedRecord.props = {} - - // CRITICAL: ALWAYS remove w/h/geo from top level (TLDraw validation fails if they exist at top level) - // Move w from top level to props (preserve value if not already in props) - if ('w' in processedRecord) { - console.log(`🔧 Geo shape fix: Removing w from top level for shape ${processedRecord.id}`) - if (!('w' in processedRecord.props) || processedRecord.props.w === undefined) { - processedRecord.props.w = (processedRecord as any).w - } - delete (processedRecord as any).w - } - - // Move h from top level to props (preserve value if not already in props) - if ('h' in processedRecord) { - console.log(`🔧 Geo shape fix: Removing h from top level for shape ${processedRecord.id}`) - if (!('h' in processedRecord.props) || processedRecord.props.h === undefined) { - processedRecord.props.h = (processedRecord as any).h - } - delete (processedRecord as any).h - } - - // Move geo from top level to props (preserve value if not already in props) - if ('geo' in processedRecord) { - console.log(`🔧 Geo shape fix: Removing geo from top level for shape ${processedRecord.id}`) - if (!('geo' in processedRecord.props) || processedRecord.props.geo === undefined) { - processedRecord.props.geo = (processedRecord as any).geo - } - delete (processedRecord as any).geo - } - - // Ensure geo property exists in props with a default value - if (!processedRecord.props.geo) { - processedRecord.props.geo = 'rectangle' - } - - // Ensure w/h exist in props with defaults if missing - if (!processedRecord.props) processedRecord.props = {} - if (processedRecord.props.w === undefined || processedRecord.props.w === null) { - processedRecord.props.w = 100 - } - if (processedRecord.props.h === undefined || processedRecord.props.h === null) { - processedRecord.props.h = 100 - } - if (processedRecord.props.geo === undefined || processedRecord.props.geo === null) { - processedRecord.props.geo = 'rectangle' - } - if (!processedRecord.props.dash) processedRecord.props.dash = 'draw' - if (!processedRecord.props.growY) processedRecord.props.growY = 0 - if (!processedRecord.props.url) processedRecord.props.url = '' - if (!processedRecord.props.scale) processedRecord.props.scale = 1 - if (!processedRecord.props.color) processedRecord.props.color = 'black' - if (!processedRecord.props.labelColor) processedRecord.props.labelColor = 'black' - if (!processedRecord.props.fill) processedRecord.props.fill = 'none' - if (!processedRecord.props.size) processedRecord.props.size = 'm' - if (!processedRecord.props.font) processedRecord.props.font = 'draw' - if (!processedRecord.props.align) processedRecord.props.align = 'middle' - if (!processedRecord.props.verticalAlign) processedRecord.props.verticalAlign = 'middle' - // Note: richText IS required for geo shapes in TLDraw - if (!processedRecord.props.richText) processedRecord.props.richText = { content: [], type: 'doc' } - // Ensure basic geo properties exist - if (!processedRecord.props.geo) processedRecord.props.geo = 'rectangle' - if (!processedRecord.props.fill) processedRecord.props.fill = 'solid' - if (!processedRecord.props.color) processedRecord.props.color = 'white' - - // Validate geo property - const validGeoTypes = [ - 'cloud', 'rectangle', 'ellipse', 'triangle', 'diamond', 'pentagon', - 'hexagon', 'octagon', 'star', 'rhombus', 'rhombus-2', 'oval', - 'trapezoid', 'arrow-right', 'arrow-left', 'arrow-up', 'arrow-down', - 'x-box', 'check-box', 'heart' - ] - - if (!validGeoTypes.includes(processedRecord.props.geo)) { - console.log(`Setting valid geo property for shape ${processedRecord.id} (was: ${processedRecord.props.geo})`) - processedRecord.props.geo = 'rectangle' - } - - // Remove invalid properties from props (only log if actually removing) - const invalidProps = ['insets', 'scribbles'] - invalidProps.forEach(prop => { - if (prop in processedRecord.props) { - delete (processedRecord.props as any)[prop] - } - }) - } - - // Handle rich text content that might be undefined or invalid - // Only process richText for shapes that actually use it (text, note, geo, etc.) - // CRITICAL: geo shapes (rectangles) can legitimately have richText in TLDraw - if (processedRecord.type === 'text' || processedRecord.type === 'note' || processedRecord.type === 'geo') { - if (processedRecord.props && processedRecord.props.richText !== undefined) { - if (!Array.isArray(processedRecord.props.richText) && typeof processedRecord.props.richText !== 'object') { - console.warn('Fixing invalid richText property for shape:', processedRecord.id, 'type:', processedRecord.type, 'was:', typeof processedRecord.props.richText) - processedRecord.props.richText = { content: [], type: 'doc' } - } else if (Array.isArray(processedRecord.props.richText)) { - // If it's an array, convert to proper richText object structure - console.log(`🔧 Converting richText array to object for shape ${processedRecord.id}`) - processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } - } - } else { - // Create default empty richText object for text shapes (but not for geo/note unless they already have it) - if (processedRecord.type === 'text') { - if (!processedRecord.props) processedRecord.props = {} - processedRecord.props.richText = { content: [], type: 'doc' } - } - } - } else if (processedRecord.props && processedRecord.props.richText !== undefined) { - // Remove richText from shapes that don't use it (but preserve for geo/note which are handled above) - delete (processedRecord.props as any).richText - } - - // Remove invalid properties that cause validation errors (after moving geo properties) - const invalidProperties = [ - 'insets', 'scribbles', 'duplicateProps', 'isAspectRatioLocked', - 'isFlippedHorizontal', 'isFlippedVertical', 'isFrozen', 'isSnappable', - 'isTransparent', 'isVisible', 'isZIndexLocked', 'isHidden' - ] - invalidProperties.forEach(prop => { - if (prop in processedRecord) { - delete (processedRecord as any)[prop] - } - }) - - // Custom shapes are supported natively by our custom schema - no conversion needed! - // Just ensure they have the required properties for their type - if (processedRecord.type === 'VideoChat' || processedRecord.type === 'ChatBox' || - processedRecord.type === 'Embed' || processedRecord.type === 'SharedPiano' || - processedRecord.type === 'MycrozineTemplate' || processedRecord.type === 'Slide') { - // These are embed-like shapes - ensure they have basic properties - if (!processedRecord.props) processedRecord.props = {} - if (processedRecord.props.w === undefined || processedRecord.props.w === null) { - processedRecord.props.w = 300 - } - if (processedRecord.props.h === undefined || processedRecord.props.h === null) { - processedRecord.props.h = 200 - } - console.log(`🔧 Ensured embed-like shape ${processedRecord.type} has required properties:`, processedRecord.props) - } else if (processedRecord.type === 'Prompt' || processedRecord.type === 'Transcription' || - processedRecord.type === 'Markdown') { - // These are text-like shapes - ensure they have text properties - if (!processedRecord.props) processedRecord.props = {} - if (processedRecord.props.w === undefined || processedRecord.props.w === null) { - processedRecord.props.w = 300 - } - - // Convert value property to richText if it exists (for Prompt shapes) - if (processedRecord.props.value && !processedRecord.props.richText) { - processedRecord.props.richText = { - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: processedRecord.props.value - } - ] - } - ], - type: 'doc' - } - console.log(`🔧 Converted value to richText for ${processedRecord.type} shape ${processedRecord.id}`) - } - - if (!processedRecord.props.richText) { - processedRecord.props.richText = { content: [], type: 'doc' } - } - console.log(`🔧 Ensured text-like shape ${processedRecord.type} has required properties:`, processedRecord.props) - } - - // Validate that the shape type is supported by our schema - // CRITICAL: Include ALL original tldraw shapes to ensure they're preserved - const validCustomShapes = ['ObsNote', 'VideoChat', 'Transcription', 'SharedPiano', 'Prompt', 'ChatBox', 'Embed', 'Markdown', 'MycrozineTemplate', 'Slide', 'FathomTranscript', 'Holon', 'ObsidianBrowser', 'HolonBrowser', 'FathomMeetingsBrowser', 'LocationShare'] - const validDefaultShapes = ['arrow', 'bookmark', 'draw', 'embed', 'frame', 'geo', 'group', 'highlight', 'image', 'line', 'note', 'text', 'video'] - const allValidShapes = [...validCustomShapes, ...validDefaultShapes] - - // Normalize shape type to handle case variations and known aliases - const normalizedType = processedRecord.type?.toLowerCase() - const isDefaultShape = validDefaultShapes.includes(normalizedType) - const isCustomShape = validCustomShapes.includes(processedRecord.type) - - // Handle known shape type aliases/variations - const shapeTypeAliases: Record = { - 'transcribe': 'Transcription', // "Transcribe" -> "Transcription" - 'transcription': 'Transcription', // lowercase -> proper case - } - const aliasType = shapeTypeAliases[normalizedType] || shapeTypeAliases[processedRecord.type] - if (aliasType) { - console.log(`🔧 Normalizing shape type from "${processedRecord.type}" to "${aliasType}" for shape:`, processedRecord.id) - processedRecord.type = aliasType - } else if (isDefaultShape && processedRecord.type !== normalizedType) { - // If it's a valid default shape but with wrong casing, normalize it - console.log(`🔧 Normalizing shape type from "${processedRecord.type}" to "${normalizedType}" for shape:`, processedRecord.id) - processedRecord.type = normalizedType - } else if (!isDefaultShape && !isCustomShape) { - // Only convert to text if it's truly unknown - console.log(`🔧 Unknown shape type ${processedRecord.type}, converting to text shape for shape:`, processedRecord.id) - processedRecord.type = 'text' - if (!processedRecord.props) processedRecord.props = {} - // Preserve existing props and only set defaults for missing required text shape properties - // This prevents losing metadata or other valid properties - processedRecord.props = { - ...processedRecord.props, // Preserve existing props - w: processedRecord.props.w || 300, - color: processedRecord.props.color || 'black', - size: processedRecord.props.size || 'm', - font: processedRecord.props.font || 'draw', - textAlign: processedRecord.props.textAlign || 'start', - autoSize: processedRecord.props.autoSize !== undefined ? processedRecord.props.autoSize : false, - scale: processedRecord.props.scale || 1, - richText: processedRecord.props.richText || { content: [], type: 'doc' } - } - // Remove invalid properties for text shapes (but preserve meta and other valid top-level properties) - const invalidTextProps = ['h', 'geo', 'insets', 'scribbles', 'isMinimized', 'roomUrl', 'text', 'align', 'verticalAlign', 'growY', 'url'] - invalidTextProps.forEach(prop => { - if (prop in processedRecord.props) { - delete (processedRecord.props as any)[prop] - } - }) - console.log(`🔧 Converted unknown shape to text:`, processedRecord.props) - } - - // Universal shape validation - ensure any shape type can be imported - // CRITICAL: Fix image and video shapes FIRST - ensure crop has correct structure - // Tldraw expects crop to be { topLeft: { x, y }, bottomRight: { x, y } } or null - if (processedRecord.type === 'image' || processedRecord.type === 'video') { - // Ensure props exists for image/video shapes - if (!processedRecord.props) { - processedRecord.props = {} - } - // Fix crop structure - if (processedRecord.props.crop !== null && processedRecord.props.crop !== undefined) { - // If crop exists but has wrong structure, fix it - if (!processedRecord.props.crop.topLeft || !processedRecord.props.crop.bottomRight) { - // Convert old format { x, y, w, h } to new format, or set default - if (processedRecord.props.crop.x !== undefined && processedRecord.props.crop.y !== undefined) { - // Old format: convert to new format - processedRecord.props.crop = { - topLeft: { x: processedRecord.props.crop.x || 0, y: processedRecord.props.crop.y || 0 }, - bottomRight: { - x: (processedRecord.props.crop.x || 0) + (processedRecord.props.crop.w || 1), - y: (processedRecord.props.crop.y || 0) + (processedRecord.props.crop.h || 1) - } - } - } else { - // Invalid structure: set to default (full crop) - processedRecord.props.crop = { - topLeft: { x: 0, y: 0 }, - bottomRight: { x: 1, y: 1 } - } - } - } else { - // Ensure topLeft and bottomRight are proper objects - if (!processedRecord.props.crop.topLeft || typeof processedRecord.props.crop.topLeft !== 'object') { - processedRecord.props.crop.topLeft = { x: 0, y: 0 } - } - if (!processedRecord.props.crop.bottomRight || typeof processedRecord.props.crop.bottomRight !== 'object') { - processedRecord.props.crop.bottomRight = { x: 1, y: 1 } - } - } - } else { - // Crop is null/undefined: set to null (no crop) - processedRecord.props.crop = null - } - } - - // CRITICAL: Fix line shapes - ensure valid points and remove invalid w/h properties - if (processedRecord.type === 'line') { - if (!processedRecord.props) { - processedRecord.props = {} - } - // Line shapes should NOT have w or h properties - if ('w' in processedRecord.props) { - console.log(`🔧 Universal fix: Removing invalid w property from line shape ${processedRecord.id}`) - delete processedRecord.props.w - } - if ('h' in processedRecord.props) { - console.log(`🔧 Universal fix: Removing invalid h property from line shape ${processedRecord.id}`) - delete processedRecord.props.h - } - - // Line shapes REQUIRE points property: Record - if (!processedRecord.props.points || typeof processedRecord.props.points !== 'object' || Array.isArray(processedRecord.props.points)) { - console.log(`🔧 Universal fix: Creating default points for line shape ${processedRecord.id}`) - // Create default points with at least 2 points - const point1 = { id: 'a1', index: 'a1' as any, x: 0, y: 0 } - const point2 = { id: 'a2', index: 'a2' as any, x: 100, y: 0 } - processedRecord.props.points = { - 'a1': point1, - 'a2': point2 - } - } else { - // Validate and fix existing points - const validPoints: Record = {} - let pointIndex = 0 - const indices = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10'] - - for (const [key, point] of Object.entries(processedRecord.props.points)) { - if (point && typeof point === 'object' && - typeof (point as any).x === 'number' && - typeof (point as any).y === 'number' && - !isNaN((point as any).x) && !isNaN((point as any).y)) { - const index = indices[pointIndex] || `a${pointIndex + 1}` - validPoints[index] = { - id: index, - index: index as any, - x: (point as any).x, - y: (point as any).y - } - pointIndex++ - } - } - - if (Object.keys(validPoints).length === 0) { - // No valid points, create default - console.log(`🔧 Universal fix: No valid points found for line shape ${processedRecord.id}, creating default points`) - processedRecord.props.points = { - 'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 }, - 'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 } - } - } else if (Object.keys(validPoints).length === 1) { - // Only one point, add a second one - const firstPoint = Object.values(validPoints)[0] - const secondIndex = indices[1] || 'a2' - validPoints[secondIndex] = { - id: secondIndex, - index: secondIndex as any, - x: firstPoint.x + 100, - y: firstPoint.y - } - processedRecord.props.points = validPoints - } else { - processedRecord.props.points = validPoints - } - } - - // Ensure other required line shape properties exist - if (!processedRecord.props.color) processedRecord.props.color = 'black' - if (!processedRecord.props.dash) processedRecord.props.dash = 'draw' - if (!processedRecord.props.size) processedRecord.props.size = 'm' - if (!processedRecord.props.spline) processedRecord.props.spline = 'line' - if (processedRecord.props.scale === undefined || processedRecord.props.scale === null) { - processedRecord.props.scale = 1 - } - } - - // CRITICAL: Fix group shapes - remove invalid w/h properties - if (processedRecord.type === 'group') { - if (!processedRecord.props) { - processedRecord.props = {} - } - // Group shapes should NOT have w or h properties - if ('w' in processedRecord.props) { - console.log(`🔧 Universal fix: Removing invalid w property from group shape ${processedRecord.id}`) - delete processedRecord.props.w - } - if ('h' in processedRecord.props) { - console.log(`🔧 Universal fix: Removing invalid h property from group shape ${processedRecord.id}`) - delete processedRecord.props.h - } - } - - if (processedRecord.props) { - - // Fix any richText issues for text shapes only - if (processedRecord.type === 'text' && processedRecord.props.richText !== undefined) { - if (!Array.isArray(processedRecord.props.richText)) { - console.log(`🔧 Universal fix: Converting richText to proper object for text shape ${processedRecord.id}`) - processedRecord.props.richText = { content: [], type: 'doc' } - } else { - // Convert array to proper object structure - console.log(`🔧 Universal fix: Converting richText array to object for text shape ${processedRecord.id}`) - processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } - } - } - - // Special handling for geo shapes - if (processedRecord.type === 'geo') { - // Geo shapes should have richText property but not text property - if ('text' in processedRecord.props) { - console.log(`🔧 Removing invalid text property from geo shape ${processedRecord.id}`) - delete processedRecord.props.text - } - - // Ensure richText property exists and is properly structured for geo shapes - if (!processedRecord.props.richText) { - console.log(`🔧 Adding missing richText property for geo shape ${processedRecord.id}`) - processedRecord.props.richText = { content: [], type: 'doc' } - } else if (Array.isArray(processedRecord.props.richText)) { - console.log(`🔧 Converting richText array to object for geo shape ${processedRecord.id}`) - processedRecord.props.richText = { content: processedRecord.props.richText, type: 'doc' } - } else if (typeof processedRecord.props.richText !== 'object' || processedRecord.props.richText === null) { - console.log(`🔧 Fixing invalid richText structure for geo shape ${processedRecord.id}`) - processedRecord.props.richText = { content: [], type: 'doc' } - } else if (!processedRecord.props.richText.content) { - // If richText exists but content is missing, preserve the rest and add empty content - console.log(`🔧 Adding missing content to richText for geo shape ${processedRecord.id}`) - processedRecord.props.richText = { - ...processedRecord.props.richText, - content: processedRecord.props.richText.content || [], - type: processedRecord.props.richText.type || 'doc' - } - } - - // Ensure geo shape has proper structure - if (!processedRecord.props.geo) { - processedRecord.props.geo = 'rectangle' - } - if (processedRecord.props.w === undefined || processedRecord.props.w === null) { - processedRecord.props.w = 100 - } - if (processedRecord.props.h === undefined || processedRecord.props.h === null) { - processedRecord.props.h = 100 - } - - // Fix dash property - ensure it's a valid value - if (processedRecord.props.dash === '' || processedRecord.props.dash === undefined) { - processedRecord.props.dash = 'solid' - } else if (!['draw', 'solid', 'dashed', 'dotted'].includes(processedRecord.props.dash)) { - console.log(`🔧 Fixing invalid dash value '${processedRecord.props.dash}' for geo shape:`, processedRecord.id) - processedRecord.props.dash = 'solid' - } - - // Fix scale property - ensure it's a number - if (processedRecord.props.scale === undefined || processedRecord.props.scale === null) { - processedRecord.props.scale = 1 - } else if (typeof processedRecord.props.scale !== 'number') { - console.log(`🔧 Fixing invalid scale value '${processedRecord.props.scale}' for geo shape:`, processedRecord.id) - processedRecord.props.scale = 1 - } - - // Remove invalid properties for geo shapes (including insets) - but NOT richText as it's required - const invalidGeoOtherProps = ['transcript', 'isTranscribing', 'isPaused', 'isEditing', 'roomUrl', 'roomId', 'prompt', 'value', 'agentBinding', 'isMinimized', 'noteId', 'title', 'content', 'tags', 'showPreview', 'backgroundColor', 'textColor', 'editingContent', 'vaultName', 'insets'] - invalidGeoOtherProps.forEach(prop => { - if (prop in processedRecord.props) { - console.log(`🔧 Removing invalid ${prop} property from geo shape:`, processedRecord.id) - delete processedRecord.props[prop] - } - }) - } - - // Fix note shapes - ensure richText exists and remove invalid w/h properties - if (processedRecord.type === 'note') { - // Note shapes REQUIRE richText property (it's part of the schema) - if (!processedRecord.props.richText || typeof processedRecord.props.richText !== 'object') { - console.log(`🔧 Adding missing richText property for note shape ${processedRecord.id}`) - processedRecord.props.richText = { content: [], type: 'doc' } - } - if ('w' in processedRecord.props) { - console.log(`🔧 Removing invalid w property from note shape:`, processedRecord.id) - delete processedRecord.props.w - } - if ('h' in processedRecord.props) { - console.log(`🔧 Removing invalid h property from note shape:`, processedRecord.id) - delete processedRecord.props.h - } - } - - // Fix text shapes - remove h property - if (processedRecord.type === 'text') { - if ('h' in processedRecord.props) { - console.log(`🔧 Removing invalid h property from text shape:`, processedRecord.id) - delete processedRecord.props.h - } - } - - // Fix embed shapes - ensure required properties and remove invalid ones - if (processedRecord.type === 'embed') { - if (!processedRecord.props.url) { - console.log(`🔧 Adding missing url property for embed shape:`, processedRecord.id) - processedRecord.props.url = '' - } - if (!processedRecord.props.w) { - processedRecord.props.w = 400 - } - if (!processedRecord.props.h) { - processedRecord.props.h = 300 - } - - // Remove invalid properties for embed shapes - const invalidEmbedProps = ['isMinimized', 'roomUrl', 'roomId', 'color', 'fill', 'dash', 'size', 'text', 'font', 'align', 'verticalAlign', 'growY', 'richText'] - invalidEmbedProps.forEach(prop => { - if (prop in processedRecord.props) { - console.log(`🔧 Removing invalid prop '${prop}' from embed shape ${processedRecord.id}`) - delete (processedRecord.props as any)[prop] - } - }) - } - - // Ensure all required properties exist for any shape type (except arrow, draw, line, text, note, and group) - if (processedRecord.type !== 'arrow' && processedRecord.type !== 'draw' && processedRecord.type !== 'line' && processedRecord.type !== 'text' && processedRecord.type !== 'note' && processedRecord.type !== 'group') { - const requiredProps = ['w', 'h'] - requiredProps.forEach(prop => { - if (processedRecord.props[prop] === undefined) { - console.log(`🔧 Universal fix: Adding missing ${prop} for shape ${processedRecord.id} (type: ${processedRecord.type})`) - if (prop === 'w' && processedRecord.props.w === undefined) processedRecord.props.w = 100 - if (prop === 'h' && processedRecord.props.h === undefined) processedRecord.props.h = 100 - } - }) - } else if (processedRecord.type === 'text') { - // Text shapes only need w, not h - if (processedRecord.props.w === undefined || processedRecord.props.w === null) { - console.log(`🔧 Universal fix: Adding missing w for text shape ${processedRecord.id}`) - processedRecord.props.w = 100 - } - } - - // Clean up any null/undefined values in props (but preserve required objects like crop for images/videos) - // IMPORTANT: crop is already set above for image/video shapes, so we must skip it here - Object.keys(processedRecord.props).forEach(propKey => { - // Skip crop for image/video shapes - it must be an object, not undefined - if ((processedRecord.type === 'image' || processedRecord.type === 'video') && propKey === 'crop') { - return // crop is required and already set above - } - if (processedRecord.props[propKey] === null || processedRecord.props[propKey] === undefined) { - console.log(`🔧 Universal fix: Removing null/undefined prop ${propKey} from shape ${processedRecord.id}`) - delete processedRecord.props[propKey] - } - }) - } + // CRITICAL: Use the same sanitizeRecord function that dev mode uses + // This ensures production uses the exact same sanitization logic + try { + return sanitizeRecord(processedRecord) + } catch (error) { + console.error(`Failed to sanitize record ${processedRecord?.id}:`, error) + // Return unsanitized record as fallback (will likely fail validation) + return processedRecord } - - // Fix instance records - if (processedRecord.typeName === 'instance') { - if (!processedRecord.meta) processedRecord.meta = {} - if ('insets' in processedRecord && !Array.isArray(processedRecord.insets)) { - processedRecord.insets = [false, false, false, false] - } - // Always ensure scribbles is an array, even if undefined - if (!Array.isArray(processedRecord.scribbles)) { - processedRecord.scribbles = [] - } - // Always ensure duplicateProps is an object with required properties - if (typeof processedRecord.duplicateProps !== 'object' || processedRecord.duplicateProps === null) { - processedRecord.duplicateProps = {} - } - // Ensure duplicateProps has the required shapeIds array - if (!Array.isArray(processedRecord.duplicateProps.shapeIds)) { - processedRecord.duplicateProps.shapeIds = [] - } - // Ensure duplicateProps has the required offset object - if (typeof processedRecord.duplicateProps.offset !== 'object' || processedRecord.duplicateProps.offset === null) { - processedRecord.duplicateProps.offset = { x: 0, y: 0 } - } - } - - return processedRecord - }) + }).filter((r): r is TLRecord => r !== null && r !== undefined) - console.log(`Processed ${processedRecords.length} records for loading`) + // OLD COMPLEX SANITIZATION CODE REMOVED - now using sanitizeRecord from AutomergeToTLStore + // This matches dev mode behavior exactly + + console.log(`Processed ${processedRecords.length} records for loading (using same sanitization as dev mode)`) // Debug: Log what record types we have const recordTypes = processedRecords.reduce((acc: any, r: any) => { @@ -1488,93 +574,64 @@ export function useAutomergeStoreV2({ if (processedRecords.length > 0) { console.log("Attempting to load records into store...") - // Final validation: ensure all shapes are properly structured - processedRecords.forEach(record => { - if (record.typeName === 'shape') { - // Final check for geo shapes - ALWAYS remove w/h/geo from top level (even if in props) - if (record.type === 'geo') { - // ALWAYS delete w from top level (TLDraw validation fails if it exists at top level) - if ('w' in record) { - console.log(`🔧 FINAL PRE-STORE FIX: Removing w from top level for geo shape ${record.id}`) - if (!record.props) record.props = {} - if (!('w' in record.props) || record.props.w === undefined) { - record.props.w = (record as any).w - } - delete (record as any).w - } - // ALWAYS delete h from top level - if ('h' in record) { - console.log(`🔧 FINAL PRE-STORE FIX: Removing h from top level for geo shape ${record.id}`) - if (!record.props) record.props = {} - if (!('h' in record.props) || record.props.h === undefined) { - record.props.h = (record as any).h - } - delete (record as any).h - } - // ALWAYS delete geo from top level - if ('geo' in record) { - console.log(`🔧 FINAL PRE-STORE FIX: Removing geo from top level for geo shape ${record.id}`) - if (!record.props) record.props = {} - if (!('geo' in record.props) || record.props.geo === undefined) { - record.props.geo = (record as any).geo - } - delete (record as any).geo - } - } - - // Ensure text shapes have richText - if (record.type === 'text') { - if (!record.props) { - record.props = {} - } - if (!record.props.richText) { - console.log(`🔧 Final fix: Adding richText to text shape ${record.id}`) - record.props.richText = { content: [], type: 'doc' } - } - } - } - }) - try { + // CRITICAL: Ensure page exists before adding shapes + // Get all page records from processed records + const pageRecords = processedRecords.filter(r => r.typeName === 'page') + const shapeRecords = processedRecords.filter(r => r.typeName === 'shape') + const otherRecords = processedRecords.filter(r => r.typeName !== 'page' && r.typeName !== 'shape') + + console.log(`📊 Loading order: ${pageRecords.length} pages, ${shapeRecords.length} shapes, ${otherRecords.length} other records`) + + // Ensure default page exists if no pages in data + if (pageRecords.length === 0) { + console.log(`📊 No page records found, ensuring default page exists`) + const defaultPage = { + id: 'page:page' as any, + typeName: 'page' as const, + name: 'Page 1', + index: 'a0' as any, + } + pageRecords.push(defaultPage as any) + } + store.mergeRemoteChanges(() => { - // CRITICAL: Final safety check - ensure no geo shapes have w/h/geo at top level - // Note: obsidian_vault records are already filtered out above - const sanitizedRecords = processedRecords.map(record => { - if (record.typeName === 'shape' && record.type === 'geo') { - const sanitized = { ...record } - // ALWAYS remove from top level if present - if ('w' in sanitized) { - console.log(`🔧 LAST-CHANCE FIX: Removing w from top level for geo shape ${sanitized.id}`) - if (!sanitized.props) sanitized.props = {} - if (!('w' in sanitized.props) || sanitized.props.w === undefined) { - sanitized.props.w = (sanitized as any).w - } - delete (sanitized as any).w - } - if ('h' in sanitized) { - console.log(`🔧 LAST-CHANCE FIX: Removing h from top level for geo shape ${sanitized.id}`) - if (!sanitized.props) sanitized.props = {} - if (!('h' in sanitized.props) || sanitized.props.h === undefined) { - sanitized.props.h = (sanitized as any).h - } - delete (sanitized as any).h - } - if ('geo' in sanitized) { - console.log(`🔧 LAST-CHANCE FIX: Removing geo from top level for geo shape ${sanitized.id}`) - if (!sanitized.props) sanitized.props = {} - if (!('geo' in sanitized.props) || sanitized.props.geo === undefined) { - sanitized.props.geo = (sanitized as any).geo - } - delete (sanitized as any).geo - } - return sanitized + // CRITICAL: Add pages first, then other records, then shapes + // This ensures pages exist before shapes reference them + const recordsToAdd = [...pageRecords, ...otherRecords, ...shapeRecords] + + // Verify all shapes have valid parentId pointing to an existing page + const pageIds = new Set(pageRecords.map(p => p.id)) + const shapesWithInvalidParent = recordsToAdd.filter(r => { + if (r.typeName === 'shape' && r.parentId) { + return !pageIds.has(r.parentId) } - return record + return false }) - // Put TLDraw records into store - if (sanitizedRecords.length > 0) { - store.put(sanitizedRecords) + if (shapesWithInvalidParent.length > 0) { + console.warn(`âš ī¸ Found ${shapesWithInvalidParent.length} shapes with invalid parentId, fixing...`) + shapesWithInvalidParent.forEach(shape => { + const defaultPageId = pageRecords[0]?.id || 'page:page' + console.log(`🔧 Fixing shape ${shape.id}: parentId ${shape.parentId} -> ${defaultPageId}`) + ;(shape as any).parentId = defaultPageId + }) + } + + // Put TLDraw records into store in correct order + if (recordsToAdd.length > 0) { + console.log(`📊 Adding ${recordsToAdd.length} records to store (${pageRecords.length} pages, ${recordsToAdd.filter(r => r.typeName === 'shape').length} shapes)`) + store.put(recordsToAdd) + + // Verify shapes were added + setTimeout(() => { + const allShapes = store.allRecords().filter(r => r.typeName === 'shape') + const shapesOnPages = allShapes.filter(s => { + const shape = s as any + return shape.parentId && pageIds.has(shape.parentId) + }) + console.log(`📊 Verification: Store now has ${allShapes.length} total shapes, ${shapesOnPages.length} with valid parentId`) + }, 100) } }) console.log("Successfully loaded all records into store") @@ -1586,620 +643,31 @@ export function useAutomergeStoreV2({ const failedRecords = [] for (const record of processedRecords) { - // Final validation for individual record: ensure text shapes have richText - if (record.type === 'text') { - if (!record.props) { - record.props = {} - } - if (!record.props.richText) { - console.log(`🔧 Individual fix: Adding richText to text shape ${record.id}`) - record.props.richText = { content: [], type: 'doc' } - } - } - try { - // CRITICAL: Final validation before putting record into store - if (record.typeName === 'shape' && record.type === 'geo') { - // ALWAYS remove w/h/geo from top level (TLDraw validation fails if they exist at top level) - if ('w' in record) { - console.log(`🔧 INDIVIDUAL PRE-STORE FIX: Removing w from top level for geo shape ${record.id}`) - if (!record.props) record.props = {} - if (!('w' in record.props) || record.props.w === undefined) { - record.props.w = (record as any).w - } - delete (record as any).w - } - if ('h' in record) { - console.log(`🔧 INDIVIDUAL PRE-STORE FIX: Removing h from top level for geo shape ${record.id}`) - if (!record.props) record.props = {} - if (!('h' in record.props) || record.props.h === undefined) { - record.props.h = (record as any).h - } - delete (record as any).h - } - if ('geo' in record) { - console.log(`🔧 INDIVIDUAL PRE-STORE FIX: Removing geo from top level for geo shape ${record.id}`) - if (!record.props) record.props = {} - if (!('geo' in record.props) || record.props.geo === undefined) { - record.props.geo = (record as any).geo - } - delete (record as any).geo - } - - // CRITICAL: props.geo is REQUIRED for geo shapes - TLDraw validation will fail without it - // Ensure it's always set, defaulting to 'rectangle' if missing - if (!record.props) record.props = {} - if (!record.props.geo || record.props.geo === undefined || record.props.geo === null) { - record.props.geo = 'rectangle' - } - - // CRITICAL: props.dash is REQUIRED for geo shapes - TLDraw validation will fail without it - // Ensure it's always set, defaulting to 'draw' if missing - if (!record.props.dash || record.props.dash === undefined || record.props.dash === null) { - record.props.dash = 'draw' - } - } - - // CRITICAL: Final safety check - ensure no geo shapes have w/h/geo at top level - let recordToPut = record - if (record.typeName === 'shape' && record.type === 'geo') { - // Store values before removing from top level - const wValue = 'w' in record ? (record as any).w : undefined - const hValue = 'h' in record ? (record as any).h : undefined - const geoValue = 'geo' in record ? (record as any).geo : undefined - - // Create cleaned record without w/h/geo at top level - const cleaned: any = {} - for (const key in record) { - if (key !== 'w' && key !== 'h' && key !== 'geo') { - cleaned[key] = (record as any)[key] - } - } - - // Ensure props exists and move values there if needed - if (!cleaned.props) cleaned.props = {} - if (wValue !== undefined && (!('w' in cleaned.props) || cleaned.props.w === undefined)) { - cleaned.props.w = wValue - } - if (hValue !== undefined && (!('h' in cleaned.props) || cleaned.props.h === undefined)) { - cleaned.props.h = hValue - } - if (geoValue !== undefined && (!('geo' in cleaned.props) || cleaned.props.geo === undefined)) { - cleaned.props.geo = geoValue - } - - recordToPut = cleaned as any - } - store.mergeRemoteChanges(() => { - store.put([recordToPut]) + store.put([record]) }) successCount++ console.log(`✅ Successfully loaded record ${record.id} (${record.typeName})`) } catch (individualError) { console.error(`❌ Failed to load record ${record.id} (${record.typeName}):`, individualError) - console.log("Problematic record structure:", { - id: record.id, - typeName: record.typeName, - type: record.type, - hasW: 'w' in record, - hasH: 'h' in record, - w: record.w, - h: record.h, - propsW: record.props?.w, - propsH: record.props?.h, - allKeys: Object.keys(record) - }) failedRecords.push(record) } } - // Only log if there are failures or many records if (successCount < processedRecords.length || processedRecords.length > 50) { console.log(`Successfully loaded ${successCount} out of ${processedRecords.length} records`) } - // Only log if debugging is needed - // console.log(`Failed records: ${failedRecords.length}`, failedRecords.map(r => r.id)) - - // Try to fix and reload failed records - if (failedRecords.length > 0) { - // Only log if debugging is needed - // console.log("Attempting to fix and reload failed records...") - for (const record of failedRecords) { - try { - // Additional cleanup for failed records - create deep copy - let fixedRecord = JSON.parse(JSON.stringify(record)) - - // Fix instance records specifically - if (fixedRecord.typeName === 'instance') { - if (!fixedRecord.meta) fixedRecord.meta = {} - if (!Array.isArray(fixedRecord.insets)) { - fixedRecord.insets = [false, false, false, false] - } - if (!Array.isArray(fixedRecord.scribbles)) { - fixedRecord.scribbles = [] - } - if (typeof fixedRecord.duplicateProps !== 'object' || fixedRecord.duplicateProps === null) { - fixedRecord.duplicateProps = {} - } - if (!Array.isArray(fixedRecord.duplicateProps.shapeIds)) { - fixedRecord.duplicateProps.shapeIds = [] - } - if (typeof fixedRecord.duplicateProps.offset !== 'object' || fixedRecord.duplicateProps.offset === null) { - fixedRecord.duplicateProps.offset = { x: 0, y: 0 } - } - } - - // Remove any remaining top-level w/h properties for shapes (except arrow, draw, and text) - if (fixedRecord.typeName === 'shape') { - if (fixedRecord.type !== 'arrow' && fixedRecord.type !== 'draw' && fixedRecord.type !== 'text') { - if ('w' in fixedRecord) { - if (!fixedRecord.props) fixedRecord.props = {} - fixedRecord.props.w = fixedRecord.w - delete (fixedRecord as any).w - } - if ('h' in fixedRecord) { - if (!fixedRecord.props) fixedRecord.props = {} - fixedRecord.props.h = fixedRecord.h - delete (fixedRecord as any).h - } - } else if (fixedRecord.type === 'text') { - // Text shapes only need w, not h - if ('w' in fixedRecord) { - if (!fixedRecord.props) fixedRecord.props = {} - fixedRecord.props.w = fixedRecord.w - delete (fixedRecord as any).w - } - if ('h' in fixedRecord) { - delete (fixedRecord as any).h - } - } else { - // For arrow and draw shapes, remove w/h entirely - if ('w' in fixedRecord) { - delete (fixedRecord as any).w - } - if ('h' in fixedRecord) { - delete (fixedRecord as any).h - } - } - } - - // Comprehensive richText validation - ensure it's always an object with content and type for text shapes - if (fixedRecord.type === 'text' && fixedRecord.props) { - if (fixedRecord.props.richText !== undefined) { - if (!Array.isArray(fixedRecord.props.richText)) { - console.log(`🔧 Fixing richText for text shape ${fixedRecord.id}: was ${typeof fixedRecord.props.richText}, setting to proper object`) - fixedRecord.props.richText = { content: [], type: 'doc' } - } else { - // If it's an array, convert to proper richText object structure - console.log(`🔧 Converting richText array to object for text shape ${fixedRecord.id}`) - fixedRecord.props.richText = { content: fixedRecord.props.richText, type: 'doc' } - } - } else { - // Text shapes must have richText as an object - console.log(`🔧 Creating default richText object for text shape ${fixedRecord.id}`) - fixedRecord.props.richText = { content: [], type: 'doc' } - } - } else if (fixedRecord.type === 'text' && !fixedRecord.props) { - // Ensure props object exists for text shapes - fixedRecord.props = { richText: { content: [], type: 'doc' } } - } - - // Fix text shapes - ensure they have required properties including color - if (fixedRecord.type === 'text') { - if (!fixedRecord.props.color) { - console.log(`🔧 Adding missing color property for text shape ${fixedRecord.id}`) - fixedRecord.props.color = 'black' - } - if (!fixedRecord.props.size) { - fixedRecord.props.size = 'm' - } - if (!fixedRecord.props.font) { - fixedRecord.props.font = 'draw' - } - if (!fixedRecord.props.textAlign) { - fixedRecord.props.textAlign = 'start' - } - if (!fixedRecord.props.w) { - fixedRecord.props.w = 100 - } - if (fixedRecord.props.scale === undefined) { - fixedRecord.props.scale = 1 - } - if (fixedRecord.props.autoSize === undefined) { - fixedRecord.props.autoSize = false - } - if (!fixedRecord.props.richText) { - console.log(`🔧 Creating default richText object for text shape ${fixedRecord.id}`) - fixedRecord.props.richText = { content: [], type: 'doc' } - } - - // Remove invalid properties for text shapes (matching default text shape schema) - // Note: richText is actually required for text shapes, so don't remove it - const invalidTextProps = ['h', 'geo', 'insets', 'scribbles', 'isMinimized', 'roomUrl', 'text', 'align', 'verticalAlign', 'growY', 'url'] - invalidTextProps.forEach(prop => { - if (prop in fixedRecord.props) { - console.log(`🔧 Removing invalid prop '${prop}' from text shape ${fixedRecord.id}`) - delete (fixedRecord.props as any)[prop] - } - }) - } - - // Fix embed shapes - ensure they have required properties and remove invalid ones - if (fixedRecord.type === 'Embed' || fixedRecord.type === 'embed') { - if (!fixedRecord.props.url) { - console.log(`🔧 Adding missing url property for embed shape ${fixedRecord.id}`) - fixedRecord.props.url = '' - } - if (!fixedRecord.props.w) { - fixedRecord.props.w = 400 - } - if (!fixedRecord.props.h) { - fixedRecord.props.h = 300 - } - if (fixedRecord.props.isMinimized === undefined) { - fixedRecord.props.isMinimized = false - } - - // Remove invalid properties for embed shapes (matching custom EmbedShape schema) - const invalidEmbedProps = ['doesResize', 'doesResizeHeight', 'roomUrl', 'roomId', 'color', 'fill', 'dash', 'size', 'text', 'font', 'align', 'verticalAlign', 'growY', 'richText'] - invalidEmbedProps.forEach(prop => { - if (prop in fixedRecord.props) { - console.log(`🔧 Removing invalid prop '${prop}' from embed shape ${fixedRecord.id}`) - delete (fixedRecord.props as any)[prop] - } - }) - } - - // Remove any other problematic properties from shapes - const invalidProps = ['insets', 'scribbles', 'geo'] - invalidProps.forEach(prop => { - if (prop in fixedRecord) { - delete (fixedRecord as any)[prop] - } - }) - - // Final validation - ensure all required properties exist - if (fixedRecord.typeName === 'shape') { - // Ensure basic required properties - if (fixedRecord.x === undefined) fixedRecord.x = 0 - if (fixedRecord.y === undefined) fixedRecord.y = 0 - if (fixedRecord.rotation === undefined) fixedRecord.rotation = 0 - if (fixedRecord.isLocked === undefined) fixedRecord.isLocked = false - if (fixedRecord.opacity === undefined) fixedRecord.opacity = 1 - if (!fixedRecord.meta) fixedRecord.meta = {} - - // CRITICAL: Final geo shape validation - ALWAYS remove w/h/geo from top level - if (fixedRecord.type === 'geo') { - // Store values before removing from top level - const wValue = 'w' in fixedRecord ? (fixedRecord as any).w : undefined - const hValue = 'h' in fixedRecord ? (fixedRecord as any).h : undefined - const geoValue = 'geo' in fixedRecord ? (fixedRecord as any).geo : undefined - - // Ensure props exists - if (!fixedRecord.props) fixedRecord.props = {} - - // ALWAYS remove w from top level (even if value is 0 or undefined) - if ('w' in fixedRecord) { - if (!('w' in fixedRecord.props) || fixedRecord.props.w === undefined) { - fixedRecord.props.w = wValue !== undefined ? wValue : 100 - } - delete (fixedRecord as any).w - } - - // ALWAYS remove h from top level (even if value is 0 or undefined) - if ('h' in fixedRecord) { - if (!('h' in fixedRecord.props) || fixedRecord.props.h === undefined) { - fixedRecord.props.h = hValue !== undefined ? hValue : 100 - } - delete (fixedRecord as any).h - } - - // ALWAYS remove geo from top level (even if value is undefined) - if ('geo' in fixedRecord) { - if (!('geo' in fixedRecord.props) || fixedRecord.props.geo === undefined) { - fixedRecord.props.geo = geoValue !== undefined ? geoValue : 'rectangle' - } - delete (fixedRecord as any).geo - } - - // Ensure geo property exists in props - if (!fixedRecord.props.geo) { - fixedRecord.props.geo = 'rectangle' - } - - // Ensure w and h are in props - if (fixedRecord.props.w === undefined) fixedRecord.props.w = 100 - if (fixedRecord.props.h === undefined) fixedRecord.props.h = 100 - } - - // Ensure parentId exists - if (!fixedRecord.parentId) { - const pageRecord = records.find((r: any) => r.typeName === 'page') as any - if (pageRecord && pageRecord.id) { - fixedRecord.parentId = pageRecord.id - } - } - - // Ensure props object exists - if (!fixedRecord.props) fixedRecord.props = {} - - // Ensure w and h exist in props (except for arrow, draw, line, text, note, and group shapes) - if (fixedRecord.type !== 'arrow' && fixedRecord.type !== 'draw' && fixedRecord.type !== 'line' && fixedRecord.type !== 'text' && fixedRecord.type !== 'note' && fixedRecord.type !== 'group') { - if (fixedRecord.props.w === undefined) fixedRecord.props.w = 100 - if (fixedRecord.props.h === undefined) fixedRecord.props.h = 100 - } else if (fixedRecord.type === 'text') { - // Text shapes only need w, not h - if (fixedRecord.props.w === undefined) fixedRecord.props.w = 100 - } else if (fixedRecord.type === 'line') { - // Line shapes should NOT have w or h properties - if ('w' in fixedRecord.props) { - console.log(`🔧 FINAL FIX: Removing invalid w property from line shape ${fixedRecord.id}`) - delete fixedRecord.props.w - } - if ('h' in fixedRecord.props) { - console.log(`🔧 FINAL FIX: Removing invalid h property from line shape ${fixedRecord.id}`) - delete fixedRecord.props.h - } - - // Ensure line shapes have valid points - if (!fixedRecord.props.points || typeof fixedRecord.props.points !== 'object' || Array.isArray(fixedRecord.props.points)) { - console.log(`🔧 FINAL FIX: Creating default points for line shape ${fixedRecord.id}`) - fixedRecord.props.points = { - 'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 }, - 'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 } - } - } else { - // Validate points - const validPoints: Record = {} - let pointIndex = 0 - const indices = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10'] - - for (const [key, point] of Object.entries(fixedRecord.props.points)) { - if (point && typeof point === 'object' && - typeof (point as any).x === 'number' && - typeof (point as any).y === 'number' && - !isNaN((point as any).x) && !isNaN((point as any).y)) { - const index = indices[pointIndex] || `a${pointIndex + 1}` - validPoints[index] = { - id: index, - index: index as any, - x: (point as any).x, - y: (point as any).y - } - pointIndex++ - } - } - - if (Object.keys(validPoints).length === 0) { - fixedRecord.props.points = { - 'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 }, - 'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 } - } - } else if (Object.keys(validPoints).length === 1) { - const firstPoint = Object.values(validPoints)[0] - const secondIndex = indices[1] || 'a2' - validPoints[secondIndex] = { - id: secondIndex, - index: secondIndex as any, - x: firstPoint.x + 100, - y: firstPoint.y - } - fixedRecord.props.points = validPoints - } else { - fixedRecord.props.points = validPoints - } - } - - // Ensure other required line shape properties - if (!fixedRecord.props.color) fixedRecord.props.color = 'black' - if (!fixedRecord.props.dash) fixedRecord.props.dash = 'draw' - if (!fixedRecord.props.size) fixedRecord.props.size = 'm' - if (!fixedRecord.props.spline) fixedRecord.props.spline = 'line' - if (fixedRecord.props.scale === undefined || fixedRecord.props.scale === null) { - fixedRecord.props.scale = 1 - } - } else if (fixedRecord.type === 'note') { - // Note shapes should NOT have w or h properties, but DO need richText - if ('w' in fixedRecord.props) { - console.log(`🔧 FINAL FIX: Removing invalid w property from note shape ${fixedRecord.id}`) - delete fixedRecord.props.w - } - if ('h' in fixedRecord.props) { - console.log(`🔧 FINAL FIX: Removing invalid h property from note shape ${fixedRecord.id}`) - delete fixedRecord.props.h - } - // Note shapes REQUIRE richText property - if (!fixedRecord.props.richText || typeof fixedRecord.props.richText !== 'object') { - console.log(`🔧 FINAL FIX: Adding missing richText property for note shape ${fixedRecord.id}`) - fixedRecord.props.richText = { content: [], type: 'doc' } - } - } else if (fixedRecord.type === 'group') { - // Group shapes should NOT have w or h properties - if ('w' in fixedRecord.props) { - console.log(`🔧 FINAL FIX: Removing invalid w property from group shape ${fixedRecord.id}`) - delete fixedRecord.props.w - } - if ('h' in fixedRecord.props) { - console.log(`🔧 FINAL FIX: Removing invalid h property from group shape ${fixedRecord.id}`) - delete fixedRecord.props.h - } - } - } - - // CRITICAL: Final safety check - ensure no geo shapes have w/h/geo at top level - if (fixedRecord.typeName === 'shape' && fixedRecord.type === 'geo') { - // Store values before removing from top level - const wValue = 'w' in fixedRecord ? (fixedRecord as any).w : undefined - const hValue = 'h' in fixedRecord ? (fixedRecord as any).h : undefined - const geoValue = 'geo' in fixedRecord ? (fixedRecord as any).geo : undefined - - // Create cleaned record without w/h/geo at top level - const cleaned: any = {} - for (const key in fixedRecord) { - if (key !== 'w' && key !== 'h' && key !== 'geo') { - cleaned[key] = (fixedRecord as any)[key] - } - } - - // Ensure props exists and move values there if needed - if (!cleaned.props) cleaned.props = {} - if (wValue !== undefined && (!('w' in cleaned.props) || cleaned.props.w === undefined)) { - cleaned.props.w = wValue - } - if (hValue !== undefined && (!('h' in cleaned.props) || cleaned.props.h === undefined)) { - cleaned.props.h = hValue - } - if (geoValue !== undefined && (!('geo' in cleaned.props) || cleaned.props.geo === undefined)) { - cleaned.props.geo = geoValue - } - - fixedRecord = cleaned as any - } - - // CRITICAL: Final safety check - ensure text shapes don't have props.text (TLDraw schema doesn't allow it) - // Text shapes should only use props.richText, not props.text - if (fixedRecord.typeName === 'shape' && fixedRecord.type === 'text' && fixedRecord.props && 'text' in fixedRecord.props) { - delete (fixedRecord.props as any).text - } - - store.mergeRemoteChanges(() => { - store.put([fixedRecord]) - }) - console.log(`✅ Successfully loaded fixed record ${fixedRecord.id}`) - successCount++ - } catch (retryError) { - console.error(`❌ Still failed to load record ${record.id} after fix attempt:`, retryError) - } - } - } } } - - // Verify loading - track ALL shape types that were successfully loaded - const storeRecords = store.allRecords() - const shapes = storeRecords.filter(r => r.typeName === 'shape') - const shapeTypeCountsAfter = shapes.reduce((acc: any, r: any) => { - const type = (r as any).type || 'unknown' - acc[type] = (acc[type] || 0) + 1 - return acc - }, {}) - console.log(`📊 Store verification: ${processedRecords.length} processed records, ${storeRecords.length} total store records, ${shapes.length} shapes`) - console.log(`📊 Shape type breakdown after loading:`, shapeTypeCountsAfter) - - // Debug: Check if shapes have the right structure - if (shapes.length > 0) { - console.log("📊 Sample loaded shape:", { - id: shapes[0].id, - type: shapes[0].type, - x: shapes[0].x, - y: shapes[0].y, - hasProps: !!shapes[0].props, - propsKeys: shapes[0].props ? Object.keys(shapes[0].props) : [], - allKeys: Object.keys(shapes[0]) - }) - - // Validate all shapes have proper structure - const invalidShapes = shapes.filter(shape => { - const issues = [] - if (!shape.props) issues.push('missing props') - // Only check w/h for shapes that actually need them - const shapesWithoutWH = ['arrow', 'draw', 'text', 'note', 'line'] - if (!shapesWithoutWH.includes(shape.type) && (!(shape.props as any)?.w || !(shape.props as any)?.h)) { - issues.push('missing w/h in props') - } - if ('w' in shape || 'h' in shape) { - issues.push('w/h at top level instead of props') - } - return issues.length > 0 - }) - - if (invalidShapes.length > 0) { - console.warn(`âš ī¸ Found ${invalidShapes.length} shapes with structural issues:`, invalidShapes.map(s => ({ - id: s.id, - type: s.type, - issues: { - missingProps: !s.props, - missingWH: s.type !== 'arrow' && s.type !== 'draw' && (!(s.props as any)?.w || !(s.props as any)?.h), - topLevelWH: 'w' in s || 'h' in s - } - }))) - } - } - - // Debug: Check for any shapes that might have validation issues - const shapesWithTopLevelW = shapes.filter(s => 'w' in s) - const shapesWithTopLevelH = shapes.filter(s => 'h' in s) - if (shapesWithTopLevelW.length > 0 || shapesWithTopLevelH.length > 0) { - console.warn(`📊 Found ${shapesWithTopLevelW.length} shapes with top-level w, ${shapesWithTopLevelH.length} with top-level h`) - - // Fix shapes with top-level w/h properties - shapesWithTopLevelW.forEach(shape => { - console.log(`🔧 Fixing shape ${shape.id} with top-level w property`) - if (!shape.props) shape.props = {} - ;(shape.props as any).w = (shape as any).w - delete (shape as any).w - }) - - shapesWithTopLevelH.forEach(shape => { - console.log(`🔧 Fixing shape ${shape.id} with top-level h property`) - if (!shape.props) shape.props = {} - ;(shape.props as any).h = (shape as any).h - delete (shape as any).h - }) - } - - if (shapes.length === 0) { - // Only log if debugging is needed - // console.log("No store data found in Automerge document") - } } - // Only log if debugging is needed - // console.log("Setting store status to synced-remote") setStoreWithStatus({ store, status: "synced-remote", connectionStatus: "online", }) } catch (error) { - console.error("Error initializing store from Automerge:", error) - - // Try to recover by creating a minimal valid store - try { - console.log("Attempting to recover with minimal store...") - const minimalStore = createTLStore({ - schema: customSchema, - }) - - // Add basic page and camera records - minimalStore.mergeRemoteChanges(() => { - minimalStore.put([ - { - id: 'page:page' as any, - typeName: 'page', - name: 'Page', - index: 'a0' as any, - meta: {} - }, - { - id: 'camera:page:page' as any, - typeName: 'camera', - x: 0, - y: 0, - z: 1, - meta: {} - } - ]) - }) - - setStoreWithStatus({ - store: minimalStore, - status: "synced-remote", - connectionStatus: "offline", - error: error instanceof Error ? error : new Error("Store initialization failed, using minimal store") as any, - }) - } catch (recoveryError) { - console.error("Failed to recover with minimal store:", recoveryError) + console.error("Error initializing store from Automerge:", error) setStoreWithStatus({ store, status: "not-synced", @@ -2207,44 +675,43 @@ export function useAutomergeStoreV2({ }) } } - } - - initializeStore() - - return () => { - unsubs.forEach((unsub) => unsub()) - } - }, [handle, store]) - - /* -------------------- Presence -------------------- */ - // Create a safe handle that won't cause null errors - const safeHandle = handle || { - on: () => {}, - off: () => {}, - removeListener: () => {}, - whenReady: () => Promise.resolve(), - doc: () => null, - change: () => {}, - broadcast: () => {}, - } as any - - const [, updateLocalState] = useLocalAwareness({ - handle: safeHandle, - userId: _userId, - initialState: {}, - }) - - const [peerStates] = useRemoteAwareness({ - handle: safeHandle, - localUserId: _userId, - }) - - return { - ...storeWithStatus, - store, - } as TLStoreWithStatus -} - + + initializeStore() + + return () => { + unsubs.forEach((unsub) => unsub()) + } + }, [handle, store]) + + /* -------------------- Presence -------------------- */ + // Create a safe handle that won't cause null errors + const safeHandle = handle || { + on: () => {}, + off: () => {}, + removeListener: () => {}, + whenReady: () => Promise.resolve(), + doc: () => null, + change: () => {}, + broadcast: () => {}, + } as any + + const [, updateLocalState] = useLocalAwareness({ + handle: safeHandle, + userId: _userId, + initialState: {}, + }) + + const [peerStates] = useRemoteAwareness({ + handle: safeHandle, + localUserId: _userId, + }) + + return { + ...storeWithStatus, + store, + } as TLStoreWithStatus + } + // Presence hook (simplified version) export function useAutomergePresence(params: { handle: DocHandle | null @@ -2273,4 +740,4 @@ export function useAutomergePresence(params: { updatePresence: () => {}, presence: {}, } -} \ No newline at end of file +} diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index 23681fb..3b6831a 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -469,11 +469,17 @@ export function Board() { // Initial check after a short delay to ensure editor is ready setTimeout(checkAndFixMissingShapes, 500) + // Also check after shapes are loaded (give more time for initial load) + setTimeout(checkAndFixMissingShapes, 2000) + setTimeout(checkAndFixMissingShapes, 5000) + // Listen to store changes to continuously monitor for missing shapes // Listen to ALL sources (user, remote, etc.) to catch shapes loaded from Automerge + let checkTimeout: NodeJS.Timeout | null = null const unsubscribe = store.store.listen(() => { // Use consistent debounce for both dev and prod - setTimeout(checkAndFixMissingShapes, 500) + if (checkTimeout) clearTimeout(checkTimeout) + checkTimeout = setTimeout(checkAndFixMissingShapes, 500) }) return () => {