/ src / components / ContextMenu.jsx
ContextMenu.jsx
  1  import React, { useState, useEffect, useRef } from 'react';
  2  import { BLACK, BLUE, WHITE, RED } from '../constants/colors';
  3  import { getAllRepoNamesAndTypes, addSubmodule, updateSubmodules, createEmailDraft, getPersonNodes, triggerCoherenceBeacon, runAider, openCanvas } from '../services/electronService';
  4  
  5  const ContextMenu = ({ repoName, position, onClose, onEditMetadata, onRename, onOpenInGitFox }) => {
  6    const [showSubmoduleMenu, setShowSubmoduleMenu] = useState(false);
  7    const [showShareMenu, setShowShareMenu] = useState(false);
  8    const [availableRepos, setAvailableRepos] = useState([]);
  9    const [personNodes, setPersonNodes] = useState([]);
 10    const submenuRef = useRef(null);
 11    const shareMenuRef = useRef(null);
 12  
 13    useEffect(() => {
 14      const fetchRepos = async () => {
 15        const repos = await getAllRepoNamesAndTypes();
 16        const filteredRepos = repos.filter(repo => repo.name !== repoName);
 17        console.log('Available repos for submodules:', filteredRepos);
 18        setAvailableRepos(filteredRepos);
 19      };
 20      const fetchPersonNodes = async () => {
 21        const persons = await getPersonNodes();
 22        console.log('Available person nodes for sharing:', persons);
 23        setPersonNodes(persons);
 24      };
 25      fetchRepos();
 26      fetchPersonNodes();
 27    }, [repoName]);
 28  
 29    useEffect(() => {
 30      const positionSubmenu = (menuRef, showMenu, parentElement) => {
 31        if (showMenu && menuRef.current && parentElement) {
 32          const menu = menuRef.current;
 33          const parentRect = parentElement.getBoundingClientRect();
 34          const viewportHeight = window.innerHeight;
 35          const menuHeight = menu.offsetHeight;
 36  
 37          let topPosition = parentRect.top;
 38          if (topPosition + menuHeight > viewportHeight) {
 39            topPosition = Math.max(0, viewportHeight - menuHeight);
 40          }
 41  
 42          menu.style.top = `${topPosition}px`;
 43          menu.style.left = `${parentRect.right}px`;
 44          menu.style.maxHeight = `${viewportHeight * 0.8}px`; // Limit to 80% of viewport height
 45        }
 46      };
 47  
 48      positionSubmenu(submenuRef, showSubmoduleMenu, submenuRef.current?.parentElement);
 49      positionSubmenu(shareMenuRef, showShareMenu, shareMenuRef.current?.parentElement);
 50    }, [showSubmoduleMenu, showShareMenu]);
 51    const handleEditMetadata = () => {
 52      onEditMetadata(repoName);
 53      onClose();
 54    };
 55  
 56    const handleRename = () => {
 57      onRename(repoName);
 58      onClose();
 59    };
 60  
 61    const handleOpenInFinder = () => {
 62      if (window.electron && window.electron.openInFinder) {
 63        window.electron.openInFinder(repoName);
 64      } else {
 65        console.error('openInFinder is not available');
 66      }
 67      onClose();
 68    };
 69  
 70    const handleOpenInGitFox = () => {
 71      if (window.electron && window.electron.openInGitFox) {
 72        window.electron.openInGitFox(repoName);
 73      } else {
 74        console.error('openInGitFox is not available');
 75      }
 76      onClose();
 77    };
 78  
 79    const handleAddSubmodule = (e) => {
 80      e.stopPropagation();
 81      setShowSubmoduleMenu(!showSubmoduleMenu);
 82    };
 83  
 84    const [error, setError] = useState(null);
 85  
 86    const handleSelectSubmodule = async (submoduleName) => {
 87      console.log(`Adding Submodule ${submoduleName} to ${repoName}`);
 88      try {
 89        await addSubmodule(repoName, submoduleName);
 90        console.log(`Successfully added submodule ${submoduleName} to ${repoName}`);
 91        onClose();
 92      } catch (err) {
 93        console.error(`Error adding submodule:`, err);
 94        setError(`Failed to add submodule: ${err.message}`);
 95      }
 96    };
 97  
 98    return (
 99      <div 
100        style={{
101          position: 'fixed',
102          top: position.y,
103          left: position.x,
104          backgroundColor: BLACK,
105          color: WHITE,
106          borderRadius: '4px',
107          overflow: 'visible',
108          zIndex: 1000,
109          border: `1px solid ${BLUE}`,
110        }}
111        onClick={(e) => e.stopPropagation()}
112      >
113        {error && (
114          <div style={{ color: RED, padding: '10px', borderBottom: `1px solid ${BLUE}` }}>
115            {error}
116          </div>
117        )}
118        <ul style={{ listStyle: 'none', padding: 0, margin: 0, fontSize: '0.9em' }}>
119          <li 
120            onClick={handleEditMetadata}
121            style={{ 
122              padding: '6px 10px',
123              cursor: 'pointer',
124              transition: 'background-color 0.2s ease',
125            }}
126            onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
127            onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
128          >
129            Edit Metadata
130          </li>
131          <li 
132            onClick={handleRename}
133            style={{ 
134              padding: '6px 10px',
135              cursor: 'pointer',
136              transition: 'background-color 0.2s ease',
137            }}
138            onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
139            onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
140          >
141            Rename
142          </li>
143          <li 
144            onClick={handleOpenInFinder}
145            style={{ 
146              padding: '6px 10px',
147              cursor: 'pointer',
148              transition: 'background-color 0.2s ease',
149            }}
150            onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
151            onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
152          >
153            Open in Finder
154          </li>
155          <li 
156            onClick={handleOpenInGitFox}
157            style={{ 
158              padding: '6px 10px',
159              cursor: 'pointer',
160              transition: 'background-color 0.2s ease',
161            }}
162            onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
163            onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
164          >
165            Open in GitFox
166          </li>
167          <li 
168            onMouseEnter={(e) => {
169              e.target.style.backgroundColor = BLUE;
170              setShowSubmoduleMenu(true);
171            }}
172            style={{ 
173              padding: '6px 10px',
174              cursor: 'pointer',
175              transition: 'background-color 0.2s ease',
176              position: 'relative',
177            }}
178          >
179            Add Submodule ▶
180            {showSubmoduleMenu && (
181              <ul 
182                ref={submenuRef}
183                onMouseEnter={() => setShowSubmoduleMenu(true)}
184                onMouseLeave={() => setShowSubmoduleMenu(false)}
185                style={{
186                  position: 'fixed',
187                  backgroundColor: BLACK,
188                  border: `1px solid ${BLUE}`,
189                  borderRadius: '4px',
190                  padding: 0,
191                  margin: 0,
192                  listStyle: 'none',
193                  zIndex: 1001,
194                  minWidth: '150px',
195                  maxHeight: '80vh',
196                  overflowY: 'auto',
197                  scrollbarWidth: 'thin',
198                  scrollbarColor: `${BLUE} ${BLACK}`,
199                }}
200              >
201                {console.log('Rendering submodule menu with repos:', availableRepos)}
202                {availableRepos.map((repo) => (
203                  <li
204                    key={repo.name}
205                    onClick={(e) => {
206                      e.stopPropagation();
207                      handleSelectSubmodule(repo.name);
208                    }}
209                    style={{
210                      padding: '6px 10px',
211                      cursor: 'pointer',
212                      transition: 'background-color 0.2s ease',
213                      whiteSpace: 'nowrap',
214                    }}
215                    onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
216                    onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
217                  >
218                    {repo.name}
219                  </li>
220                ))}
221              </ul>
222            )}
223          </li>
224          <li 
225            onClick={() => handleUpdateSubmodules(repoName)}
226            style={{ 
227              padding: '6px 10px',
228              cursor: 'pointer',
229              transition: 'background-color 0.2s ease',
230            }}
231            onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
232            onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
233          >
234            Update Submodules
235          </li>
236          <li 
237            onClick={() => handleTriggerCoherenceBeacon(repoName)}
238            style={{ 
239              padding: '6px 10px',
240              cursor: 'pointer',
241              transition: 'background-color 0.2s ease',
242            }}
243            onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
244            onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
245          >
246            Trigger Coherence Beacon
247          </li>
248          <li 
249            onClick={() => handleRunAider(repoName)}
250            style={{ 
251              padding: '6px 10px',
252              cursor: 'pointer',
253              transition: 'background-color 0.2s ease',
254            }}
255            onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
256            onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
257          >
258            Run Aider
259          </li>
260          <li 
261            onClick={() => handleOpenCanvas(repoName)}
262            style={{ 
263              padding: '6px 10px',
264              cursor: 'pointer',
265              transition: 'background-color 0.2s ease',
266            }}
267            onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
268            onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
269          >
270            Open Canvas
271          </li>
272          <li 
273            onMouseEnter={(e) => {
274              e.target.style.backgroundColor = BLUE;
275              setShowShareMenu(true);
276            }}
277            style={{ 
278              padding: '6px 10px',
279              cursor: 'pointer',
280              transition: 'background-color 0.2s ease',
281              position: 'relative',
282            }}
283          >
284            Share via Email ▶
285            {showShareMenu && (
286              <ul 
287                ref={shareMenuRef}
288                onMouseEnter={() => setShowShareMenu(true)}
289                onMouseLeave={() => setShowShareMenu(false)}
290                style={{
291                  position: 'fixed',
292                  backgroundColor: BLACK,
293                  border: `1px solid ${BLUE}`,
294                  borderRadius: '4px',
295                  padding: 0,
296                  margin: 0,
297                  listStyle: 'none',
298                  zIndex: 1001,
299                  minWidth: '150px',
300                  maxHeight: '80vh',
301                  overflowY: 'auto',
302                  scrollbarWidth: 'thin',
303                  scrollbarColor: `${BLUE} ${BLACK}`,
304                }}
305              >
306                {personNodes.map((person) => (
307                  <li
308                    key={person.name}
309                    onClick={(e) => {
310                      e.stopPropagation();
311                      handleShareViaEmail(repoName, person.name);
312                    }}
313                    style={{
314                      padding: '6px 10px',
315                      cursor: 'pointer',
316                      transition: 'background-color 0.2s ease',
317                      whiteSpace: 'nowrap',
318                    }}
319                    onMouseEnter={(e) => e.target.style.backgroundColor = BLUE}
320                    onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
321                  >
322                    {person.name}
323                  </li>
324                ))}
325              </ul>
326            )}
327          </li>
328        </ul>
329      </div>
330    );
331  };
332  
333  const handleShareViaEmail = async (repoName, personName) => {
334    try {
335      const result = await createEmailDraft(repoName, personName);
336      if (result.success) {
337        alert(result.message);
338      } else {
339        alert(result.message || 'Failed to create email draft');
340      }
341    } catch (error) {
342      console.error('Error sharing via email:', error);
343      alert(`Error sharing via email: ${error.message}`);
344    }
345  };
346  
347  const handleUpdateSubmodules = async (repoName) => {
348    try {
349      const result = await updateSubmodules(repoName);
350      console.log('Submodules update result:', result);
351      if (result.newSubmodules && result.newSubmodules.length > 0) {
352        alert(`Submodules updated successfully. New submodules: ${result.newSubmodules.join(', ')}`);
353      } else {
354        alert("Everything is up to date. No new submodules to add.");
355      }
356    } catch (error) {
357      console.error('Error updating submodules:', error);
358      alert(`Error updating submodules: ${error.message}`);
359    }
360  };
361  
362  const handleTriggerCoherenceBeacon = async (repoName) => {
363    try {
364      const result = await triggerCoherenceBeacon(repoName);
365      console.log('Coherence Beacon result:', result);
366      if (result.friendsNotified && result.friendsNotified.length > 0) {
367        const groupedFriends = groupFriendsBySubmodules(result.friendsNotified);
368        let message = 'Coherence Beacon triggered successfully. Email drafts created for:\n\n';
369        
370        for (const [submodules, friends] of Object.entries(groupedFriends)) {
371          message += `Group with common submodules (${submodules}):\n`;
372          friends.forEach(friend => {
373            message += `- ${friend.name} (${friend.email})\n`;
374          });
375          message += '\n';
376        }
377        
378        alert(message);
379      } else {
380        alert("No novel submodules to notify about at this time.");
381      }
382    } catch (error) {
383      console.error('Error triggering Coherence Beacon:', error);
384      alert(`Error triggering Coherence Beacon: ${error.message}`);
385    }
386  };
387  
388  const handleRunAider = async (repoName) => {
389    try {
390      const result = await runAider(repoName);
391      if (result.success) {
392        alert(`Aider started successfully for ${repoName}`);
393      } else {
394        alert(`Failed to start Aider: ${result.error}`);
395      }
396    } catch (error) {
397      console.error('Error running Aider:', error);
398      alert(`Error running Aider: ${error.message}`);
399    }
400  };
401  
402  const handleOpenCanvas = async (repoName) => {
403    try {
404      const result = await openCanvas(repoName);
405      if (result.success) {
406        console.log(result.message);
407      } else {
408        alert(`Failed to open canvas: ${result.error}`);
409      }
410    } catch (error) {
411      console.error('Error opening canvas:', error);
412      alert(`Error opening canvas: ${error.message}`);
413    }
414  };
415  
416  const groupFriendsBySubmodules = (friends) => {
417    const groups = {};
418    for (const friend of friends) {
419      const key = friend.commonSubmodules.sort().join(',');
420      if (!groups[key]) {
421        groups[key] = [];
422      }
423      groups[key].push(friend);
424    }
425    return groups;
426  };
427  
428  export default ContextMenu;