From 213501b177175c9bb7f8fab063be9dddd119461d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 00:26:56 +0000 Subject: [PATCH] =?UTF-8?q?fix(example-todo,docs):=20a=20modal=20action=20?= =?UTF-8?q?is=20client-only=20=E2=80=94=20defer/reminder=20are=20script=20?= =?UTF-8?q?actions=20(#3959)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app-todo declared defer_task and set_reminder as type:'modal' with targets naming modal pages that do not exist, while task.handlers.ts registered deferTask and setReminder under keys no declaration could address. A modal action has no server dispatch — headlessActionTypeError rejects it over REST — so neither handler had ever executed. ADR-0110 D5's boot inventory flagged both on its first pass, and the example was teaching the pattern to every app scaffolded from it. Both actions already declared the params their handlers read, so they were always 'collect input, then run server-side': that is type:'script' with params. The runner collects the same dialog and the handler now runs. Also corrects the action-type table in actions.mdx, which described modal as 'collect input, then submit to a handler' while the same page's REST table correctly said modal returns 400 with nothing for the server to run. runtime 857, lint 540, app-todo tsc — all clean. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01DTTKgYriDB6RVrkYjxxnG1 --- .changeset/modal-actions-are-client-only.md | 21 +++++++++++++++++ content/docs/ui/actions.mdx | 2 +- examples/app-todo/src/actions/task.actions.ts | 23 ++++++++++++++----- .../app-todo/src/actions/task.handlers.ts | 4 ++-- 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 .changeset/modal-actions-are-client-only.md 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, {