fix: name the allowed-paths action for what it adds, a path or a file

"Add folder or path" promised one thing and offered another: the listing is
folders only, because the Host's browse primitive has no file rows, while the
extras legitimately name files — `../../CONTROLLER.md` is the example the panel
itself gives.

The action is "Add path or file", its field is "Path or file", and the hint says
which of the two the list can show and how the other one is added. Nothing about
the interaction moved; the copy was the part that was wrong.
This commit is contained in:
NotExpectedYet committed 2026-09-27 07:31:50 +01:00
1 parent 6b92656ed2
commit b0902017fd
3 files changed
+54 -7

No files matched your search

+2 -1
View File
@@ -161,7 +161,8 @@ a breadcrumb trail, a hidden-entries toggle — driven through
stored in the form that keeps its meaning: `/work/root/src/shared` becomes
`src/shared` when the session started in `/work/root`. The picker's own path field
stays editable, because the Host lists folders and an extra may legitimately name
a file.
a file — which is why that action reads **Add path or file** and its field **Path
or file**, rather than promising a folder the list would then fail to show.
Because the panel owns widening, `facet action=add` is the *other* path and the
narrower one: it is gated on an approval channel, and a deployment with none — the
+13 -4
View File
@@ -680,7 +680,7 @@ window.__ModuleLoader__.load({
return h(Modal, {
open: true,
onClose,
title: target === 'focus' ? 'Choose the focus folder' : 'Add a folder or a path',
title: target === 'focus' ? 'Choose the focus folder' : 'Add a path or a file',
closeLabel: 'Close',
className: PICKER_CLASS,
// One scroll region around everything but the footer, so the card is
@@ -694,7 +694,7 @@ window.__ModuleLoader__.load({
h(
'div',
{ key: 'path' },
h('label', { style: labelStyle, htmlFor: 'facet-picker-path' }, target === 'focus' ? 'Folder' : 'Path'),
h('label', { style: labelStyle, htmlFor: 'facet-picker-path' }, target === 'focus' ? 'Folder' : 'Path or file'),
h('input', {
id: 'facet-picker-path',
type: 'text',
@@ -713,7 +713,16 @@ window.__ModuleLoader__.load({
model.open(typed)
},
}),
h('p', { style: hintStyle }, 'Relative to the session folder, or absolute. Enter lists whatever is there.'),
h(
'p',
{ style: hintStyle },
target === 'focus'
? 'Relative to the session folder, or absolute. Enter lists whatever is there.'
: // The list holds folders only — the Host's browse primitive has no
// file rows — so the box is where a file is named, and saying so
// is the difference between a dead end and an instruction.
'Relative to the session folder, or absolute. The list holds folders; type a file\u2019s name to add the file itself.',
),
),
h(
'div',
@@ -1015,7 +1024,7 @@ window.__ModuleLoader__.load({
h(
'button',
{ type: 'button', style: quietButtonStyle, disabled: !canBrowse, onClick: () => openPicker('extra') },
'+ Add folder or path\u2026',
'+ Add path or file\u2026',
),
),
h(
+39 -2
View File
@@ -423,7 +423,7 @@ test('the panel renders the scope, the guidance, and a way to add and remove', a
// already did.
assert.ok(buttons.some((button) => labelOf(button) === 'Browse\u2026'))
assert.ok(buttons.some((button) => labelOf(button) === 'Clear'))
assert.ok(buttons.some((button) => labelOf(button) === '+ Add folder or path\u2026'))
assert.ok(buttons.some((button) => labelOf(button) === '+ Add path or file\u2026'))
assert.ok(!buttons.some((button) => labelOf(button) === 'Set'))
assert.deepEqual(inputs, [], 'the panel itself has no free-text box left')
})
@@ -456,7 +456,7 @@ test('a deployment with no directory browser keeps the panel and says so', async
)
assert.deepEqual(
buttonsOf(tree)
.filter((button) => label(button) === '+ Add folder or path\u2026')
.filter((button) => label(button) === '+ Add path or file\u2026')
.map((button) => button.props.disabled),
[true],
)
@@ -520,7 +520,10 @@ function walkTree(node, visit) {
}
visit(node)
if (Array.isArray(node.children)) node.children.forEach((child) => walkTree(child, visit))
// `footer` is a prop rather than a child, and it is rendered: a control there
// is as much part of the tree as one in the body.
walkTree(node.props?.children, visit)
walkTree(node.props?.footer, visit)
}
test('a path is declared relative to the session folder wherever it can be', async () => {
@@ -661,3 +664,37 @@ test('the picker lists folders, hides hidden ones until asked, and answers in de
await settle()
})
test('the extras picker names what it accepts and says what the list holds', async () => {
const { module: mod } = await loadClient()
const model = new mod.DirectoryPickerModel({
list: () => Promise.resolve(listingFixture()),
root: '/work/root',
start: '/work/root/src',
})
await settle()
const tree = mod.DirectoryPicker({
model,
root: '/work/root',
target: 'extra',
disabled: false,
onClose: () => {},
onPick: () => {},
})
// "folder or path" promised one thing and offered another: the rows are
// folders, and a file is added by naming it. The copy says both.
assert.equal(tree.props.title, 'Add a path or a file')
const texts = textsOf(tree)
assert.ok(texts.includes('Path or file'), 'the field is named for what it accepts')
assert.ok(
texts.some((text) => text.includes('The list holds folders')),
'the copy explains why no file rows are shown',
)
assert.ok(
buttonsOf(tree).some((button) => (button.children ?? [])[0] === 'Add'),
'the action is Add for an extra, not Set focus',
)
})