refactor(ssr): compose the bulk-code form entirely in Selmer
Each bulk-code route now ends in a single sel/render call; all composition
(modal chrome, body, account grid, rows, footer, errors) happens in the
templates via {% extends %}/{% block %}/{% include %}/{% with %}, reading one
nested view-model (form-ctx). No HTML is stitched together in Clojure.
- Add components/modal-card.html: a base chrome with head/body/footer blocks;
bulk-code/card.html extends it. (Transaction Edit keeps its string-slot
edit-modal.html for now.)
- New top-level templates: open.html, form.html, card.html, body.html; rework
account-grid/account-row/footer/head to pull the shared component partials in
via {% include %}+{% with %} instead of hardcoding class strings or receiving
pre-rendered HTML strings.
- render-form / open-handler collapse to one sel/render of form.html / open.html.
bulk-code-body*, footer*, form-errors-html, account-grid*, the *errors* dynamic
var and ferr are gone; field errors are read straight from :form-errors.
- Extract sc/{select,button,a-button,a-icon-button}-ctx so templates can include
those partials with computed context (the render wrappers now call the -ctx fns).
Verified: rendered output is DOM-identical to the prior version across empty /
populated / error scenarios (whitespace-normalized token compare), plus browser
QA (open, vendor auto-populate, add/remove row, typeahead, per-row location swap,
percentage validation, submit); no JS errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
15
resources/templates/components/modal-card.html
Normal file
15
resources/templates/components/modal-card.html
Normal file
@@ -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). #}
|
||||
<div class="modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen md:w-[950px] md:h-[650px] w-full h-full last-modal-step transition duration-150"
|
||||
@keydown.enter.prevent.stop="if ($refs.next) {$refs.next.click()}"
|
||||
x-data="">
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0">{% block head %}{% endblock %}</div>
|
||||
<div class="flex shrink overflow-auto grow">
|
||||
{% block side_panel %}{% endblock %}
|
||||
<div class="px-6 py-2 space-y-6 overflow-y-scroll w-full shrink grow">{% block body %}{% endblock %}</div>
|
||||
</div>
|
||||
<div class="p-4 border-t">{% block footer %}{% endblock %}</div>
|
||||
</div>
|
||||
@@ -1,2 +0,0 @@
|
||||
{# Wrapper around the expense-account grid (the body of the accounts validated-field). #}
|
||||
<div id="account-entries" class="space-y-3">{{ grid|safe }}</div>
|
||||
@@ -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). #}
|
||||
<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">
|
||||
@@ -13,9 +13,9 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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 %}
|
||||
<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>
|
||||
<td class="px-4 py-2" colspan="4">{% 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 %}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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. #}
|
||||
<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">
|
||||
@@ -15,9 +13,7 @@
|
||||
</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>
|
||||
{% with name=row.location.name classes=row.location.classes options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %}
|
||||
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.location_error }}</p>
|
||||
</div>
|
||||
</td>
|
||||
@@ -27,9 +23,5 @@
|
||||
<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>
|
||||
<td class="px-4 py-2 align-top"><a class="{{ row.remove.classes }}"{{ row.remove.attrs|safe }}><div class="h-4 w-4">{% include "templates/components/svg-x.html" %}</div></a></td>
|
||||
</tr>
|
||||
|
||||
29
resources/templates/transaction-bulk-code/body.html
Normal file
29
resources/templates/transaction-bulk-code/body.html
Normal file
@@ -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 <p>) and pulls its control in from the shared
|
||||
component partials; all data comes from the form view-model. #}
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div {{ vendor.changed_attrs|safe }}>
|
||||
<div class="{{ vendor.field_classes }}">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Vendor</label>
|
||||
{% 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 %}
|
||||
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ vendor.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="{{ status.field_classes }}">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
|
||||
{% with name=status.name classes=status.classes attrs=status.attrs options=status.options %}{% include "templates/components/select.html" %}{% endwith %}
|
||||
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ status.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2 pt-4">
|
||||
<h3 class="text-lg font-medium mb-3">Expense Accounts</h3>
|
||||
<div class="{{ accounts.field_classes }}">
|
||||
<div id="account-entries" class="space-y-3">{% include "templates/transaction-bulk-code/account-grid.html" %}</div>
|
||||
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ accounts.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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. #}
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div {{ vendor_changed_attrs|safe }}>{{ vendor_field|safe }}
|
||||
</div>
|
||||
<div>{{ status_field|safe }}</div>
|
||||
<div class="col-span-2 pt-4">
|
||||
<h3 class="text-lg font-medium mb-3">Expense Accounts</h3>
|
||||
{{ accounts_field|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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. #}
|
||||
<form id="bulk-code-form"{{ form_attrs|safe }}>{{ ids_hidden|safe }}{{ modal|safe }}
|
||||
</form>
|
||||
6
resources/templates/transaction-bulk-code/card.html
Normal file
6
resources/templates/transaction-bulk-code/card.html
Normal file
@@ -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 %}
|
||||
@@ -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. #}
|
||||
<div class="flex justify-end">
|
||||
<div class="flex items-baseline gap-x-4">{{ form_errors|safe }}{{ save_button|safe }}</div>
|
||||
<div class="flex items-baseline gap-x-4">{% 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 %}</div>
|
||||
</div>
|
||||
|
||||
5
resources/templates/transaction-bulk-code/form.html
Normal file
5
resources/templates/transaction-bulk-code/form.html
Normal file
@@ -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. #}
|
||||
<form id="bulk-code-form"{{ form_attrs|safe }}>{% for id in ids %}<input type="hidden" name="{{ id.name }}" value="{{ id.value }}">{% endfor %}<div class="" @click.outside="open=false" id="bulkcodemodal">{% include "templates/transaction-bulk-code/card.html" %}</div></form>
|
||||
@@ -1,2 +1,2 @@
|
||||
{# Modal header label: how many transactions this bulk-code operation will touch. #}
|
||||
<div class="p-2">Bulk editing {{ count }} transactions</div>
|
||||
<div class="p-2">Bulk editing {{ head_count }} transactions</div>
|
||||
|
||||
3
resources/templates/transaction-bulk-code/open.html
Normal file
3
resources/templates/transaction-bulk-code/open.html
Normal file
@@ -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. #}
|
||||
<div id="transitioner" class="flex-1">{% include "templates/transaction-bulk-code/form.html" %}</div>
|
||||
@@ -86,23 +86,27 @@
|
||||
(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,
|
||||
generalized). options = [[value label] ...]; `value` (string or keyword) marks the
|
||||
selected option. Class defaults to the standard input classes, like com/select. Extra
|
||||
attrs (hx-*, x-*) ride through onto the element."
|
||||
(defn select-ctx
|
||||
"Plain-data context for templates/components/select.html. options = [[value label] ...];
|
||||
`value` (string or keyword) marks the selected option. Split out so a template can
|
||||
{% include %} the partial via {% with %} without re-deriving classes/selection."
|
||||
[{:keys [name value options class] :as params}]
|
||||
(let [classes (-> ""
|
||||
(hh/add-class inputs/default-input-classes)
|
||||
(hh/add-class (or class "")))
|
||||
sel (cond-> value (keyword? value) clojure.core/name)
|
||||
attrs (dissoc params :name :value :options :class)]
|
||||
(render "templates/components/select.html"
|
||||
{:name name
|
||||
:classes classes
|
||||
:attrs (attrs->str attrs)
|
||||
:options (for [[v label] options]
|
||||
{:value v :label label :selected (= (str v) (str sel))})})))
|
||||
{:name name
|
||||
:classes classes
|
||||
:attrs (attrs->str attrs)
|
||||
:options (for [[v label] options]
|
||||
{:value v :label label :selected (= (str v) (str sel))})}))
|
||||
|
||||
(defn select
|
||||
"Generic <select> 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)
|
||||
|
||||
@@ -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 <select> contexts come from the shared edit/* ctx
|
||||
builders so the URL / content-fn / options logic is not duplicated."
|
||||
[{:keys [value client-id index]}]
|
||||
account-row.html). Attribute maps are pre-serialized to HTML attribute strings; the
|
||||
account typeahead, location <select>, 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."
|
||||
|
||||
Reference in New Issue
Block a user