feat: workspace-first UI refresh - PR-2 workspace detail page
- Add workspace detail page (/workspaces/:id) with 4 tabs: - Files: file tree, viewer, editor, git toolbar (commit/push/pull/fetch) - Git: branch selector, commit history - Tools: instance grid, start tool modal - Settings: workspace info read-only - Add workspace API clients: workspace-files, workspace-git, workspace-instances - Add hooks: useWorkspaceFiles, useWorkspaceGit, useWorkspaceInstances - WorkspaceCard links to detail page via router Link - Add comprehensive CSS for workspace detail layout - Mobile: bottom tab bar, responsive file tree/split - TypeScript + eslint clean Quality gates: tsc --noEmit clean, eslint clean
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/** Hook for workspace file operations. */
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
listWorkspaceFiles,
|
||||
getWorkspaceFileContent,
|
||||
saveWorkspaceFile,
|
||||
type FileEntry,
|
||||
} from "../api/workspace-files";
|
||||
|
||||
export interface UseWorkspaceFilesResult {
|
||||
entries: FileEntry[];
|
||||
content: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
loadFile: (path: string) => Promise<void>;
|
||||
saveFile: (path: string, content: string, message?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useWorkspaceFiles(
|
||||
workspaceId: string,
|
||||
): UseWorkspaceFilesResult {
|
||||
const [entries, setEntries] = useState<FileEntry[]>([]);
|
||||
const [content, setContent] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await listWorkspaceFiles(workspaceId);
|
||||
setEntries(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load files");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [workspaceId]);
|
||||
|
||||
const loadFile = useCallback(
|
||||
async (path: string) => {
|
||||
try {
|
||||
const data = await getWorkspaceFileContent(workspaceId, path);
|
||||
setContent(data);
|
||||
} catch (err) {
|
||||
setContent(null);
|
||||
setError(err instanceof Error ? err.message : "Failed to load file");
|
||||
}
|
||||
},
|
||||
[workspaceId],
|
||||
);
|
||||
|
||||
const saveFile = useCallback(
|
||||
async (path: string, fileContent: string, message?: string) => {
|
||||
await saveWorkspaceFile(workspaceId, path, fileContent, message);
|
||||
await refresh();
|
||||
},
|
||||
[workspaceId, refresh],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
return { entries, content, loading, error, refresh, loadFile, saveFile };
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/** Hook for workspace git operations. */
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
getGitStatus,
|
||||
getGitBranches,
|
||||
gitCommit,
|
||||
gitPush,
|
||||
gitPull,
|
||||
gitFetch,
|
||||
gitCheckout,
|
||||
getGitHistory,
|
||||
type GitStatus,
|
||||
type Commit,
|
||||
} from "../api/workspace-git";
|
||||
|
||||
export interface UseWorkspaceGitResult {
|
||||
status: GitStatus | null;
|
||||
branches: string[];
|
||||
currentBranch: string;
|
||||
history: Commit[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
commit: (message: string) => Promise<void>;
|
||||
push: () => Promise<void>;
|
||||
pull: () => Promise<void>;
|
||||
fetch: () => Promise<void>;
|
||||
checkout: (branch: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useWorkspaceGit(workspaceId: string): UseWorkspaceGitResult {
|
||||
const [status, setStatus] = useState<GitStatus | null>(null);
|
||||
const [branches, setBranches] = useState<string[]>([]);
|
||||
const [currentBranch, setCurrentBranch] = useState("");
|
||||
const [history, setHistory] = useState<Commit[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [statusData, branchesData, historyData] = await Promise.all([
|
||||
getGitStatus(workspaceId),
|
||||
getGitBranches(workspaceId),
|
||||
getGitHistory(workspaceId),
|
||||
]);
|
||||
setStatus(statusData);
|
||||
setBranches(branchesData.branches);
|
||||
setCurrentBranch(branchesData.current_branch);
|
||||
setHistory(historyData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load git data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [workspaceId]);
|
||||
|
||||
const commit = useCallback(
|
||||
async (message: string) => {
|
||||
await gitCommit(workspaceId, message);
|
||||
await refresh();
|
||||
},
|
||||
[workspaceId, refresh],
|
||||
);
|
||||
|
||||
const push = useCallback(async () => {
|
||||
await gitPush(workspaceId);
|
||||
await refresh();
|
||||
}, [workspaceId, refresh]);
|
||||
|
||||
const pull = useCallback(async () => {
|
||||
await gitPull(workspaceId);
|
||||
await refresh();
|
||||
}, [workspaceId, refresh]);
|
||||
|
||||
const fetch = useCallback(async () => {
|
||||
await gitFetch(workspaceId);
|
||||
await refresh();
|
||||
}, [workspaceId, refresh]);
|
||||
|
||||
const checkout = useCallback(
|
||||
async (branch: string) => {
|
||||
await gitCheckout(workspaceId, branch);
|
||||
await refresh();
|
||||
},
|
||||
[workspaceId, refresh],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
return {
|
||||
status,
|
||||
branches,
|
||||
currentBranch,
|
||||
history,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
commit,
|
||||
push,
|
||||
pull,
|
||||
fetch,
|
||||
checkout,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/** Hook for workspace instance operations. */
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
listWorkspaceInstances,
|
||||
createWorkspaceInstance,
|
||||
} from "../api/workspace-instances";
|
||||
import type { ToolInstance } from "../api/sessions";
|
||||
|
||||
export interface UseWorkspaceInstancesResult {
|
||||
instances: ToolInstance[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
create: (
|
||||
toolTypeId: string,
|
||||
displayName?: string,
|
||||
configProfileId?: string,
|
||||
) => Promise<ToolInstance>;
|
||||
}
|
||||
|
||||
export function useWorkspaceInstances(
|
||||
workspaceId: string,
|
||||
): UseWorkspaceInstancesResult {
|
||||
const [instances, setInstances] = useState<ToolInstance[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await listWorkspaceInstances(workspaceId);
|
||||
setInstances(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load instances");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [workspaceId]);
|
||||
|
||||
const create = useCallback(
|
||||
async (
|
||||
toolTypeId: string,
|
||||
displayName?: string,
|
||||
configProfileId?: string,
|
||||
) => {
|
||||
const instance = await createWorkspaceInstance(
|
||||
workspaceId,
|
||||
toolTypeId,
|
||||
displayName,
|
||||
configProfileId,
|
||||
);
|
||||
await refresh();
|
||||
return instance;
|
||||
},
|
||||
[workspaceId, refresh],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
return { instances, loading, error, refresh, create };
|
||||
}
|
||||
Reference in New Issue
Block a user