diff --git a/.changeset/modal-actions-are-client-only.md b/.changeset/modal-actions-are-client-only.md new file mode 100644 index 0000000000..946a5bbd4e --- /dev/null +++ b/.changeset/modal-actions-are-client-only.md @@ -0,0 +1,21 @@ +--- +'@objectstack/example-todo': patch +--- + +**[#3959] `app-todo`'s `defer_task` / `set_reminder` are `type: 'script'`, not `type: 'modal'`.** + +Both declared `type: 'modal'` with a `target` naming a modal page that does not +exist (`defer_task_modal`, `set_reminder_modal`), while their handlers sat +registered under `deferTask` / `setReminder` — keys no declaration could +address. A `modal` action has no server dispatch (`headlessActionTypeError` +rejects it over REST), so neither handler had ever executed: the example +shipped business logic that could not run, and ADR-0110 D5's boot inventory +flagged both on its first pass. + +Both already declared the `params` their handlers read, so they were always +"collect input, then run server-side" actions — which is `type: 'script'` with +`params`. The runner collects the same dialog and the handler now actually runs. + +The action-type table in `content/docs/ui/actions.mdx` said `modal` meant +"collect input, then submit to a handler", contradicting the same page's own +REST table (`modal` → 400, nothing for the server to run). Corrected. diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx index 15f01f59a5..c33cb25380 100644 --- a/content/docs/ui/actions.mdx +++ b/content/docs/ui/actions.mdx @@ -19,7 +19,7 @@ The types you'll actually use: | `script` *(default)* | Run server-side logic | Inline `body` **or** a handler registered via `target` | | `flow` | Launch a flow (e.g. a screen-flow wizard) | `target` names the flow | | `url` | Navigate / open a link | `target` is the URL (`${ctx.record.id}` interpolation supported) | -| `modal` | Open a modal page to collect input, then submit to a handler | `target` names the modal page | +| `modal` | Open a modal page — client-side only, no server dispatch | `target` names the modal page (to collect input *and* run logic, use `script` + `params`) | | `api` | Call an HTTP endpoint directly | `target` is the endpoint; `method` / `bodyShape` / `bodyExtra` shape the request | | `form` | Open a form view, prefilled with the current record | `target` names the FormView; routed to `/forms/:target?recordId=…` | diff --git a/examples/app-todo/src/actions/task.actions.ts b/examples/app-todo/src/actions/task.actions.ts index 2956f69bcf..0303797eda 100644 --- a/examples/app-todo/src/actions/task.actions.ts +++ b/examples/app-todo/src/actions/task.actions.ts @@ -36,14 +36,25 @@ export const StartTaskAction = defineAction({ }, }); -/** Defer Task */ +/** + * Defer Task — collect input, then run server-side. + * + * [ADR-0110] This is `type: 'script'` with `params`, NOT `type: 'modal'`. A + * `modal` action's `target` names a page to OPEN and has no server dispatch + * (`headlessActionTypeError` rejects it), so the old + * `type: 'modal', target: 'defer_task_modal'` pointed at a page that does not + * exist while `deferTask` sat registered under a key no declaration could + * address — business logic that had never once executed (#3959). `script` + + * `params` is the supported shape: the runner collects the same dialog, then + * the handler runs with those values. + */ export const DeferTaskAction = defineAction({ name: 'defer_task', label: 'Defer Task', objectName: 'todo_task', icon: 'clock', - type: 'modal', - target: 'defer_task_modal', + type: 'script', + target: 'deferTask', locations: ['record_header'], params: [ { @@ -63,14 +74,14 @@ export const DeferTaskAction = defineAction({ refreshAfter: true, }); -/** Set Reminder */ +/** Set Reminder — collect input, then run server-side (see DeferTaskAction). */ export const SetReminderAction = defineAction({ name: 'set_reminder', label: 'Set Reminder', objectName: 'todo_task', icon: 'bell', - type: 'modal', - target: 'set_reminder_modal', + type: 'script', + target: 'setReminder', locations: ['record_header', 'list_item'], params: [ { diff --git a/examples/app-todo/src/actions/task.handlers.ts b/examples/app-todo/src/actions/task.handlers.ts index 2baffe585d..714d36a48e 100644 --- a/examples/app-todo/src/actions/task.handlers.ts +++ b/examples/app-todo/src/actions/task.handlers.ts @@ -82,7 +82,7 @@ export async function deleteCompletedTasks(ctx: ActionContext): Promise { } } -/** Defer a task by updating its due date (modal form submission handler) */ +/** Defer a task by updating its due date (params collected by the action dialog) */ export async function deferTask(ctx: ActionContext): Promise { const { record, engine, params } = ctx; await engine.update('todo_task', record.id as string, { @@ -92,7 +92,7 @@ export async function deferTask(ctx: ActionContext): Promise { }); } -/** Set a reminder on a task (modal form submission handler) */ +/** Set a reminder on a task (params collected by the action dialog) */ export async function setReminder(ctx: ActionContext): Promise { const { record, engine, params } = ctx; await engine.update('todo_task', record.id as string, {