markwalet / markwalet/nova-modal-response
v2: interactive modal responses (action dispatch + form blocks)
@markwalet is already working on this.
Since May 27, 2026.
- Dominant language
- PHP
- Stars
- 17
- Forks
- 9
- PR merge metrics
- No merged PRs in 30d
Description
# Interactive modal responses
Umbrella for turning the modal response from a **one-shot render** into an **interactive round-trip**: buttons inside the modal that dispatch Nova actions and re-render with the result. High-level overview only — progress tracked via the sub-issues below.
## Why
Today a `ModalResponse` is terminal: an action runs server-side, returns a payload, and `ModalActionResponse.vue` renders the stack once. There is no path back to the server from inside the modal. CONTEXT.md deliberately reserved this gap (the link block "does not trigger a Nova action"). This work fills it.
Motivating flow: *preview an API request → press Execute → see the response in the same modal* — and the more general "press a button, dispatch an action, render what comes back."
## Shape
Two new blocks, sharing one **dispatch core**:
- **`Block::action($label?, ActionClass)`** — a fieldless dispatcher. Throws if the target action declares `fields()` (use a form instead). Inlineable (an atom) — safe by construction. Label defaults to the action's name.
- **`Block::form($label?, ActionClass)`** — renders the target action's `fields()` inline (essentially Nova's confirm-action modal, inline), collects values, submits. Block-level, not an atom. Submit label defaults to the action's `confirmButtonText`.
### Core mechanism (shared)
- **Single-modal constraint**: Nova is not built to stack multiple modals on top of each other, so we don't. **Dispatching any action always closes the current modal.** If the action returns a modal response, it opens in place of the old one (close-then-open); if it returns a non-modal response (toast, redirect, download, openInNewTab), the modal still closes and Nova handles the rest.
- **Intercept**: the Vue side POSTs to Nova's action endpoint itself, captures the response, and drives the outcome — chosen over deferring to Nova's runner, because deferring forfeits the close-then-open sequencing the form block also needs.
- **Origin context**: every dispatch runs against the parent modal's resource + selection, captured server-side at serialize time.
- **Failed dispatch**: surface the error via `Nova.error` and leave the modal open so the user can retry — the only case where the modal stays.
- **Confirmation guard**: honor the action's `confirmText` + a `->confirm()` knob (interception otherwise bypasses Nova's confirm — matters most for fieldless actions).
- **Resource reload**: emit Nova's action-completed event so the resource view refreshes; `->withoutReload()` opt-out.
## Phasing
1. **Action block** (fieldless) — ships the whole dispatch core + default behavior end-to-end.
2. **Form block** (inline fields) — gated by a feasibility spike on rendering Nova action fields detached from `ConfirmActionModal`.
## Security
Action uriKey + resource + ids ride the (tamperable) payload, but Nova's endpoint still enforces `authorizedToRun` + validation — no privilege escalation.
## Out of scope (for now)
- Server-set `->withInputs()` on the action block (selection-only); arbitrary-resource / standalone targets; client-only buttons (copy-to-clipboard, tabs).
- **Return-to-previous-modal after dispatch** (#86) — tracked as a separate explore ticket. Today the user re-triggers the originating resource action manually if they want to get back.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.