feat: wire workspace CRUD UI to backend

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-03 11:05:56 +08:00
co-authored by Claude
parent b3d0b63f4b
commit 867f1d204e
5 changed files with 213 additions and 2 deletions
+5 -2
View File
@@ -7,6 +7,7 @@ import { EditorPanel } from "@/components/editor/EditorPanel";
import { TerminalPanel } from "@/components/terminal/TerminalPanel";
import { FileExplorerPanel } from "@/components/workspace/FileExplorerPanel";
import { StatusBar } from "@/components/workspace/StatusBar";
import { WorkspaceSelector } from "@/components/workspace/WorkspaceSelector";
import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store";
import {
findFileByPath,
@@ -28,9 +29,11 @@ export function WorkspaceShell({ files }: WorkspaceShellProps) {
return (
<div className="flex h-full w-full flex-col bg-background text-foreground">
{/* Toolbar */}
<header className="flex h-9 shrink-0 items-center border-b border-border bg-sidebar px-3">
<header className="flex h-9 shrink-0 items-center gap-1 border-b border-border bg-sidebar px-3">
<h1 className="text-sm font-semibold">codespace</h1>
<Separator orientation="vertical" className="mx-3 h-4" />
<Separator orientation="vertical" className="mx-2 h-4" />
<WorkspaceSelector />
<Separator orientation="vertical" className="mx-2 h-4" />
<div className="flex items-center gap-1">
<Button
variant="ghost"
@@ -0,0 +1,106 @@
import { Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
useCreateWorkspace,
useDeleteWorkspace,
useWorkspaces,
} from "@/lib/api/workspaces";
import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store";
export function WorkspaceSelector() {
const { data: workspaces, isLoading, isError, error } = useWorkspaces();
const createMutation = useCreateWorkspace();
const deleteMutation = useDeleteWorkspace();
const currentWorkspaceId = useWorkspaceUiStore((s) => s.currentWorkspaceId);
const setCurrentWorkspaceId = useWorkspaceUiStore(
(s) => s.setCurrentWorkspaceId,
);
const handleCreate = () => {
const id = window.prompt("New workspace id:");
if (!id) return;
const trimmed = id.trim();
if (!trimmed) return;
createMutation.mutate(trimmed, {
onSuccess: (ws) => setCurrentWorkspaceId(ws.id),
});
};
const handleDelete = () => {
if (!currentWorkspaceId) return;
const ok = window.confirm(
`Delete workspace "${currentWorkspaceId}"? This stops its process and removes the directory.`,
);
if (!ok) return;
deleteMutation.mutate(currentWorkspaceId, {
onSuccess: () => setCurrentWorkspaceId(null),
});
};
return (
<div className="flex items-center gap-1">
{isError ? (
<span
className="text-xs text-destructive"
title={error?.message ?? "load failed"}
>
load failed
</span>
) : (
<select
className="h-7 rounded-md border border-input bg-background px-2 text-xs text-foreground shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
value={currentWorkspaceId ?? ""}
onChange={(e) =>
setCurrentWorkspaceId(e.target.value === "" ? null : e.target.value)
}
disabled={isLoading}
>
<option value="">
{isLoading ? "loading…" : "select workspace"}
</option>
{workspaces?.map((ws) => (
<option key={ws.id} value={ws.id}>
{ws.id}
</option>
))}
</select>
)}
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
onClick={handleCreate}
disabled={createMutation.isPending}
aria-label="create workspace"
>
<Plus className="size-4" aria-hidden="true" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
onClick={handleDelete}
disabled={!currentWorkspaceId || deleteMutation.isPending}
aria-label="delete workspace"
>
<Trash2 className="size-4" aria-hidden="true" />
</Button>
{(createMutation.isError || deleteMutation.isError) && (
<span
className="text-xs text-destructive"
title={
createMutation.error?.message ??
deleteMutation.error?.message ??
"operation failed"
}
>
op failed
</span>
)}
</div>
);
}