Skip to content

feat: Server-Driven Action Flows + theme-dynamical-vue - #85

Merged
marioserrano09 merged 19 commits into
mainfrom
feature/action-flows
Sep 21, 2026
Merged

marioserrano09 merged 19 commits into
mainfrom
feature/action-flows

Conversation

@marioserrano09

@marioserrano09 marioserrano09 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Resumen

Experimental. Server-Driven Action Flows para RemoteAction (protocolo stateless con resumeToken firmado, sin store server-side) + el tema theme-dynamical-vue (Vue 3 + Tailwind 4) como primer consumidor real, sin fallback ZK.

Diseño: docs/design/SERVER_DRIVEN_ACTION_FLOWS.md.

Qué incluye

  • Fases 0-3 (platform/core/actions): ActionFlowStep/FlowRemoteAction/ActionFlows, resumeToken HMAC (FlowTokenSigner). Frontend: ConfirmManager/ToastManager, runActionFlow, SDK types.
  • Fase 4 (platform/core/crud): SaveRemoteAction/SaveFlowRemoteAction/DeleteRemoteAction/DeleteFlowRemoteAction en tools.dynamia.crud.actions.remote (las acciones ZK quedan intactas).
  • Action Flows: generic CrudState enforcement for any CrudRemoteAction #83: enforcement genérico de CrudState en ApplicationMetadataController.executeAction + test de regresión.
  • Action Flows: reconcile validation-error response shape (422 vs 406) #82: unificación de la forma de error de validación (422/ErrorResult).
  • Action Flows: implement INPUT/DIALOG/REDIRECT/CALL/CUSTOM step renderers on the Vue side #81: renderers INPUT/DIALOG/CUSTOM (PromptManager, FlowStepRendererRegistry, runActionFlow) y, en d33ebcbe, REDIRECT/CALL (experimental): REDIRECT terminal (solo URLs relativas/http(s), awaitReturn rechazado); CALL anida en el mismo loop y retoma al llamador con la respuesta anidada (máx. 5 niveles). Semántica en el doc §6. Todo el protocolo queda marcado experimental (doc, javadoc, jsdoc). Pendiente: awaitReturn.
  • theme-dynamical-vue: 3 skins, login nativo, shell, CrudPage vía <DynamiaCrudPage>.
  • Demo end-to-end en examples/demo-zk-books (perfil vue): ApplyDiscountAction (INPUT), QuickEditPricingAction (DIALOG), RateBookAction (CUSTOM).
  • New/Edit/Cancel builtin como ClientAction reales (sustituyen el matching hardcodeado en Actions.vue); extracción de dispatchCrudSave/Delete.

Cosas a tener en cuenta al revisar

  • El commit 89fafca9 incluye el bump de versión repo-wide 26.8.0 -> 26.9.0 (estaba staged de trabajo previo, no relacionado con flows). Se puede revisar por separado.
  • df5eddfb (fix imports JUnit4 en MicroFrontendViewTest) duplica 8a8ec905 que ya está en main; el merge es limpio.
  • El commit 7f138812 dice Closes #81 (partially ...), así que el merge commit cerrará Action Flows: implement INPUT/DIALOG/REDIRECT/CALL/CUSTOM step renderers on the Vue side #81 aunque queda pendiente awaitReturn + demo e2e: reabrir tras el merge.
  • pnpm typecheck de theme-dynamical-vue falla con 4 errores TS (App.vue, main.ts); son preexistentes (idénticos sin estos cambios de runActionFlow).

Validación

Closes #82
Closes #83
Part of #79, #81, #84

🤖 Generated with Claude Code

marioserrano09 and others added 16 commits September 4, 2026 00:24
…ive login

New tools.dynamia.themes.dynamical-vue Maven module: Vue 3 + Tailwind 4 app shell
(sidebar/topbar/content/footer), built all-in-one via frontend-maven-plugin
(Node 24 + corepack/pnpm), with a session-based native login (POST /login/json)
and per-page rendering — DynamiaCrudPage for CrudPage nodes, <dynamia-embed>
(auto-detecting HTML vs JS, sandboxed) for everything else.

Framework/package fixes found by dogfooding this theme against demo-zk-books:
- PageEmbedController + a bare embed.zul view (platform/core/web, platform/ui/zk):
  a ZK page can now be embedded standalone via /page-embed/{path}, without
  doubling up on the full app-shell chrome that /page/{path} always renders.
