refactor(ssr): collapse 5 manual-coding operation routes into edit-form-changed (heuristic 6)
The 5 manual-coding operations (vendor change, simple/advanced toggle, add row, remove
row, $/% toggle) each had their own route + handler, all doing "mutate form state ->
render-full-form". Fold them into the single edit-form-changed endpoint, which now
dispatches on an `op` form-param to the relevant pure apply-* mutation fn (apply-vendor-
changed / apply-toggle-mode / apply-new-account / apply-remove-account /
apply-toggle-amount-mode) then re-renders. A missing/unknown op (a plain dependent-field
change, e.g. account->location or amount->totals) just re-renders, as before.
- edit.clj: 6 handlers -> 1 dispatcher + 5 pure apply-* fns; markup posts to
edit-form-changed with :hx-vals {:op "..."}.
- routes/transactions.cljc: remove the 5 now-unused route keys.
- e2e specs: retarget the vendor selector by op (div[hx-vals*="vendor-changed"]) and
point the toggle-amount-mode / vendor response waits at edit-form-changed, since the
old per-op route names are gone. (Behavioral assertions unchanged.)
Scorecard: manual-coding routes ~10 -> ~5 (operations now one dispatcher). Parity held:
swap spec 6/6, full suite 32 pass (Shared Location green; no new regression).
This commit is contained in:
@@ -39,7 +39,7 @@ Each migration appends one row (after-numbers), referencing the before in the di
|
||||
| Phase | Modal | LOC | Routes | no-cursor twins | faked roots | snapshot merges | OOB | mixed hx- | cookbook reused / added |
|
||||
|-------|-------|-----|--------|-----------------|-------------|-----------------|-----|-----------|-------------------------|
|
||||
| 1 (baseline) | Transaction Edit `transaction/edit.clj` | 1608 | ~12 | 1 | 2 | ~75 | 0 | 8 | — / seeded 7 entries |
|
||||
| 2 (in progress) | Transaction Edit `transaction/edit.clj` | ~1558 | **~10** | **0** | **0** | ~75 | 0 | 8 | — / 0 |
|
||||
| 2 (in progress) | Transaction Edit `transaction/edit.clj` | ~1530 | **~5** | **0** | **0** | ~75 | 0 | 8 | — / 0 |
|
||||
|
||||
> **Phase 2 progress (partial).** Achieved with parity held (swap spec 6/6 + Shared
|
||||
> Location green, full suite 31 pass / no regression): deleted the dead `*-no-cursor*`
|
||||
|
||||
@@ -48,7 +48,7 @@ async function openManualAdvanced(page: any, transactionIndex = 0) {
|
||||
// (Solr is unavailable in tests), click it, and wait for the whole-form swap.
|
||||
async function selectVendor(page: any, vendorId: number, label: string) {
|
||||
const vendor = page
|
||||
.locator('div[hx-post*="edit-vendor-changed"]')
|
||||
.locator('div[hx-vals*="vendor-changed"]')
|
||||
.first()
|
||||
.locator('div.relative[x-data]')
|
||||
.first();
|
||||
@@ -62,7 +62,7 @@ async function selectVendor(page: any, vendorId: number, label: string) {
|
||||
|
||||
const swap = page.waitForResponse(
|
||||
(r: any) =>
|
||||
r.url().includes('edit-vendor-changed') &&
|
||||
r.url().includes('edit-form-changed') &&
|
||||
r.request().method() === 'POST' &&
|
||||
r.status() === 200
|
||||
);
|
||||
@@ -303,7 +303,7 @@ test.describe('Transaction Edit whole-form swap', () => {
|
||||
await page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(0).click();
|
||||
await page.waitForSelector('#wizardmodal');
|
||||
await page.click('button:has-text("Manual")');
|
||||
await page.waitForSelector('div[hx-post*="edit-vendor-changed"]');
|
||||
await page.waitForSelector('div[hx-vals*="vendor-changed"]');
|
||||
|
||||
const testInfo = await (await page.request.get('/test-info')).json();
|
||||
const vendorId: number = testInfo.accounts.vendor;
|
||||
@@ -311,7 +311,7 @@ test.describe('Transaction Edit whole-form swap', () => {
|
||||
|
||||
// Drive the vendor typeahead like a user: open dropdown, inject a result
|
||||
// (Solr is unavailable in tests), click it.
|
||||
const vendor = page.locator('div[hx-post*="edit-vendor-changed"]').first().locator('div.relative[x-data]').first();
|
||||
const vendor = page.locator('div[hx-vals*="vendor-changed"]').first().locator('div.relative[x-data]').first();
|
||||
await vendor.locator('a[x-ref="input"]').click();
|
||||
const search = page.locator('[data-tippy-root] input[x-model="search"]').first();
|
||||
await search.waitFor({ state: 'visible' });
|
||||
@@ -322,7 +322,7 @@ test.describe('Transaction Edit whole-form swap', () => {
|
||||
|
||||
const swap = page.waitForResponse(
|
||||
(r: any) =>
|
||||
r.url().includes('edit-vendor-changed') &&
|
||||
r.url().includes('edit-form-changed') &&
|
||||
r.request().method() === 'POST' &&
|
||||
r.status() === 200
|
||||
);
|
||||
@@ -352,7 +352,7 @@ test.describe('Transaction Edit whole-form swap', () => {
|
||||
await page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(0).click();
|
||||
await page.waitForSelector('#wizardmodal');
|
||||
await page.click('button:has-text("Manual")');
|
||||
await page.waitForSelector('div[hx-post*="edit-vendor-changed"]');
|
||||
await page.waitForSelector('div[hx-vals*="vendor-changed"]');
|
||||
|
||||
const testInfo = await (await page.request.get('/test-info')).json();
|
||||
const vendor1: number = testInfo.accounts.vendor;
|
||||
@@ -361,7 +361,7 @@ test.describe('Transaction Edit whole-form swap', () => {
|
||||
const account2: number = testInfo.accounts['second-account'];
|
||||
|
||||
const vendorLabel = page
|
||||
.locator('div[hx-post*="edit-vendor-changed"] span[x-text="value.label"]')
|
||||
.locator('div[hx-vals*="vendor-changed"] span[x-text="value.label"]')
|
||||
.first();
|
||||
const accountHidden = page
|
||||
.locator('input[type="hidden"][name*="transaction-account/account"]')
|
||||
|
||||
@@ -150,7 +150,7 @@ async function toggleToPercentMode(page: any) {
|
||||
|
||||
// Wait for HTMX to swap the grid body
|
||||
await page.waitForResponse(response =>
|
||||
response.url().includes('/toggle-amount-mode') && response.status() === 200
|
||||
response.url().includes('/edit-form-changed') && response.status() === 200
|
||||
);
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
@@ -161,7 +161,7 @@ async function toggleToDollarMode(page: any) {
|
||||
|
||||
// Wait for HTMX to swap the grid body
|
||||
await page.waitForResponse(response =>
|
||||
response.url().includes('/toggle-amount-mode') && response.status() === 200
|
||||
response.url().includes('/edit-form-changed') && response.status() === 200
|
||||
);
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
@@ -359,7 +359,7 @@ async function selectVendorFromTypeahead(page: any, vendorName: string) {
|
||||
throw new Error(`Could not find vendor with name ${vendorName}`);
|
||||
}
|
||||
|
||||
const vendorContainer = page.locator('div[hx-post*="edit-vendor-changed"]').first();
|
||||
const vendorContainer = page.locator('div[hx-vals*="vendor-changed"]').first();
|
||||
const vendorHidden = vendorContainer.locator('input[type="hidden"]').first();
|
||||
|
||||
await vendorHidden.evaluate((el: HTMLInputElement, value: string) => {
|
||||
@@ -374,7 +374,7 @@ async function selectVendorFromTypeahead(page: any, vendorName: string) {
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
|
||||
await page.waitForResponse(response => response.url().includes('/edit-vendor-changed') && response.status() === 200);
|
||||
await page.waitForResponse(response => response.url().includes('/edit-form-changed') && response.status() === 200);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
@@ -434,11 +434,11 @@ test.describe('Transaction Edit Vendor Pre-population', () => {
|
||||
// `elements` instead of being fetched. Everything else -- the dropdown's own
|
||||
// search input firing a native `change` on blur, the `value = element` click
|
||||
// handler, the Alpine reactivity, and the HTMX round-trip to
|
||||
// `edit-vendor-changed` -- runs exactly as in production. This is the flow that
|
||||
// `edit-form-changed` (op=vendor-changed) -- runs exactly as in production. This is the flow that
|
||||
// regressed: a stale native `change` from the search input used to win the race
|
||||
// and revert the vendor to its previous value.
|
||||
async function selectVendorViaDropdown(page: any, vendorId: number, vendorName: string) {
|
||||
const wrapper = page.locator('div[hx-post*="edit-vendor-changed"]').first();
|
||||
const wrapper = page.locator('div[hx-vals*="vendor-changed"]').first();
|
||||
const typeahead = wrapper.locator('div.relative[x-data]').first();
|
||||
|
||||
// Open the dropdown (tippy renders the popper into [data-tippy-root]).
|
||||
@@ -466,7 +466,7 @@ async function selectVendorViaDropdown(page: any, vendorId: number, vendorName:
|
||||
|
||||
await page.waitForResponse(
|
||||
(response: any) =>
|
||||
response.url().includes('/edit-vendor-changed') && response.status() === 200
|
||||
response.url().includes('/edit-form-changed') && response.status() === 200
|
||||
);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
@@ -485,7 +485,7 @@ async function openManualVendorSection(page: any, transactionIndex: number) {
|
||||
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
|
||||
await page.waitForSelector('#wizardmodal');
|
||||
await page.click('button:has-text("Manual")');
|
||||
await page.waitForSelector('div[hx-post*="edit-vendor-changed"]');
|
||||
await page.waitForSelector('div[hx-vals*="vendor-changed"]');
|
||||
}
|
||||
|
||||
test.describe('Transaction Edit Vendor Selection', () => {
|
||||
@@ -501,14 +501,14 @@ test.describe('Transaction Edit Vendor Selection', () => {
|
||||
// round-trip. Before the fix this reverted to blank because a stale
|
||||
// `change` event submitted the previous vendor and its response won.
|
||||
const label = page
|
||||
.locator('div[hx-post*="edit-vendor-changed"] span[x-text="value.label"]')
|
||||
.locator('div[hx-vals*="vendor-changed"] span[x-text="value.label"]')
|
||||
.first();
|
||||
await expect(label).toHaveText('Test Vendor');
|
||||
|
||||
// The server-rendered hidden input must carry the newly selected vendor id.
|
||||
const hidden = page
|
||||
.locator(
|
||||
'div[hx-post*="edit-vendor-changed"] input[type="hidden"][name="step-params[transaction/vendor]"]'
|
||||
'div[hx-vals*="vendor-changed"] input[type="hidden"][name="step-params[transaction/vendor]"]'
|
||||
)
|
||||
.first();
|
||||
await expect(hidden).toHaveValue(vendorId.toString());
|
||||
|
||||
@@ -263,7 +263,8 @@
|
||||
:value total})]]
|
||||
[:div.mt-1
|
||||
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
|
||||
:hx-vals (hx/json {:op "toggle-mode"})
|
||||
:hx-include "closest form"
|
||||
:hx-target "#wizard-form"
|
||||
:hx-select "#wizard-form"
|
||||
@@ -344,8 +345,8 @@
|
||||
(com/text-input (assoc amount-attrs :type "number" :step "0.01"))
|
||||
(com/money-input amount-attrs))))))
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(com/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-remove-account)
|
||||
:hx-vals (hx/json {:row-index (or index 0)})
|
||||
(com/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
|
||||
:hx-vals (hx/json {:op "remove-account" :row-index (or index 0)})
|
||||
:hx-target "#wizard-form"
|
||||
:hx-select "#wizard-form"
|
||||
:hx-swap "outerHTML"
|
||||
@@ -428,7 +429,8 @@
|
||||
:value amount-mode
|
||||
:name "step-params[amount-mode]"
|
||||
:orientation :horizontal
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/toggle-amount-mode)
|
||||
:hx-vals (hx/json {:op "toggle-amount-mode"})
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
|
||||
:hx-target "#wizard-form"
|
||||
:hx-select "#wizard-form"
|
||||
:hx-swap "outerHTML"
|
||||
@@ -468,7 +470,8 @@
|
||||
|
||||
(com/data-grid-row {:class "new-row"}
|
||||
(com/data-grid-cell {:colspan 4}
|
||||
(com/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-new-account)
|
||||
(com/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
|
||||
:hx-vals (hx/json {:op "new-account"})
|
||||
:hx-target "#wizard-form"
|
||||
:hx-select "#wizard-form"
|
||||
:hx-swap "outerHTML"
|
||||
@@ -490,7 +493,8 @@
|
||||
[:div#manual-coding-section
|
||||
(com/hidden {:name "step-params[mode]" :value (name mode)})
|
||||
[:div {:hx-trigger "change"
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
|
||||
:hx-vals (hx/json {:op "vendor-changed"})
|
||||
:hx-target "#wizard-form"
|
||||
:hx-select "#wizard-form"
|
||||
:hx-swap "outerHTML"
|
||||
@@ -516,7 +520,8 @@
|
||||
(when (<= row-count 1)
|
||||
[:div.mb-2
|
||||
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed)
|
||||
:hx-vals (hx/json {:op "toggle-mode"})
|
||||
:hx-include "closest form"
|
||||
:hx-target "#wizard-form"
|
||||
:hx-select "#wizard-form"
|
||||
@@ -528,17 +533,17 @@
|
||||
[:div#account-grid-body
|
||||
(account-grid-body* request)]))])]))
|
||||
|
||||
(defn toggle-amount-mode [request]
|
||||
(defn apply-toggle-amount-mode
|
||||
"edit-form-changed op: convert account amounts between $ and % and record the new mode."
|
||||
[request]
|
||||
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||
old-mode (or (:amount-mode snapshot) "$")
|
||||
new-mode (or (get-in request [:multi-form-state :step-params :amount-mode]) "$")
|
||||
total (Math/abs (or (:transaction/amount snapshot) 0.0))
|
||||
accounts (convert-accounts-mode (:transaction/accounts snapshot) old-mode new-mode total)
|
||||
updated-request (-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts] accounts)
|
||||
(assoc-in [:multi-form-state :snapshot :amount-mode] new-mode))]
|
||||
(html-response
|
||||
(render-full-form updated-request))))
|
||||
accounts (convert-accounts-mode (:transaction/accounts snapshot) old-mode new-mode total)]
|
||||
(-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts] accounts)
|
||||
(assoc-in [:multi-form-state :snapshot :amount-mode] new-mode))))
|
||||
|
||||
(defn transaction-details-panel [tx]
|
||||
[:div.p-4.space-y-4
|
||||
@@ -1372,14 +1377,7 @@
|
||||
[request]
|
||||
(mm/render-wizard edit-wizard request))
|
||||
|
||||
(defn edit-form-changed-handler
|
||||
"Generic handler that re-renders the whole form. Used when any field changes
|
||||
and we need the server to re-compute dependent fields."
|
||||
[request]
|
||||
(html-response
|
||||
(render-full-form request)))
|
||||
|
||||
(defn edit-vendor-changed-handler [request]
|
||||
(defn apply-vendor-changed [request]
|
||||
(let [multi-form-state (:multi-form-state request)
|
||||
snapshot (:snapshot multi-form-state)
|
||||
step-params (:step-params multi-form-state)
|
||||
@@ -1421,10 +1419,9 @@
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts] [new-account])))
|
||||
request)
|
||||
(assoc-in [:multi-form-state :step-params :transaction/vendor] vendor-id))]
|
||||
(html-response
|
||||
(render-full-form render-request))))
|
||||
render-request))
|
||||
|
||||
(defn edit-wizard-toggle-mode-handler [request]
|
||||
(defn apply-toggle-mode [request]
|
||||
(let [step-params (-> request :multi-form-state :step-params)
|
||||
snapshot (-> request :multi-form-state :snapshot)
|
||||
current-mode (keyword (or (:mode step-params) "simple"))
|
||||
@@ -1453,11 +1450,10 @@
|
||||
(if first-row [first-row] []))
|
||||
(assoc-in [:multi-form-state :step-params :mode]
|
||||
(name target-mode)))))]
|
||||
(html-response
|
||||
(render-full-form render-request))))
|
||||
render-request))
|
||||
|
||||
(defn edit-wizard-new-account-handler
|
||||
"Adds a new account row and re-renders the whole form."
|
||||
(defn apply-new-account
|
||||
"edit-form-changed op: append a fresh account row."
|
||||
[request]
|
||||
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||
amount-mode (or (:amount-mode snapshot) "$")
|
||||
@@ -1471,12 +1467,10 @@
|
||||
updated-request (-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts] updated-accounts)
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts] updated-accounts))]
|
||||
(html-response
|
||||
(render-full-form updated-request))))
|
||||
updated-request))
|
||||
|
||||
(defn edit-wizard-remove-account-handler
|
||||
"Removes an account row and re-renders the whole form.
|
||||
Expects a row-index in the form params."
|
||||
(defn apply-remove-account
|
||||
"edit-form-changed op: remove the account row at form-param row-index."
|
||||
[request]
|
||||
(let [row-index (some-> request :form-params (get "row-index") Integer/parseInt)
|
||||
snapshot (-> request :multi-form-state :snapshot)
|
||||
@@ -1488,8 +1482,24 @@
|
||||
updated-request (-> request
|
||||
(assoc-in [:multi-form-state :snapshot :transaction/accounts] updated-accounts)
|
||||
(assoc-in [:multi-form-state :step-params :transaction/accounts] updated-accounts))]
|
||||
updated-request))
|
||||
|
||||
(defn edit-form-changed-handler
|
||||
"Single whole-form re-render endpoint. Dispatches on the `op` form-param to apply the
|
||||
relevant state mutation (vendor change, mode toggle, add/remove row, $/% toggle), then
|
||||
re-renders the whole form. A missing/unknown op (a plain dependent-field change) just
|
||||
re-renders. Replaces the per-operation edit-wizard-* / toggle-amount-mode routes."
|
||||
[request]
|
||||
(let [op (get-in request [:form-params "op"])
|
||||
request' (case op
|
||||
"vendor-changed" (apply-vendor-changed request)
|
||||
"toggle-mode" (apply-toggle-mode request)
|
||||
"new-account" (apply-new-account request)
|
||||
"remove-account" (apply-remove-account request)
|
||||
"toggle-amount-mode" (apply-toggle-amount-mode request)
|
||||
request)]
|
||||
(html-response
|
||||
(render-full-form updated-request))))
|
||||
(render-full-form request'))))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
@@ -1509,10 +1519,6 @@
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-vendor-changed (-> edit-vendor-changed-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/location-select (-> location-select
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:name :string]
|
||||
@@ -1524,23 +1530,6 @@
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/toggle-amount-mode (-> toggle-amount-mode
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-wizard-toggle-mode (-> edit-wizard-toggle-mode-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-wizard-new-account (-> edit-wizard-new-account-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-wizard-remove-account (-> edit-wizard-remove-account-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
|
||||
::route/unlink-payment (-> unlink-payment
|
||||
(wrap-must {:activity :edit :subject :transaction} (fn get-client [request] (-> request :entity :transaction/client)))
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
|
||||
@@ -28,13 +28,8 @@
|
||||
|
||||
["/" [#"\d+" :db/id]] {"/edit" {:get ::edit-wizard}}
|
||||
"/edit-submit" ::edit-submit
|
||||
"/edit-vendor-changed" ::edit-vendor-changed
|
||||
"/location-select" ::location-select
|
||||
"/toggle-amount-mode" ::toggle-amount-mode
|
||||
"/edit-form-changed" ::edit-form-changed
|
||||
"/edit-wizard-new-account" ::edit-wizard-new-account
|
||||
"/edit-wizard-remove-account" ::edit-wizard-remove-account
|
||||
"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode
|
||||
"/match-payment" ::link-payment
|
||||
"/match-autopay-invoices" ::link-autopay-invoices
|
||||
"/match-unpaid-invoices" ::link-unpaid-invoices
|
||||
|
||||
Reference in New Issue
Block a user