diff --git a/resources/templates/components/modal-card.html b/resources/templates/components/modal-card.html new file mode 100644 index 00000000..0ebc7085 --- /dev/null +++ b/resources/templates/components/modal-card.html @@ -0,0 +1,15 @@ +{# Base modal-card chrome (single-step: header / optional side panel / body / footer). + A child template extends this and fills the head / side_panel / body / footer blocks, + so the whole card renders from one shared context in a single render call. Enter + triggers the footer save button via $refs.next. Mirrors transaction-edit/edit-modal + (the string-slot version still used by Transaction Edit). #} + diff --git a/resources/templates/transaction-bulk-code/account-entries.html b/resources/templates/transaction-bulk-code/account-entries.html deleted file mode 100644 index b0bd047e..00000000 --- a/resources/templates/transaction-bulk-code/account-entries.html +++ /dev/null @@ -1,2 +0,0 @@ -{# Wrapper around the expense-account grid (the body of the accounts validated-field). #} -
{{ grid|safe }}
diff --git a/resources/templates/transaction-bulk-code/account-grid.html b/resources/templates/transaction-bulk-code/account-grid.html index 16f1a2c7..8697e3cf 100644 --- a/resources/templates/transaction-bulk-code/account-grid.html +++ b/resources/templates/transaction-bulk-code/account-grid.html @@ -1,7 +1,7 @@ {# Expense-account grid -- fully template-driven. A single for-loop over the per-row - view-models (bulk-code/account-row-vm), each delegating to account-row.html. Replaces - the Clojure data-grid / data-grid-row / data-grid-cell composition. The trailing - "New account" button posts the whole #bulk-code-form (op=new-account). #} + view-models (bulk-code/account-row-vm), each delegating to account-row.html. The + trailing "New account" button (a-button partial) posts the whole #bulk-code-form + (op=new-account). #}
@@ -13,9 +13,9 @@ - {% for row in rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %} + {% for row in accounts.rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %} - +
{{ new_account_button|safe }}{% with classes=accounts.new_account.classes attrs=accounts.new_account.attrs indicator=accounts.new_account.indicator body=accounts.new_account.body %}{% include "templates/components/a-button.html" %}{% endwith %}
diff --git a/resources/templates/transaction-bulk-code/account-row.html b/resources/templates/transaction-bulk-code/account-row.html index fc220848..0a343e63 100644 --- a/resources/templates/transaction-bulk-code/account-row.html +++ b/resources/templates/transaction-bulk-code/account-row.html @@ -1,10 +1,8 @@ -{# One expense-account row, read entirely from a loop-bound `row` view-model - (bulk-code/account-row-vm). The account typeahead reuses the shared - components/typeahead.html partial (its context mapped in from row.account via a with - block); the location select, percentage input, and remove button are inlined plain - HTML. The location cell (#account-location-N) swaps just itself on account change; the - remove button swaps the whole #bulk-code-form. Every dynamic attribute arrives - pre-serialized as a string. #} +{# One expense-account row, read from a loop-bound `row` view-model (account-row-vm). The + account typeahead, location select, and remove button all reuse the shared component + partials (typeahead.html / location-select.html / a-icon-button via its ctx); only the + table layout is inline. The location cell (#account-location-N) swaps just itself on + account change; the remove button swaps the whole #bulk-code-form. #} @@ -15,9 +13,7 @@
- + {% with name=row.location.name classes=row.location.classes options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %}

{{ row.location_error }}

@@ -27,9 +23,5 @@

{{ row.pct_error }}

- - -
{% include "templates/components/svg-x.html" %}
-
- +
{% include "templates/components/svg-x.html" %}
diff --git a/resources/templates/transaction-bulk-code/body.html b/resources/templates/transaction-bulk-code/body.html new file mode 100644 index 00000000..6bb0d44e --- /dev/null +++ b/resources/templates/transaction-bulk-code/body.html @@ -0,0 +1,29 @@ +{# Bulk-code modal body: vendor typeahead (a change repopulates the default account via a + whole-form swap), status select, and the expense-account grid. Each field inlines the + validated-field wrapper (label + error

) and pulls its control in from the shared + component partials; all data comes from the form view-model. #} +

+
+
+
+ + {% with x_data=vendor.ta.x_data x_model=vendor.ta.x_model key=vendor.ta.key disabled=vendor.ta.disabled a_class=vendor.ta.a_class a_xinit=vendor.ta.a_xinit search_class=vendor.ta.search_class placeholder=vendor.ta.placeholder hidden_attrs=vendor.ta.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %} +

{{ vendor.error }}

+
+
+
+
+ + {% with name=status.name classes=status.classes attrs=status.attrs options=status.options %}{% include "templates/components/select.html" %}{% endwith %} +

{{ status.error }}

+
+
+
+

Expense Accounts

+
+
{% include "templates/transaction-bulk-code/account-grid.html" %}
+

{{ accounts.error }}

+
+
+
+
diff --git a/resources/templates/transaction-bulk-code/bulk-code-body.html b/resources/templates/transaction-bulk-code/bulk-code-body.html deleted file mode 100644 index b76a613f..00000000 --- a/resources/templates/transaction-bulk-code/bulk-code-body.html +++ /dev/null @@ -1,13 +0,0 @@ -{# Bulk-code modal body: vendor field (a change repopulates the default account via a - whole-form swap), status select, and the expense-account grid. #} -
-
-
{{ vendor_field|safe }} -
-
{{ status_field|safe }}
-
-

Expense Accounts

- {{ accounts_field|safe }} -
-
-
diff --git a/resources/templates/transaction-bulk-code/bulk-code-form.html b/resources/templates/transaction-bulk-code/bulk-code-form.html deleted file mode 100644 index eeb851b0..00000000 --- a/resources/templates/transaction-bulk-code/bulk-code-form.html +++ /dev/null @@ -1,6 +0,0 @@ -{# Top-level plain form for bulk-code (no wizard). The resolved (not-locked) transaction - id set rides in hidden ids[] fields -- the analog of the edit modal's single db/id - hidden -- so the selection survives form-changed / submit posts without an EDN snapshot - or a filter round-trip. #} -
{{ ids_hidden|safe }}{{ modal|safe }} -
diff --git a/resources/templates/transaction-bulk-code/card.html b/resources/templates/transaction-bulk-code/card.html new file mode 100644 index 00000000..6bb79a87 --- /dev/null +++ b/resources/templates/transaction-bulk-code/card.html @@ -0,0 +1,6 @@ +{# Bulk-code modal card: extends the shared modal-card base and fills its blocks with the + bulk-code head / body / footer partials. No side panel. #} +{% extends "templates/components/modal-card.html" %} +{% block head %}{% include "templates/transaction-bulk-code/head.html" %}{% endblock %} +{% block body %}{% include "templates/transaction-bulk-code/body.html" %}{% endblock %} +{% block footer %}{% include "templates/transaction-bulk-code/footer.html" %}{% endblock %} diff --git a/resources/templates/transaction-bulk-code/footer.html b/resources/templates/transaction-bulk-code/footer.html index 8f2c5bab..62294ce4 100644 --- a/resources/templates/transaction-bulk-code/footer.html +++ b/resources/templates/transaction-bulk-code/footer.html @@ -1,5 +1,5 @@ -{# Modal footer: the form-errors sink on the left, the Save button on the right. - Both are pre-rendered fragments (errors sink = form-errors.html, save = sc/button). #} +{# Modal footer: the form-errors sink on the left, the Save button on the right. Both + pull from the shared view-model; the button reuses components/button.html. #}
-
{{ form_errors|safe }}{{ save_button|safe }}
+
{% include "templates/transaction-bulk-code/form-errors.html" %}{% with classes=save.classes attrs=save.attrs loading_label=save.loading_label body=save.body %}{% include "templates/components/button.html" %}{% endwith %}
diff --git a/resources/templates/transaction-bulk-code/form.html b/resources/templates/transaction-bulk-code/form.html new file mode 100644 index 00000000..78d1f5ee --- /dev/null +++ b/resources/templates/transaction-bulk-code/form.html @@ -0,0 +1,5 @@ +{# Single render entrypoint for the bulk-code form. The route passes one view-model and + every sub-template composes from it via includes/blocks. The resolved (not-locked) + transaction id set rides in hidden ids[] fields so the selection survives + form-changed / submit posts without an EDN snapshot or a filter round-trip. #} +
{% for id in ids %}{% endfor %}
{% include "templates/transaction-bulk-code/card.html" %}
diff --git a/resources/templates/transaction-bulk-code/head.html b/resources/templates/transaction-bulk-code/head.html index 8c86db0f..a573d4eb 100644 --- a/resources/templates/transaction-bulk-code/head.html +++ b/resources/templates/transaction-bulk-code/head.html @@ -1,2 +1,2 @@ {# Modal header label: how many transactions this bulk-code operation will touch. #} -
Bulk editing {{ count }} transactions
+
Bulk editing {{ head_count }} transactions
diff --git a/resources/templates/transaction-bulk-code/open.html b/resources/templates/transaction-bulk-code/open.html new file mode 100644 index 00000000..1f5cf720 --- /dev/null +++ b/resources/templates/transaction-bulk-code/open.html @@ -0,0 +1,3 @@ +{# Modal-open response: the transitioner shell the modal stack expects, wrapping the whole + form. Composes in one render from the shared view-model. #} +
{% include "templates/transaction-bulk-code/form.html" %}
diff --git a/src/clj/auto_ap/ssr/components/selmer.clj b/src/clj/auto_ap/ssr/components/selmer.clj index 3b441bc8..dc9a4985 100644 --- a/src/clj/auto_ap/ssr/components/selmer.clj +++ b/src/clj/auto_ap/ssr/components/selmer.clj @@ -86,23 +86,27 @@ (defn money-input [params] (render "templates/components/money-input.html" {:attrs (money-input-attrs params)})) -(defn select - "Generic rendered from a Selmer partial (the location-select.html shape, + generalized). See select-ctx." + [params] + (render "templates/components/select.html" (select-ctx params))) ;; --- field wrapper --------------------------------------------------------------- @@ -153,7 +157,9 @@ :attrs (attrs->str (dissoc params :class)) :body (body->html children)})) -(defn button [{:keys [color disabled minimal-loading?] :as params} & children] +(defn button-ctx + "Plain-data context for templates/components/button.html (classes/attrs/loading_label/body)." + [{:keys [color disabled minimal-loading?] :as params} & children] (let [classes (cond-> (:class params) true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center relative justify-center disabled:opacity-50" (btn/bg-colors color disabled)) @@ -161,13 +167,17 @@ disabled (str " cursor-not-allowed") (some? color) (str " text-white ") (nil? color) (str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700"))] - (render "templates/components/button.html" - {:classes classes - :attrs (attrs->str (dissoc params :class)) - :loading_label (not minimal-loading?) - :body (body->html children)}))) + {:classes classes + :attrs (attrs->str (dissoc params :class)) + :loading_label (not minimal-loading?) + :body (body->html children)})) -(defn a-button [{:keys [color disabled] :as params} & children] +(defn button [params & children] + (render "templates/components/button.html" (apply button-ctx params children))) + +(defn a-button-ctx + "Plain-data context for templates/components/a-button.html (classes/attrs/indicator/body)." + [{:keys [color disabled] :as params} & children] (let [indicator? (:indicator? params true) classes (cond-> (:class params) true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center hover:scale-105 transition duration-100 justify-center") @@ -176,23 +186,29 @@ (= :secondary-light color) (str " text-blue-800 bg-white-200 border-gray-100 border hover:bg-blue-100 focus:ring-blue-100 dark:bg-blue-400 dark:hover:bg-blue-800 ") (some? color) (str " text-white " (btn/bg-colors color disabled)) (nil? color) (str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700"))] - (render "templates/components/a-button.html" - {:classes classes - :attrs (attrs->str (-> (dissoc params :class) - (assoc :tabindex 0 :href (:href params "#")))) - :indicator indicator? - :body (body->html children)}))) + {:classes classes + :attrs (attrs->str (-> (dissoc params :class) + (assoc :tabindex 0 :href (:href params "#")))) + :indicator indicator? + :body (body->html children)})) -(defn a-icon-button [{:keys [class] :as params} & children] +(defn a-button [params & children] + (render "templates/components/a-button.html" (apply a-button-ctx params children))) + +(defn a-icon-button-ctx + "Plain-data context for templates/components/a-icon-button.html (classes/attrs/body)." + [{:keys [class] :as params} & children] (let [class-str (or class "") has-padding? (re-find #"\bp[xy]?-\d+(\.\d+)?\b" class-str) classes (str class-str (if has-padding? "" " p-3") " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100")] - (render "templates/components/a-icon-button.html" - {:classes classes - :attrs (attrs->str (-> (dissoc params :class) - (assoc :href (or (:href params) "")))) - :body (body->html children)}))) + {:classes classes + :attrs (attrs->str (-> (dissoc params :class) + (assoc :href (or (:href params) "")))) + :body (body->html children)})) + +(defn a-icon-button [params & children] + (render "templates/components/a-icon-button.html" (apply a-icon-button-ctx params children))) (defn button-group-button [{:keys [size] :or {size :normal} :as params} & children] (let [classes (cond-> (:class params) diff --git a/src/clj/auto_ap/ssr/transaction/bulk_code.clj b/src/clj/auto_ap/ssr/transaction/bulk_code.clj index 74a66a1f..11497282 100644 --- a/src/clj/auto_ap/ssr/transaction/bulk_code.clj +++ b/src/clj/auto_ap/ssr/transaction/bulk_code.clj @@ -39,23 +39,9 @@ ;; decode straight into bulk-code-schema, mirroring transaction/edit.clj). ;; --------------------------------------------------------------------------- -(def ^:dynamic *errors* - "Humanized form errors for the current render, keyed by bulk-code-schema paths - (e.g. {:accounts {0 {:location [\"required\"]}}}). Bound by render-form from the - request's :form-errors. Plain map -- no wizard, no cursor." - {}) - -(defn- ferr - "Field errors at a schema path, read from *errors* (no step-params prefix)." - [& path] - (get-in *errors* (vec path))) - (defn- account-field-name [index field] (path->name2 :accounts index field)) -(defn- account-field-errors [index field] - (ferr :accounts index field)) - ;; --------------------------------------------------------------------------- ;; Schema + decode ;; --------------------------------------------------------------------------- @@ -119,17 +105,20 @@ (-> request :clients first :db/id))) ;; --------------------------------------------------------------------------- -;; Render (100% Selmer -- reuses the transaction/edit.clj sc/* component library -;; and the shared edit-modal / transitioner chrome). +;; Render. Each route ends in a single sel/render call (render-form / open-handler) +;; from one nested view-model (form-ctx); all composition lives in the Selmer +;; templates under transaction-bulk-code/ (form -> card extends components/modal-card +;; -> head/body/footer -> account-grid -> account-row), pulling the shared sc/* +;; component partials in via {% include %} + {% with %}. No HTML is built in Clojure. ;; --------------------------------------------------------------------------- (defn- account-row-vm "Build the plain-data view-model for one expense-account row (rendered by - account-row.html). Every dynamic attribute is pre-serialized to an HTML attribute - string here; the structure (cells, controls, the loop) lives in the templates. The - account typeahead and location , and remove button contexts come from the shared + sc/* and edit/* ctx builders, so the row template just {% include %}s those partials. + `errors` is the request's :form-errors map; field errors live under [:accounts i field]." + [{:keys [value client-id index errors]}] (let [account-val (let [av (:account value)] (if (map? av) (:db/id av) av)) tr-map (hx/alpine-mount-then-appear @@ -153,16 +142,16 @@ :tr_attrs (sc/attrs->str (dissoc tr-map :class)) :db_id_name (account-field-name index :db/id) :db_id_value (:db/id value) - :account_field_classes (sc/validated-field-classes {:errors (account-field-errors index :account)}) - :account_error (sc/errors-str (account-field-errors index :account)) + :account_field_classes (sc/validated-field-classes {:errors (get-in errors [:accounts index :account])}) + :account_error (sc/errors-str (get-in errors [:accounts index :account])) :account (account-typeahead-ctx {:value account-val :client-id client-id :name (account-field-name index :account) :x-model "accountId"}) :location_cell_id (str "account-location-" index) - :location_field_classes (sc/validated-field-classes {:errors (account-field-errors index :location)}) + :location_field_classes (sc/validated-field-classes {:errors (get-in errors [:accounts index :location])}) :location_field_attrs (sc/attrs->str location-attrs) - :location_error (sc/errors-str (account-field-errors index :location)) + :location_error (sc/errors-str (get-in errors [:accounts index :location])) :location (location-select-ctx {:name (account-field-name index :location) :account-location (:account/location (when (nat-int? account-val) (dc/pull (dc/db conn) '[:account/location] account-val))) @@ -171,114 +160,85 @@ :pct_attrs (sc/money-input-attrs {:name (account-field-name index :percentage) :class "w-16" :value (some-> (:percentage value) (* 100) long)}) - :pct_field_classes (sc/validated-field-classes {:errors (account-field-errors index :percentage)}) - :pct_error (sc/errors-str (account-field-errors index :percentage)) - :remove_attrs (sc/attrs->str {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) - :hx-vals (hx/json {:op "remove-account" :row-index (or index 0)}) - :hx-target "#bulk-code-form" - :hx-select "#bulk-code-form" - :hx-swap "outerHTML" - :hx-include "closest form" - :href ""})})) + :pct_field_classes (sc/validated-field-classes {:errors (get-in errors [:accounts index :percentage])}) + :pct_error (sc/errors-str (get-in errors [:accounts index :percentage])) + :remove (sc/a-icon-button-ctx {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) + :hx-vals (hx/json {:op "remove-account" :row-index (or index 0)}) + :hx-target "#bulk-code-form" + :hx-select "#bulk-code-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :class "account-remove-action"})})) -(defn- account-grid* - "Render the whole expense-account grid from account-grid.html: a {% for %} over the - per-row view-models, plus the pre-rendered New-account button. All grid structure is - in the template -- the Clojure here only assembles data." +(defn- form-ctx + "The whole bulk-code form as one nested view-model. render-form / open-handler each make + a single sel/render call with this; all composition (modal chrome, body, account grid, + rows, footer, errors) happens in the Selmer templates -- no markup is built in Clojure." [request] - (let [client-id (single-client-id request) - accounts (vec (:accounts (:bulk-state request)))] - (sel/render->hiccup - "templates/transaction-bulk-code/account-grid.html" - {:rows (map-indexed - (fn [index account] - (account-row-vm {:value account - :client-id client-id - :index index})) - accounts) - :new_account_button (sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) - :hx-vals (hx/json {:op "new-account"}) - :hx-target "#bulk-code-form" - :hx-select "#bulk-code-form" - :hx-swap "outerHTML" - :hx-include "closest form" - :color :secondary} - "New account")}))) - -(defn- bulk-code-body* [request] (let [bulk-state (:bulk-state request) + errors (or (:form-errors request) {}) + client-id (single-client-id request) + ids (:ids bulk-state) + accounts (vec (:accounts bulk-state)) vendor-val (:vendor bulk-state) status-val (some-> (:approval-status bulk-state) name)] - (sel/render->hiccup - "templates/transaction-bulk-code/bulk-code-body.html" - {:vendor_changed_attrs (sc/attrs->str {:hx-trigger "change" - :hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) - :hx-vals (hx/json {:op "vendor-changed"}) - :hx-target "#bulk-code-form" - :hx-select "#bulk-code-form" - :hx-swap "outerHTML" - :hx-sync "this:replace" - :hx-include "closest form"}) - :vendor_field (str (sc/validated-field - {:label "Vendor" :errors (ferr :vendor)} - (sc/typeahead {:name (path->name2 :vendor) - :id (path->name2 :vendor) - :error? (boolean (seq (ferr :vendor))) - :class "w-96" - :placeholder "Search for vendor..." - :url (bidi/path-for ssr-routes/only-routes :vendor-search) - :value vendor-val - :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))}))) - :status_field (str (sc/validated-field - {:label "Status" :errors (ferr :approval-status)} - (sc/select {:name (path->name2 :approval-status) - :value status-val - :options [["" "No Change"] - ["approved" "Approved"] - ["unapproved" "Unapproved"] - ["suppressed" "Suppressed"] - ["requires-feedback" "Client Review"]]}))) - :accounts_field (str (sc/validated-field - {:errors (ferr :accounts)} - (sc/render "templates/transaction-bulk-code/account-entries.html" - {:grid (str (account-grid* request))})))}))) - -(defn- form-errors-html [errors] - (sc/render "templates/transaction-bulk-code/form-errors.html" - {:errors_str (when (seq errors) - (str/join ", " (filter string? errors)))})) - -(defn- footer* [request] - (sc/render "templates/transaction-bulk-code/footer.html" - {:form_errors (form-errors-html (:errors (:form-errors request))) - :save_button (sc/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Save")})) + {:form_attrs (sc/attrs->str {:hx-ext "response-targets" + :hx-swap "outerHTML" + :hx-target-400 "#form-errors .error-content" + :hx-trigger "submit" + :hx-target "this" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-submit)}) + :head_count (count ids) + :ids (map-indexed (fn [i id] {:name (path->name2 :ids i) :value id}) ids) + :vendor {:changed_attrs (sc/attrs->str {:hx-trigger "change" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) + :hx-vals (hx/json {:op "vendor-changed"}) + :hx-target "#bulk-code-form" + :hx-select "#bulk-code-form" + :hx-swap "outerHTML" + :hx-sync "this:replace" + :hx-include "closest form"}) + :field_classes (sc/validated-field-classes {:errors (:vendor errors)}) + :error (sc/errors-str (:vendor errors)) + :ta (sc/typeahead-ctx {:name (path->name2 :vendor) + :id (path->name2 :vendor) + :error? (boolean (seq (:vendor errors))) + :class "w-96" + :placeholder "Search for vendor..." + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :value vendor-val + :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})} + :status (merge (sc/select-ctx {:name (path->name2 :approval-status) + :value status-val + :options [["" "No Change"] + ["approved" "Approved"] + ["unapproved" "Unapproved"] + ["suppressed" "Suppressed"] + ["requires-feedback" "Client Review"]]}) + {:field_classes (sc/validated-field-classes {:errors (:approval-status errors)}) + :error (sc/errors-str (:approval-status errors))}) + :accounts {:field_classes (sc/validated-field-classes {:errors (:accounts errors)}) + :error (sc/errors-str (:accounts errors)) + :new_account (sc/a-button-ctx {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) + :hx-vals (hx/json {:op "new-account"}) + :hx-target "#bulk-code-form" + :hx-select "#bulk-code-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :color :secondary} + "New account") + :rows (map-indexed (fn [i a] + (account-row-vm {:value a :client-id client-id :index i :errors errors})) + accounts)} + :errors_str (when-let [e (seq (:errors errors))] + (str/join ", " (filter string? e))) + :save (sc/button-ctx {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Save")})) (defn render-form - "Renders the whole plain bulk-code form (no wizard). Binds *errors* from the request's - :form-errors so the field-level error lookups (ferr) resolve. Reuses the edit modal's - chrome (edit-modal.html), with no side panel." + "Render the whole bulk-code form as a single Selmer render of form.html from the form + view-model -- no Clojure-side string stitching, no wizard, no cursor." [request] - (binding [*errors* (or (:form-errors request) {})] - (let [ids (:ids (:bulk-state request)) - ids-hidden (apply str - (map-indexed (fn [i id] - (str (sc/hidden {:name (path->name2 :ids i) :value id}))) - ids)) - modal-card (sel/render "templates/transaction-edit/edit-modal.html" - {:head (sc/render "templates/transaction-bulk-code/head.html" {:count (count ids)}) - :side_panel nil - :body (str (bulk-code-body* request)) - :footer (str (footer* request))})] - (sel/render->hiccup - "templates/transaction-bulk-code/bulk-code-form.html" - {:ids_hidden ids-hidden - :form_attrs (sc/attrs->str {:hx-ext "response-targets" - :hx-swap "outerHTML" - :hx-target-400 "#form-errors .error-content" - :hx-trigger "submit" - :hx-target "this" - :hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-submit)}) - :modal (str (sc/modal {:id "bulkcodemodal"} (sel/raw modal-card)))})))) + (sel/render->hiccup "templates/transaction-bulk-code/form.html" (form-ctx request))) ;; --------------------------------------------------------------------------- ;; edit-form-changed ops (whole-form re-render). Replaces the per-operation @@ -457,12 +417,11 @@ ;; --------------------------------------------------------------------------- (defn open-handler - "Initial modal open (GET). Wraps the rendered form in the #transitioner shell expected - by the modal stack (reuses the edit modal's transitioner)." + "Initial modal open (GET). A single Selmer render of open.html (the #transitioner shell + that includes the whole form) from the form view-model." [request] (modal-response - (sel/render->hiccup "templates/transaction-edit/transitioner.html" - {:body (str (render-form request))}))) + (sel/render->hiccup "templates/transaction-bulk-code/open.html" (form-ctx request)))) (defn- render-form-response "wrap-form-4xx-2 form-handler: re-render the whole form with field/form errors."