summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/files/RepoSyncIndicator.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/files/RepoSyncIndicator.tsx')
-rw-r--r--makima/frontend/src/components/files/RepoSyncIndicator.tsx190
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>
+ );
+}