- FieldResolver now resolves entity-reference fields to EntityRefLabel/EntityRefPicker
  instead of falling through to Textbox (was rendering "[object Object]").
- New entityDisplayLabel/resolveConverter/resolveIconClass utilities (ui-core),
  new Label/EnumLabel field components (vue) wired into Field.vue's componentMap,
  fixing raw-object table cells and two previously-unmapped descriptor component
  names ("label", "enumlabel").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the design in docs/design/SERVER_DRIVEN_ACTION_FLOWS.md:

- platform/core/actions: ActionFlowStep/ActionFlowStepType/ActionFlowContext,
  FlowRemoteAction, ActionFlows dispatcher, HMAC-signed stateless resumeToken
  (FlowTokenSigner/FlowTokenPayload/FlowTokenException), optional flow/flowId/
  resumeToken fields on ActionExecutionResponse/Request. No controller changes
  needed - FlowRemoteAction.execute() bridges into ActionFlows.dispatch().
- platform/packages/ui-core: framework-agnostic ConfirmManager/ToastManager
  feedback primitives (Phase 0 - none existed before).
- platform/packages/vue: DynamiaDialog/DynamiaConfirmHost/DynamiaToastHost
  components, useConfirm/useToast composables, and Actions.vue's runFlow()
  loop driving CONFIRM/NOTIFY steps to completion (INPUT/DIALOG/REDIRECT/
  CALL/CUSTOM renderers left as documented future work).
- platform/packages/sdk: TS mirrors of the new wire fields/types.
- examples/demo-zk-books: MarkOutOfStockAction, the Phase 3 end-to-end
  FlowRemoteAction example (confirm-then-mutate on Book).
- theme-dynamical-vue: mounts DynamiaConfirmHost/DynamiaToastHost once at
  the app root; styles for dialog/confirm/toast.

Phase 4 (CRUD parity: SaveRemoteAction/DeleteFlowRemoteAction etc.) not
included here - tracked separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… fix (§7)

Implements docs/design/SERVER_DRIVEN_ACTION_FLOWS.md §7/§8 Phase 4:

- platform/core/crud/actions/remote (new package): SaveRemoteAction/
  SaveFlowRemoteAction (create-or-update from a json-form/json/form
  ViewDescriptor, same field-resolution approach as
  RestNavigationUpdateOperation) and DeleteRemoteAction/DeleteFlowRemoteAction
  (single id or bulk "ids" list, no separate BulkDeleteRemoteAction). Flow
  variants registered under the conventional "save"/"delete" ids so the
  existing isSaveCrudAction/isDeleteCrudAction matchers pick them up for
  free; composition (SaveSupport/DeleteSupport static helpers) instead of
  extends, per §7.4's FlowRemoteAction/default-method gotcha.
- SaveSupport.persist enforces applicableStates server-side against the
  request's inferred CREATE/UPDATE state (§7.6 bug #2, scoped to these
  actions rather than a speculative generic controller-level state model).
- ActionMetadata: fixed §7.6 bug #1 - applicableStates now also serializes
  for CrudRemoteAction, not just the ZK-only CrudAction.
- ActionFlowContext: added asMap() - a flow's whole entity payload (a Map)
  is spread flat into the context by ActionFlows.dispatch, so
  SaveFlowRemoteAction needs a way to recover it as one map across
  start()/resume().
- platform/packages/vue: extracted the runFlow loop out of Actions.vue into
  a shared runActionFlow.ts, and wired useCrudPage's save/delete handlers
  to use it when the entity has a registered save/delete CrudRemoteAction
  (§7.5) - falls back to plain REST verbs otherwise, so entities with no
  such action behave exactly as before.

Not addressed here (left as documented open items): §7.6 point 3
(422 vs 406 validation-error shape reconciliation) and generic
CrudState enforcement in ApplicationMetadataController for arbitrary
future CrudRemoteActions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The theme-dynamical-vue commit's lockfile conflicted with main's newer
root devDependency ranges (@types/node, vite, vitest, eslint, prettier,
vite-plugin-dts) during the rebase. Resolved by regenerating via
`pnpm install --lockfile-only` so the lockfile matches package.json
across the workspace.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
…ementation

