fix: remove TLStore from useState and dead state vars in singlePlayerPage
This commit is contained in:
parent
bf592886c6
commit
d3bd25d544
@ -15,7 +15,6 @@ import { useUser } from '../../contexts/UserContext';
|
|||||||
// Tldraw services
|
// Tldraw services
|
||||||
import { localStoreService } from '../../services/tldraw/localStoreService';
|
import { localStoreService } from '../../services/tldraw/localStoreService';
|
||||||
import { PresentationService } from '../../services/tldraw/presentationService';
|
import { PresentationService } from '../../services/tldraw/presentationService';
|
||||||
import { NodeCanvasService } from '../../services/tldraw/nodeCanvasService';
|
|
||||||
import { NavigationSnapshotService } from '../../services/tldraw/snapshotService';
|
import { NavigationSnapshotService } from '../../services/tldraw/snapshotService';
|
||||||
// Tldraw utils
|
// Tldraw utils
|
||||||
import { getUiOverrides, getUiComponents } from '../../utils/tldraw/ui-overrides';
|
import { getUiOverrides, getUiComponents } from '../../utils/tldraw/ui-overrides';
|
||||||
@ -34,9 +33,6 @@ import '../../utils/tldraw/tldraw.css';
|
|||||||
// App debug
|
// App debug
|
||||||
import { logger } from '../../debugConfig';
|
import { logger } from '../../debugConfig';
|
||||||
import { CircularProgress, Alert, Snackbar } from '@mui/material';
|
import { CircularProgress, Alert, Snackbar } from '@mui/material';
|
||||||
import { getThemeFromLabel } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-styles';
|
|
||||||
import { NodeData } from '../../types/graph-shape';
|
|
||||||
import { NavigationNode } from '../../types/navigation';
|
|
||||||
|
|
||||||
interface LoadingState {
|
interface LoadingState {
|
||||||
status: 'ready' | 'loading' | 'error';
|
status: 'ready' | 'loading' | 'error';
|
||||||
@ -63,13 +59,12 @@ export default function SinglePlayerPage() {
|
|||||||
const snapshotServiceRef = useRef<NavigationSnapshotService | null>(null);
|
const snapshotServiceRef = useRef<NavigationSnapshotService | null>(null);
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [loadingState, setLoadingState] = useState<LoadingState>({
|
const [loadingState, setLoadingState] = useState<LoadingState>({
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
error: ''
|
error: ''
|
||||||
});
|
});
|
||||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
const storeRef = useRef<TLStore | null>(null);
|
||||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
const [storeReady, setStoreReady] = useState(false);
|
||||||
const [store, setStore] = useState<TLStore | TLStoreWithStatus | undefined>(undefined);
|
|
||||||
|
|
||||||
// TLDraw user preferences
|
// TLDraw user preferences
|
||||||
const tldrawUser = useTldrawUser({
|
const tldrawUser = useTldrawUser({
|
||||||
@ -93,7 +88,6 @@ export default function SinglePlayerPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info('single-player-page', '🔄 Starting store initialization', {
|
logger.info('single-player-page', '🔄 Starting store initialization', {
|
||||||
isEditorReady,
|
|
||||||
hasUser: !!user,
|
hasUser: !!user,
|
||||||
userType: user.user_type,
|
userType: user.user_type,
|
||||||
username: user.username
|
username: user.username
|
||||||
@ -102,7 +96,7 @@ export default function SinglePlayerPage() {
|
|||||||
const initializeStoreAndSnapshot = async () => {
|
const initializeStoreAndSnapshot = async () => {
|
||||||
try {
|
try {
|
||||||
setLoadingState({ status: 'loading', error: '' });
|
setLoadingState({ status: 'loading', error: '' });
|
||||||
|
|
||||||
// 1. Create store
|
// 1. Create store
|
||||||
logger.debug('single-player-page', '🔄 Creating TLStore');
|
logger.debug('single-player-page', '🔄 Creating TLStore');
|
||||||
const newStore = localStoreService.getStore({
|
const newStore = localStoreService.getStore({
|
||||||
@ -119,73 +113,51 @@ export default function SinglePlayerPage() {
|
|||||||
logger.debug('single-player-page', '✨ Initialized NavigationSnapshotService');
|
logger.debug('single-player-page', '✨ Initialized NavigationSnapshotService');
|
||||||
|
|
||||||
// 3. Load initial snapshot if we have a node
|
// 3. Load initial snapshot if we have a node
|
||||||
if (context.node) {
|
const storagePath = context.node?.node_storage_path;
|
||||||
const nodeStoragePath = getNodeStoragePath(context.node);
|
if (storagePath) {
|
||||||
if (nodeStoragePath) {
|
logger.debug('single-player-page', '📥 Loading snapshot from database', {
|
||||||
logger.debug('single-player-page', '📥 Loading snapshot from database', {
|
node_storage_path: storagePath,
|
||||||
dbName: null,
|
user_type: user.user_type,
|
||||||
node: context.node,
|
});
|
||||||
node_storage_path: nodeStoragePath,
|
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
||||||
user_type: user.user_type,
|
storagePath,
|
||||||
username: user.username
|
accessToken || '',
|
||||||
});
|
newStore,
|
||||||
|
setLoadingState,
|
||||||
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
undefined,
|
||||||
nodeStoragePath,
|
editorRef.current || undefined
|
||||||
accessToken || '',
|
);
|
||||||
newStore,
|
snapshotService.setCurrentNodePath(storagePath);
|
||||||
setLoadingState,
|
logger.debug('single-player-page', '✅ Snapshot loaded from database');
|
||||||
undefined,
|
|
||||||
editorRef.current || undefined
|
|
||||||
);
|
|
||||||
// Wire auto-save: set the current path on the service instance
|
|
||||||
snapshotService.setCurrentNodePath(nodeStoragePath);
|
|
||||||
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 {
|
} else {
|
||||||
logger.debug('single-player-page', '⚠️ No node in context, skipping snapshot load');
|
logger.debug('single-player-page', '⚠️ No node in context, skipping snapshot load');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Set up auto-save with debouncing (only after initial load is complete)
|
// 4. Set up debounced auto-save
|
||||||
let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null;
|
let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let isAutoSaving = false;
|
let isAutoSaving = false;
|
||||||
|
|
||||||
newStore.listen(() => {
|
newStore.listen(() => {
|
||||||
if (snapshotServiceRef.current && snapshotServiceRef.current.getCurrentNodePath()) {
|
if (!snapshotServiceRef.current?.getCurrentNodePath()) return;
|
||||||
// Skip if already saving
|
if (isAutoSaving) return;
|
||||||
if (isAutoSaving) {
|
if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
|
||||||
logger.debug('single-player-page', '⚠️ Skipping auto-save - already saving');
|
autoSaveTimeout = setTimeout(async () => {
|
||||||
return;
|
if (isAutoSaving) return;
|
||||||
|
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);
|
||||||
// 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
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. Update store state
|
// 5. Update store state
|
||||||
setStore(newStore);
|
storeRef.current = newStore;
|
||||||
|
setStoreReady(true);
|
||||||
setLoadingState({ status: 'ready', error: '' });
|
setLoadingState({ status: 'ready', error: '' });
|
||||||
logger.info('single-player-page', '✅ Store initialization complete');
|
logger.info('single-player-page', '✅ Store initialization complete');
|
||||||
|
|
||||||
@ -199,8 +171,9 @@ export default function SinglePlayerPage() {
|
|||||||
snapshotServiceRef.current.clearCurrentNode();
|
snapshotServiceRef.current.clearCurrentNode();
|
||||||
snapshotServiceRef.current = null;
|
snapshotServiceRef.current = null;
|
||||||
}
|
}
|
||||||
newStore.dispose();
|
storeRef.current?.dispose();
|
||||||
setStore(undefined);
|
storeRef.current = null;
|
||||||
|
setStoreReady(false);
|
||||||
logger.debug('single-player-page', '🧹 Cleanup complete');
|
logger.debug('single-player-page', '🧹 Cleanup complete');
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -214,119 +187,39 @@ export default function SinglePlayerPage() {
|
|||||||
initializeStoreAndSnapshot();
|
initializeStoreAndSnapshot();
|
||||||
}, [user, context.node]);
|
}, [user, context.node]);
|
||||||
|
|
||||||
// Handle initial node placement
|
// Handle navigation changes — save previous snapshot, load next one.
|
||||||
useEffect(() => {
|
// No automatic node shape placement: the canvas shows only persisted state.
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoadingState({ status: 'loading', error: '' });
|
|
||||||
|
|
||||||
if (context.node.type !== 'workspace') {
|
|
||||||
try {
|
|
||||||
const nodeData = await loadNodeData(context.node);
|
|
||||||
await NodeCanvasService.centerCurrentNode(editorRef.current, context.node, nodeData);
|
|
||||||
} catch (shapeErr) {
|
|
||||||
logger.warn('single-player-page', '⚠️ Could not place node shape', { type: context.node.type, error: shapeErr });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsInitialLoad(false);
|
|
||||||
setLoadingState({ status: 'ready', error: '' });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('single-player-page', '❌ Failed to place initial node', error);
|
|
||||||
setLoadingState({
|
|
||||||
status: 'error',
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to place initial node'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
placeInitialNode();
|
|
||||||
}, [context.node, store, isInitialLoad]);
|
|
||||||
|
|
||||||
// Handle navigation changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNodeChange = async () => {
|
const handleNodeChange = async () => {
|
||||||
if (!context.node?.id || !editorRef.current || !snapshotServiceRef.current || !store) {
|
if (!context.node?.id || !snapshotServiceRef.current || !storeRef.current) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can safely assert these types because we've checked for null above
|
|
||||||
const editor = editorRef.current as Editor;
|
|
||||||
const snapshotService = snapshotServiceRef.current;
|
const snapshotService = snapshotServiceRef.current;
|
||||||
const currentNode = context.node;
|
const currentNode = context.node;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoadingState({ status: 'loading', error: '' });
|
setLoadingState({ status: 'loading', error: '' });
|
||||||
logger.debug('single-player-page', '🔄 Loading node data', {
|
logger.debug('single-player-page', '🔄 Handling navigation to node', {
|
||||||
nodeId: currentNode.id,
|
nodeId: currentNode.id,
|
||||||
node_storage_path: currentNode.node_storage_path,
|
node_storage_path: currentNode.node_storage_path,
|
||||||
isInitialLoad
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the previous node from navigation history
|
const previousNode = context.history.currentIndex > 0
|
||||||
const previousNode = context.history.currentIndex > 0
|
? context.history.nodes[context.history.currentIndex - 1]
|
||||||
? context.history.nodes[context.history.currentIndex - 1]
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Handle navigation in snapshot service (load/save snapshot)
|
|
||||||
await snapshotService.handleNavigationStart(previousNode, currentNode);
|
await snapshotService.handleNavigationStart(previousNode, currentNode);
|
||||||
|
|
||||||
if (currentNode.type !== 'workspace') {
|
|
||||||
try {
|
|
||||||
const nodeData = await loadNodeData(currentNode);
|
|
||||||
await NodeCanvasService.centerCurrentNode(editor, currentNode, nodeData);
|
|
||||||
} catch (shapeErr) {
|
|
||||||
logger.warn('single-player-page', '⚠️ Could not place node shape', { type: currentNode.type, error: shapeErr });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingState({ status: 'ready', error: '' });
|
setLoadingState({ status: 'ready', error: '' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('single-player-page', '❌ Failed to load node data', error);
|
logger.error('single-player-page', '❌ Failed to load node snapshot', error);
|
||||||
setLoadingState({
|
setLoadingState({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: error instanceof Error ? error.message : 'Failed to load node data'
|
error: error instanceof Error ? error.message : 'Failed to load node data'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleNodeChange();
|
handleNodeChange();
|
||||||
}, [context.node, context.history, store]);
|
}, [context.node, context.history, storeReady]);
|
||||||
|
|
||||||
// Inject auth and trigger initial context when token is ready
|
// Inject auth and trigger initial context when token is ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -485,7 +378,7 @@ export default function SinglePlayerPage() {
|
|||||||
top: `${HEADER_HEIGHT}px`,
|
top: `${HEADER_HEIGHT}px`,
|
||||||
}}>
|
}}>
|
||||||
{/* Loading overlay - show when loading or contexts not initialized */}
|
{/* Loading overlay - show when loading or contexts not initialized */}
|
||||||
{(loadingState.status === 'loading' || !store) && (
|
{(loadingState.status === 'loading' || !storeReady) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -513,9 +406,9 @@ export default function SinglePlayerPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
|
|
||||||
{store && <Tldraw
|
{storeReady && storeRef.current && <Tldraw
|
||||||
user={tldrawUser}
|
user={tldrawUser}
|
||||||
store={store}
|
store={storeRef.current}
|
||||||
tools={singlePlayerTools}
|
tools={singlePlayerTools}
|
||||||
shapeUtils={allShapeUtils}
|
shapeUtils={allShapeUtils}
|
||||||
bindingUtils={allBindingUtils}
|
bindingUtils={allBindingUtils}
|
||||||
@ -541,18 +434,16 @@ export default function SinglePlayerPage() {
|
|||||||
|
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
logger.debug('single-player-page', '✅ Editor ref set');
|
logger.debug('single-player-page', '✅ Editor ref set');
|
||||||
|
|
||||||
// Update snapshot service with editor reference
|
// Update snapshot service with editor reference
|
||||||
if (snapshotServiceRef.current) {
|
if (snapshotServiceRef.current) {
|
||||||
snapshotServiceRef.current.setEditor(editor);
|
snapshotServiceRef.current.setEditor(editor);
|
||||||
if (accessToken) snapshotServiceRef.current.setAccessToken(accessToken);
|
if (accessToken) snapshotServiceRef.current.setAccessToken(accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsEditorReady(true);
|
|
||||||
logger.info('single-player-page', '✅ Tldraw mounted successfully', {
|
logger.info('single-player-page', '✅ Tldraw mounted successfully', {
|
||||||
editorId: editor.store.id,
|
editorId: editor.store.id,
|
||||||
presentationMode,
|
presentationMode,
|
||||||
isEditorReady: true
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('single-player-page', '❌ Error in onMount', error);
|
logger.error('single-player-page', '❌ Error in onMount', error);
|
||||||
@ -563,42 +454,3 @@ 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> => {
|
|
||||||
if (!node?.id) throw new Error('Node parameter is required');
|
|
||||||
const nodeStoragePath = getNodeStoragePath(node);
|
|
||||||
if (!nodeStoragePath) throw new Error(`Node ${node.id} is missing node_storage_path`);
|
|
||||||
|
|
||||||
const theme = getThemeFromLabel(node.type);
|
|
||||||
return {
|
|
||||||
title: node.label || node.type || '',
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user