latest
This commit is contained in:
@@ -123,37 +123,73 @@ export default function SinglePlayerPage() {
|
||||
logger.debug('single-player-page', '✅ TLStore created');
|
||||
|
||||
// 2. Initialize snapshot service
|
||||
const snapshotService = new NavigationSnapshotService(newStore);
|
||||
const snapshotService = new NavigationSnapshotService(newStore, editorRef.current || undefined);
|
||||
snapshotServiceRef.current = snapshotService;
|
||||
logger.debug('single-player-page', '✨ Initialized NavigationSnapshotService');
|
||||
|
||||
// 3. Load initial snapshot if we have a node
|
||||
if (context.node) {
|
||||
logger.debug('single-player-page', '📥 Loading snapshot from database', {
|
||||
dbName: user.user_db_name,
|
||||
tldraw_snapshot: context.node.tldraw_snapshot,
|
||||
user_type: user.user_type,
|
||||
username: user.username
|
||||
});
|
||||
const nodeStoragePath = getNodeStoragePath(context.node);
|
||||
if (nodeStoragePath) {
|
||||
logger.debug('single-player-page', '📥 Loading snapshot from database', {
|
||||
dbName: user.user_db_name,
|
||||
node: context.node,
|
||||
node_storage_path: nodeStoragePath,
|
||||
user_type: user.user_type,
|
||||
username: user.username
|
||||
});
|
||||
|
||||
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
||||
context.node.tldraw_snapshot,
|
||||
user.user_db_name,
|
||||
newStore,
|
||||
setLoadingState
|
||||
);
|
||||
logger.debug('single-player-page', '✅ Snapshot loaded from database');
|
||||
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
||||
nodeStoragePath,
|
||||
user.user_db_name,
|
||||
newStore,
|
||||
setLoadingState,
|
||||
undefined, // sharedStore
|
||||
editorRef.current || undefined // editor
|
||||
);
|
||||
logger.debug('single-player-page', '✅ Snapshot loaded from database');
|
||||
} else {
|
||||
logger.debug('single-player-page', '⚠️ No node_storage_path found in node, skipping snapshot load', {
|
||||
node: context.node
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.debug('single-player-page', '⚠️ No node in context, skipping snapshot load');
|
||||
}
|
||||
|
||||
// 4. Set up auto-save
|
||||
// 4. Set up auto-save with debouncing (only after initial load is complete)
|
||||
let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let isAutoSaving = false;
|
||||
|
||||
newStore.listen(() => {
|
||||
if (snapshotServiceRef.current && context.node) {
|
||||
logger.debug('single-player-page', '💾 Auto-saving changes');
|
||||
snapshotServiceRef.current.forceSaveCurrentNode().catch(error => {
|
||||
logger.error('single-player-page', '❌ Auto-save failed', error);
|
||||
});
|
||||
if (snapshotServiceRef.current && context.node && snapshotServiceRef.current.getCurrentNodePath()) {
|
||||
// Skip if already saving
|
||||
if (isAutoSaving) {
|
||||
logger.debug('single-player-page', '⚠️ Skipping auto-save - already saving');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (autoSaveTimeout) {
|
||||
clearTimeout(autoSaveTimeout);
|
||||
}
|
||||
|
||||
// Debounce auto-save to prevent excessive saves
|
||||
autoSaveTimeout = setTimeout(async () => {
|
||||
if (isAutoSaving) return; // Double-check
|
||||
|
||||
isAutoSaving = true;
|
||||
try {
|
||||
logger.debug('single-player-page', '💾 Auto-saving changes (debounced)');
|
||||
await snapshotServiceRef.current?.forceSaveCurrentNode();
|
||||
} catch (error) {
|
||||
logger.error('single-player-page', '❌ Auto-save failed', error);
|
||||
} finally {
|
||||
isAutoSaving = false;
|
||||
}
|
||||
}, 2000); // Increased to 2 seconds debounce
|
||||
} else if (snapshotServiceRef.current && context.node && !snapshotServiceRef.current.getCurrentNodePath()) {
|
||||
logger.debug('single-player-page', '⚠️ Skipping auto-save - no current node path set yet');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -185,12 +221,43 @@ export default function SinglePlayerPage() {
|
||||
};
|
||||
|
||||
initializeStoreAndSnapshot();
|
||||
}, [isEditorReady, user, context.node, editorRef.current]);
|
||||
}, [isEditorReady, user, context.node]);
|
||||
|
||||
// Handle initial node placement
|
||||
useEffect(() => {
|
||||
const placeInitialNode = async () => {
|
||||
if (!context.node || !editorRef.current || !store || !isInitialLoad) {
|
||||
logger.debug('single-player-page', '⚠️ Skipping placeInitialNode - missing dependencies', {
|
||||
hasNode: !!context.node,
|
||||
hasEditor: !!editorRef.current,
|
||||
hasStore: !!store,
|
||||
isInitialLoad
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: Log the actual node structure
|
||||
logger.debug('single-player-page', '🔍 Node structure for placeInitialNode', {
|
||||
node: context.node,
|
||||
nodeKeys: Object.keys(context.node),
|
||||
hasId: !!context.node.id,
|
||||
hasStoragePath: !!context.node.node_storage_path,
|
||||
hasData: !!context.node.data,
|
||||
dataKeys: context.node.data ? Object.keys(context.node.data) : null
|
||||
});
|
||||
|
||||
// Validate that the node has required properties
|
||||
const nodeStoragePath = getNodeStoragePath(context.node);
|
||||
if (!context.node.id || !nodeStoragePath) {
|
||||
logger.error('single-player-page', '❌ Node missing required properties', {
|
||||
nodeId: context.node.id,
|
||||
hasStoragePath: !!nodeStoragePath,
|
||||
node: context.node
|
||||
});
|
||||
setLoadingState({
|
||||
status: 'error',
|
||||
error: 'Node is missing required information'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -231,7 +298,7 @@ export default function SinglePlayerPage() {
|
||||
setLoadingState({ status: 'loading', error: '' });
|
||||
logger.debug('single-player-page', '🔄 Loading node data', {
|
||||
nodeId: currentNode.id,
|
||||
tldraw_snapshot: currentNode.tldraw_snapshot,
|
||||
node_storage_path: currentNode.node_storage_path,
|
||||
isInitialLoad
|
||||
});
|
||||
|
||||
@@ -258,7 +325,7 @@ export default function SinglePlayerPage() {
|
||||
};
|
||||
|
||||
handleNodeChange();
|
||||
}, [context.node?.id, context.history, store]);
|
||||
}, [context.node, context.history, store, isInitialLoad]);
|
||||
|
||||
// Initialize preferences when user is available
|
||||
useEffect(() => {
|
||||
@@ -270,7 +337,7 @@ export default function SinglePlayerPage() {
|
||||
|
||||
// Redirect if no user or incorrect role
|
||||
useEffect(() => {
|
||||
if (!user || user.user_type !== 'admin') {
|
||||
if (!user || !['admin', 'email_teacher', 'school_admin', 'teacher'].includes(user.user_type || '')) {
|
||||
logger.info('single-player-page', '🚪 Redirecting to home - no user or incorrect role', {
|
||||
hasUser: !!user,
|
||||
userType: user?.user_type
|
||||
@@ -467,6 +534,11 @@ export default function SinglePlayerPage() {
|
||||
editorRef.current = editor;
|
||||
logger.debug('single-player-page', '✅ Editor ref set');
|
||||
|
||||
// Update snapshot service with editor reference
|
||||
if (snapshotServiceRef.current) {
|
||||
snapshotServiceRef.current.setEditor(editor);
|
||||
}
|
||||
|
||||
setIsEditorReady(true);
|
||||
logger.info('single-player-page', '✅ Tldraw mounted successfully', {
|
||||
editorId: editor.store.id,
|
||||
@@ -482,33 +554,89 @@ export default function SinglePlayerPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to safely extract node_storage_path from different node structures
|
||||
const getNodeStoragePath = (node: NavigationNode): string | null => {
|
||||
// Try direct access first
|
||||
if (node.node_storage_path) {
|
||||
return node.node_storage_path;
|
||||
}
|
||||
|
||||
// Try nested under data
|
||||
if (node.data?.node_storage_path) {
|
||||
return node.data.node_storage_path;
|
||||
}
|
||||
|
||||
// Try other possible locations
|
||||
if (node.data?.storage_path && typeof node.data.storage_path === 'string') {
|
||||
return node.data.storage_path;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const loadNodeData = async (node: NavigationNode): Promise<NodeData> => {
|
||||
// 1. Always fetch fresh data
|
||||
const dbName = UserNeoDBService.getNodeDatabaseName(node);
|
||||
const fetchedData = await UserNeoDBService.fetchNodeData(node.id, dbName);
|
||||
// Validate the node parameter
|
||||
if (!node) {
|
||||
throw new Error('Node parameter is required');
|
||||
}
|
||||
|
||||
if (!node.id) {
|
||||
throw new Error('Node must have an ID');
|
||||
}
|
||||
|
||||
const nodeStoragePath = getNodeStoragePath(node);
|
||||
if (!nodeStoragePath) {
|
||||
throw new Error(`Node ${node.id} is missing node_storage_path`);
|
||||
}
|
||||
|
||||
if (!fetchedData?.node_data) {
|
||||
throw new Error('Failed to fetch node data');
|
||||
logger.debug('single-player-page', '🔄 Loading node data', {
|
||||
nodeId: node.id,
|
||||
nodeType: node.type,
|
||||
nodeLabel: node.label,
|
||||
nodeStoragePath: nodeStoragePath
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. Always fetch fresh data
|
||||
// Create a temporary node object with the correct structure for the service
|
||||
const normalizedNode = {
|
||||
...node,
|
||||
node_storage_path: nodeStoragePath
|
||||
};
|
||||
|
||||
const dbName = UserNeoDBService.getNodeDatabaseName(normalizedNode);
|
||||
const fetchedData = await UserNeoDBService.fetchNodeData(node.id, dbName);
|
||||
|
||||
if (!fetchedData?.node_data) {
|
||||
throw new Error('Failed to fetch node data');
|
||||
}
|
||||
|
||||
// 2. Process the data into the correct shape
|
||||
const theme = getThemeFromLabel(node.type);
|
||||
return {
|
||||
...fetchedData.node_data,
|
||||
title: String(fetchedData.node_data.title || node.label || ''),
|
||||
w: 500,
|
||||
h: 350,
|
||||
state: {
|
||||
parentId: null,
|
||||
isPageChild: true,
|
||||
hasChildren: null,
|
||||
bindings: null
|
||||
},
|
||||
headerColor: theme.headerColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
isLocked: false,
|
||||
__primarylabel__: node.type,
|
||||
uuid_string: node.id,
|
||||
node_storage_path: nodeStoragePath
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('single-player-page', '❌ Error in loadNodeData', {
|
||||
nodeId: node.id,
|
||||
nodeType: node.type,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 2. Process the data into the correct shape
|
||||
const theme = getThemeFromLabel(node.type);
|
||||
return {
|
||||
...fetchedData.node_data,
|
||||
title: fetchedData.node_data.title || node.label,
|
||||
w: 500,
|
||||
h: 350,
|
||||
state: {
|
||||
parentId: null,
|
||||
isPageChild: true,
|
||||
hasChildren: null,
|
||||
bindings: null
|
||||
},
|
||||
headerColor: theme.headerColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
isLocked: false,
|
||||
__primarylabel__: node.type,
|
||||
unique_id: node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user