diff options
Diffstat (limited to 'makima/frontend/src/components/files/RepoSyncIndicator.tsx')
| -rw-r--r-- | makima/frontend/src/components/files/RepoSyncIndicator.tsx | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/makima/frontend/src/components/files/RepoSyncIndicator.tsx b/makima/frontend/src/components/files/RepoSyncIndicator.tsx new file mode 100644 index 0000000..82d79f7 --- /dev/null +++ b/makima/frontend/src/components/files/RepoSyncIndicator.tsx @@ -0,0 +1,190 @@ +import { useState, useCallback } from "react"; +import { syncFileFromRepo } from "../../lib/api"; + +interface RepoSyncIndicatorProps { + fileId: string; + repoFilePath: string | null | undefined; + repoSyncStatus: string | null | undefined; + repoSyncedAt: string | null | undefined; + onSyncComplete?: () => void; + /** Callback to push file content to repo (creates a task) */ + onPushToRepo?: () => void; + /** Whether a push operation is in progress */ + isPushing?: boolean; +} + +/** + * Shows repository file link status and provides sync functionality. + * Displays the linked file path and allows updating from the repo via daemon, + * or pushing local changes back to the repo. + */ +export function RepoSyncIndicator({ + fileId, + repoFilePath, + repoSyncStatus, + repoSyncedAt, + onSyncComplete, + onPushToRepo, + isPushing = false, +}: RepoSyncIndicatorProps) { + const [isSyncing, setIsSyncing] = useState(false); + const [error, setError] = useState<string | null>(null); + + const handleSync = useCallback(async () => { + setIsSyncing(true); + setError(null); + try { + await syncFileFromRepo(fileId); + // The actual update happens via WebSocket notification + // Give a brief delay then notify parent + setTimeout(() => { + onSyncComplete?.(); + }, 500); + } catch (err) { + setError(err instanceof Error ? err.message : "Sync failed"); + } finally { + setIsSyncing(false); + } + }, [fileId, onSyncComplete]); + + // Don't render if no repo file path is set + if (!repoFilePath) { + return null; + } + + const isActuallySyncing = isSyncing || repoSyncStatus === "syncing"; + const isSynced = repoSyncStatus === "synced"; + const isModified = repoSyncStatus === "modified"; + + // Format the synced timestamp + const syncedAtFormatted = repoSyncedAt + ? new Date(repoSyncedAt).toLocaleString() + : null; + + return ( + <div className="flex items-center gap-2 text-xs font-mono"> + {/* File path icon and link */} + <div className="flex items-center gap-1 text-[#555]"> + <svg + width="12" + height="12" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + className="flex-shrink-0" + > + <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /> + <polyline points="14 2 14 8 20 8" /> + </svg> + <span className="text-[#75aafc]" title={`Linked to repository file: ${repoFilePath}`}> + {repoFilePath} + </span> + </div> + + {/* Status indicator */} + {isSynced && ( + <span + className="text-green-500 flex items-center gap-1" + title={syncedAtFormatted ? `Last synced: ${syncedAtFormatted}` : "Synced"} + > + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"> + <polyline points="20 6 9 17 4 12" /> + </svg> + </span> + )} + {isModified && ( + <span className="text-yellow-500" title="File modified, may need sync"> + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"> + <circle cx="12" cy="12" r="10" /> + <line x1="12" y1="8" x2="12" y2="12" /> + <line x1="12" y1="16" x2="12.01" y2="16" /> + </svg> + </span> + )} + + {/* Pull from repo button */} + <button + onClick={handleSync} + disabled={isActuallySyncing || isPushing} + className={`flex items-center gap-1 px-1.5 py-0.5 rounded transition-colors ${ + isActuallySyncing || isPushing + ? "text-[#555] cursor-wait" + : "text-[#555] hover:text-[#75aafc] hover:bg-[rgba(117,170,252,0.1)]" + }`} + title="Pull latest from repository" + > + {isActuallySyncing ? ( + <> + <svg + width="10" + height="10" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + className="animate-spin" + > + <circle cx="12" cy="12" r="10" strokeDasharray="32" strokeDashoffset="8" /> + </svg> + <span>Pulling...</span> + </> + ) : ( + <> + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> + <path d="M12 19V5" /> + <path d="M5 12l7-7 7 7" /> + </svg> + <span>Pull</span> + </> + )} + </button> + + {/* Push to repo button */} + {onPushToRepo && ( + <button + onClick={onPushToRepo} + disabled={isActuallySyncing || isPushing} + className={`flex items-center gap-1 px-1.5 py-0.5 rounded transition-colors ${ + isPushing + ? "text-[#555] cursor-wait" + : "text-[#555] hover:text-green-500 hover:bg-[rgba(34,197,94,0.1)]" + }`} + title="Push changes to repository (creates a task)" + > + {isPushing ? ( + <> + <svg + width="10" + height="10" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + className="animate-spin" + > + <circle cx="12" cy="12" r="10" strokeDasharray="32" strokeDashoffset="8" /> + </svg> + <span>Pushing...</span> + </> + ) : ( + <> + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> + <path d="M12 5v14" /> + <path d="M19 12l-7 7-7-7" /> + </svg> + <span>Push</span> + </> + )} + </button> + )} + + {/* Error message */} + {error && ( + <span className="text-red-500 text-[10px]" title={error}> + Failed + </span> + )} + </div> + ); +} |