Reviewed the design doc against the shipped code (0ddfbe6 "Phases 0-3",
d03b14c "Phase 4") and corrected it where it drifted from what actually
landed:

- Status header: was "Draft — not implemented", now reflects Phases 0-4
  shipped plus what's still genuinely open.
- §4: resumeToken signing does NOT reuse the JWT cookie key as designed -
  FlowTokenSigner manages its own independent secret
  (dynamia.actions.flow.secret). TTL question resolved: 10min default,
  configurable via dynamia.actions.flow.token-ttl.
- §4: token verification is not an ActionFilter hook as originally
  planned - it's inline in ActionFlows.dispatch(), called from
  FlowRemoteAction.execute()'s default method.
- §5: removed a fabricated ActionFlowStep.thenDone() from the example
  (doesn't exist); documented the real done(data, message, messageType)
  overload and the asMap() accessor added in Phase 4.
- §6: runFlow -> runActionFlow (platform/packages/vue/src/actions/
  runActionFlow.ts, extracted from Actions.vue in Phase 4). Only
  CONFIRM/NOTIFY are actually wired - INPUT/DIALOG/REDIRECT/CALL/CUSTOM
  throw at runtime, no CUSTOM registry exists.
- §7.6: marked bugs #1 and #2 fixed in Phase 4 (bug #2 scoped to
  SaveSupport, not generic); bug #3 (422 vs 406 validation shape)
  explicitly left open, now a live inconsistency rather than a
  hypothetical to avoid.
- §8: rollout plan marked phase-by-phase against actual commits/files.
- §9: open questions updated - TTL resolved, REDIRECT/CALL/INPUT/
  DIALOG/CUSTOM renderers still unimplemented (not just undesigned),
  validation-shape reconciliation still open, signer key source noted
  as a real deviation worth a deliberate decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
