mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): show hidden directories in workspace browser (#1331)
* feat(web): show hidden directories in workspace browser Add optional includeHidden param to the machine list-directory RPC so the WorkspaceBrowser can toggle hidden (dot-prefixed) entries. Default remains filtered for backward compatibility; the toggle persists via localStorage. * fix(web): disable show-hidden toggle while directory loading Prevent overlapping list-directory requests with opposite includeHidden values; the toggle is now disabled while a directory load is active.
This commit is contained in:
@@ -53,6 +53,22 @@ function RefreshIcon(props: { className?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function CheckboxBlankIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className={props.className}>
|
||||
<path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CheckboxCheckedIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className={props.className}>
|
||||
<path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM5 5V19H19V5H5ZM11.0026 16L6.75999 11.7574L8.17421 10.3431L11.0026 13.1716L16.6595 7.51472L18.0737 8.92893L11.0026 16Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function getMachineTitle(machine: Machine): string {
|
||||
if (machine.metadata?.displayName) return machine.metadata.displayName
|
||||
if (machine.metadata?.host) return machine.metadata.host
|
||||
@@ -142,6 +158,16 @@ function buildBreadcrumbs(currentPath: string, root: string): { label: string; p
|
||||
return crumbs
|
||||
}
|
||||
|
||||
const SHOW_HIDDEN_STORAGE_KEY = 'hapi:workspaceBrowserShowHidden'
|
||||
|
||||
function readShowHidden(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(SHOW_HIDDEN_STORAGE_KEY) === '1'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function WorkspaceBrowser(props: {
|
||||
api: ApiClient
|
||||
machines: Machine[]
|
||||
@@ -159,6 +185,7 @@ export function WorkspaceBrowser(props: {
|
||||
const [entries, setEntries] = useState<MachineDirectoryEntry[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showHidden, setShowHidden] = useState<boolean>(readShowHidden)
|
||||
|
||||
useEffect(() => {
|
||||
if (machines.length === 0) {
|
||||
@@ -190,12 +217,12 @@ export function WorkspaceBrowser(props: {
|
||||
[selectedMachine?.metadata?.workspaceRoots]
|
||||
)
|
||||
|
||||
const loadDirectory = useCallback(async (path: string) => {
|
||||
const loadDirectory = useCallback(async (path: string, includeHiddenOverride?: boolean) => {
|
||||
if (!machineId) return
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await api.listMachineDirectory(machineId, path)
|
||||
const result = await api.listMachineDirectory(machineId, path, { includeHidden: includeHiddenOverride ?? showHidden })
|
||||
if (result.success && result.entries) {
|
||||
setEntries(result.entries)
|
||||
setCurrentPath(path)
|
||||
@@ -212,7 +239,7 @@ export function WorkspaceBrowser(props: {
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [api, machineId, queryClient])
|
||||
}, [api, machineId, queryClient, showHidden])
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceRoots.length === 0) {
|
||||
@@ -256,6 +283,16 @@ export function WorkspaceBrowser(props: {
|
||||
if (currentPath) void loadDirectory(currentPath)
|
||||
}, [currentPath, loadDirectory])
|
||||
|
||||
const handleToggleHidden = useCallback(() => {
|
||||
const next = !showHidden
|
||||
setShowHidden(next)
|
||||
try {
|
||||
localStorage.setItem(SHOW_HIDDEN_STORAGE_KEY, next ? '1' : '0')
|
||||
} catch {
|
||||
}
|
||||
if (currentPath) void loadDirectory(currentPath, next)
|
||||
}, [showHidden, currentPath, loadDirectory])
|
||||
|
||||
const handleStartSession = useCallback(() => {
|
||||
if (!machineId || !currentPath) return
|
||||
props.onStartSession(machineId, currentPath)
|
||||
@@ -368,15 +405,31 @@ export function WorkspaceBrowser(props: {
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="ml-auto shrink-0 p-0.5 rounded hover:bg-[var(--app-subtle-bg)] text-[var(--app-hint)] hover:text-[var(--app-fg)] transition-colors"
|
||||
title={t('browse.refresh')}
|
||||
>
|
||||
<RefreshIcon className={`h-3.5 w-3.5 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<div className="ml-auto shrink-0 flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleHidden}
|
||||
aria-pressed={showHidden}
|
||||
disabled={isLoading}
|
||||
className="shrink-0 flex items-center gap-1.5 rounded px-1.5 py-0.5 text-xs text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] transition-colors disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{showHidden ? (
|
||||
<CheckboxCheckedIcon className="h-3.5 w-3.5 text-[var(--app-link)] shrink-0" />
|
||||
) : (
|
||||
<CheckboxBlankIcon className="h-3.5 w-3.5 shrink-0" />
|
||||
)}
|
||||
{t('browse.showHidden')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="shrink-0 p-0.5 rounded hover:bg-[var(--app-subtle-bg)] text-[var(--app-hint)] hover:text-[var(--app-fg)] transition-colors"
|
||||
title={t('browse.refresh')}
|
||||
>
|
||||
<RefreshIcon className={`h-3.5 w-3.5 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user