feat: add project settings page with tabbed layout
- Create SettingsTabLayout component with sidebar navigation - Create ProjectSettingsPage with General settings tab - Create RepositoriesSettingsTab for repo management - Add Members placeholder tab - Update router with settings routes - Add CSS styles for settings layout - Navigate to /projects/:id/settings from workspace header
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams, Link, useNavigate, Routes, Route } from "react-router-dom";
|
||||
import { SettingsTabLayout } from "../components/settings-tab-layout";
|
||||
import { RepositoriesSettingsTab } from "../components/repositories-settings-tab";
|
||||
import { apiClient } from "../api/client";
|
||||
import { Project } from "../types";
|
||||
|
||||
const tabs = [
|
||||
{ id: "general", label: "General", path: "general" },
|
||||
{ id: "repositories", label: "Repositories", path: "repositories" },
|
||||
{ id: "members", label: "Members", path: "members" },
|
||||
];
|
||||
|
||||
const GeneralSettings: React.FC<{
|
||||
project: Project;
|
||||
projectId: string;
|
||||
}> = ({ project, projectId }) => {
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState(project.name);
|
||||
const [description, setDescription] = useState(project.description || "");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await apiClient.patch(`/projects/${projectId}`, {
|
||||
name,
|
||||
description,
|
||||
});
|
||||
} catch (err) {
|
||||
setError("Failed to save changes");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!window.confirm("Are you sure you want to delete this project?")) return;
|
||||
try {
|
||||
await apiClient.delete(`/projects/${projectId}`);
|
||||
navigate("/projects");
|
||||
} catch (err) {
|
||||
setError("Failed to delete project");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-panel">
|
||||
<h2>General Settings</h2>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="form-group">
|
||||
<label>Project Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="form-textarea"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="btn-primary"
|
||||
>
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="danger-zone">
|
||||
<h3>Danger Zone</h3>
|
||||
<button onClick={handleDelete} className="btn-danger">
|
||||
Delete Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MembersSettingsTab: React.FC = () => (
|
||||
<div className="settings-panel">
|
||||
<h2>Members</h2>
|
||||
<p>Team members management coming soon.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ProjectSettingsPage: React.FC = () => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProject = async () => {
|
||||
try {
|
||||
const response = await apiClient.get(`/projects/${projectId}`);
|
||||
setProject(response.data);
|
||||
} catch (err) {
|
||||
setError("Failed to load project");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProject();
|
||||
}, [projectId]);
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (error) return <div className="error-message">{error}</div>;
|
||||
if (!project) return <div>Project not found</div>;
|
||||
|
||||
return (
|
||||
<div className="project-settings-page">
|
||||
<div className="settings-breadcrumb">
|
||||
<Link to="/projects">Projects</Link>
|
||||
<span> > </span>
|
||||
<Link to={`/projects/${projectId}`}>{project.name}</Link>
|
||||
<span> > </span>
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
|
||||
<SettingsTabLayout tabs={tabs} basePath={`/projects/${projectId}/settings`}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="general"
|
||||
element={<GeneralSettings project={project} projectId={projectId!} />}
|
||||
/>
|
||||
<Route path="repositories" element={<RepositoriesSettingsTab />} />
|
||||
<Route path="members" element={<MembersSettingsTab />} />
|
||||
<Route path="*" element={<GeneralSettings project={project} projectId={projectId!} />} />
|
||||
</Routes>
|
||||
</SettingsTabLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../api/git_repositories";
|
||||
import { CommitPanel } from "../components/commit-panel";
|
||||
import { GitToolbar } from "../components/git-toolbar";
|
||||
import { WorkspaceHeader } from "../components/workspace-header";
|
||||
|
||||
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
|
||||
|
||||
@@ -28,11 +29,18 @@ interface FileTreeEntry {
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export const RepoWorkspace = () => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [status, setStatus] = useState<WorkspaceStatus>("loading");
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(
|
||||
searchParams.get("repo")
|
||||
@@ -41,6 +49,16 @@ export const RepoWorkspace = () => {
|
||||
const [currentBranch, setCurrentBranch] = useState<string>("main");
|
||||
const [gitStatus, setGitStatus] = useState<GitStatus | null>(null);
|
||||
|
||||
const loadProject = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
const response = await apiClient.get(`/projects/${projectId}`);
|
||||
setProject(response.data);
|
||||
} catch {
|
||||
setProject(null);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const loadRepositories = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
|
||||
@@ -95,8 +113,9 @@ export const RepoWorkspace = () => {
|
||||
}, [projectId, selectedRepoId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadProject();
|
||||
void loadRepositories();
|
||||
}, [loadRepositories]);
|
||||
}, [loadProject, loadRepositories]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadBranches();
|
||||
@@ -116,20 +135,12 @@ export const RepoWorkspace = () => {
|
||||
|
||||
return (
|
||||
<section className="repo-workspace">
|
||||
<div className="workspace-header">
|
||||
<div className="workspace-title">
|
||||
<h1>Repository Workspace</h1>
|
||||
{selectedRepo && <span className="repo-name">{selectedRepo.name}</span>}
|
||||
</div>
|
||||
<div className="workspace-actions">
|
||||
<Link
|
||||
className="secondary-button"
|
||||
to={`/projects/${projectId}/repositories`}
|
||||
>
|
||||
Manage Repositories
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{project && (
|
||||
<WorkspaceHeader
|
||||
project={project}
|
||||
currentRepo={selectedRepo || null}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === "loading" && (
|
||||
<p className="muted">Loading repositories...</p>
|
||||
@@ -160,6 +171,13 @@ export const RepoWorkspace = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{project && (
|
||||
<WorkspaceHeader
|
||||
project={project}
|
||||
currentRepo={selectedRepo || null}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === "ready" && repositories.length > 0 && (
|
||||
<div className="workspace-layout">
|
||||
<aside className="workspace-sidebar">
|
||||
|
||||
Reference in New Issue
Block a user