415aecc0dd
- Add dynamic viewport bounds and scrollable body regions to dialogs and modals\n- Make tool-launch popups use the shared scrollable body pattern\n- Keep mobile sheets, action sheets, and notification popups scroll-contained\n- Restore the ProjectsPage test setup required for the frontend suite\n\nOpenSpec: fix-dialog-scroll-containment\nQuality gates: npm run typecheck, npm run lint, npm test (88 passed), npm run build
121 lines
3.1 KiB
TypeScript
121 lines
3.1 KiB
TypeScript
/** Floating action button to start a tool from any page. */
|
|
|
|
import { useState } from "react";
|
|
import { Icon } from "../../icon";
|
|
import { ToolStarter } from "./tool-starter";
|
|
import type { Workspace } from "../../../types/workspace";
|
|
import { listAllWorkspaces } from "../../../api/workspaces";
|
|
|
|
export function StartToolFAB() {
|
|
const [open, setOpen] = useState(false);
|
|
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
|
const [workspacesLoading, setWorkspacesLoading] = useState(false);
|
|
const [selectedWorkspace, setSelectedWorkspace] = useState<Workspace | null>(
|
|
null,
|
|
);
|
|
|
|
const handleOpen = async () => {
|
|
setOpen(true);
|
|
setWorkspacesLoading(true);
|
|
try {
|
|
const data = await listAllWorkspaces();
|
|
setWorkspaces(data);
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setWorkspacesLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setOpen(false);
|
|
setSelectedWorkspace(null);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
className="start-tool-fab"
|
|
onClick={handleOpen}
|
|
title="Start a new tool"
|
|
type="button"
|
|
aria-label="Start a new tool"
|
|
>
|
|
<Icon name="play" size="md" />
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="modal-overlay" onClick={handleClose}>
|
|
<div
|
|
className="modal-content start-tool-modal"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="modal-header">
|
|
<h3>Start Tool</h3>
|
|
<button
|
|
className="ghost-button small"
|
|
onClick={handleClose}
|
|
type="button"
|
|
aria-label="Close"
|
|
>
|
|
<Icon name="close" size="sm" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="modal-body">
|
|
{workspacesLoading ? (
|
|
<p className="muted">Loading workspaces...</p>
|
|
) : workspaces.length === 0 ? (
|
|
<p className="muted">
|
|
No workspaces yet.{" "}
|
|
<a href="/workspaces">Create a workspace first</a>.
|
|
</p>
|
|
) : !selectedWorkspace ? (
|
|
<div className="form-group">
|
|
<label htmlFor="fab-workspace-select">Select a workspace</label>
|
|
<select
|
|
id="fab-workspace-select"
|
|
value=""
|
|
onChange={(e) => {
|
|
const ws = workspaces.find((w) => w.id === e.target.value);
|
|
if (ws) setSelectedWorkspace(ws);
|
|
}}
|
|
>
|
|
<option value="">Choose a workspace...</option>
|
|
{workspaces.map((ws) => (
|
|
<option key={ws.id} value={ws.id}>
|
|
{ws.project_name} / {ws.repo_name} / {ws.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="tool-starter-header">
|
|
<h4>
|
|
{selectedWorkspace.project_name} /{" "}
|
|
{selectedWorkspace.repo_name} / {selectedWorkspace.name}
|
|
</h4>
|
|
<button
|
|
className="ghost-button small"
|
|
onClick={() => setSelectedWorkspace(null)}
|
|
type="button"
|
|
>
|
|
Change
|
|
</button>
|
|
</div>
|
|
<ToolStarter
|
|
workspace={selectedWorkspace}
|
|
onStarted={handleClose}
|
|
onCancel={() => setSelectedWorkspace(null)}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|