refactor: extract dashboard, workspace, tool-types, and tool-configs components (Task 4.3)

- Extract DashboardSummary, ActiveSessionsList, ProjectsSection, QuickCreateForm,
  RecentSessionsSection from dashboard.tsx (480 → 110 lines)
- Extract WorkspaceSidebar from repo-workspace.tsx
- Extract ToolTypeList + ToolTypeForm from tool-types.tsx (409 → 135 lines)
- Extract ToolConfigList + ToolConfigForm from tool-configs.tsx (391 → 178 lines)
- Add use-dashboard-actions hook for shared dashboard action handlers
- Update feature barrels with new exports

Quality gates: tsc (pass), eslint (pass)
Refs: repo-restructure Task 4.3
This commit is contained in:
Developer
2026-06-02 22:23:13 +00:00
parent 6bd7443e68
commit a6eb6ec788
32 changed files with 1583 additions and 1104 deletions
@@ -0,0 +1,62 @@
import { render, screen, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { MemoryRouter } from "react-router-dom";
import { FileBrowser } from "./FileBrowser";
// Mock apiClient
vi.mock("../../../api/client", () => ({
apiClient: {
get: vi.fn(),
},
}));
import { apiClient } from "../../../api/client";
describe("FileBrowser", () => {
it("renders loading state initially", () => {
render(
<MemoryRouter>
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
</MemoryRouter>
);
expect(screen.getByText(/loading files/i)).toBeInTheDocument();
});
it("renders file entries after loading", async () => {
const mockedGet = apiClient.get as ReturnType<typeof vi.fn>;
mockedGet.mockResolvedValueOnce({
data: {
entries: [
{ name: "src", type: "directory", path: "src" },
{ name: "README.md", type: "file", path: "README.md" },
],
},
});
render(
<MemoryRouter>
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("src")).toBeInTheDocument();
});
expect(screen.getByText("README.md")).toBeInTheDocument();
});
it("renders error state on failure", async () => {
const mockedGet = apiClient.get as ReturnType<typeof vi.fn>;
mockedGet.mockRejectedValueOnce(new Error("Network error"));
render(
<MemoryRouter>
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/failed to load files/i)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,67 @@
import type { GitRepository } from "../../../types/git-repository";
import type { GitStatus } from "../../../api/git_repositories";
import type { ToolType } from "../../../types/tool-type";
import { FileBrowser } from "./FileBrowser";
import { CommitPanel } from "../../../components/commit-panel";
import { InstanceList } from "../../../components/instance-list";
interface WorkspaceSidebarProps {
projectId: string;
repoId: string;
repositories: GitRepository[];
gitStatus: GitStatus | null;
toolTypes: ToolType[];
onRepoChange: (repoId: string) => void;
onCommit: () => void;
}
export const WorkspaceSidebar = ({
projectId,
repoId,
repositories,
gitStatus,
toolTypes,
onRepoChange,
onCommit,
}: WorkspaceSidebarProps) => {
return (
<aside className="workspace-sidebar">
<div className="sidebar-section">
<label className="form-field">
Repository
<select
value={repoId}
onChange={(e) => onRepoChange(e.target.value)}
>
{repositories.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.name}
</option>
))}
</select>
</label>
</div>
<FileBrowser
projectId={projectId}
repoId={repoId}
gitStatus={gitStatus}
/>
{gitStatus && (
<CommitPanel
projectId={projectId}
repoId={repoId}
modified={gitStatus.modified}
added={gitStatus.added}
deleted={gitStatus.deleted}
untracked={gitStatus.untracked}
onCommit={onCommit}
/>
)}
<InstanceList
projectId={projectId}
repoId={repoId}
toolTypes={toolTypes}
/>
</aside>
);
};
@@ -1 +1,2 @@
export { FileBrowser } from "./FileBrowser";
export { WorkspaceSidebar } from "./WorkspaceSidebar";