feat: Server-Driven Action Flows + theme-dynamical-vue - #85
Merged
Merged
Conversation
…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
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
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
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>
…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>
This was referenced Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumen
Experimental. Server-Driven Action Flows para
RemoteAction(protocolo stateless conresumeTokenfirmado, sin store server-side) + el tematheme-dynamical-vue(Vue 3 + Tailwind 4) como primer consumidor real, sin fallback ZK.Diseño:
docs/design/SERVER_DRIVEN_ACTION_FLOWS.md.Qué incluye
platform/core/actions):ActionFlowStep/FlowRemoteAction/ActionFlows,resumeTokenHMAC (FlowTokenSigner). Frontend:ConfirmManager/ToastManager,runActionFlow, SDK types.platform/core/crud):SaveRemoteAction/SaveFlowRemoteAction/DeleteRemoteAction/DeleteFlowRemoteActionentools.dynamia.crud.actions.remote(las acciones ZK quedan intactas).CrudStateenApplicationMetadataController.executeAction+ test de regresión.ErrorResult).INPUT/DIALOG/CUSTOM(PromptManager,FlowStepRendererRegistry,runActionFlow) y, end33ebcbe,REDIRECT/CALL(experimental):REDIRECTterminal (solo URLs relativas/http(s),awaitReturnrechazado);CALLanida 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.CrudPagevía<DynamiaCrudPage>.examples/demo-zk-books(perfilvue):ApplyDiscountAction(INPUT),QuickEditPricingAction(DIALOG),RateBookAction(CUSTOM).ClientActionreales (sustituyen el matching hardcodeado enActions.vue); extracción dedispatchCrudSave/Delete.Cosas a tener en cuenta al revisar
89fafca9incluye el bump de versión repo-wide26.8.0 -> 26.9.0(estaba staged de trabajo previo, no relacionado con flows). Se puede revisar por separado.df5eddfb(fix imports JUnit4 enMicroFrontendViewTest) duplica8a8ec905que ya está enmain; el merge es limpio.7f138812diceCloses #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 pendienteawaitReturn+ demo e2e: reabrir tras el merge.pnpm typecheckdetheme-dynamical-vuefalla con 4 errores TS (App.vue,main.ts); son preexistentes (idénticos sin estos cambios derunActionFlow).Validación
pnpm -r test: todo en verde (ui-core 107, vue 13, sdk 22, bridge 6, extensions).mvn testenplatform/core/{actions,crud,web}yplatform/app: BUILD SUCCESS (incl.ApplicationMetadataControllerTest14,ActionFlowsTest,FlowTokenSignerTest).REDIRECT/CALLsolo con tests unitarios (cliente mockeado), sin prueba en navegador.ApplicationMetadataControllercon el template Vue activo (detalle en Action Flows: implement INPUT/DIALOG/REDIRECT/CALL/CUSTOM step renderers on the Vue side #81/theme-dynamical-vue: Vue+Tailwind enterprise theme #84).Closes #82
Closes #83
Part of #79, #81, #84
🤖 Generated with Claude Code