feat(KB-175): move file browser to header with workspace support
- Add workspace file APIs and hooks for session-scoped file browsing - Create WorkspaceSelector component for switching between workspaces - Update FileBrowserModal with workspace mode and improved UX - Add files button to header with workspace-aware file browsing - Add session files indicator to task cards for quick file access - Include comprehensive tests for all new components and hooks
This commit is contained in:
97
packages/dashboard/app/components/WorkspaceSelector.tsx
Normal file
97
packages/dashboard/app/components/WorkspaceSelector.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { ChevronDown, FolderGit2, FolderRoot } from "lucide-react";
|
||||
import type { WorkspaceInfo } from "../hooks/useWorkspaces";
|
||||
import "./WorkspaceSelector.css";
|
||||
|
||||
interface WorkspaceSelectorProps {
|
||||
currentWorkspace: string;
|
||||
projectName: string;
|
||||
workspaces: WorkspaceInfo[];
|
||||
onSelect: (workspace: string) => void;
|
||||
}
|
||||
|
||||
function truncateTitle(title?: string, maxLength = 44): string | undefined {
|
||||
if (!title) return undefined;
|
||||
if (title.length <= maxLength) return title;
|
||||
return `${title.slice(0, maxLength - 1)}…`;
|
||||
}
|
||||
|
||||
export function WorkspaceSelector({
|
||||
currentWorkspace,
|
||||
projectName,
|
||||
workspaces,
|
||||
onSelect,
|
||||
}: WorkspaceSelectorProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const currentLabel = useMemo(() => {
|
||||
if (currentWorkspace === "project") {
|
||||
return projectName || "Project Root";
|
||||
}
|
||||
|
||||
const match = workspaces.find((workspace) => workspace.id === currentWorkspace);
|
||||
return match?.label ?? currentWorkspace;
|
||||
}, [currentWorkspace, projectName, workspaces]);
|
||||
|
||||
return (
|
||||
<div className="workspace-selector">
|
||||
<button
|
||||
type="button"
|
||||
className="workspace-selector-trigger"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
{currentWorkspace === "project" ? <FolderRoot size={14} /> : <FolderGit2 size={14} />}
|
||||
<span className="workspace-selector-trigger-label">{currentLabel}</span>
|
||||
<ChevronDown size={14} className={`workspace-selector-trigger-icon${open ? " open" : ""}`} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="workspace-selector-menu" role="listbox" aria-label="Select workspace">
|
||||
<button
|
||||
type="button"
|
||||
className={`workspace-selector-option${currentWorkspace === "project" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onSelect("project");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="workspace-selector-option-main">
|
||||
<FolderRoot size={14} />
|
||||
<span>Project Root</span>
|
||||
</div>
|
||||
<span className="workspace-selector-option-meta">{projectName}</span>
|
||||
</button>
|
||||
|
||||
{workspaces.length > 0 && (
|
||||
<div className="workspace-selector-group">
|
||||
<div className="workspace-selector-group-label">Task Worktrees</div>
|
||||
{workspaces.map((workspace) => (
|
||||
<button
|
||||
key={workspace.id}
|
||||
type="button"
|
||||
className={`workspace-selector-option${currentWorkspace === workspace.id ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onSelect(workspace.id);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="workspace-selector-option-main">
|
||||
<FolderGit2 size={14} />
|
||||
<span>{workspace.id}</span>
|
||||
</div>
|
||||
{workspace.title && (
|
||||
<span className="workspace-selector-option-meta" title={workspace.title}>
|
||||
{truncateTitle(workspace.title)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user