feat(events): a param that holds several values (MODULE_API 1.14.0)
Rust PLAN_REDESIGNS §12.3 (D344): an event action param may declare `multiple: true`, with an optional `max` (1-100, 20 when left out), on a `string` or `int` param. The permission reward steps take several permissions in one step through it. - registries: the flag and `max` are checked at registration and served in the catalog; refused on other types, on a duration, on a template's inputs, and with a list as the example. A param that does not say it reads back exactly as declared, without the keys. - spec: the value is a JSON array, each item the type's literal, duplicates dropped in order, at most `max`; a bare value is a list of one; `required` means at least one; an empty item is refused. - authorize: a multiple param's example is priced as a list of one, since cost() is handed the array. - editor: a multi-select. Chips above a grouped, filterable checklist for a source that answered; the typeahead for a searchable source; a tag box otherwise. Ticking past max is refused in words. A new step holds the example as a list of one. - run console: params read as `name: value`, a list joined by commas. - MODULE_API 1.13.0 -> 1.14.0 on both halves; engagement-triggers.json and the catalog's OpenAPI note regenerated. Additive only: Module-uo (`^1.10.0`) declares no multiple param. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -133,7 +133,9 @@ const nextPhaseKey = (phases) => {
|
||||
export function blankStep(action) {
|
||||
const params = {}
|
||||
for (const p of action?.params || []) {
|
||||
if (p.required || p.example !== undefined) params[p.name] = p.example
|
||||
// A list's example is one value (MODULE_API 1.14.0), so the step starts
|
||||
// holding that one.
|
||||
if (p.required || p.example !== undefined) params[p.name] = p.multiple ? [p.example] : p.example
|
||||
}
|
||||
return {
|
||||
actionId: action?.id || '',
|
||||
@@ -545,7 +547,9 @@ export const PARAM_JSON = 'json'
|
||||
* is what the author needs to see — and a form that dropped it would post a
|
||||
* step that saves cleanly having deleted something they typed.
|
||||
* • **a value no single control can hold** — an object or an array against a
|
||||
* scalar declaration.
|
||||
* scalar declaration, or anything but a flat list against a `multiple` one
|
||||
* (MODULE_API 1.14.0). A bare value there is a list of one, as the server
|
||||
* reads it, and the multi-select shows it as one chip.
|
||||
*/
|
||||
export function paramsRenderable(action, params) {
|
||||
if (!action) return { ok: false, reason: 'the module that registered this action is not installed' }
|
||||
@@ -554,6 +558,13 @@ export function paramsRenderable(action, params) {
|
||||
if (!declared.has(name)) {
|
||||
return { ok: false, reason: `this step carries "${name}", which ${action.id} does not declare` }
|
||||
}
|
||||
if (declared.get(name).multiple) {
|
||||
const items = Array.isArray(value) ? value : [value]
|
||||
if (items.some((v) => v !== null && typeof v === 'object')) {
|
||||
return { ok: false, reason: `"${name}" holds a structure, which a list of values cannot hold` }
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return { ok: false, reason: `"${name}" holds a ${Array.isArray(value) ? 'list' : 'structure'}, which no single field can hold` }
|
||||
}
|
||||
@@ -625,6 +636,81 @@ export function setParam(step, name, raw, type) {
|
||||
*/
|
||||
export const datetimeInputValue = (value) => (typeof value === 'string' ? value.slice(0, 16) : '')
|
||||
|
||||
// ── A param that holds several values (MODULE_API 1.14.0, Rust PLAN_REDESIGNS §12.3) ──
|
||||
|
||||
/**
|
||||
* A `multiple` param's value as the list the multi-select draws: an array as
|
||||
* stored, a bare value as a list of one (the server's own reading), and nothing
|
||||
* as the empty list.
|
||||
*/
|
||||
export function listValue(value) {
|
||||
if (value === undefined || value === null || value === '') return []
|
||||
return Array.isArray(value) ? value : [value]
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a `multiple` param's whole list, and give back the whole box.
|
||||
*
|
||||
* Each item is coerced as `setParam` coerces one value, duplicates dropped
|
||||
* keeping the first, and **an empty list removes the key** — `setParam`'s rule,
|
||||
* for its reason: the server reads absent and `[]` alike, and "is required" is
|
||||
* the error a required list left empty should get.
|
||||
*/
|
||||
export function setParamList(step, name, items, type) {
|
||||
const parsed = parseParams(step?.paramsText)
|
||||
if (parsed.error) return step?.paramsText || '{}'
|
||||
const next = { ...parsed.params }
|
||||
const out = []
|
||||
for (const raw of items || []) {
|
||||
if (raw === '' || raw === undefined || raw === null) continue
|
||||
const value = coerceLiteral(type, raw)
|
||||
if (!out.some((v) => v === value)) out.push(value)
|
||||
}
|
||||
if (out.length) next[name] = out
|
||||
else delete next[name]
|
||||
return JSON.stringify(next, null, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one value to a list, or say why not. `{ list }` or `{ refused }`, so the
|
||||
* control can put the sentence where the click was rather than silently doing
|
||||
* nothing past `max`.
|
||||
*/
|
||||
export function addToList(list, value, max) {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) return { list }
|
||||
if (list.some((v) => String(v) === text)) return { list }
|
||||
if (max && list.length >= max) return { refused: `This takes at most ${max} — remove one first.` }
|
||||
return { list: [...list, text] }
|
||||
}
|
||||
|
||||
/**
|
||||
* The options a filter box leaves, matched on value, label and group alike,
|
||||
* case-insensitively. The order is the source's own — a source sorts its rows
|
||||
* (§12.2) and the editor keeps it.
|
||||
*/
|
||||
export function filterOptions(options, term) {
|
||||
const t = String(term || '').trim().toLowerCase()
|
||||
if (!t) return options || []
|
||||
return (options || []).filter((o) =>
|
||||
[o.value, o.label, o.group].some((s) => String(s || '').toLowerCase().includes(t)))
|
||||
}
|
||||
|
||||
/**
|
||||
* One step's params as a line the run console can show: `name: value`, a list
|
||||
* joined by commas, anything else as its JSON. Shown rather than the raw object
|
||||
* so a list of ten permissions reads as ten names, not as brackets and quotes.
|
||||
*/
|
||||
export function describeParams(params) {
|
||||
if (!params || typeof params !== 'object') return ''
|
||||
return Object.entries(params)
|
||||
.map(([k, v]) => {
|
||||
if (Array.isArray(v) && v.every((x) => x === null || typeof x !== 'object')) return `${k}: ${v.join(', ')}`
|
||||
return `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`
|
||||
})
|
||||
.join(' · ')
|
||||
}
|
||||
|
||||
// ── The step editor's own rules (MODULE_API 1.13.0; Rust PLAN_REDESIGNS §2) ──
|
||||
//
|
||||
// What the controls need to decide and nothing the server re-decides: which
|
||||
@@ -671,7 +757,7 @@ export function missingParams(step, action, leasesById = new Map()) {
|
||||
if (!action) return []
|
||||
const parsed = parseParams(step?.paramsText)
|
||||
if (parsed.error) return []
|
||||
const empty = (v) => v === undefined || v === null || v === ''
|
||||
const empty = (v) => v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0)
|
||||
const missing = (action.params || []).filter((p) => p.required && empty(parsed.params[p.name])).map((p) => p.name)
|
||||
if (action.id === 'core.lease') {
|
||||
const lease = leasesById.get(parsed.params.lease)
|
||||
|
||||
Reference in New Issue
Block a user