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;