fix: allow workspace creation from workspaces page

- Replace awkward first-workspace-guessing logic with inline project/repo selector
- New WorkspaceCreateInline component with cascading dropdowns:
  - Select project → loads repositories for that project
  - Select repository → enter workspace name + branch
  - Submit creates workspace via top-level POST /workspaces/
- Add createWorkspaceTopLevel() API client for flat endpoint
- Works even with zero existing workspaces (shows create button in empty state)
- Add CSS grid layout for inline create form
- TypeScript + eslint clean
This commit is contained in:
2026-06-01 17:32:35 +02:00
parent 88a973dc68
commit ab1843b1c3
12 changed files with 870 additions and 561 deletions
+21 -34
View File
@@ -15,7 +15,11 @@ import {
deleteWorkspace,
syncWorkspace,
} from "../api/workspaces";
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import {
EmptyState,
ErrorState,
LoadingState,
} from "../components/data-states";
import { Icon } from "../components/icon";
import { WorkspaceCreateForm } from "../components/workspace-create-form";
import { useAsyncData } from "../hooks/use-async-data";
@@ -24,10 +28,11 @@ import type { ProjectWithRepos, WorkspaceSummary } from "../types";
type DialogMode = "none" | "create" | "edit";
export const ProjectsPage = () => {
const { data: projects, status, reload } = useAsyncData<ProjectWithRepos[]>(
listProjects,
[],
);
const {
data: projects,
status,
reload,
} = useAsyncData<ProjectWithRepos[]>(listProjects, []);
const [dialogMode, setDialogMode] = useState<DialogMode>("none");
const [editingProject, setEditingProject] = useState<ProjectWithRepos | null>(
null,
@@ -203,11 +208,7 @@ export const ProjectsPage = () => {
if (action === "sync") {
void handleSyncWorkspace(project.id, repoId, workspace);
} else if (action === "delete") {
void handleDeleteWorkspace(
project.id,
repoId,
workspace,
);
void handleDeleteWorkspace(project.id, repoId, workspace);
}
}}
workspaceLoading={workspaceLoading}
@@ -317,7 +318,10 @@ function ProjectCard({
workspaceLoading: string | null;
onCancelCreate: () => void;
showCreateForm: string | null;
onSubmitCreate: (repoId: string, data: { name: string; branch: string }) => Promise<void>;
onSubmitCreate: (
repoId: string,
data: { name: string; branch: string },
) => Promise<void>;
}) {
return (
<article className="card project-card">
@@ -328,10 +332,7 @@ function ProjectCard({
type="button"
aria-expanded={expanded}
>
<Icon
name={expanded ? "chevron-down" : "chevron-right"}
size="sm"
/>
<Icon name={expanded ? "chevron-down" : "chevron-right"} size="sm" />
<h3>{project.name}</h3>
{project.repositories.length > 0 && (
<span className="repo-count">
@@ -400,9 +401,7 @@ function ProjectCard({
<WorkspaceCreateForm
projectId={project.id}
repoId={repo.id}
onSubmit={(data) =>
onSubmitCreate(repo.id, data)
}
onSubmit={(data) => onSubmitCreate(repo.id, data)}
onCancel={onCancelCreate}
/>
)}
@@ -428,15 +427,9 @@ function ProjectCard({
<div className="ws-actions">
<button
type="button"
disabled={
workspaceLoading === ws.id
}
disabled={workspaceLoading === ws.id}
onClick={() =>
onWorkspaceAction(
repo.id,
ws,
"sync",
)
onWorkspaceAction(repo.id, ws, "sync")
}
>
<Icon name="refresh" size="sm" />
@@ -444,15 +437,9 @@ function ProjectCard({
<button
type="button"
className="danger-text"
disabled={
workspaceLoading === ws.id
}
disabled={workspaceLoading === ws.id}
onClick={() =>
onWorkspaceAction(
repo.id,
ws,
"delete",
)
onWorkspaceAction(repo.id, ws, "delete")
}
>
<Icon name="delete" size="sm" />