refactor(ssr): render the bulk-code modal fully through Selmer

Move all markup in the Transaction Bulk Code modal out of Clojure and into
Selmer templates so bulk_code.clj only assembles data.

- Replace the inline sel/raw HTML strings and one Hiccup [:p] with templates:
  head, form-errors, footer, account-entries, success-body.
- Render the expense-account grid from a {% for %} template (account-grid.html
  + account-row.html) driven by a per-row view-model (account-row-vm); the row
  reuses the shared components/typeahead.html via a {% with %} include (no fork).
- Extract behaviour-preserving data-prep helpers reused by the view-model:
  sc/typeahead-ctx, sc/money-input-attrs, sc/validated-field-classes,
  sc/errors-str, edit/account-typeahead-ctx, edit/location-select-ctx.

Verified: REPL render parity + browser QA (add/remove row, typeahead select,
per-row location swap, percentage validation, submit, vendor auto-populate);
no JS errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 19:22:37 -07:00
parent 314c47b7a6
commit a760d15509
10 changed files with 242 additions and 143 deletions

View File

@@ -72,14 +72,19 @@
(update :class #(str % (inputs/use-size size))))]
(render "templates/components/text-input.html" {:attrs (attrs->str attrs)})))
(defn money-input [{:keys [size] :as params}]
(let [attrs (-> params
(defn money-input-attrs
"The serialized attribute string for a money input. Split out so a template-driven
grid can inline `<input {{ attrs|safe }}>` without re-deriving the class logic."
[{:keys [size] :as params}]
(attrs->str (-> params
(dissoc :size)
(update :class (fnil hh/add-class "") inputs/default-input-classes)
(update :class hh/add-class "appearance-none text-right")
(update :class #(str % (inputs/use-size size)))
(assoc :type "number" :step "0.01"))]
(render "templates/components/money-input.html" {:attrs (attrs->str attrs)})))
(assoc :type "number" :step "0.01"))))
(defn money-input [params]
(render "templates/components/money-input.html" {:attrs (money-input-attrs params)}))
(defn select
"Generic <select> rendered from a Selmer partial (the location-select.html shape,
@@ -101,23 +106,34 @@
;; --- field wrapper ---------------------------------------------------------------
(defn validated-field-classes
"The wrapping-div class string for a validated field (group + optional has-error +
caller class). Split out so a template-driven row can stamp the same classes."
[{:keys [errors] :as params}]
(cond-> (or (:class params) "")
(sequential? errors) (hh/add-class "has-error")
:always (hh/add-class "group")))
(defn errors-str
"Comma-join the string errors at a field (nil/empty -> empty string), matching the
validated-field error <p>."
[errors]
(or (when (sequential? errors)
(str/join ", " (filter string? errors)))
""))
(defn validated-field
"Selmer port of com/validated-field (the errors- variant of field-): label + body +
an always-present error <p>. Pass-through attrs land on the wrapping div (the account
row's location cell hangs its swap wiring here)."
[{:keys [label errors] :as params} & body]
(let [classes (cond-> (or (:class params) "")
(sequential? errors) (hh/add-class "has-error")
:always (hh/add-class "group"))
attrs (dissoc params :label :errors :error-source :error-key :class)
errors-str (when (sequential? errors)
(str/join ", " (filter string? errors)))]
(let [attrs (dissoc params :label :errors :error-source :error-key :class)]
(render "templates/components/validated-field.html"
{:label label
:classes classes
:classes (validated-field-classes params)
:attrs (attrs->str attrs)
:body (body->html body)
:errors_str (or errors-str "")})))
:errors_str (errors-str errors)})))
;; --- buttons / badges / links ----------------------------------------------------
@@ -274,10 +290,12 @@
:attrs (attrs->str (dissoc params :handle-unexpected-error? :class))
:body (body->html children)}))
(defn typeahead
"Selmer port of com/typeahead. Resolves the initial {value,label} server-side via
value-fn/content-fn (DB lookups), builds the Alpine x-data, and serializes the
hidden posting-input attributes. Preserves every tippy?. null-guard."
(defn typeahead-ctx
"Build the plain-data context map for templates/components/typeahead.html. Resolves the
initial {value,label} server-side via value-fn/content-fn (DB lookups), builds the
Alpine x-data, and serializes the hidden posting-input attributes. Split out from
`typeahead` so a fully template-driven grid can feed the same partial per row (via
{% with %}) without re-deriving any of this logic. Every value is a string/boolean."
[{:keys [value value-fn content-fn x-model x-init id class placeholder disabled url]
:as params}]
(let [vf (or value-fn identity)
@@ -298,13 +316,17 @@
(dissoc :class :value-fn :content-fn :placeholder :x-model)
(assoc "x-ref" "hidden" :type "hidden" ":value" "value.value"
:x-init "$watch('value', v => { $el.value = (v && v.value != null) ? v.value : ''; $nextTick(() => $dispatch('change')); }); "))]
(render "templates/components/typeahead.html"
{:x_data x-data
:x_model x-model
:key (when id (str id "--" vval))
:disabled disabled
:a_class a-class
:a_xinit a-xinit
:search_class search-class
:placeholder placeholder
:hidden_attrs (attrs->str hidden-attrs)})))
{:x_data x-data
:x_model x-model
:key (when id (str id "--" vval))
:disabled disabled
:a_class a-class
:a_xinit a-xinit
:search_class search-class
:placeholder placeholder
:hidden_attrs (attrs->str hidden-attrs)}))
(defn typeahead
"Selmer port of com/typeahead. Preserves every tippy?. null-guard. See typeahead-ctx."
[params]
(render "templates/components/typeahead.html" (typeahead-ctx params)))