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,71 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { apiClient } from "../api/client";
|
||||
import { GitRepository } from "../api/git_repositories";
|
||||
|
||||
export const RepositoriesSettingsTab: React.FC = () => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(">");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRepositories = async () => {
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories`
|
||||
);
|
||||
setRepositories(response.data);
|
||||
} catch (err) {
|
||||
setError("Failed to load repositories");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRepositories();
|
||||
}, [projectId]);
|
||||
|
||||
const handleDelete = async (repoId: string) => {
|
||||
if (!window.confirm("Are you sure you want to delete this repository?")) return;
|
||||
try {
|
||||
await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`);
|
||||
setRepositories(repositories.filter((r) => r.id !== repoId));
|
||||
} catch (err) {
|
||||
setError("Failed to delete repository");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="repositories-settings-tab">
|
||||
<h2>Repositories</h2>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="repositories-list">
|
||||
{repositories.length === 0 ? (
|
||||
<p>No repositories yet.</p>
|
||||
) : (
|
||||
repositories.map((repo) => (
|
||||
<div key={repo.id} className="repository-card">
|
||||
<div className="repository-info">
|
||||
<h3>{repo.name}</h3>
|
||||
<p>{repo.remote_url}</p>
|
||||
<span className="repo-type">
|
||||
{repo.is_mirror ? "Mirror" : "Clone"}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(repo.id)}
|
||||
className="btn-danger"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface SettingsTabLayoutProps {
|
||||
tabs: Tab[];
|
||||
children: React.ReactNode;
|
||||
basePath: string;
|
||||
}
|
||||
|
||||
export const SettingsTabLayout: React.FC<SettingsTabLayoutProps> = ({
|
||||
tabs,
|
||||
children,
|
||||
basePath,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="settings-layout">
|
||||
<aside className="settings-sidebar">
|
||||
<nav className="settings-nav">
|
||||
{tabs.map((tab) => (
|
||||
<Link
|
||||
key={tab.id}
|
||||
to={`${basePath}/${tab.path}`}
|
||||
className={`settings-nav-link ${
|
||||
location.pathname.includes(tab.path) ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="settings-content">{children}</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface WorkspaceHeaderProps {
|
||||
project: {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
};
|
||||
currentRepo?: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const WorkspaceHeader = ({ project, currentRepo }: WorkspaceHeaderProps) => {
|
||||
return (
|
||||
<div className="workspace-header">
|
||||
<div className="workspace-header-left">
|
||||
<div className="workspace-header-icon">📁</div>
|
||||
<div className="workspace-header-info">
|
||||
<h1 className="workspace-header-title">{project.name}</h1>
|
||||
{currentRepo && (
|
||||
<span className="workspace-header-subtitle">{currentRepo.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="workspace-header-actions">
|
||||
<Link
|
||||
className="workspace-header-action-btn"
|
||||
to={`/projects/${project.id}/repositories/${currentRepo?.id || ""}/history`}
|
||||
>
|
||||
<span className="icon">🕐</span>
|
||||
History
|
||||
</Link>
|
||||
<Link
|
||||
className="workspace-header-action-btn"
|
||||
to={`/projects/${project.id}/settings`}
|
||||
>
|
||||
<span className="icon">⚙️</span>
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ProfilePage } from "./pages/profile";
|
||||
import { ProjectsPage } from "./pages/projects";
|
||||
import { GitRepositoriesPage } from "./pages/git-repositories";
|
||||
import { GitHistoryPage } from "./pages/git-history";
|
||||
import { ProjectSettingsPage } from "./pages/project-settings";
|
||||
import { RepoWorkspace } from "./pages/repo-workspace";
|
||||
import { SSHKeysPage } from "./pages/ssh-keys";
|
||||
import { SettingsPage } from "./pages/settings";
|
||||
@@ -30,6 +31,7 @@ export const AppRouter = () => {
|
||||
<Route path="projects/:projectId" element={<RepoWorkspace />} />
|
||||
<Route path="projects/:projectId/repositories" element={<GitRepositoriesPage />} />
|
||||
<Route path="projects/:projectId/repositories/:repoId/history" element={<GitHistoryPage />} />
|
||||
<Route path="projects/:projectId/settings/*" element={<ProjectSettingsPage />} />
|
||||
<Route path="ssh-keys" element={<SSHKeysPage />} />
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
|
||||
+170
-1
@@ -707,11 +707,62 @@ a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.workspace-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.workspace-header-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.workspace-header-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.workspace-header-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-header-subtitle {
|
||||
color: var(--muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.workspace-header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.workspace-header-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
color: var(--ink);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.workspace-header-action-btn:hover {
|
||||
background: var(--bg);
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.workspace-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1034,3 +1085,121 @@ a {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Settings Layout */
|
||||
.settings-layout {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.settings-nav-link {
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.settings-nav-link:hover {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.settings-nav-link.active {
|
||||
background: var(--brand);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-panel h2 {
|
||||
margin: 0 0 1.25rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.settings-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-breadcrumb a {
|
||||
color: var(--brand);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.settings-breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.project-settings-page {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Repositories Settings Tab */
|
||||
.repositories-settings-tab h2 {
|
||||
margin: 0 0 1.25rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.repositories-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.repository-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.repository-info h3 {
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.repository-info p {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.repo-type {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: var(--brand);
|
||||
color: white;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-19
|
||||
@@ -0,0 +1,270 @@
|
||||
# Workspace Visual Overhaul - Design
|
||||
|
||||
## Layout Architecture
|
||||
|
||||
### New Workspace Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ App Shell (Header + Nav) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ Project Workspace Header │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📁 My Project [History] [⚙️ Settings] │ │
|
||||
│ │ (secondary actions on right) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
├──────────────────────┬───────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ Sidebar (280px) │ Main Content │
|
||||
│ ┌────────────────┐ │ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Branch ▼ │ │ │ Breadcrumbs: src > components │ │
|
||||
│ ├────────────────┤ │ ├─────────────────────────────────────────┤ │
|
||||
│ │ 📁 src/ │ │ │ Toolbar: [Edit] [Raw] [Blame] │ │
|
||||
│ │ 📁 tests/ │ │ ├─────────────────────────────────────────┤ │
|
||||
│ │ 📄 README.md │ │ │ │ │
|
||||
│ │ ... │ │ │ function hello() { │ │
|
||||
│ │ │ │ │ return "world"; │ │
|
||||
│ └────────────────┘ │ │ } │ │
|
||||
│ │ │ │ │
|
||||
│ │ └─────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└──────────────────────┴───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Project Settings Page
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ App Shell (Header + Nav) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ Settings Header │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ← Back to Project My Project / Settings │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
├──────────────────┬───────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ Settings Tabs │ Tab Content │
|
||||
│ ┌──────────────┐│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ General ││ │ General Settings │ │
|
||||
│ ├──────────────┤│ │ │ │
|
||||
│ │ Repositories ││ │ Project Name: [My Project ] │ │
|
||||
│ ├──────────────┤│ │ Description: [A test project... ] │ │
|
||||
│ │ Members ││ │ │ │
|
||||
│ ├──────────────┤│ │ [Save Changes] │ │
|
||||
│ │ Danger Zone ││ │ │ │
|
||||
│ └──────────────┘│ └─────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└──────────────────┴───────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Component Changes
|
||||
|
||||
### 1. WorkspaceHeader (New Component)
|
||||
|
||||
**Location**: `apps/web/src/components/workspace-header.tsx`
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface WorkspaceHeaderProps {
|
||||
project: Project;
|
||||
onNavigateToHistory: () => void;
|
||||
onNavigateToSettings: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Layout:**
|
||||
- Left: Project icon + Project name (bold)
|
||||
- Right: Action buttons group
|
||||
- "History" button (clock icon)
|
||||
- "Settings" button (gear icon)
|
||||
- Subtitle (optional): Project description or current repo name
|
||||
|
||||
### 2. ProjectSettingsPage (New Page)
|
||||
|
||||
**Location**: `apps/web/src/pages/project-settings.tsx`
|
||||
|
||||
**Route**: `/projects/:projectId/settings`
|
||||
|
||||
**Tabs:**
|
||||
1. **General**
|
||||
- Project name input
|
||||
- Description textarea
|
||||
- Created date (read-only)
|
||||
- Save button
|
||||
|
||||
2. **Repositories** (moved from separate page)
|
||||
- List of project repositories
|
||||
- Add repository button
|
||||
- Repository cards with actions (delete)
|
||||
- Same functionality as current repo management
|
||||
|
||||
3. **Members** (placeholder for future)
|
||||
- "Coming soon" message
|
||||
|
||||
4. **Danger Zone**
|
||||
- Delete project button (with confirmation)
|
||||
|
||||
### 3. Sidebar Simplification
|
||||
|
||||
**Remove from sidebar:**
|
||||
- "Manage Repositories" link (moved to settings)
|
||||
- "History" link (moved to header)
|
||||
|
||||
**Keep in sidebar:**
|
||||
- Branch selector
|
||||
- File tree
|
||||
|
||||
### 4. Navigation Updates
|
||||
|
||||
**Current routes:**
|
||||
- `/projects` → Project list (unchanged)
|
||||
- `/projects/:id` → Workspace (new header)
|
||||
- `/projects/:id/repositories` → Repository list (REMOVE)
|
||||
- `/projects/:id/repositories/:repoId/history` → History (keep)
|
||||
- `/projects/:id/settings` → NEW
|
||||
|
||||
**New navigation flow:**
|
||||
```
|
||||
Projects List
|
||||
↓ click
|
||||
Workspace (with header actions)
|
||||
↓ click Settings
|
||||
Project Settings
|
||||
↓ click Repositories tab
|
||||
Repository Management (in settings)
|
||||
↓ click Back
|
||||
Workspace
|
||||
```
|
||||
|
||||
## CSS Changes
|
||||
|
||||
### Workspace Header
|
||||
```css
|
||||
.workspace-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--surface-color);
|
||||
}
|
||||
|
||||
.workspace-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.workspace-header-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.workspace-header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.workspace-header-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-color);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.workspace-header-action-btn:hover {
|
||||
background: var(--hover-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Layout
|
||||
```css
|
||||
.settings-container {
|
||||
display: flex;
|
||||
min-height: calc(100vh - var(--header-height));
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 240px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.settings-tab {
|
||||
display: block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.settings-tab:hover {
|
||||
background: var(--hover-color);
|
||||
}
|
||||
|
||||
.settings-tab.active {
|
||||
color: var(--primary-color);
|
||||
border-left-color: var(--primary-color);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
}
|
||||
```
|
||||
|
||||
## API Changes
|
||||
|
||||
No new API endpoints needed. The settings page will use existing APIs:
|
||||
- `GET /projects/:id` - project details
|
||||
- `PUT /projects/:id` - update project (already exists? if not, add)
|
||||
- `DELETE /projects/:id` - delete project (already exists)
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Create WorkspaceHeader component**
|
||||
- Extract from current workspace page
|
||||
- Add action buttons
|
||||
- Style appropriately
|
||||
|
||||
2. **Create ProjectSettingsPage**
|
||||
- Settings layout with tabs
|
||||
- General tab with project info
|
||||
- Danger zone with delete button
|
||||
|
||||
3. **Move repository management**
|
||||
- Move repo list from separate page to settings tab
|
||||
- Update routes
|
||||
|
||||
4. **Update workspace page**
|
||||
- Remove sidebar links
|
||||
- Add WorkspaceHeader
|
||||
- Keep file tree and viewer
|
||||
|
||||
5. **Update router**
|
||||
- Remove `/projects/:id/repositories` route
|
||||
- Add `/projects/:id/settings` route
|
||||
- Add tab routes: `/projects/:id/settings/general`, etc.
|
||||
|
||||
6. **Polish**
|
||||
- Ensure responsive design
|
||||
- Test navigation flow
|
||||
- Update breadcrumbs if needed
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Users currently accessing `/projects/:id/repositories` will need to navigate to Settings > Repositories
|
||||
- Can add a redirect or show a message on the old route temporarily
|
||||
- Update any hardcoded links in the codebase
|
||||
@@ -0,0 +1,49 @@
|
||||
# Workspace Visual Overhaul
|
||||
|
||||
## Problem
|
||||
|
||||
The current repository workspace layout has usability issues:
|
||||
|
||||
1. **Scattered navigation**: Git toolbar buttons, repository management, and project actions are in different places
|
||||
2. **No centralized project settings**: Managing repositories requires leaving the workspace
|
||||
3. **Cluttered sidebar**: Too many elements competing for attention
|
||||
4. **Inconsistent placement**: Actions aren't where users expect them (GitHub-style top-right)
|
||||
|
||||
## Solution
|
||||
|
||||
Redesign the workspace layout to follow GitHub-style conventions:
|
||||
|
||||
1. **Header actions on the right**: All primary actions (History, Settings, etc.) in the top-right
|
||||
2. **Project settings page**: Centralized location for all project configuration
|
||||
3. **Repository management in settings**: Move "Manage Repositories" into project settings
|
||||
4. **Cleaner sidebar**: Focus on file tree and branch switching
|
||||
5. **Consistent navigation patterns**: Match familiar GitHub/GitLab layouts
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Familiar interface**: Users already know GitHub's layout
|
||||
- **Cleaner workspace**: Less clutter in the main view
|
||||
- **Centralized settings**: All project config in one place
|
||||
- **Better hierarchy**: Clear separation between workspace and management
|
||||
|
||||
## Scope
|
||||
|
||||
### What's changing:
|
||||
- Workspace header layout (buttons moved to right)
|
||||
- New Project Settings page
|
||||
- Repository management moved to settings
|
||||
- Sidebar simplification
|
||||
- Navigation restructuring
|
||||
|
||||
### What's staying:
|
||||
- File browser functionality
|
||||
- Git toolbar in workspace
|
||||
- File viewer/editor
|
||||
- Branch selector
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **GitHub-like header**: Project name left, actions right
|
||||
2. **Settings as primary action**: Gear icon in header
|
||||
3. **Streamlined sidebar**: Only file tree + branch selector
|
||||
4. **Settings tabs**: General, Repositories, Members (future), etc.
|
||||
@@ -0,0 +1,184 @@
|
||||
# Workspace Visual Overhaul Specification
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Header Actions**: All primary project actions in workspace header (right-aligned)
|
||||
2. **Project Settings Page**: Dedicated settings page with tabs
|
||||
3. **Repository Management in Settings**: Move repo management to settings tab
|
||||
4. **Sidebar Simplification**: Remove management links from sidebar
|
||||
5. **Consistent Navigation**: Follow GitHub-style patterns
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Responsive**: Works on desktop, tablet, and mobile
|
||||
2. **Accessible**: Keyboard navigation, screen reader support
|
||||
3. **Performant**: No layout shifts, smooth transitions
|
||||
4. **Maintainable**: Clean component separation
|
||||
|
||||
## UI Specification
|
||||
|
||||
### Workspace Header
|
||||
|
||||
**Left Side:**
|
||||
- Project icon (📁 or custom)
|
||||
- Project name (bold, 1.25rem)
|
||||
- Optional: Current repository name (smaller, muted)
|
||||
|
||||
**Right Side (Actions):**
|
||||
- "History" button (clock icon) → navigates to history
|
||||
- "Settings" button (gear icon) → navigates to settings
|
||||
|
||||
**States:**
|
||||
- Loading: Show skeleton or spinner
|
||||
- Error: Show error message with retry
|
||||
|
||||
### Project Settings Page
|
||||
|
||||
**Layout:**
|
||||
- Two-column layout (sidebar tabs + content)
|
||||
- Mobile: Tabs at top, content below
|
||||
|
||||
**Tabs:**
|
||||
|
||||
#### General Tab
|
||||
- Project name input (required)
|
||||
- Description textarea (optional)
|
||||
- Created at (read-only)
|
||||
- Updated at (read-only)
|
||||
- Save changes button
|
||||
- Cancel button (if dirty)
|
||||
|
||||
#### Repositories Tab
|
||||
- Repository list (same as current repo management)
|
||||
- Add repository button
|
||||
- Each repo card:
|
||||
- Name
|
||||
- Remote URL (truncated)
|
||||
- Last updated
|
||||
- Actions: Delete
|
||||
|
||||
#### Members Tab (Placeholder)
|
||||
- "Coming soon" message
|
||||
- Brief description of future feature
|
||||
|
||||
#### Danger Zone
|
||||
- Red background/warning styling
|
||||
- Delete project button
|
||||
- Confirmation dialog on click
|
||||
|
||||
### Sidebar Changes
|
||||
|
||||
**Current sidebar items:**
|
||||
- Branch selector
|
||||
- File tree
|
||||
- Manage Repositories link
|
||||
|
||||
**New sidebar items:**
|
||||
- Branch selector (keep)
|
||||
- File tree (keep)
|
||||
- ~~Manage Repositories~~ (remove)
|
||||
|
||||
## Route Changes
|
||||
|
||||
### Remove Routes
|
||||
- `/projects/:projectId/repositories` → Moved to settings
|
||||
|
||||
### New Routes
|
||||
- `/projects/:projectId/settings` → Settings (defaults to General tab)
|
||||
- `/projects/:projectId/settings/general` → General settings
|
||||
- `/projects/:projectId/settings/repositories` → Repository management
|
||||
- `/projects/:projectId/settings/members` → Members (placeholder)
|
||||
|
||||
### Keep Routes
|
||||
- `/projects` → Project list
|
||||
- `/projects/:projectId` → Workspace
|
||||
- `/projects/:projectId/repositories/:repoId/history` → History
|
||||
|
||||
## Component Specification
|
||||
|
||||
### WorkspaceHeader
|
||||
```typescript
|
||||
interface WorkspaceHeaderProps {
|
||||
project: {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
currentRepo?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
onNavigateToHistory: () => void;
|
||||
onNavigateToSettings: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### ProjectSettingsPage
|
||||
```typescript
|
||||
interface ProjectSettingsProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
// Tabs configuration
|
||||
const TABS = [
|
||||
{ id: 'general', label: 'General', icon: 'settings' },
|
||||
{ id: 'repositories', label: 'Repositories', icon: 'repo' },
|
||||
{ id: 'members', label: 'Members', icon: 'people' },
|
||||
];
|
||||
```
|
||||
|
||||
### SettingsTabLayout
|
||||
```typescript
|
||||
interface SettingsTabLayoutProps {
|
||||
projectId: string;
|
||||
activeTab: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
## API Requirements
|
||||
|
||||
### PUT /projects/:id
|
||||
Update project details.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Updated Project Name",
|
||||
"description": "Updated description"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Updated Project Name",
|
||||
"description": "Updated description",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-02T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Project not found**: Show 404 page
|
||||
- **Permission denied**: Show auth error, redirect to login
|
||||
- **Save failed**: Show toast notification, keep form state
|
||||
- **Delete failed**: Show error in danger zone
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Frontend Tests
|
||||
- Test header navigation (History, Settings buttons)
|
||||
- Test settings tabs switching
|
||||
- Test form validation (name required)
|
||||
- Test delete confirmation flow
|
||||
- Test responsive layout
|
||||
|
||||
### Integration Tests
|
||||
- Test navigation flow: workspace → settings → back
|
||||
- Test route changes
|
||||
- Test data persistence (save project changes)
|
||||
@@ -0,0 +1,119 @@
|
||||
# Workspace Visual Overhaul - Tasks
|
||||
|
||||
## Phase 1: Workspace Header
|
||||
|
||||
- [ ] **Task 1.1**: Create WorkspaceHeader component
|
||||
- Create `components/workspace-header.tsx`
|
||||
- Project icon + name on left
|
||||
- History + Settings buttons on right
|
||||
- Props: project, currentRepo, onNavigateToHistory, onNavigateToSettings
|
||||
- Add CSS styles
|
||||
|
||||
- [ ] **Task 1.2**: Integrate WorkspaceHeader into RepoWorkspace
|
||||
- Replace current project header in `pages/repo-workspace.tsx`
|
||||
- Add navigation handlers
|
||||
- Remove old header styles
|
||||
|
||||
## Phase 2: Project Settings Page
|
||||
|
||||
- [ ] **Task 2.1**: Create SettingsTabLayout component
|
||||
- Create `components/settings-tab-layout.tsx`
|
||||
- Sidebar with tab links
|
||||
- Content area for tab content
|
||||
- Active tab highlighting
|
||||
- Mobile responsive (tabs at top)
|
||||
|
||||
- [ ] **Task 2.2**: Create ProjectSettingsPage
|
||||
- Create `pages/project-settings.tsx`
|
||||
- Use SettingsTabLayout
|
||||
- Implement General tab (name, description, dates)
|
||||
- Save/cancel buttons
|
||||
- Danger zone section
|
||||
|
||||
- [ ] **Task 2.3**: Add API endpoint for project updates
|
||||
- Add `PUT /projects/:id` to `src/api/projects.py`
|
||||
- Update name and description
|
||||
- Return updated project
|
||||
- Add validation
|
||||
|
||||
## Phase 3: Move Repository Management
|
||||
|
||||
- [ ] **Task 3.1**: Create RepositoriesSettingsTab
|
||||
- Create `components/repositories-settings-tab.tsx`
|
||||
- Move logic from `pages/git-repositories.tsx`
|
||||
- Repository list with cards
|
||||
- Add/delete functionality
|
||||
- Integrate into settings page
|
||||
|
||||
- [ ] **Task 3.2**: Update routes
|
||||
- Remove `/projects/:id/repositories` route
|
||||
- Add `/projects/:id/settings` route
|
||||
- Add tab routes: `/settings/general`, `/settings/repositories`
|
||||
- Add redirect from old repo management URL
|
||||
|
||||
- [ ] **Task 3.3**: Update navigation
|
||||
- Remove "Manage Repositories" from sidebar
|
||||
- Update any links to old repo management page
|
||||
- Add "Settings" link to workspace header
|
||||
|
||||
## Phase 4: Sidebar Cleanup
|
||||
|
||||
- [ ] **Task 4.1**: Simplify sidebar
|
||||
- Remove management links from sidebar
|
||||
- Keep only: Branch selector, File tree
|
||||
- Adjust sidebar width if needed
|
||||
- Update sidebar styles
|
||||
|
||||
- [ ] **Task 4.2**: Update workspace layout
|
||||
- Ensure clean separation between header, sidebar, and main content
|
||||
- Fix any layout issues from header changes
|
||||
- Ensure consistent spacing
|
||||
|
||||
## Phase 5: Members Placeholder
|
||||
|
||||
- [ ] **Task 5.1**: Create Members tab placeholder
|
||||
- Create `components/members-settings-tab.tsx`
|
||||
- "Coming soon" message
|
||||
- Brief feature description
|
||||
- Add to settings tabs
|
||||
|
||||
## Phase 6: Polish & Integration
|
||||
|
||||
- [ ] **Task 6.1**: Add breadcrumbs
|
||||
- Add breadcrumb navigation to settings page
|
||||
- Format: Projects > My Project > Settings > General
|
||||
- Make segments clickable
|
||||
|
||||
- [ ] **Task 6.2**: Add mobile responsiveness
|
||||
- Settings tabs become horizontal on mobile
|
||||
- Header actions collapse to icons only
|
||||
- Sidebar becomes toggleable overlay
|
||||
|
||||
- [ ] **Task 6.3**: Add transitions
|
||||
- Tab switching animations
|
||||
- Page transition animations
|
||||
- Button hover effects
|
||||
|
||||
- [ ] **Task 6.4**: Test navigation flow
|
||||
- Workspace → Settings → back
|
||||
- Settings tabs switching
|
||||
- Save changes flow
|
||||
- Delete project flow
|
||||
|
||||
## Phase 7: Quality Gates
|
||||
|
||||
- [ ] **Task 7.1**: Run backend checks
|
||||
- ruff check
|
||||
- mypy
|
||||
- pytest
|
||||
|
||||
- [ ] **Task 7.2**: Run frontend checks
|
||||
- TypeScript typecheck
|
||||
- ESLint
|
||||
- Build
|
||||
|
||||
- [ ] **Task 7.3**: Manual testing
|
||||
- Test all navigation flows
|
||||
- Test responsive breakpoints
|
||||
- Test form validation
|
||||
- Verify no broken links
|
||||
Reference in New Issue
Block a user