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>
|
||||
Reference in New Issue
Block a user