From e58857c133b9cb89664341949f9130fab6aa48c0 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 12 Oct 2026 02:23:18 -0500 Subject: [PATCH] feat(events): a searchable source's chosen note is found when the page lacks it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A searchable source answers the editor a bounded first page, so the value an author picked (or a template filled in) may not be on it, and its note with it. useChosenNote asks the source for that value as a search term, once per value, and shows the exact match's note. Found building the Rust kit warning (PLAN_REDESIGNS §2.6), whose source - rust.options.grouppermissions - is searchable. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- client/src/routes/admin/views/EventEditor.jsx | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/client/src/routes/admin/views/EventEditor.jsx b/client/src/routes/admin/views/EventEditor.jsx index ef6fdea..f48bc25 100644 --- a/client/src/routes/admin/views/EventEditor.jsx +++ b/client/src/routes/admin/views/EventEditor.jsx @@ -220,6 +220,46 @@ function SearchableOptions({ sourceId, label, disabled, onPick }) { ) } +/** + * The chosen option's note (MODULE_API 1.13.0), found even when the list the + * form holds does not reach it. + * + * A searchable source answers the form a bounded first page, so the value an + * author picked — or a template filled in — may not be in it, and its note with + * it. Then the source is asked for that value as a search term, once per value, + * and the row whose value matches exactly is the one whose note is shown. A + * failure is no note: advice never breaks the field it advises. + */ +function useChosenNote(sourceId, value, entry) { + const [asked, setAsked] = useState({ key: null, note: null }) + const text = value === undefined || value === null ? '' : String(value) + const known = noteFor(entry, value) + const needAsk = Boolean(sourceId && entry?.state === 'ok' && entry.searchable && text && !known && + !entry.options.some((o) => o.value === text)) + const key = needAsk ? `${sourceId}\u0000${text}` : null + + useEffect(() => { + if (!key || asked.key === key) return undefined + let alive = true + const timer = setTimeout(async () => { + try { + const answer = await api.admin.eventOptions(sourceId, text) + const row = answer?.ok ? (answer.options || []).find((o) => o.value === text) : null + if (alive) setAsked({ key, note: row?.note || null }) + } catch { + if (alive) setAsked({ key, note: null }) + } + }, 300) + return () => { + alive = false + clearTimeout(timer) + } + }, [key, asked.key, sourceId, text]) + + if (known) return known + return key && asked.key === key ? asked.note : null +} + /** * A row of buttons for a choice of two or three (Rust PLAN_REDESIGNS §2.4). * @@ -366,7 +406,7 @@ function ParamField({ param, value, sourceEntry, disabled, onChange, missing, la // editable rather than look unset. const segmented = param.source && param.type !== 'boolean' && !values && isSegmentable(sourceEntry) const offList = segmented && asText !== '' && !sourceEntry.options.some((o) => o.value === asText) - const note = param.source ? noteFor(sourceEntry, value) : null + const note = useChosenNote(param.source, value, sourceEntry) return (