feat(events): the step editor reworked, and event templates (MODULE_API 1.13.0) #212

Merged
whitlocktech merged 2 commits from feat/event-step-editor into main 2026-10-12 09:53:45 +00:00
Showing only changes of commit e58857c133 - Show all commits

View File

@@ -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 (
<div style={{ marginBottom: 12 }}>