hide dot-directories in the folder picker, with a toggle
The file-browser API does not filter them — readdir returns everything — so browsing for a working directory opened onto .cache, .local, .npm and thirty more before anything worth picking. Hidden by default, one toggle in the footer to reveal, and shown dimmed when revealed so they read as a different class of thing. The count sits on the toggle and the empty state names it too: a folder holding only dot-directories used to say 'No subfolders here', which is a lie with no way to notice it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Folder, ChevronRight, Loader2, FolderPlus, Check } from 'lucide-react';
|
||||
import { Folder, ChevronRight, Loader2, FolderPlus, Check, Eye, EyeOff } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { useFilesAPI, type DirEntry } from '../../hooks/useFilesAPI';
|
||||
|
||||
@@ -17,6 +17,10 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
||||
const [path, setPath] = useState('/'); // root-relative, always starts with '/'
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
// Off by default. The server does not filter these — `readdir` returns everything — so a home
|
||||
// directory opened straight onto `.cache`, `.local`, `.npm` and thirty more before anything you
|
||||
// recognise. They are still reachable, just not in the way by default.
|
||||
const [showHidden, setShowHidden] = useState(false);
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['dir-picker', path],
|
||||
@@ -25,9 +29,11 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
||||
});
|
||||
|
||||
const rootDir = data?.rootDir ?? '';
|
||||
const dirs = ((data?.entries ?? []) as DirEntry[])
|
||||
const allDirs = ((data?.entries ?? []) as DirEntry[])
|
||||
.filter((e) => e.type === 'directory')
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const hiddenCount = allDirs.filter((d) => d.name.startsWith('.')).length;
|
||||
const dirs = showHidden ? allDirs : allDirs.filter((d) => !d.name.startsWith('.'));
|
||||
const absCurrent = path === '/' ? rootDir : `${rootDir}${path}`;
|
||||
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
@@ -72,7 +78,13 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : dirs.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-sm opacity-40">No subfolders here</div>
|
||||
// Say when the folder isn't actually empty, just filtered — "No subfolders here" over a
|
||||
// directory holding thirty dot-folders is a lie the toggle below can't help you spot.
|
||||
<div className="flex h-full items-center justify-center px-4 text-center text-sm opacity-40">
|
||||
{hiddenCount > 0
|
||||
? `No visible subfolders — ${hiddenCount} hidden ${hiddenCount === 1 ? 'one is' : 'ones are'} filtered out`
|
||||
: 'No subfolders here'}
|
||||
</div>
|
||||
) : (
|
||||
dirs.map((d) => (
|
||||
<button
|
||||
@@ -80,8 +92,10 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
||||
onClick={() => enter(d.name)}
|
||||
className="flex w-full items-center gap-2 rounded px-3 py-2 text-left text-sm hover:bg-accent cursor-pointer"
|
||||
>
|
||||
<Folder className="h-4 w-4 shrink-0 text-duck-teal/70" />
|
||||
<span className="truncate">{d.name}</span>
|
||||
<Folder
|
||||
className={`h-4 w-4 shrink-0 text-duck-teal/70 ${d.name.startsWith('.') ? 'opacity-50' : ''}`}
|
||||
/>
|
||||
<span className={`truncate ${d.name.startsWith('.') ? 'opacity-60' : ''}`}>{d.name}</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
@@ -110,12 +124,24 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setCreating(true)}
|
||||
className="flex items-center gap-1.5 self-start text-xs opacity-60 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
<FolderPlus className="h-3.5 w-3.5" /> New folder
|
||||
</button>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<button
|
||||
onClick={() => setCreating(true)}
|
||||
className="flex items-center gap-1.5 text-xs opacity-60 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
<FolderPlus className="h-3.5 w-3.5" /> New folder
|
||||
</button>
|
||||
{/* Shown even at zero, so the control's state is always legible: a folder with nothing
|
||||
hidden and one with everything hidden must not look identical. */}
|
||||
<button
|
||||
onClick={() => setShowHidden((prev) => !prev)}
|
||||
title={showHidden ? 'Hide dot-directories' : 'Show dot-directories'}
|
||||
className="flex items-center gap-1.5 text-xs opacity-60 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
{showHidden ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
{showHidden ? 'Hide hidden' : `Show hidden${hiddenCount > 0 ? ` (${hiddenCount})` : ''}`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="min-w-0 flex-1 truncate text-xs opacity-60" title={absCurrent}>
|
||||
|
||||
Reference in New Issue
Block a user