fix: workspace delete mixed-content error via top-level endpoint
- Add top-level DELETE /workspaces/{workspace_id} endpoint (avoids nested path)
- Frontend deleteWorkspace now uses /workspaces/{id}/?force=... (no project/repo needed)
- Update useWorkspaceActions, ProjectsPage, WorkspacesPage to match new signature
Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
This commit is contained in:
@@ -67,6 +67,41 @@ async def list_all_workspaces(
|
||||
]
|
||||
|
||||
|
||||
@all_workspaces_router.delete("/{workspace_id}")
|
||||
async def delete_workspace_top_level(
|
||||
workspace_id: uuid.UUID,
|
||||
force: bool = Query(False),
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Delete a workspace via top-level path."""
|
||||
workspace = await session.get(Workspace, workspace_id)
|
||||
if not workspace or workspace.user_id != user_id:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
|
||||
manager = WorkspaceManager()
|
||||
try:
|
||||
await manager.delete(workspace, force=force, session=session)
|
||||
await session.commit()
|
||||
except WorkspaceHasInstancesError as exc:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail={
|
||||
"message": "Workspace has running tool instances",
|
||||
"instances": [{"id": str(i.id), "name": i.name} for i in exc.instances],
|
||||
},
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
await session.rollback()
|
||||
logger.error("Failed to delete workspace: %s", exc)
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Failed to delete workspace"
|
||||
) from exc
|
||||
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@all_workspaces_router.post("/")
|
||||
async def create_workspace_top_level(
|
||||
data: dict,
|
||||
|
||||
@@ -147,7 +147,9 @@ class GitService:
|
||||
os.unlink(key_path)
|
||||
|
||||
@staticmethod
|
||||
def branch_exists_remotely(path: str, branch: str, ssh_key: str | None = None) -> bool:
|
||||
def branch_exists_remotely(
|
||||
path: str, branch: str, ssh_key: str | None = None
|
||||
) -> bool:
|
||||
"""Check if a branch exists on the remote.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -71,13 +71,11 @@ export async function updateWorkspace(
|
||||
}
|
||||
|
||||
export async function deleteWorkspace(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
workspaceId: string,
|
||||
force = false,
|
||||
): Promise<{ status: string }> {
|
||||
const response = await apiClient.delete<{ status: string }>(
|
||||
`${workspaceUrl(projectId, repoId, workspaceId)}?force=${force}`,
|
||||
`/workspaces/${workspaceId}/?force=${force}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ export interface UseWorkspaceActionsResult {
|
||||
data: CreateWorkspaceRequest,
|
||||
) => Promise<Workspace>;
|
||||
delete: (
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
workspace: Workspace,
|
||||
onRefresh: () => Promise<void>,
|
||||
) => Promise<void>;
|
||||
@@ -61,14 +59,12 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult {
|
||||
|
||||
const deleteAction = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
workspace: Workspace,
|
||||
onRefresh: () => Promise<void>,
|
||||
) => {
|
||||
setLoadingId(workspace.id);
|
||||
try {
|
||||
await deleteWorkspace(projectId, repoId, workspace.id);
|
||||
await deleteWorkspace(workspace.id);
|
||||
await onRefresh();
|
||||
} catch (err) {
|
||||
const error = err as ApiError;
|
||||
@@ -81,7 +77,7 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult {
|
||||
`\n\nDelete workspace and all instances?`,
|
||||
);
|
||||
if (confirmed) {
|
||||
await deleteWorkspace(projectId, repoId, workspace.id, true);
|
||||
await deleteWorkspace(workspace.id, true);
|
||||
await onRefresh();
|
||||
}
|
||||
} else {
|
||||
@@ -118,7 +114,7 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult {
|
||||
`${message}\n\nDelete this workspace?`,
|
||||
);
|
||||
if (confirmed) {
|
||||
await deleteWorkspace(projectId, repoId, workspace.id, true);
|
||||
await deleteWorkspace(workspace.id, true);
|
||||
await onRefresh();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -124,15 +124,11 @@ export const ProjectsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorkspace = async (
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
workspace: WorkspaceSummary,
|
||||
) => {
|
||||
const handleDeleteWorkspace = async (workspace: WorkspaceSummary) => {
|
||||
if (!confirm(`Delete workspace "${workspace.name}"?`)) return;
|
||||
setWorkspaceLoading(workspace.id);
|
||||
try {
|
||||
await deleteWorkspace(projectId, repoId, workspace.id);
|
||||
await deleteWorkspace(workspace.id);
|
||||
reload();
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : "Failed to delete workspace");
|
||||
@@ -187,7 +183,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(workspace);
|
||||
}
|
||||
}}
|
||||
workspaceLoading={workspaceLoading}
|
||||
|
||||
@@ -18,12 +18,7 @@ export function WorkspacesPage() {
|
||||
const actions = useWorkspaceActions();
|
||||
|
||||
const handleDelete = async (workspace: Workspace) => {
|
||||
await actions.delete(
|
||||
workspace.project_id,
|
||||
workspace.repo_id,
|
||||
workspace,
|
||||
refresh,
|
||||
);
|
||||
await actions.delete(workspace, refresh);
|
||||
};
|
||||
|
||||
const handleSync = async (workspace: Workspace) => {
|
||||
|
||||
Reference in New Issue
Block a user