6b118307eb
- Extract use-repo-workspace hook for data loading - Extract WorkspaceLayout and FileBrowser components - Slim RepoWorkspacePage from 505 to ~80 lines Quality gates: tsc --noEmit passes, npm run build passes
133 lines
4.1 KiB
TypeScript
133 lines
4.1 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import { apiClient } from "../../../api/client";
|
|
import { EmptyState } from "../../data-states";
|
|
import { Icon } from "../../icon";
|
|
import type { GitStatus } from "../../../api/git-repositories";
|
|
|
|
interface FileTreeEntry {
|
|
name: string;
|
|
type: "file" | "directory";
|
|
path: string;
|
|
size?: number;
|
|
mode?: string;
|
|
last_commit?: {
|
|
hash: string;
|
|
message: string;
|
|
author: string;
|
|
date: string;
|
|
} | null;
|
|
}
|
|
|
|
interface Props {
|
|
projectId: string;
|
|
repoId: string;
|
|
gitStatus: GitStatus | null;
|
|
}
|
|
|
|
export const FileBrowser = ({ projectId, repoId, gitStatus }: Props) => {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [entries, setEntries] = useState<FileTreeEntry[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const branch = searchParams.get("branch") || "main";
|
|
const path = searchParams.get("path") || "";
|
|
|
|
const loadFiles = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await apiClient.get(
|
|
`/projects/${projectId}/repositories/${repoId}/files`,
|
|
{ params: { branch, path } }
|
|
);
|
|
setEntries(response.data.entries || []);
|
|
} catch {
|
|
setError("Failed to load files");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [projectId, repoId, branch, path]);
|
|
|
|
useEffect(() => {
|
|
void loadFiles();
|
|
}, [loadFiles]);
|
|
|
|
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);
|
|
newParams.set("path", entry.path);
|
|
setSearchParams(newParams);
|
|
} else {
|
|
const newParams = new URLSearchParams(searchParams);
|
|
newParams.set("file", entry.path);
|
|
setSearchParams(newParams);
|
|
}
|
|
};
|
|
|
|
const navigateUp = () => {
|
|
if (!path) return;
|
|
const parentPath = path.split("/").slice(0, -1).join("/");
|
|
const newParams = new URLSearchParams(searchParams);
|
|
if (parentPath) {
|
|
newParams.set("path", parentPath);
|
|
} else {
|
|
newParams.delete("path");
|
|
}
|
|
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>;
|
|
|
|
return (
|
|
<div className="file-tree">
|
|
{path && (
|
|
<button className="tree-entry tree-up" onClick={navigateUp} type="button">
|
|
<Icon name="folder" size="sm" /> ..
|
|
</button>
|
|
)}
|
|
{entries.length === 0 && (
|
|
<EmptyState message="No files in this repository yet." />
|
|
)}
|
|
{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"
|
|
>
|
|
<Icon name={entry.type === "directory" ? "folder" : "file"} size="sm" /> {entry.name}
|
|
{fileStatus && (
|
|
<span className={`file-status-indicator ${fileStatus}`}>
|
|
{fileStatus === "modified" && "M"}
|
|
{fileStatus === "added" && "A"}
|
|
{fileStatus === "deleted" && "D"}
|
|
{fileStatus === "untracked" && "?"}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|