feat(host): show-hidden toggle in the directory browser footer

This commit is contained in:
creatixchu
2026-07-29 12:52:59 +08:00
parent 75b32f7d76
commit cdcdd2221e
5 changed files with 67 additions and 4 deletions
@@ -242,6 +242,32 @@
border-top: 1px solid var(--dsw-alias-border-l3);
}
/* Show-hidden toggle: a subtle text button in the footer, left of the gap. */
.showHiddenToggle {
border: none;
background: transparent;
padding: 0;
font-size: 13px;
line-height: 20px;
font-weight: 500;
color: var(--dsw-alias-label-secondary);
cursor: pointer;
white-space: nowrap;
}
.showHiddenToggle:hover {
color: var(--dsw-alias-label-primary);
}
.showHiddenToggle:disabled {
color: var(--dsw-alias-label-caption);
cursor: default;
}
.showHiddenToggleActive {
color: var(--dsw-alias-label-primary);
}
.footerGap {
flex: 1 1 0;
}
@@ -10,8 +10,8 @@
* selects the created folder. Open adopts the selected folder, falling back
* to the listed level. Pure consumer of the injected browse calls — the
* owning flow decides what "Open" means and owns the workspace-creation
* error surface. Hidden entries are host-flagged and filtered here (a
* show-hidden toggle is deferred work, client-side only).
* error surface. Hidden entries are host-flagged and hidden by default;
* a "Show hidden files" toggle in the footer reveals them (client-side only).
*/
import { useCallback, useEffect, useRef, useState } from 'react'
import clsx from 'clsx'
@@ -60,16 +60,17 @@ function displayCrumbs(listing: DirectoryListing, homeLabel: string): DirectoryE
}
/** One column of folder rows (the Miller view renders one or two of these). */
function LevelColumn({ entries, selectedPath, busy, onPick, wide }: {
function LevelColumn({ entries, selectedPath, busy, onPick, wide, showHidden }: {
entries: readonly DirectoryEntry[]
selectedPath: string | null
busy: boolean
onPick: (entry: DirectoryEntry) => void
wide: boolean
showHidden: boolean
}) {
return (
<div className={clsx(css.column, wide && css.columnWide)} role="list">
{entries.filter(entry => !entry.hidden).map((entry) => {
{entries.filter(entry => showHidden || !entry.hidden).map((entry) => {
const selected = entry.path === selectedPath
return (
// The wrapper carries the list semantics; the row keeps its NATIVE
@@ -110,6 +111,8 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
const [error, setError] = useState<string | null>(null)
// Path-edit state: null = breadcrumb mode; a string = the draft being typed.
const [pathDraft, setPathDraft] = useState<string | null>(null)
// Show-hidden toggle state (pure client-side filter, reset on close).
const [showHidden, setShowHidden] = useState(false)
// Create-folder state: null = closed; a string = the nested dialog's draft.
const [folderDraft, setFolderDraft] = useState<string | null>(null)
const [creatingFolder, setCreatingFolder] = useState(false)
@@ -213,6 +216,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
setSelected(null)
setChild(null)
setCreatingFolder(false)
setShowHidden(false)
navigate()
return
}
@@ -409,6 +413,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
busy={parentInert}
onPick={select}
wide={!twoPane}
showHidden={showHidden}
/>
)}
{twoPane && <span className={css.divider} />}
@@ -419,6 +424,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
busy={parentInert}
onPick={advance}
wide={false}
showHidden={showHidden}
/>
)}
</div>
@@ -442,6 +448,14 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
>
{t('browser.newFolder')}
</Button>
<button
type="button"
className={clsx(css.showHiddenToggle, showHidden && css.showHiddenToggleActive)}
disabled={parentInert}
onClick={() => { setShowHidden(prev => !prev) }}
>
{t(showHidden ? 'browser.hideHidden' : 'browser.showHidden')}
</button>
<span className={css.footerGap} />
<Button variant="outline" className={clsx(css.footerAction)} disabled={parentInert} onClick={onClose}>{t('browser.cancel')}</Button>
<Button
@@ -46,6 +46,8 @@ export function apply(ctx: ClientContext): void {
'browser.editPath': '编辑路径',
'browser.loading': '加载中…',
'browser.truncated': '文件夹过多,仅显示开头部分。',
'browser.showHidden': '显示隐藏文件',
'browser.hideHidden': '隐藏隐藏文件',
}],
['en', {
'browser.title': 'Select Workspace Directory',
@@ -60,6 +62,8 @@ export function apply(ctx: ClientContext): void {
'browser.editPath': 'Edit path',
'browser.loading': 'Loading…',
'browser.truncated': 'Too many folders to list; only the beginning is shown.',
'browser.showHidden': 'Show hidden files',
'browser.hideHidden': 'Hide hidden files',
}],
]
try {
@@ -162,6 +162,8 @@ describe('directory-picker-browse client half', () => {
// zh is the shipped default locale.
expect(injected.t('browser.title')).toBe('选择工作区目录')
expect(injected.t('browser.newFolder')).toBe('新建文件夹')
expect(injected.t('browser.showHidden')).toBe('显示隐藏文件')
expect(injected.t('browser.hideHidden')).toBe('隐藏隐藏文件')
})
it('drives the injected browse calls through the hole entry', async () => {
@@ -103,6 +103,23 @@ describe('DirectoryBrowser', () => {
expect(screen.queryByRole('button', { name: '/' })).toBeNull()
})
it('shows hidden entries when the toggle is on and hides them again on close', async () => {
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
expect(screen.queryByText('.config')).toBeNull()
// Toggle hidden files on.
fireEvent.click(screen.getByRole('button', { name: 'browser.showHidden' }))
expect(screen.getByText('.config')).toBeTruthy()
// Toggle hidden files off.
fireEvent.click(screen.getByRole('button', { name: 'browser.hideHidden' }))
expect(screen.queryByText('.config')).toBeNull()
// Close resets the toggle.
b.view.rerender(<DirectoryBrowser {...b.props} open={false} />)
b.view.rerender(<DirectoryBrowser {...b.props} open />)
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
expect(screen.queryByText('.config')).toBeNull()
})
it('selects a row into the two-pane view: children preview right, crumbs follow the selection', async () => {
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })