feat: add commit panel and file status indicators to repo workspace
- Add CommitPanel component for viewing changed files and committing - Show file status indicators (M/A/D/?) in file tree - Integrate git status with workspace for real-time updates - Add CSS styles for commit panel and status badges Part of git-control change implementation.
This commit is contained in:
@@ -3,7 +3,14 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { Link, useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { apiClient } from "../api/client";
|
||||
import { listRepositories, type GitRepository } from "../api/git_repositories";
|
||||
import {
|
||||
getRepositoryStatus,
|
||||
listRepositories,
|
||||
type GitRepository,
|
||||
type GitStatus,
|
||||
} from "../api/git_repositories";
|
||||
import { CommitPanel } from "../components/commit-panel";
|
||||
import { GitToolbar } from "../components/git-toolbar";
|
||||
|
||||
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
|
||||
|
||||
@@ -30,6 +37,9 @@ export const RepoWorkspace = () => {
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(
|
||||
searchParams.get("repo")
|
||||
);
|
||||
const [branches, setBranches] = useState<string[]>([]);
|
||||
const [currentBranch, setCurrentBranch] = useState<string>("main");
|
||||
const [gitStatus, setGitStatus] = useState<GitStatus | null>(null);
|
||||
|
||||
const loadRepositories = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
@@ -57,10 +67,42 @@ export const RepoWorkspace = () => {
|
||||
}
|
||||
}, [projectId, selectedRepoId, searchParams, setSearchParams]);
|
||||
|
||||
const loadBranches = useCallback(async () => {
|
||||
if (!projectId || !selectedRepoId) return;
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${selectedRepoId}/branches`
|
||||
);
|
||||
const branchList = response.data.branches.map((b: { name: string }) => b.name);
|
||||
setBranches(branchList);
|
||||
const defaultBranch = response.data.default_branch;
|
||||
if (defaultBranch) {
|
||||
setCurrentBranch(defaultBranch);
|
||||
}
|
||||
} catch {
|
||||
setBranches([]);
|
||||
}
|
||||
}, [projectId, selectedRepoId]);
|
||||
|
||||
const loadGitStatus = useCallback(async () => {
|
||||
if (!projectId || !selectedRepoId) return;
|
||||
try {
|
||||
const data = await getRepositoryStatus(projectId, selectedRepoId);
|
||||
setGitStatus(data);
|
||||
} catch {
|
||||
setGitStatus(null);
|
||||
}
|
||||
}, [projectId, selectedRepoId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadRepositories();
|
||||
}, [loadRepositories]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadBranches();
|
||||
void loadGitStatus();
|
||||
}, [loadBranches, loadGitStatus]);
|
||||
|
||||
const handleRepoChange = (repoId: string) => {
|
||||
setSelectedRepoId(repoId);
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
@@ -138,10 +180,44 @@ export const RepoWorkspace = () => {
|
||||
</div>
|
||||
|
||||
{selectedRepoId && (
|
||||
<FileBrowser
|
||||
projectId={projectId!}
|
||||
repoId={selectedRepoId}
|
||||
/>
|
||||
<>
|
||||
<GitToolbar
|
||||
projectId={projectId!}
|
||||
repoId={selectedRepoId}
|
||||
currentBranch={currentBranch}
|
||||
branches={branches}
|
||||
onBranchChange={(branch) => {
|
||||
setCurrentBranch(branch);
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set("branch", branch);
|
||||
setSearchParams(newParams);
|
||||
}}
|
||||
onRefresh={() => {
|
||||
void loadBranches();
|
||||
void loadGitStatus();
|
||||
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
||||
}}
|
||||
/>
|
||||
<FileBrowser
|
||||
projectId={projectId!}
|
||||
repoId={selectedRepoId}
|
||||
gitStatus={gitStatus}
|
||||
/>
|
||||
{gitStatus && (
|
||||
<CommitPanel
|
||||
projectId={projectId!}
|
||||
repoId={selectedRepoId}
|
||||
modified={gitStatus.modified}
|
||||
added={gitStatus.added}
|
||||
deleted={gitStatus.deleted}
|
||||
untracked={gitStatus.untracked}
|
||||
onCommit={() => {
|
||||
void loadGitStatus();
|
||||
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
@@ -160,9 +236,11 @@ export const RepoWorkspace = () => {
|
||||
const FileBrowser = ({
|
||||
projectId,
|
||||
repoId,
|
||||
gitStatus,
|
||||
}: {
|
||||
projectId: string;
|
||||
repoId: string;
|
||||
gitStatus: GitStatus | null;
|
||||
}) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [entries, setEntries] = useState<FileTreeEntry[]>([]);
|
||||
@@ -197,6 +275,13 @@ const FileBrowser = ({
|
||||
void loadFiles();
|
||||
}, [loadFiles]);
|
||||
|
||||
// Listen for refresh events
|
||||
useEffect(() => {
|
||||
const handleRefresh = () => void loadFiles();
|
||||
window.addEventListener("refresh-file-tree", handleRefresh);
|
||||
return () => window.removeEventListener("refresh-file-tree", handleRefresh);
|
||||
}, [loadFiles]);
|
||||
|
||||
const handleEntryClick = (entry: FileTreeEntry) => {
|
||||
if (entry.type === "directory") {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
@@ -221,6 +306,15 @@ const FileBrowser = ({
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
const getFileStatus = (filePath: string): string | null => {
|
||||
if (!gitStatus) return null;
|
||||
if (gitStatus.modified.includes(filePath)) return "modified";
|
||||
if (gitStatus.added.includes(filePath)) return "added";
|
||||
if (gitStatus.deleted.includes(filePath)) return "deleted";
|
||||
if (gitStatus.untracked.includes(filePath)) return "untracked";
|
||||
return null;
|
||||
};
|
||||
|
||||
if (loading) return <p className="muted">Loading files...</p>;
|
||||
if (error) return <p className="error-text">{error}</p>;
|
||||
|
||||
@@ -231,16 +325,27 @@ const FileBrowser = ({
|
||||
📁 ..
|
||||
</button>
|
||||
)}
|
||||
{entries.map((entry) => (
|
||||
<button
|
||||
key={entry.path}
|
||||
className={`tree-entry ${entry.type === "directory" ? "tree-directory" : "tree-file"}`}
|
||||
onClick={() => handleEntryClick(entry)}
|
||||
type="button"
|
||||
>
|
||||
{entry.type === "directory" ? "📁" : "📄"} {entry.name}
|
||||
</button>
|
||||
))}
|
||||
{entries.map((entry) => {
|
||||
const fileStatus = entry.type === "file" ? getFileStatus(entry.path) : null;
|
||||
return (
|
||||
<button
|
||||
key={entry.path}
|
||||
className={`tree-entry ${entry.type === "directory" ? "tree-directory" : "tree-file"} ${fileStatus || ""}`}
|
||||
onClick={() => handleEntryClick(entry)}
|
||||
type="button"
|
||||
>
|
||||
{entry.type === "directory" ? "📁" : "📄"} {entry.name}
|
||||
{fileStatus && (
|
||||
<span className={`file-status-indicator ${fileStatus}`}>
|
||||
{fileStatus === "modified" && "M"}
|
||||
{fileStatus === "added" && "A"}
|
||||
{fileStatus === "deleted" && "D"}
|
||||
{fileStatus === "untracked" && "?"}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user