refactor(ssr): full Selmer migration of Transaction Edit; remove the wizard

Migrate every part of the Transaction Edit modal's HTML to Selmer templates
(zero Hiccup in the render path) and delete the mm multi-modal "wizard"
abstraction entirely -- there was only ever one step.

- New auto-ap.ssr.components.selmer (sc) + ~22 shared component partials under
  resources/templates/components/ (typeahead, button-group, radio-card,
  data-grid, validated-field, modal, buttons, inputs, SVGs). Each wrapper renders
  its own partial; dynamic HTMX/Alpine attrs bridge via attrs->str -> {{attrs|safe}}.
- 15 modal templates under resources/templates/transaction-edit/.
- Delete EditWizard/LinksStep records + all mm/* usage. Plain handlers: flat
  wrap-decode-edit (fields renamed off step-params[...], stray keys stripped),
  flat wrap-derive-state, *errors*-based field errors, generic wrap-form-4xx-2.
- Drop the edit-wizard-navigate route (routes ~12 -> 5).
- Fix: stray `method` (tab button-group hidden) leaked into the upsert -> 500;
  strip decoded map to schema keys.
- e2e selectors updated (#wizard-form->#edit-form, #wizardmodal->#editmodal,
  step-params[...] field names). Parity: swap 6/6, edit 8/8, suite 38/1
  (1 pre-existing unrelated nav test).
- ssr-form-migration skill updated with the learnings (composition mechanics,
  sc/* library, drop-the-wizard recipe, scorecard row, 3 new gotchas).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 07:47:47 -07:00
parent c892719bd1
commit a01dfc197e
47 changed files with 1161 additions and 659 deletions

View File

@@ -75,11 +75,60 @@ Lessons:
- **Embed via the bridge.** `render->hiccup` lets the Selmer fragment sit inside the
still-Hiccup row (`com/validated-field` wraps it) — strangler, one component at a time.
## Composition
## Composition — verified mechanics (selmer 1.12.61)
Selmer composes via `{% include %}` and `{% block %}`. Prefer small per-component templates
referenced by classpath path. Keep `|safe` to values the server fully controls (rendered
Hiccup, JSON for `x-data`), never raw user input.
Proven by REPL before the full migration (do the same before relying on any of these):
- **`{% include %}` works in FILE renders only.** `sel/render` = `selmer/render-file`, and
include/extends/block are *parse-stage* tags. Rendering a template **string** that
contains `{% include %}` throws `unrecognized tag: :include` (the runtime expr-tag has a
nil handler). So includes only work from a `.html` file, never from `render-str`.
- **`{% include %}` sees `{% for %}` loop bindings.** Inside `{% for row in rows %}…{% include "components/x.html" %}…{% endfor %}` the partial can read `row.*`. Good for repeated
rows — though Clojure-composing the rows (below) is usually simpler.
- **`{% include … with k=v %}` is IGNORED** in 1.12.61 (the `with` clause is dropped). To
parametrize, either wrap with the block tag `{% with k=v %}{% include … %}{% endwith %}`
(works), or — preferred — let a Clojure wrapper render the partial with an explicit ctx.
## The Clojure-wrapper composition pattern (`auto-ap.ssr.components.selmer` / `sc`)
Because `{% include with %}` can't pass args and the server computes most values anyway,
each shared component is a **thin Clojure wrapper that renders its own partial** (the
proven `location-select*` shape, generalised). The element *structure* lives 100% in the
`.html`; the only Clojure is data assembly. The modal's render fns compose these wrappers
and assemble a view-model, interpolating the pre-rendered fragments as `{{ frag|safe }}`.
```clojure
(sc/hidden {:name :value }) ; -> render "components/hidden.html"
(sc/validated-field {:label :errors } body)
(sc/typeahead {:name :url :value :content-fn }) ; resolves label server-side
(sc/data-grid {:headers [] :footer-tbody } rows)
```
### `attrs->str` — the dynamic-attribute bridge
HTMX/Alpine attributes vary per call site, so a fixed template can't enumerate them.
`sc/attrs->str` serialises an attribute *map* to an HTML attribute *string* injected with
`{{ attrs|safe }}`: `<input type="hidden"{{ attrs|safe }}>`. Rules mirror hiccup2 — nil/false
dropped, `true` → bare boolean attr, else `name="escaped"` (via `hu/escape-html`, so JSON
`x-data` and `x-init` quotes become `&quot;`/`&apos;` and the browser decodes them back).
Keyword keys keep their full name incl. namespaced/colon forms (`:x-hx-val:account-id`
`x-hx-val:account-id`). This keeps templates free of per-attribute `{% if %}` ladders while
still emitting 100% Selmer markup — `attrs->str` serialises data, it does not build Hiccup.
### Reuse the real class helpers
Wrappers reuse `inputs/default-input-classes`, `inputs/use-size`, `hh/add-class`,
`btn/bg-colors` so output matches the Hiccup component **modulo Tailwind class order**.
Verify by class-**set** equality + e2e, never byte-parity (see the cookbook entries).
### Trivial wrapper divs
A bare `<div class="w-72">…</div>` around a fragment is composed with a `wrap-div` string
helper (or put the class in the parent template), not a Hiccup vector — string composition
of a structural wrapper is not Hiccup and avoids a micro-template per div.
Keep `|safe` to values the server fully controls (rendered fragments, JSON for `x-data`),
never raw user input.
## Scope (Open decision 2)