Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 25 additions & 99 deletions apps/ui/src/components/dialogs/file-browser-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import {
FolderOpen,
Folder,
ChevronRight,
Home,
ArrowLeft,
HardDrive,
CornerDownLeft,
Clock,
X,
} from 'lucide-react';
import { useState, useEffect, useCallback } from 'react';
import { FolderOpen, Folder, ChevronRight, HardDrive, Clock, X } from 'lucide-react';
import {
Dialog,
DialogContent,
Expand All @@ -19,7 +9,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { PathInput } from '@/components/ui/path-input';
import { getJSON, setJSON } from '@/lib/storage';
import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config';

Expand Down Expand Up @@ -78,15 +68,13 @@ export function FileBrowserDialog({
initialPath,
}: FileBrowserDialogProps) {
const [currentPath, setCurrentPath] = useState<string>('');
const [pathInput, setPathInput] = useState<string>('');
const [parentPath, setParentPath] = useState<string | null>(null);
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
const [drives, setDrives] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [warning, setWarning] = useState('');
const [recentFolders, setRecentFolders] = useState<string[]>([]);
const pathInputRef = useRef<HTMLInputElement>(null);

// Load recent folders when dialog opens
useEffect(() => {
Expand Down Expand Up @@ -120,7 +108,6 @@ export function FileBrowserDialog({

if (result.success) {
setCurrentPath(result.currentPath);
setPathInput(result.currentPath);
setParentPath(result.parentPath);
setDirectories(result.directories);
setDrives(result.drives || []);
Expand All @@ -142,11 +129,10 @@ export function FileBrowserDialog({
[browseDirectory]
);

// Reset current path when dialog closes
// Reset state when dialog closes
useEffect(() => {
if (!open) {
setCurrentPath('');
setPathInput('');
setParentPath(null);
setDirectories([]);
setError('');
Expand All @@ -172,9 +158,6 @@ export function FileBrowserDialog({
const pathToUse = initialPath || defaultDir;

if (pathToUse) {
// Pre-fill the path input immediately
setPathInput(pathToUse);
// Then browse to that directory
browseDirectory(pathToUse);
} else {
// No default directory, browse home directory
Expand All @@ -183,7 +166,6 @@ export function FileBrowserDialog({
} catch {
// If config fetch fails, try initialPath or fall back to home directory
if (initialPath) {
setPathInput(initialPath);
browseDirectory(initialPath);
} else {
browseDirectory();
Expand All @@ -199,34 +181,21 @@ export function FileBrowserDialog({
browseDirectory(dir.path);
};

const handleGoToParent = () => {
if (parentPath) {
browseDirectory(parentPath);
}
};

const handleGoHome = () => {
const handleGoHome = useCallback(() => {
browseDirectory();
};
}, [browseDirectory]);

const handleNavigate = useCallback(
(path: string) => {
browseDirectory(path);
},
[browseDirectory]
);

const handleSelectDrive = (drivePath: string) => {
browseDirectory(drivePath);
};

const handleGoToPath = () => {
const trimmedPath = pathInput.trim();
if (trimmedPath) {
browseDirectory(trimmedPath);
}
};

const handlePathInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleGoToPath();
}
};

const handleSelect = useCallback(() => {
if (currentPath) {
addRecentFolder(currentPath);
Expand Down Expand Up @@ -275,31 +244,15 @@ export function FileBrowserDialog({
</DialogHeader>

<div className="flex flex-col gap-2 min-h-[350px] flex-1 overflow-hidden py-1">
{/* Direct path input */}
<div className="flex items-center gap-1.5">
<Input
ref={pathInputRef}
type="text"
placeholder="Paste or type a full path (e.g., /home/user/projects/myapp)"
value={pathInput}
onChange={(e) => setPathInput(e.target.value)}
onKeyDown={handlePathInputKeyDown}
className="flex-1 font-mono text-xs h-8"
data-testid="path-input"
disabled={loading}
/>
<Button
variant="secondary"
size="sm"
onClick={handleGoToPath}
disabled={loading || !pathInput.trim()}
data-testid="go-to-path-button"
className="h-8 px-2"
>
<CornerDownLeft className="w-3.5 h-3.5 mr-1" />
Go
</Button>
</div>
{/* Path navigation */}
<PathInput
currentPath={currentPath}
parentPath={parentPath}
loading={loading}
error={!!error}
onNavigate={handleNavigate}
onHome={handleGoHome}
/>

{/* Recent folders */}
{recentFolders.length > 0 && (
Expand Down Expand Up @@ -352,35 +305,8 @@ export function FileBrowserDialog({
</div>
)}

{/* Current path breadcrumb */}
<div className="flex items-center gap-1.5 p-2 rounded-md bg-sidebar-accent/10 border border-sidebar-border">
<Button
variant="ghost"
size="sm"
onClick={handleGoHome}
className="h-6 px-1.5"
disabled={loading}
>
<Home className="w-3.5 h-3.5" />
</Button>
{parentPath && (
<Button
variant="ghost"
size="sm"
onClick={handleGoToParent}
className="h-6 px-1.5"
disabled={loading}
>
<ArrowLeft className="w-3.5 h-3.5" />
</Button>
)}
<div className="flex-1 font-mono text-xs truncate text-muted-foreground">
{currentPath || 'Loading...'}
</div>
</div>

{/* Directory list */}
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-md">
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-md scrollbar-styled">
{loading && (
<div className="flex items-center justify-center h-full p-4">
<div className="text-xs text-muted-foreground">Loading directories...</div>
Expand Down Expand Up @@ -423,8 +349,8 @@ export function FileBrowserDialog({
</div>

<div className="text-[10px] text-muted-foreground">
Paste a full path above, or click on folders to navigate. Press Enter or click Go to
jump to a path.
Paste a full path above, or click on folders to navigate. Press Enter or click to jump
to a path.
</div>
</div>

Expand Down
102 changes: 102 additions & 0 deletions apps/ui/src/components/ui/breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';

import { cn } from '@/lib/utils';

function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}

function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
className
)}
{...props}
/>
);
}

function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
return (
<li
data-slot="breadcrumb-item"
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
);
}

function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<'a'> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'a';

return (
<Comp
data-slot="breadcrumb-link"
className={cn('hover:text-foreground transition-colors', className)}
{...props}
/>
);
}

function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn('text-foreground font-normal', className)}
{...props}
/>
);
}

function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn('[&>svg]:size-3.5', className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}

function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn('flex size-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}

export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
Loading