/ src / features / edit-mode / EditModeOverlay.tsx
EditModeOverlay.tsx
  1  import React from 'react';
  2  import { useInterBrainStore } from '../../store/interbrain-store';
  3  import { serviceManager } from '../../services/service-manager';
  4  import { UIService } from '../../services/ui-service';
  5  import { DreamNode } from '../../types/dreamnode';
  6  import EditNode3D from './EditNode3D';
  7  import EditModeSearchNode3D from './EditModeSearchNode3D';
  8  
  9  // Create UIService instance for showing user messages
 10  const uiService = new UIService();
 11  
 12  /**
 13   * EditModeOverlay - Main coordinator for edit mode functionality
 14   * 
 15   * Integrates with existing spatial layout system:
 16   * - EditNode3D for metadata editing at center position  
 17   * - Leverages existing SpatialOrchestrator search layout for relationship nodes
 18   * - Uses existing DreamNode3D components with gold glow for relationships
 19   */
 20  export default function EditModeOverlay() {
 21    const {
 22      editMode,
 23      savePendingRelationships,
 24      exitEditMode
 25    } = useInterBrainStore();
 26    
 27    // Center position for the editing node (similar to ProtoNode spawn position)
 28    const centerPosition: [number, number, number] = [0, 0, -50];
 29    
 30    // Handler functions defined before useEffect to maintain hook order
 31    const handleCancel = () => {
 32      // Exit edit mode - original data is preserved in store
 33      exitEditMode();
 34      
 35      // Return to liminal-web layout (edit mode requires a selected node)
 36      const store = useInterBrainStore.getState();
 37      if (store.selectedNode) {
 38        store.setSpatialLayout('liminal-web');
 39      }
 40    };
 41    
 42    // Global escape key handling is now managed by DreamspaceCanvas for stability
 43    
 44    // Don't render if edit mode is not active
 45    if (!editMode.isActive || !editMode.editingNode) {
 46      return null;
 47    }
 48    
 49    const handleSave = async () => {
 50      try {
 51        if (!editMode.editingNode) {
 52          console.error('No editing node available for save operation');
 53          return;
 54        }
 55  
 56        // Get the active service for persistence
 57        const dreamNodeService = serviceManager.getActive();
 58        
 59        // 1. Handle new DreamTalk media file if provided
 60        if (editMode.newDreamTalkFile) {
 61          console.log(`EditModeOverlay: Saving new DreamTalk media: ${editMode.newDreamTalkFile.name}`);
 62          await dreamNodeService.addFilesToNode(editMode.editingNode.id, [editMode.newDreamTalkFile]);
 63        }
 64        
 65        // 2. Save metadata changes (let service layer handle if no changes)
 66        const updates: Partial<DreamNode> = {
 67          name: editMode.editingNode.name,
 68          type: editMode.editingNode.type
 69        };
 70  
 71        // Include contact info only for dreamer-type nodes
 72        if (editMode.editingNode.type === 'dreamer') {
 73          updates.email = editMode.editingNode.email;
 74          updates.phone = editMode.editingNode.phone;
 75          updates.radicleId = editMode.editingNode.radicleId;
 76        }
 77  
 78        await dreamNodeService.update(editMode.editingNode.id, updates);
 79        
 80        // 3. Save relationship changes through service layer
 81        await dreamNodeService.updateRelationships(
 82          editMode.editingNode.id,
 83          editMode.pendingRelationships
 84        );
 85        
 86        // 4. Update store state with confirmed relationships
 87        savePendingRelationships();
 88        
 89        // 5. Clear the new DreamTalk file from edit mode state (successful save)
 90        if (editMode.newDreamTalkFile) {
 91          useInterBrainStore.getState().setEditModeNewDreamTalkFile(undefined);
 92        }
 93        
 94        console.log(`Edit mode changes saved for node ${editMode.editingNode.id}:`, {
 95          relationships: editMode.pendingRelationships.length,
 96          dataMode: serviceManager.getMode()
 97        });
 98        
 99        // Update the selected node in the main store with the latest data
100        const freshNode = await dreamNodeService.get(editMode.editingNode.id);
101        if (freshNode) {
102          // Update the selectedNode to reflect the saved changes
103          useInterBrainStore.getState().setSelectedNode(freshNode);
104          
105          // Immediately trigger the special edit mode save transition
106          // This starts the parallel animations: UI fade + node movements
107          const canvas = globalThis.document.querySelector('[data-dreamspace-canvas]');
108          if (canvas) {
109            const event = new globalThis.CustomEvent('edit-mode-save-transition', {
110              detail: { nodeId: freshNode.id }
111            });
112            canvas.dispatchEvent(event);
113          }
114          
115          // Exit edit mode after animations complete
116          // Don't change spatial layout - it's already handled by the event
117          globalThis.setTimeout(() => {
118            exitEditMode();
119          }, 1000);
120        }
121      } catch (error) {
122        console.error('Failed to save edit mode changes:', error);
123        console.log('Edit mode state during error:', {
124          isActive: editMode.isActive,
125          hasEditingNode: !!editMode.editingNode,
126          editingNodeName: editMode.editingNode?.name
127        });
128        
129        // Show user-friendly error message in Obsidian UI
130        uiService.showError('Failed to save changes: ' + (error instanceof Error ? error.message : 'Unknown error'));
131        
132        // Ensure edit mode state is preserved after error
133        console.log('Edit mode state after error message:', {
134          isActive: useInterBrainStore.getState().editMode.isActive,
135          hasEditingNode: !!useInterBrainStore.getState().editMode.editingNode
136        });
137        
138        // Don't exit edit mode if save fails - user can try again or cancel
139      }
140    };
141  
142    const handleSearchToggleOff = async () => {
143      const store = useInterBrainStore.getState();
144      
145      console.log(`🔍 [EditModeOverlay] Toggling off search mode - filtering to pending relationships`);
146      
147      // Close the search interface
148      store.setEditModeSearchActive(false);
149      
150      // Get current pending relationships to show only related nodes
151      const pendingRelationshipIds = store.editMode.pendingRelationships;
152      
153      if (pendingRelationshipIds.length > 0 && store.editMode.editingNode) {
154        // Get the related nodes from the service layer
155        const dreamNodeService = serviceManager.getActive();
156        const relatedNodes = await Promise.all(
157          pendingRelationshipIds.map(id => dreamNodeService.get(id))
158        );
159        
160        // Filter out any null results (in case some relationships are broken)
161        const validRelatedNodes = relatedNodes.filter(node => node !== null) as DreamNode[];
162        
163        console.log(`✅ [EditModeOverlay] Showing ${validRelatedNodes.length} pending related nodes after search toggle off`);
164        
165        // Update the edit mode search results to show only pending relationships
166        store.setEditModeSearchResults(validRelatedNodes);
167        
168        // CRITICAL: Clear stale orchestrator data before showing filtered results
169        const canvas = globalThis.document.querySelector('[data-dreamspace-canvas]');
170        if (canvas) {
171          // First clear the stale edit mode data
172          const clearEvent = new globalThis.CustomEvent('clear-edit-mode-data', {
173            detail: { source: 'search-toggle-off' }
174          });
175          canvas.dispatchEvent(clearEvent);
176          
177          // Then trigger the filtered layout after a brief delay to ensure cleanup completes
178          globalThis.setTimeout(() => {
179            if (store.editMode.editingNode) {
180              const layoutEvent = new globalThis.CustomEvent('edit-mode-search-layout', {
181                detail: { 
182                  centerNodeId: store.editMode.editingNode.id,
183                  searchResults: validRelatedNodes
184                }
185              });
186              canvas.dispatchEvent(layoutEvent);
187            }
188          }, 10);
189        }
190      } else {
191        console.log(`📭 [EditModeOverlay] No pending relationships - clearing search results`);
192        // No pending relationships, clear search results
193        store.setEditModeSearchResults([]);
194      }
195      
196      // Note: Focus management removed - global DreamspaceCanvas escape handler doesn't require it
197    };
198    
199    
200    
201    return (
202      <group>
203        {/* Central metadata editing interface */}
204        <EditNode3D
205          position={centerPosition}
206          onSave={handleSave}
207          onCancel={handleCancel}
208          onToggleSearchOff={handleSearchToggleOff}
209        />
210        
211        {/* Relationship search interface - renders on top of EditNode3D when active */}
212        {editMode.isSearchingRelationships && (
213          <EditModeSearchNode3D
214            position={centerPosition}
215          />
216        )}
217        
218        {/* Relationship management now handled by existing SpatialOrchestrator + DreamNode3D components */}
219        {/* Gold glow and click handling will be added to existing DreamNode3D components */}
220      </group>
221    );
222  }