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:
@@ -0,0 +1,2 @@
|
|||||||
|
{# Wrapper around the expense-account grid (the body of the accounts validated-field). #}
|
||||||
|
<div id="account-entries" class="space-y-3">{{ grid|safe }}</div>
|
||||||
22
resources/templates/transaction-bulk-code/account-grid.html
Normal file
22
resources/templates/transaction-bulk-code/account-grid.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{# 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). #}
|
||||||
|
<div class="shrink overflow-y-scroll">
|
||||||
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 shrink">
|
||||||
|
<thead class="text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 group-[.raw]:sticky group-[.raw]:z-10 group-[.raw]:top-0">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3" scope="col">Account</th>
|
||||||
|
<th class="px-4 py-3 w-32" scope="col">Location</th>
|
||||||
|
<th class="px-4 py-3 w-16" scope="col">%</th>
|
||||||
|
<th class="px-4 py-3 w-16" scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %}
|
||||||
|
<tr class="new-row border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<td class="px-4 py-2" colspan="4">{{ new_account_button|safe }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
35
resources/templates/transaction-bulk-code/account-row.html
Normal file
35
resources/templates/transaction-bulk-code/account-row.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{# 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. #}
|
||||||
|
<tr class="{{ row.tr_classes }}"{{ row.tr_attrs|safe }}>
|
||||||
|
<input type="hidden" name="{{ row.db_id_name }}"{% if row.db_id_value %} value="{{ row.db_id_value }}"{% endif %}>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<div class="{{ row.account_field_classes }}">
|
||||||
|
<div class="flex flex-col">{% with x_data=row.account.x_data x_model=row.account.x_model key=row.account.key disabled=row.account.disabled a_class=row.account.a_class a_xinit=row.account.a_xinit search_class=row.account.search_class placeholder=row.account.placeholder hidden_attrs=row.account.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}</div>
|
||||||
|
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.account_error }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2" id="{{ row.location_cell_id }}">
|
||||||
|
<div class="{{ row.location_field_classes }}"{{ row.location_field_attrs|safe }}>
|
||||||
|
<select name="{{ row.location.name }}" class="{{ row.location.classes }}">
|
||||||
|
{% for opt in row.location.options %}<option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.location_error }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<div class="{{ row.pct_field_classes }}">
|
||||||
|
<input {{ row.pct_attrs|safe }}>
|
||||||
|
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.pct_error }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 align-top">
|
||||||
|
<a class="account-remove-action 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"{{ row.remove_attrs|safe }}>
|
||||||
|
<div class="h-4 w-4">{% include "templates/components/svg-x.html" %}</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
5
resources/templates/transaction-bulk-code/footer.html
Normal file
5
resources/templates/transaction-bulk-code/footer.html
Normal file
@@ -0,0 +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). #}
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div class="flex items-baseline gap-x-4">{{ form_errors|safe }}{{ save_button|safe }}</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{# Submit-error sink. A 4xx submit swaps the inner `.error-content` (hx-target-400);
|
||||||
|
the span is present only when there are form-level errors, matching the prior
|
||||||
|
hand-rolled markup byte-for-byte. #}
|
||||||
|
<div id="form-errors">{% if errors_str %}<span class="error-content"><p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ errors_str }}</p></span>{% endif %}</div>
|
||||||
2
resources/templates/transaction-bulk-code/head.html
Normal file
2
resources/templates/transaction-bulk-code/head.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{# Modal header label: how many transactions this bulk-code operation will touch. #}
|
||||||
|
<div class="p-2">Bulk editing {{ count }} transactions</div>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{# Post-submit confirmation message embedded in the shared success modal. #}
|
||||||
|
<p>Successfully coded {{ count }} transactions.</p>
|
||||||
@@ -72,14 +72,19 @@
|
|||||||
(update :class #(str % (inputs/use-size size))))]
|
(update :class #(str % (inputs/use-size size))))]
|
||||||
(render "templates/components/text-input.html" {:attrs (attrs->str attrs)})))
|
(render "templates/components/text-input.html" {:attrs (attrs->str attrs)})))
|
||||||
|
|
||||||
(defn money-input [{:keys [size] :as params}]
|
(defn money-input-attrs
|
||||||
(let [attrs (-> params
|
"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)
|
(dissoc :size)
|
||||||
(update :class (fnil hh/add-class "") inputs/default-input-classes)
|
(update :class (fnil hh/add-class "") inputs/default-input-classes)
|
||||||
(update :class hh/add-class "appearance-none text-right")
|
(update :class hh/add-class "appearance-none text-right")
|
||||||
(update :class #(str % (inputs/use-size size)))
|
(update :class #(str % (inputs/use-size size)))
|
||||||
(assoc :type "number" :step "0.01"))]
|
(assoc :type "number" :step "0.01"))))
|
||||||
(render "templates/components/money-input.html" {:attrs (attrs->str attrs)})))
|
|
||||||
|
(defn money-input [params]
|
||||||
|
(render "templates/components/money-input.html" {:attrs (money-input-attrs params)}))
|
||||||
|
|
||||||
(defn select
|
(defn select
|
||||||
"Generic <select> rendered from a Selmer partial (the location-select.html shape,
|
"Generic <select> rendered from a Selmer partial (the location-select.html shape,
|
||||||
@@ -101,23 +106,34 @@
|
|||||||
|
|
||||||
;; --- field wrapper ---------------------------------------------------------------
|
;; --- 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
|
(defn validated-field
|
||||||
"Selmer port of com/validated-field (the errors- variant of field-): label + body +
|
"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
|
an always-present error <p>. Pass-through attrs land on the wrapping div (the account
|
||||||
row's location cell hangs its swap wiring here)."
|
row's location cell hangs its swap wiring here)."
|
||||||
[{:keys [label errors] :as params} & body]
|
[{:keys [label errors] :as params} & body]
|
||||||
(let [classes (cond-> (or (:class params) "")
|
(let [attrs (dissoc params :label :errors :error-source :error-key :class)]
|
||||||
(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)))]
|
|
||||||
(render "templates/components/validated-field.html"
|
(render "templates/components/validated-field.html"
|
||||||
{:label label
|
{:label label
|
||||||
:classes classes
|
:classes (validated-field-classes params)
|
||||||
:attrs (attrs->str attrs)
|
:attrs (attrs->str attrs)
|
||||||
:body (body->html body)
|
:body (body->html body)
|
||||||
:errors_str (or errors-str "")})))
|
:errors_str (errors-str errors)})))
|
||||||
|
|
||||||
;; --- buttons / badges / links ----------------------------------------------------
|
;; --- buttons / badges / links ----------------------------------------------------
|
||||||
|
|
||||||
@@ -274,10 +290,12 @@
|
|||||||
:attrs (attrs->str (dissoc params :handle-unexpected-error? :class))
|
:attrs (attrs->str (dissoc params :handle-unexpected-error? :class))
|
||||||
:body (body->html children)}))
|
:body (body->html children)}))
|
||||||
|
|
||||||
(defn typeahead
|
(defn typeahead-ctx
|
||||||
"Selmer port of com/typeahead. Resolves the initial {value,label} server-side via
|
"Build the plain-data context map for templates/components/typeahead.html. Resolves the
|
||||||
value-fn/content-fn (DB lookups), builds the Alpine x-data, and serializes the
|
initial {value,label} server-side via value-fn/content-fn (DB lookups), builds the
|
||||||
hidden posting-input attributes. Preserves every tippy?. null-guard."
|
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]
|
[{:keys [value value-fn content-fn x-model x-init id class placeholder disabled url]
|
||||||
:as params}]
|
:as params}]
|
||||||
(let [vf (or value-fn identity)
|
(let [vf (or value-fn identity)
|
||||||
@@ -298,13 +316,17 @@
|
|||||||
(dissoc :class :value-fn :content-fn :placeholder :x-model)
|
(dissoc :class :value-fn :content-fn :placeholder :x-model)
|
||||||
(assoc "x-ref" "hidden" :type "hidden" ":value" "value.value"
|
(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')); }); "))]
|
: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_data x-data
|
:x_model x-model
|
||||||
:x_model x-model
|
:key (when id (str id "--" vval))
|
||||||
:key (when id (str id "--" vval))
|
:disabled disabled
|
||||||
:disabled disabled
|
:a_class a-class
|
||||||
:a_class a-class
|
:a_xinit a-xinit
|
||||||
:a_xinit a-xinit
|
:search_class search-class
|
||||||
:search_class search-class
|
:placeholder placeholder
|
||||||
:placeholder placeholder
|
:hidden_attrs (attrs->str hidden-attrs)}))
|
||||||
: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)))
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
[auto-ap.ssr.transaction.common :refer [grid-page query-schema
|
[auto-ap.ssr.transaction.common :refer [grid-page query-schema
|
||||||
selected->ids
|
selected->ids
|
||||||
wrap-status-from-source]]
|
wrap-status-from-source]]
|
||||||
[auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead*
|
[auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead-ctx
|
||||||
location-select*]]
|
location-select-ctx]]
|
||||||
[auto-ap.ssr.utils
|
[auto-ap.ssr.utils
|
||||||
:refer [->db-id apply-middleware-to-all-handlers assert-schema entity-id
|
:refer [->db-id apply-middleware-to-all-handlers assert-schema entity-id
|
||||||
form-validation-error html-response main-transformer modal-response
|
form-validation-error html-response main-transformer modal-response
|
||||||
@@ -123,13 +123,22 @@
|
|||||||
;; and the shared edit-modal / transitioner chrome).
|
;; and the shared edit-modal / transitioner chrome).
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
(defn transaction-account-row*
|
(defn- account-row-vm
|
||||||
"One row of the bulk-code account grid, from a plain account map (no cursor). The
|
"Build the plain-data view-model for one expense-account row (rendered by
|
||||||
location cell swaps just itself (#account-location-<index>, Rule 2); remove swaps the
|
account-row.html). Every dynamic attribute is pre-serialized to an HTML attribute
|
||||||
whole #bulk-code-form (Rule 3). Percentage rides along to submit (Rule 1, no request)."
|
string here; the structure (cells, controls, the loop) lives in the templates. The
|
||||||
|
account typeahead and location <select> contexts come from the shared edit/* ctx
|
||||||
|
builders so the URL / content-fn / options logic is not duplicated."
|
||||||
[{:keys [value client-id index]}]
|
[{:keys [value client-id index]}]
|
||||||
(let [account-val (let [av (:account value)]
|
(let [account-val (let [av (:account value)]
|
||||||
(if (map? av) (:db/id av) av))
|
(if (map? av) (:db/id av) av))
|
||||||
|
tr-map (hx/alpine-mount-then-appear
|
||||||
|
{:class "account-row"
|
||||||
|
:id (str "account-row-" index)
|
||||||
|
:x-data (hx/json {:show (boolean (not (:new? value)))
|
||||||
|
:accountId account-val})
|
||||||
|
:data-key "show"
|
||||||
|
:x-ref "p"})
|
||||||
location-attrs {:x-hx-val:account-id "accountId"
|
location-attrs {:x-hx-val:account-id "accountId"
|
||||||
:hx-vals (hx/json (cond-> {:name (account-field-name index :location)}
|
:hx-vals (hx/json (cond-> {:name (account-field-name index :location)}
|
||||||
client-id (assoc :client-id client-id)))
|
client-id (assoc :client-id client-id)))
|
||||||
@@ -140,79 +149,61 @@
|
|||||||
:hx-select (str "#account-location-" index)
|
:hx-select (str "#account-location-" index)
|
||||||
:hx-swap "outerHTML"
|
:hx-swap "outerHTML"
|
||||||
:hx-include "closest form"}]
|
:hx-include "closest form"}]
|
||||||
(sc/data-grid-row
|
{:tr_classes (str (:class tr-map) " border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700")
|
||||||
(-> {:class "account-row"
|
:tr_attrs (sc/attrs->str (dissoc tr-map :class))
|
||||||
:id (str "account-row-" index)
|
:db_id_name (account-field-name index :db/id)
|
||||||
:x-data (hx/json {:show (boolean (not (:new? value)))
|
:db_id_value (:db/id value)
|
||||||
:accountId account-val})
|
:account_field_classes (sc/validated-field-classes {:errors (account-field-errors index :account)})
|
||||||
:data-key "show"
|
:account_error (sc/errors-str (account-field-errors index :account))
|
||||||
:x-ref "p"}
|
:account (account-typeahead-ctx {:value account-val
|
||||||
hx/alpine-mount-then-appear)
|
:client-id client-id
|
||||||
(sc/hidden {:name (account-field-name index :db/id)
|
:name (account-field-name index :account)
|
||||||
:value (:db/id value)})
|
:x-model "accountId"})
|
||||||
(sc/data-grid-cell
|
:location_cell_id (str "account-location-" index)
|
||||||
{}
|
:location_field_classes (sc/validated-field-classes {:errors (account-field-errors index :location)})
|
||||||
(sc/validated-field
|
:location_field_attrs (sc/attrs->str location-attrs)
|
||||||
{:errors (account-field-errors index :account)}
|
:location_error (sc/errors-str (account-field-errors index :location))
|
||||||
(account-typeahead* {:value account-val
|
:location (location-select-ctx {:name (account-field-name index :location)
|
||||||
:client-id client-id
|
:account-location (:account/location (when (nat-int? account-val)
|
||||||
:name (account-field-name index :account)
|
(dc/pull (dc/db conn) '[:account/location] account-val)))
|
||||||
:x-model "accountId"})))
|
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
|
||||||
(sc/data-grid-cell
|
:value (:location value)})
|
||||||
{:id (str "account-location-" index)}
|
:pct_attrs (sc/money-input-attrs {:name (account-field-name index :percentage)
|
||||||
(sc/validated-field
|
:class "w-16"
|
||||||
(merge {:errors (account-field-errors index :location)}
|
:value (some-> (:percentage value) (* 100) long)})
|
||||||
location-attrs)
|
:pct_field_classes (sc/validated-field-classes {:errors (account-field-errors index :percentage)})
|
||||||
(location-select* {:name (account-field-name index :location)
|
:pct_error (sc/errors-str (account-field-errors index :percentage))
|
||||||
:account-location (:account/location (when (nat-int? account-val)
|
:remove_attrs (sc/attrs->str {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed)
|
||||||
(dc/pull (dc/db conn) '[:account/location] account-val)))
|
:hx-vals (hx/json {:op "remove-account" :row-index (or index 0)})
|
||||||
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
|
:hx-target "#bulk-code-form"
|
||||||
:value (:location value)})))
|
:hx-select "#bulk-code-form"
|
||||||
(sc/data-grid-cell
|
:hx-swap "outerHTML"
|
||||||
{}
|
:hx-include "closest form"
|
||||||
(sc/validated-field
|
:href ""})}))
|
||||||
{:errors (account-field-errors index :percentage)}
|
|
||||||
(sc/money-input {:name (account-field-name index :percentage)
|
|
||||||
:class "w-16"
|
|
||||||
:value (some-> (:percentage value) (* 100) long)})))
|
|
||||||
(sc/data-grid-cell
|
|
||||||
{:class "align-top"}
|
|
||||||
(sc/a-icon-button {: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"}
|
|
||||||
(sc/render "templates/components/svg-x.html" {}))))))
|
|
||||||
|
|
||||||
(defn- account-grid* [request]
|
(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."
|
||||||
|
[request]
|
||||||
(let [client-id (single-client-id request)
|
(let [client-id (single-client-id request)
|
||||||
accounts (vec (:accounts (:bulk-state request)))]
|
accounts (vec (:accounts (:bulk-state request)))]
|
||||||
(apply
|
(sel/render->hiccup
|
||||||
sc/data-grid
|
"templates/transaction-bulk-code/account-grid.html"
|
||||||
{:headers [(sc/data-grid-header {} "Account")
|
{:rows (map-indexed
|
||||||
(sc/data-grid-header {:class "w-32"} "Location")
|
(fn [index account]
|
||||||
(sc/data-grid-header {:class "w-16"} "%")
|
(account-row-vm {:value account
|
||||||
(sc/data-grid-header {:class "w-16"})]}
|
:client-id client-id
|
||||||
(concat
|
:index index}))
|
||||||
(map-indexed
|
accounts)
|
||||||
(fn [index account]
|
:new_account_button (sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed)
|
||||||
(transaction-account-row* {:value account
|
:hx-vals (hx/json {:op "new-account"})
|
||||||
:client-id client-id
|
:hx-target "#bulk-code-form"
|
||||||
:index index}))
|
:hx-select "#bulk-code-form"
|
||||||
accounts)
|
:hx-swap "outerHTML"
|
||||||
[(sc/data-grid-row
|
:hx-include "closest form"
|
||||||
{:class "new-row"}
|
:color :secondary}
|
||||||
(sc/data-grid-cell {:colspan 4}
|
"New account")})))
|
||||||
(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]
|
(defn- bulk-code-body* [request]
|
||||||
(let [bulk-state (:bulk-state request)
|
(let [bulk-state (:bulk-state request)
|
||||||
@@ -249,24 +240,18 @@
|
|||||||
["requires-feedback" "Client Review"]]})))
|
["requires-feedback" "Client Review"]]})))
|
||||||
:accounts_field (str (sc/validated-field
|
:accounts_field (str (sc/validated-field
|
||||||
{:errors (ferr :accounts)}
|
{:errors (ferr :accounts)}
|
||||||
(sel/raw (str "<div id=\"account-entries\" class=\"space-y-3\">"
|
(sc/render "templates/transaction-bulk-code/account-entries.html"
|
||||||
(str (account-grid* request))
|
{:grid (str (account-grid* request))})))})))
|
||||||
"</div>"))))})))
|
|
||||||
|
|
||||||
(defn- form-errors-html [errors]
|
(defn- form-errors-html [errors]
|
||||||
(str "<div id=\"form-errors\">"
|
(sc/render "templates/transaction-bulk-code/form-errors.html"
|
||||||
(when (seq errors)
|
{:errors_str (when (seq errors)
|
||||||
(str "<span class=\"error-content\"><p class=\"mt-2 text-xs text-red-600 dark:text-red-500 h-4\">"
|
(str/join ", " (filter string? errors)))}))
|
||||||
(str/join ", " (filter string? errors))
|
|
||||||
"</p></span>"))
|
|
||||||
"</div>"))
|
|
||||||
|
|
||||||
(defn- footer* [request]
|
(defn- footer* [request]
|
||||||
(sel/raw
|
(sc/render "templates/transaction-bulk-code/footer.html"
|
||||||
(str "<div class=\"flex justify-end\"><div class=\"flex items-baseline gap-x-4\">"
|
{:form_errors (form-errors-html (:errors (:form-errors request)))
|
||||||
(form-errors-html (:errors (:form-errors request)))
|
:save_button (sc/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Save")}))
|
||||||
(str (sc/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Save"))
|
|
||||||
"</div></div>")))
|
|
||||||
|
|
||||||
(defn render-form
|
(defn render-form
|
||||||
"Renders the whole plain bulk-code form (no wizard). Binds *errors* from the request's
|
"Renders the whole plain bulk-code form (no wizard). Binds *errors* from the request's
|
||||||
@@ -280,7 +265,7 @@
|
|||||||
(str (sc/hidden {:name (path->name2 :ids i) :value id})))
|
(str (sc/hidden {:name (path->name2 :ids i) :value id})))
|
||||||
ids))
|
ids))
|
||||||
modal-card (sel/render "templates/transaction-edit/edit-modal.html"
|
modal-card (sel/render "templates/transaction-edit/edit-modal.html"
|
||||||
{:head (str "<div class=\"p-2\">Bulk editing " (count ids) " transactions</div>")
|
{:head (sc/render "templates/transaction-bulk-code/head.html" {:count (count ids)})
|
||||||
:side_panel nil
|
:side_panel nil
|
||||||
:body (str (bulk-code-body* request))
|
:body (str (bulk-code-body* request))
|
||||||
:footer (str (footer* request))})]
|
:footer (str (footer* request))})]
|
||||||
@@ -463,7 +448,8 @@
|
|||||||
;; Return success modal
|
;; Return success modal
|
||||||
(html-response
|
(html-response
|
||||||
(com/success-modal {:title "Transactions Coded"}
|
(com/success-modal {:title "Transactions Coded"}
|
||||||
[:p (str "Successfully coded " (count ids) " transactions.")])
|
(sel/render->hiccup "templates/transaction-bulk-code/success-body.html"
|
||||||
|
{:count (count ids)}))
|
||||||
:headers {"hx-trigger" "refreshTable, reset-selection"}))))
|
:headers {"hx-trigger" "refreshTable, reset-selection"}))))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -159,11 +159,10 @@
|
|||||||
clientized (clientize-vendor vendor client-id)]
|
clientized (clientize-vendor vendor client-id)]
|
||||||
(:vendor/default-account clientized))))
|
(:vendor/default-account clientized))))
|
||||||
|
|
||||||
(defn location-select*
|
(defn location-select-ctx
|
||||||
"The location <select> for an account row, rendered from a Selmer template
|
"Plain-data context for templates/components/location-select.html: {:name :classes
|
||||||
(templates/components/location-select.html) -- the first interactive modal component
|
:options [{:value :label :selected}]}. Split out from location-select* so a fully
|
||||||
migrated off Hiccup. Same options/selection/styling as the old com/select, emitted as
|
template-driven account grid can stamp the same <select> from its own row loop."
|
||||||
plain HTML and embedded back into the Hiccup row via the interop bridge."
|
|
||||||
[{:keys [name account-location client-locations value]}]
|
[{:keys [name account-location client-locations value]}]
|
||||||
(let [options (cond account-location
|
(let [options (cond account-location
|
||||||
[[account-location account-location]]
|
[[account-location account-location]]
|
||||||
@@ -177,28 +176,48 @@
|
|||||||
[["Shared" "Shared"]])
|
[["Shared" "Shared"]])
|
||||||
selected (or value (ffirst options))
|
selected (or value (ffirst options))
|
||||||
classes (str/join " " (conj (vec inputs/default-input-classes) "w-full"))]
|
classes (str/join " " (conj (vec inputs/default-input-classes) "w-full"))]
|
||||||
(sel/render->hiccup
|
{:name name
|
||||||
"templates/components/location-select.html"
|
:classes classes
|
||||||
{:name name
|
:options (for [[v label] options]
|
||||||
:classes classes
|
{:value v :label label :selected (= v selected)})}))
|
||||||
:options (for [[v label] options]
|
|
||||||
{:value v :label label :selected (= v selected)})})))
|
(defn location-select*
|
||||||
|
"The location <select> for an account row, rendered from a Selmer template
|
||||||
|
(templates/components/location-select.html) -- the first interactive modal component
|
||||||
|
migrated off Hiccup. Same options/selection/styling as the old com/select, emitted as
|
||||||
|
plain HTML and embedded back into the Hiccup row via the interop bridge."
|
||||||
|
[params]
|
||||||
|
(sel/render->hiccup
|
||||||
|
"templates/components/location-select.html"
|
||||||
|
(location-select-ctx params)))
|
||||||
|
|
||||||
|
(defn- account-typeahead-params
|
||||||
|
"Shared param map for the account typeahead (account-search url + clientized label
|
||||||
|
content-fn). Used by both account-typeahead* (renders) and account-typeahead-ctx
|
||||||
|
(returns the typeahead context for a template-driven grid)."
|
||||||
|
[{:keys [name value client-id x-model]}]
|
||||||
|
{:name name
|
||||||
|
:placeholder "Search..."
|
||||||
|
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||||
|
(cond-> {:purpose "transaction"}
|
||||||
|
client-id (assoc :client-id client-id)))
|
||||||
|
:id name
|
||||||
|
:x-model x-model
|
||||||
|
:value value
|
||||||
|
:content-fn (fn [value]
|
||||||
|
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
||||||
|
client-id)))})
|
||||||
|
|
||||||
|
(defn account-typeahead-ctx
|
||||||
|
"Plain-data typeahead context (sc/typeahead-ctx) for the account cell -- no flex-col
|
||||||
|
wrapper. Lets a template-driven row feed templates/components/typeahead.html via
|
||||||
|
{% with %} without re-deriving the url/content-fn."
|
||||||
|
[params]
|
||||||
|
(sc/typeahead-ctx (account-typeahead-params params)))
|
||||||
|
|
||||||
(defn account-typeahead*
|
(defn account-typeahead*
|
||||||
[{:keys [name value client-id x-model]}]
|
[params]
|
||||||
(wrap-div
|
(wrap-div "flex flex-col" (sc/typeahead (account-typeahead-params params))))
|
||||||
"flex flex-col"
|
|
||||||
(sc/typeahead {:name name
|
|
||||||
:placeholder "Search..."
|
|
||||||
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
|
||||||
(cond-> {:purpose "transaction"}
|
|
||||||
client-id (assoc :client-id client-id)))
|
|
||||||
:id name
|
|
||||||
:x-model x-model
|
|
||||||
:value value
|
|
||||||
:content-fn (fn [value]
|
|
||||||
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
|
||||||
client-id)))})))
|
|
||||||
|
|
||||||
(def ^:dynamic *errors*
|
(def ^:dynamic *errors*
|
||||||
"Humanized form errors for the current render, keyed by edit-form-schema paths (e.g.
|
"Humanized form errors for the current render, keyed by edit-form-schema paths (e.g.
|
||||||
|
|||||||
Reference in New Issue
Block a user