aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-07 00:33:51 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-07 00:33:51 +0200
commit6828e64a256caea3c4e51829ae1aa09dfa725324 (patch)
tree23d93fdb473fbcbded12bc9b68a4006aed5ce0df
parent80ca7dd0765a6c08fa5ea54c2e14972bba6fa564 (diff)
downloadmeshbay-6828e64a256caea3c4e51829ae1aa09dfa725324.tar.gz
fix(client): report a directory result under the button that caused it
The message was the first thing in the section, wedged between the intro and the table header — above everything the eye has already moved past by the time it appears. It goes last now, under Add directory. And it was a `settings-hint`: dim grey body text. So "two roots would both be called uploads" read as a footnote about the section rather than as the reason nothing happened. A refusal is styled as one and carries `role="alert"`, so it is announced rather than only drawn; a success stays quiet. Measured in a real Electron window rather than assumed — the failing add driven through the typed-path form, then the message's box compared against the table's and the button's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js32
-rw-r--r--packages/meshbay-hub/tests/test_app_settings_plugin.py42
2 files changed, 65 insertions, 9 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index 9646405..6c6f3d9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -55,8 +55,22 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
const isLocal = mode === 'local';
const serverRoots = isLocal ? (localRoots || []) : (roots || []);
const [busy, setBusy] = useState(false);
- const [msg, setMsg] = useState('');
+ // `{ text, error }` — a refusal has to look like one. Every message here was
+ // a `settings-hint`, which is dim grey body text, so "two roots would both
+ // be called uploads" read as a footnote to the section rather than as the
+ // reason nothing happened.
+ const [msg, setMsg] = useState(null);
const [indexProgress, setIndexProgress] = useState(null);
+ const say = useCallback((text) => setMsg(text ? { text, error: false } : null), []);
+ const refuse = useCallback((text) => setMsg({ text, error: true }), []);
+
+ // Rendered at the foot of the section, under the Add button — the last thing
+ // below the control that caused it, rather than above a table the eye has
+ // already moved past.
+ const message = !msg ? '' : html`
+ <p class=${msg.error ? 'error-msg' : 'settings-hint'}
+ role=${msg.error ? 'alert' : 'status'}
+ style="margin-top:10px">${msg.text}</p>`;
const [pathDraft, setPathDraft] = useState('');
const [addingByPath, setAddingByPath] = useState(false);
@@ -120,16 +134,16 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
// the delta the node pushes when the scan finishes carries it again along
// with the files.
const run = useCallback(async (work) => {
- setBusy(true); setMsg('');
+ setBusy(true); setMsg(null);
try {
await work();
if (onRootsChange) await onRootsChange();
return true;
} catch (err) {
- setMsg(platform.bridgeMessage(err));
+ refuse(platform.bridgeMessage(err));
return false;
} finally { setBusy(false); }
- }, [onRootsChange]);
+ }, [onRootsChange, refuse]);
const doUpdateRoot = useCallback(async (rootName, updates) => {
if (isLocal) {
@@ -184,7 +198,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
await platform.node.call('POST', '/api/reload');
} else throw new Error(t('node.root_no_route'));
});
- if (ok) setMsg(t('node.root_removed'));
+ if (ok) say(t('node.root_removed'));
}, [isLocal, localRoots, onLocalRootsChange, overMnp, overLoopback,
transport, groupId, signFn, run]);
@@ -223,7 +237,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
const chosen = await platform.rootPicker.choose();
if (!chosen) return;
const ok = await addRootAtPath(chosen.path, chosen.name);
- if (ok && !isLocal) setMsg(t('node.root_added'));
+ if (ok && !isLocal) say(t('node.root_added'));
}, [addRootAtPath, isLocal]);
const doAddByPath = useCallback(async () => {
@@ -233,7 +247,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
// duplicate. Sending one guessed from a string typed here would be a
// second opinion about something already decided in one place.
const ok = await addRootAtPath(path, '');
- if (ok) { setPathDraft(''); setAddingByPath(false); if (!isLocal) setMsg(t('node.root_added')); }
+ if (ok) { setPathDraft(''); setAddingByPath(false); if (!isLocal) say(t('node.root_added')); }
}, [pathDraft, addRootAtPath, isLocal]);
const addControls = !canEdit ? '' : html`
@@ -267,16 +281,15 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
if (!displayRoots.length) {
return html`
<div class="shared-directories-table">
- ${msg && html`<p class="settings-hint">${msg}</p>`}
<p class="settings-hint">${t('settings_node.shared_directories_hint')}</p>
${addControls}
+ ${message}
</div>
`;
}
return html`
<div class="shared-directories-table">
- ${msg && html`<p class="settings-hint">${msg}</p>`}
<table class="shared-dirs-tbl">
<thead>
<tr>
@@ -340,6 +353,7 @@ function SharedDirectoriesTable({ roots, groupId, transport, signFn,
</tbody>
</table>
${addControls}
+ ${message}
${indexProgress && indexProgress.scanning && html`
<div class="index-progress" style="margin-top:8px">
<div class="index-progress-bar">
diff --git a/packages/meshbay-hub/tests/test_app_settings_plugin.py b/packages/meshbay-hub/tests/test_app_settings_plugin.py
index 1837c28..6a768a9 100644
--- a/packages/meshbay-hub/tests/test_app_settings_plugin.py
+++ b/packages/meshbay-hub/tests/test_app_settings_plugin.py
@@ -278,3 +278,45 @@ def test_the_search_cache_reads_both_shapes():
fn = _component(source, "cachedDirs")
assert "legacyKey" in fn
assert "videoRoot" in source and "audioRoot" in source and "photoRoots" in source
+
+
+# ── Where a result is reported ──────────────────────────────────────────────
+
+def test_the_directory_result_is_reported_below_the_controls():
+ """
+ It was rendered first, between the section's intro and the table header —
+ above everything the eye had already moved past by the time it appeared.
+ It belongs under the button that caused it, which is the last thing in the
+ section.
+ """
+ table = _component(GROUP_SETTINGS.read_text(encoding="utf-8"),
+ "SharedDirectoriesTable")
+ # Two of them: the early return for a group with no directories yet, and
+ # the real one. Both report, and both must report last — keyed on the
+ # wrapper rather than on `return`, of which there are more.
+ blocks = table.split('<div class="shared-directories-table">')[1:]
+ assert len(blocks) == 2, "the section's shape changed; this test is stale"
+
+ for block in blocks:
+ assert block.index("${addControls}") < block.index("${message}"), (
+ "the result is rendered above the Add button rather than below it")
+
+ with_table = next(b for b in blocks if "<table" in b)
+ assert with_table.index("<table") < with_table.index("${message}"), (
+ "the result is rendered above the table")
+
+
+def test_a_refusal_does_not_look_like_a_footnote():
+ """
+ Every message here was a `settings-hint` — dim grey body text — so "two
+ roots would both be called uploads" read as an aside about the section
+ rather than as the reason nothing happened.
+ """
+ table = _component(GROUP_SETTINGS.read_text(encoding="utf-8"),
+ "SharedDirectoriesTable")
+ block = table[table.index("const message ="):]
+ block = block[:block.index("`;") + 2]
+ assert "error-msg" in block
+ assert "role=" in block and "alert" in block, (
+ "a refusal that appears after a click has to be announced, not just "
+ "drawn")