summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/files/RepoSyncIndicator.tsx
blob: 82d79f7918ad6f463d9a213320356200149fbfac (plain) (tree)





























































































































































































                                                                                                                
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>
  );
}