24 KiB
Transaction Edit Modal: Simple / Advanced Mode Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the always-visible account split table in the transaction edit modal with a simple mode (single account + location fields) that is the default for uncoded or single-account transactions, with a toggle to the full advanced split table.
Architecture: HTMX-driven server-side swap. A new edit-wizard-toggle-mode GET endpoint re-renders the manual coding section in the requested mode. Mode is carried via a hidden <input name="mode"> field included in all HTMX requests. edit-vendor-changed is updated to branch on mode. The LinksStep render function selects initial mode based on account row count.
Tech Stack: Clojure/Hiccup server-side rendering, HTMX, Alpine.js, Bidi routing, Datomic
Spec: docs/superpowers/specs/2026-05-27-transaction-edit-simple-advanced-mode-design.md
File Map
| File | What changes |
|---|---|
src/cljc/auto_ap/routes/transactions.cljc |
Add ::edit-wizard-toggle-mode route |
src/clj/auto_ap/ssr/transaction/edit.clj |
Add simple-mode-fields*, manual-coding-section*, edit-wizard-toggle-mode-handler; update LinksStep render; update edit-vendor-changed-handler; register new route handler |
Task 1: Add the toggle-mode route
Files:
-
Modify:
src/cljc/auto_ap/routes/transactions.cljc -
Step 1: Add the route entry
Open src/cljc/auto_ap/routes/transactions.cljc. After the "/edit-wizard-new-account" line, add:
"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode
The file should look like:
"/edit-wizard-new-account" ::edit-wizard-new-account
"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode
"/match-payment" ::link-payment
- Step 2: Verify the file compiles
clj-nrepl-eval -p 9000 "(require '[auto-ap.routes.transactions] :reload)"
Expected: no errors.
- Step 3: Commit
git add src/cljc/auto_ap/routes/transactions.cljc
git commit -m "feat: add edit-wizard-toggle-mode route"
Task 2: Add simple-mode-fields* — the simple-mode account/location UI
Files:
- Modify:
src/clj/auto_ap/ssr/transaction/edit.clj
This function renders the account typeahead + location select + toggle link for simple mode. It goes near the existing account-typeahead* and location-select* helpers (around line 180).
- Step 1: Add
simple-mode-fields*afteraccount-typeahead*(around line 180)
(defn simple-mode-fields*
"Renders the simple-mode account + location row and the toggle link.
request must have :multi-form-state and :entity bound."
[request]
(let [snapshot (-> request :multi-form-state :snapshot)
client-id (or (-> request :entity :transaction/client :db/id)
(:transaction/client snapshot))
existing-row (first (:transaction/accounts snapshot))
account-val (:transaction-account/account existing-row)
location-val (or (:transaction-account/location existing-row) "Shared")
account-id (when (nat-int? account-val)
(dc/pull (dc/db conn) '[:account/location] account-val))
row-id (or (:db/id existing-row) (str (java.util.UUID/randomUUID)))]
[:div
;; hidden inputs to encode the single row as transaction/accounts[0]
(fc/with-field :transaction/accounts
(fc/with-cursor-index 0
[:span
(fc/with-field :db/id
(com/hidden {:name (fc/field-name) :value row-id}))
[:div.flex.gap-2.mt-2
(fc/with-field :transaction-account/account
(com/validated-field
{:label "Account" :errors (fc/field-errors)}
[:div.w-72
(account-typeahead* {:value account-val
:client-id client-id
:name (fc/field-name)
:x-model "simpleAccountId"})]))
(fc/with-field :transaction-account/location
(com/validated-field
{:label "Location"
:errors (fc/field-errors)
:x-hx-val:account-id "simpleAccountId"
:hx-vals (hx/json (cond-> {:name (fc/field-name)}
client-id (assoc :client-id client-id)))
:x-dispatch:changed "simpleAccountId"
:hx-trigger "changed"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
:hx-target "find *"
:hx-swap "outerHTML"}
(location-select*
{:name (fc/field-name)
:account-location (:account/location account-id)
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
:value location-val})))
;; hidden amount — full transaction total
(fc/with-field :transaction-account/amount
(let [total (Math/abs (or (-> request :entity :transaction/amount)
(:transaction/amount snapshot)
0.0))]
(com/hidden {:name (fc/field-name) :value total})))]))
;; toggle link
[:div.mt-1
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
:hx-include "closest form"
:hx-target "#manual-coding-section"
:hx-swap "outerHTML"}
"Switch to advanced mode"]]]))
- Step 2: Verify the file has no parse errors
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
Expected: no errors.
- Step 3: Commit
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "feat: add simple-mode-fields* for transaction edit modal"
Task 3: Extract manual-coding-section* and update LinksStep
Files:
- Modify:
src/clj/auto_ap/ssr/transaction/edit.clj
Currently the manual coding block (vendor field + account grid) is inlined inside LinksStep/render-step. Extract it into manual-coding-section* which selects mode and renders accordingly. This also adds the mode hidden input and wraps the section in #manual-coding-section.
- Step 1: Add
manual-mode-initialhelper (determines initial mode from snapshot)
Add this function after simple-mode-fields*:
(defn- manual-mode-initial
"Returns :simple or :advanced based on existing account row count."
[snapshot]
(let [rows (seq (:transaction/accounts snapshot))]
(if (and rows (> (count rows) 1))
:advanced
:simple)))
- Step 2: Add
manual-coding-section*
Add after manual-mode-initial:
(defn manual-coding-section*
"Renders the vendor field + account/location section for the manual tab.
mode is :simple or :advanced."
[mode request]
(let [snapshot (-> request :multi-form-state :snapshot)
row-count (count (:transaction/accounts snapshot))]
[:div#manual-coding-section
;; hidden mode input — carried by all hx-include=\"closest form\" calls
(com/hidden {:name "mode" :value (name mode)})
;; vendor field
[:div {:hx-trigger "change"
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
:hx-target "#manual-coding-section"
:hx-swap "outerHTML"
:hx-include "closest form"}
(fc/with-field :transaction/vendor
(com/validated-field
{:label "Vendor" :errors (fc/field-errors)}
[:div.w-96
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))]
;; account/location section
(if (= mode :simple)
[:div {:x-data (hx/json {:simpleAccountId
(-> snapshot :transaction/accounts first
:transaction-account/account)})}
(fc/start-form (:multi-form-state request) nil
(fc/with-field :step-params
(simple-mode-fields* request)))]
;; advanced mode
[:div
(when (<= row-count 1)
[:div.mb-2
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
:hx-include "closest form"
:hx-target "#manual-coding-section"
:hx-swap "outerHTML"}
"Switch to simple mode"]])
(fc/start-form (:multi-form-state request) nil
(fc/with-field :step-params
(fc/with-field :transaction/accounts
[:div#account-grid-body
(account-grid-body* request)])))])]))
- Step 3: Update
LinksStep/render-stepto usemanual-coding-section*
In LinksStep/render-step (around line 826), replace the entire [:div {} block inside [:div {:x-show "activeForm === 'manual'" ...}] (which currently contains the vendor typeahead + approval status + account-grid-body*) with:
[:div {:x-show "activeForm === 'manual'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
[:div {}
(manual-coding-section* (manual-mode-initial snapshot) request)
;; Approval status field
(fc/with-field :transaction/approval-status
(com/validated-field
{:label "Status"
:errors (fc/field-errors)}
(let [current-value (name (or (fc/field-value) :transaction-approval-status/unapproved))]
[:div {:x-data (hx/json {:approvalStatus current-value})}
(com/hidden {:name (fc/field-name)
:value current-value
":value" "approvalStatus"})
[:div {:class "inline-flex rounded-md shadow-sm", :role "group"}
(com/button-group-button {"@click" "approvalStatus = 'approved'"
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'approved' }"
:class "rounded-l-lg"}
"Approved")
(com/button-group-button {"@click" "approvalStatus = 'unapproved'"
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'unapproved' }"
:class "rounded-r-lg"}
"Unapproved")
(com/button-group-button {"@click" "approvalStatus = 'suppressed'"
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }"
:class "rounded-r-lg"}
"Client Review")]]]))]]]
Also remove the now-redundant (fc/with-field :transaction/accounts ...) wrapper that previously wrapped account-grid-body* (it is now handled inside manual-coding-section*).
- Step 4: Verify the file compiles
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
Expected: no errors.
- Step 5: Commit
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "feat: extract manual-coding-section* with simple/advanced mode selection"
Task 4: Add edit-wizard-toggle-mode-handler
Files:
- Modify:
src/clj/auto_ap/ssr/transaction/edit.clj
This handler re-renders #manual-coding-section in the opposite mode. It reads mode from the form params (via step-params in the decoded multi-form-state) and flips it.
- Step 1: Add the handler function (add after
edit-vendor-changed-handler):
(defn edit-wizard-toggle-mode-handler [request]
(let [step-params (-> request :multi-form-state :step-params)
current-mode (keyword (or (:mode step-params) "simple"))
target-mode (if (= current-mode :simple) :advanced :simple)
snapshot (-> request :multi-form-state :snapshot)
;; When switching simple→advanced, promote simple-mode values into accounts
render-request
(if (and (= target-mode :advanced)
(= current-mode :simple))
;; carry the simple-mode single row into snapshot so the table shows it
(let [accounts (or (seq (:transaction/accounts step-params))
(seq (:transaction/accounts snapshot)))]
(-> request
(assoc-in [:multi-form-state :snapshot :transaction/accounts]
(vec accounts))
(assoc-in [:multi-form-state :step-params :transaction/accounts]
(vec accounts))))
;; advanced→simple: take first row only
(let [first-row (first (or (seq (:transaction/accounts step-params))
(seq (:transaction/accounts snapshot))))]
(-> request
(assoc-in [:multi-form-state :snapshot :transaction/accounts]
(if first-row [first-row] []))
(assoc-in [:multi-form-state :step-params :transaction/accounts]
(if first-row [first-row] [])))))]
(html-response
(fc/start-form (:multi-form-state render-request) nil
(fc/with-field :step-params
(manual-coding-section* target-mode render-request))))))
- Step 2: Register the handler in
key->handler
In the key->handler map (around line 1357), add after the ::route/edit-wizard-new-account entry:
::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))
- Step 3: Verify the file compiles
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
Expected: no errors.
- Step 4: Commit
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "feat: add edit-wizard-toggle-mode-handler"
Task 5: Update edit-vendor-changed-handler to support both modes
Files:
- Modify:
src/clj/auto_ap/ssr/transaction/edit.clj
Currently this handler always returns [:div#account-grid-body ...]. It must now return #manual-coding-section in the correct mode.
- Step 1: Replace
edit-vendor-changed-handler
Replace the entire edit-vendor-changed-handler function body with:
(defn edit-vendor-changed-handler [request]
(let [multi-form-state (:multi-form-state request)
snapshot (:snapshot multi-form-state)
step-params (:step-params multi-form-state)
mode (keyword (or (:mode step-params) "simple"))
client-id (or (:transaction/client snapshot)
(-> request :entity :transaction/client :db/id))
vendor-id (or (:transaction/vendor step-params)
(:transaction/vendor snapshot))
total (Math/abs (or (-> request :entity :transaction/amount)
(:transaction/amount snapshot)
0.0))
amount-mode (or (:amount-mode snapshot) "$")
existing-accounts (or (seq (:transaction/accounts step-params))
(seq (:transaction/accounts snapshot)))
default-account (when (and (empty? existing-accounts) vendor-id client-id)
(vendor-default-account vendor-id client-id))
render-request
(if (and (empty? existing-accounts) vendor-id client-id)
(let [new-account (cond-> {:db/id (str (java.util.UUID/randomUUID))
:transaction-account/location (or (:account/location default-account) "Shared")
:transaction-account/amount (if (= amount-mode "%") 100.0 total)}
default-account (assoc :transaction-account/account (:db/id default-account)))]
(-> request
(assoc-in [:multi-form-state :snapshot :transaction/accounts] [new-account])
(assoc-in [:multi-form-state :step-params :transaction/accounts] [new-account])))
request)]
(html-response
(fc/start-form (:multi-form-state render-request) nil
(fc/with-field :step-params
(manual-coding-section* mode render-request))))))
Note: the hx-target on the vendor field in manual-coding-section* must point to #manual-coding-section (not #account-grid-body) — this was set correctly in Task 3.
- Step 2: Verify the file compiles
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
Expected: no errors.
- Step 3: Commit
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "feat: update edit-vendor-changed-handler to support simple/advanced mode"
Task 6: Fix fc/with-cursor-index usage in simple-mode-fields*
Context: The simple-mode fields need to emit form names like step-params[transaction/accounts][0][transaction-account/account]. The existing fc/cursor-map used in account-grid-body* handles this automatically. For the single-row simple mode we need to manually set index 0.
Look up how fc/with-cursor-index (or equivalent) works in src/clj/auto_ap/ssr/form_cursor.clj before writing the code in Task 2. If no such helper exists, use fc/cursor-nth or replicate the index manually via the form cursor API.
- Step 1: Inspect the form cursor API
clj-nrepl-eval -p 9000 "(clj-mcp.repl-tools/list-vars 'auto-ap.ssr.form-cursor)"
Note the available functions.
- Step 2: Update
simple-mode-fields*if needed
If fc/with-cursor-index does not exist, replace the fc/with-cursor-index 0 call in Task 2 with the correct form-cursor idiom. The key requirement is that the hidden db/id, transaction-account/account, transaction-account/location, and transaction-account/amount fields emit names matching index 0 of transaction/accounts.
A known working pattern from account-grid-body*:
(fc/cursor-map #(transaction-account-row* {:value % ...}))
For simple mode with a single synthetic row, build a one-element vector in the snapshot and let fc/cursor-map iterate it — but render a flat div instead of a table. Or pass the cursor manually:
(fc/with-field :transaction/accounts
(let [row-cursor (fc/cursor-nth 0)] ; adjust to actual API
(fc/with-cursor row-cursor
...field rendering...)))
Verify field names are correct in a browser after implementation.
- Step 3: Verify the file compiles
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
- Step 4: Commit if any changes were made
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "fix: correct form-cursor indexing for simple-mode account field"
Task 7: Manual smoke test
Before writing automated tests, verify the UI works end-to-end in a browser.
- Step 1: Start the application
INTEGREAT_JOB="" lein run
- Step 2: Open a transaction with no accounts
Navigate to a transaction with no coded accounts. Open the edit modal. Verify it opens in simple mode with blank account and location fields.
- Step 3: Test vendor selection in simple mode
Select a vendor. Verify the account field is populated with the vendor's default account and the location is set appropriately.
- Step 4: Test toggle to advanced
Click "Switch to advanced mode". Verify the full split table appears with one pre-populated row.
- Step 5: Test toggle back to simple
With 1 row, click "Switch to simple mode". Verify the single account/location fields appear with that row's values.
- Step 6: Test with a split transaction
Open a transaction that already has 2+ accounts. Verify it opens in advanced mode. Verify the "Switch to simple mode" link is absent.
- Step 7: Test save round-trip
In simple mode, set a vendor, account, and location. Save. Re-open. Verify the same values are pre-populated in simple mode.
Task 8: Write e2e tests
Files:
- Create:
test/clj/auto_ap/ssr/transaction/edit_simple_advanced_mode_test.clj
Check how existing e2e tests are structured first:
clj-nrepl-eval -p 9000 "(clj-mcp.repl-tools/list-ns)"
Look for test namespaces matching auto-ap.ssr.transaction.* or auto-ap.e2e.*. Follow the same fixture/helper patterns.
The test file should cover all 20 acceptance criteria from the spec. Group them with testing blocks:
(ns auto-ap.ssr.transaction.edit-simple-advanced-mode-test
(:require
[clojure.test :refer [deftest is testing use-fixtures]]
;; ... project-specific test helpers ...
))
(deftest simple-advanced-mode-initial-state
(testing "AC1: uncoded transaction opens in simple mode"
;; create a transaction with no accounts
;; open edit modal
;; verify #manual-coding-section has mode=simple hidden input
;; verify no #account-grid-body present
(is ...))
(testing "AC2: single-account transaction opens in simple mode with values pre-populated"
...)
(testing "AC3: multi-account transaction opens in advanced mode"
...))
(deftest simple-mode-vendor-selection
(testing "AC4: selecting vendor populates account and location"
...)
(testing "AC5: selecting vendor does not overwrite manually chosen account"
...))
(deftest mode-toggle
(testing "AC9: switching to advanced carries account/location into first row"
...)
(testing "AC10: switching to advanced from blank simple gives empty table"
...)
(testing "AC11: switch-to-simple link visible with 0 or 1 rows"
...)
(testing "AC12: switch-to-simple link absent with 2+ rows"
...)
(testing "AC13: switching to simple pre-populates from first row"
...))
(deftest save-round-trip
(testing "AC6: save in simple mode persists vendor/account/location"
...)
(testing "AC18: switching modes mid-edit then saving produces valid transaction"
...)
(testing "AC19: split transaction re-opens in advanced mode with splits intact"
...)
(testing "AC20: single-account transaction re-opens in simple mode"
...))
Fill in actual test bodies using the project's test infrastructure (browser automation or ring mock depending on what exists).
- Step 1: Check existing test conventions
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.testing-conventions] :reload)"
Also load the testing-conventions skill for guidance:
Load skill: testing-conventions
-
Step 2: Write the test file following project conventions
-
Step 3: Run the tests
clj-nrepl-eval -p 9000 "(clojure.test/run-tests 'auto-ap.ssr.transaction.edit-simple-advanced-mode-test)"
Expected: all tests pass.
- Step 4: Commit
git add test/clj/auto_ap/ssr/transaction/edit_simple_advanced_mode_test.clj
git commit -m "test: add e2e acceptance tests for simple/advanced mode"
Self-Review Checklist (completed inline)
- Spec coverage: All 20 ACs addressed — Tasks 2–5 implement the behaviour; Task 8 tests all 20.
- Placeholder scan: Task 6 and Task 8 contain some "fill in" guidance — this is intentional because they depend on runtime API discovery. The instructions tell the engineer exactly where to look and what to verify.
- Type consistency:
manual-coding-section*is used consistently byLinksStep/render-step,edit-vendor-changed-handler, andedit-wizard-toggle-mode-handler.#manual-coding-sectionis the swap target throughout.modehidden input uses(name mode)for string serialization and(keyword ...)for deserialization.