ActionFlowsTest/FlowTokenSignerTest (originally written for JUnit4)
never got migrated when the feature branch was rebased onto main,
which already carries the JUnit5 migration (PR #77). They referenced
org.junit.Test / org.junit.Assert.* which no longer resolve, breaking
compilation for the whole tools.dynamia.actions test sources (and
anything -am building on top of it, e.g. platform/app).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
Pre-existing on main since #78 (microfrontend-mvc-view), predating
this branch's rebase. Same JUnit4->JUnit5 import breakage as the
actions module fix in this branch - fixing it here since it blocks
any -am reactor build that includes platform/core/web (a dependency
of platform/app, needed to verify the #83 controller fix below).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
…ntroller

Closes #83.

ApplicationMetadataController.executeAction now rejects a request
whose inferred CrudState isn't in CrudRemoteAction.getApplicableStates()
before execute()/start() ever runs, for any CrudRemoteAction - not just
SaveRemoteAction/SaveFlowRemoteAction (which already enforced this
narrowly via SaveSupport.persist, see d03b14c).

The inference mirrors SaveSupport's own heuristic instead of hitting
the database: an entity id present anywhere the client is expected to
carry one (dataId, data.id, a non-empty data.ids, or a raw list body)
means "existing entity" - allowed when READ/UPDATE/DELETE is declared
applicable; no id means "new entity" - allowed only when CREATE is
declared. This only distinguishes existing-vs-new, not the finer
READ/UPDATE/DELETE distinction a specific action may still need to
enforce itself - see docs/design/SERVER_DRIVEN_ACTION_FLOWS.md §7.6
point 2 for the full reasoning and known limits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
…ign doc

FlowTokenSigner's independent secret (dynamia.actions.flow.secret)
turns out to already match an established repo convention:
JWTServiceImpl does the exact same thing for JWT_SECRET (same 32-char
minimum, same warn-and-fall-back-to-an-ephemeral-secret instead of
failing startup, extensions/security/.../JWTServiceImpl.java). So this
isn't a gap to patch with a new fail-fast mechanism - that would be an
unprecedented pattern in this codebase, not a bug fix. Documented as
the deliberate, permanent answer instead.

Also updates the doc to reflect #83's generic CrudState enforcement
(previously scoped to SaveSupport only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
…USTOM flow steps

Part of #81.

- PromptManager: framework-agnostic single-value-input queue, the
  INPUT counterpart to the existing ConfirmManager (same single-flight
  queue/subscribe shape). Resolves to the user's input or null on
  cancel.
- FlowStepRendererRegistry: client-registered handler for a
  FlowRemoteAction's CUSTOM step, keyed by step.data.component - same
  registry shape as ClientActionRegistry, built on the shared Registry
  base class.

Both fully unit tested mirroring ConfirmManager.test.ts/
ClientActionRegistry.test.ts's own coverage style.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
Closes #81 (partially - REDIRECT/CALL stay unwired, see the design
doc's updated §9: their semantics are still an open question, not
implemented here on purpose).

- useInput()/<DynamiaPromptHost> - Vue adapter over PromptManager,
  mirrors useConfirm()/<DynamiaConfirmHost>.
- DialogFormManager/useFormDialog()/<DynamiaFormDialogHost> - new,
  Vue-specific (queues a VueFormView instance, so it can't live in
  ui-core). runActionFlow's DIALOG case fetches step.viewDescriptor
  for the request's dataType via client.metadata.getEntityView/
  getEntity, builds and prefills a VueFormView from step.data, and
  hands it to the handler - closing the loop on the design doc's "no
  new form-description language" constraint (§2) exactly as planned.
- CUSTOM steps resolve through ui-core's FlowStepRendererRegistry.
- runActionFlow.ts: FlowStepHandlers gains optional prompt/
  showFormDialog fields; both call sites (Actions.vue, useCrudPage.ts)
  wire useInput()/useFormDialog() alongside the existing useConfirm()/
  useToast().
- theme-dynamical-vue: mounts <DynamiaPromptHost>/<DynamiaFormDialogHost>
  at the app root alongside the existing hosts, plus matching Tailwind
  styles in app.css.

Verified: pnpm typecheck/build clean for ui-core and vue. The theme
frontend's own pnpm typecheck has 3 pre-existing, unrelated failures
(App.vue NavigationNode typing, main.ts plugin overload) confirmed via
git stash to predate this branch's work - not touched here; `vite
build` for the theme succeeds regardless.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
Closes #82.

ApplicationMetadataController.executeGlobalAction/executeEntityAction
now return a genuine HTTP 422 with an ErrorResult body for
ValidationError, byte-for-byte matching RestApiExceptionHandler.
handleValidationError's shape (VALIDATION_ERROR code,
invalidProperty/invalidValue detail keys) - reconciling §7.6 point 3,
left open since Phase 4.

Deliberate breaking change, approved explicitly (decision-authority
item per public-API-contract rules): every other outcome (success,
403/404/409/500) is unchanged, still a real HTTP 200 carrying an
ActionExecutionResponse with those pseudo-status codes embedded in the
body - only the validation-error case moved off the old 200/
ActionExecutionResponse-with-statusCode-406 shape.

No SDK/frontend changes needed: HttpClient.request already throws
DynamiaApiError uniformly for any non-2xx response regardless of body
shape (this is how client.crud(path).create() already surfaced 422
today), and useCrudPage.ts's save/delete handlers already catch
generically. Verified with new executeAction unit tests covering the
success/validation-error/not-found paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3MNM7KKfEtqMjVkwe1dXU
…DIALOG/CUSTOM flow steps

Wires theme-dynamical-vue into examples/demo-zk-books (new "vue" Spring profile,
dynamia.app.template=DynamicalVue, both themes coexist on the classpath) to exercise
the Server-Driven Action Flows system against a real ZK-free frontend end-to-end (#79, #84).

Three new FlowRemoteAction examples in mybookstore.actions, one per step type that until
now only had client-side unit test coverage (#81):
- ApplyDiscountAction — INPUT
- QuickEditPricingAction — DIALOG (reopens Book's own "form" descriptor)
- RateBookAction — CUSTOM ("star-rating" component)

CUSTOM needed an app-specific renderer: starRatingManager.ts + StarRatingHost.vue in
theme-dynamical-vue, same shape as ui-core's PromptManager/PromptHost but deliberately
kept out of ui-core/vue since CUSTOM is the per-app escape hatch, not a core component.

Also fixes theme-dynamical-vue/sources/pom.xml's parent version (26.7.0 -> 26.8.0),
drifted since the theme was added after themes/pom.xml's version bump.

Verified all 4 step types (CONFIRM regression + INPUT/DIALOG/CUSTOM) end-to-end via direct
HTTP calls against ApplicationMetadataController with the Vue template active — see #81/#84
for full verification notes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAAUpEWfda8UZX9ZfZNMDw
…ract crud save/delete dispatch

Actions.vue's tryHandleCrudActionLocally hardcoded New/Edit/Cancel by identity-string
matching (isCreateCrudAction/isEditCrudAction/isCancelCrudAction). Replaced with real
ClientAction registrations so they're resolved through the same pluggable mechanism as
any app-registered action, and can be overridden per app (last registration wins).

- ui-core: new builtinCrudActions.ts — registerBuiltinCrudActions() registers
  NewAction/EditAction/CancelAction via Registry's native `aliases` param (one
  registration per action, covering the same identity aliases matchesActionIdentity
  used). Save/Delete deliberately excluded — they keep their CrudRemoteAction/
  FlowRemoteAction resolution path (see docs/design/SERVER_DRIVEN_ACTION_FLOWS.md §7),
  turning them into ClientActions would short-circuit them before runActionFlow.
- vue: DynamiaVue plugin calls registerBuiltinCrudActions() at install(), before any
  app-specific ClientAction registrations. Actions.vue's tryHandleCrudActionLocally
  drops the now-unreachable New/Edit/Cancel branches (resolved one step earlier by
  tryHandleClientAction); Save/Delete branches untouched.
- vue: extracted dispatchCrudSave/dispatchCrudDelete out of useCrudPage.ts into
  actions/crudActionDispatch.ts (pure extraction, no behavior change) and exported them
  (plus runActionFlow/FlowStepHandlers) so a standalone <Crud> can reuse the same
  action-aware save/delete resolution instead of reimplementing it.
- ui-core: 12 new tests for registerBuiltinCrudActions (alias resolution, app override,
  execute behavior against a real CrudView, no-op without one).

Also includes the repo-wide 26.8.0 -> 26.9.0 version bump already staged in the working
tree from prior, unrelated work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XU8Gpku4fX5DtFzhGsEEno
@marioserrano09

Copy link
Copy Markdown
Contributor Author

Cerrado por ahora: la rama queda en origin para pruebas manuales; se reabre/recrea el PR cuando esté validada.

… Flows experimental

runActionFlow now handles the last two step types (#81):
- REDIRECT is terminal: navigates via handlers.navigate (default
  window.location.assign) and ends the loop. Only relative/http(s) URLs are
  followed; awaitReturn=true is rejected explicitly instead of being ignored.
- CALL runs data.action inside the same loop (so it can itself be a flow) via
  the global endpoint, or the entity endpoint when data.className is set, and
  resumes the caller with the nested response (minus `flow`) as the answer.
  Nesting capped at 5 levels; a nested REDIRECT ends the outer flow too.

Server side needed no change (ActionFlowStep.redirect/call already existed);
added Java tests for the CALL round-trip and REDIRECT payload shape.

The vue package gets vitest (same setup as ui-core) with 13 tests against a
mocked client. Not yet exercised by a real FlowRemoteAction in a browser.

The whole protocol is now documented as experimental (design doc banner,
@APinote on FlowRemoteAction/ActionFlowStep, @experimental on runActionFlow and
the SDK type). Design doc §6/§8/§9 updated with the chosen semantics; the only
open item left is REDIRECT with awaitReturn.

Part of #81.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
marioserrano09 and others added 2 commits September 21, 2026 15:25
…ntend in Maven

CI ("Maven Build") failed on the theme's `pnpm run build` with
"Rolldown failed to resolve import @dynamia-tools/vue": the frontend depends
on the workspace packages sdk/ui-core/vue, which resolve to their gitignored
dist/ outputs. Those only existed on machines where they'd been built by hand,
so it passed locally but not on a clean checkout.

Add a `pnpm --filter theme-dynamical-vue-frontend^... run build` execution
before `pnpm-build` so the dependencies are built first, in dependency order.
Reproduced on a clean clone, and verified the fix there.

Part of #84.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The comment added in the previous commit contained '--filter', which is
illegal inside an XML comment and made the whole reactor fail to parse.

Part of #84.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@marioserrano09
marioserrano09 merged commit 4a08b86 into main Sep 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Action Flows: generic CrudState enforcement for any CrudRemoteAction Action Flows: reconcile validation-error response shape (422 vs 406)

1 participant