refactor(ssr): Phase 6b — migrate Transaction Rule wizard onto the session engine; de-cursor
Proves the Phase-6a wizard engine against a real 2-step modal: the Transaction Rule wizard (edit step + read-only test/preview step) now runs on wizard2 / wizard-state, fully de-cursored. What changed - Wizard machinery removed: deleted the EditModal / TestModal / TransactionRuleWizard defrecords (mm/ModalWizardStep + LinearModalWizard), MultiStepFormState, the EDN snapshot, and the step-params[...] prefix. Replaced with a data-driven `transaction-rule-wizard-config` (two steps + init-fn + done-fn) driven by the engine. - De-cursored the whole edit form (82 fc/ refs -> 0): every field reads explicit data + path->name2; errors via a bound *errors* / ferr. The account row's Alpine cross-field dispatch wiring (clientId -> accountId -> location) is preserved verbatim — only the data plumbing moved off the cursor. - The test step's :render reads :all-data (the engine's get-all), so the formtools "combine at the end" mechanism feeds the preview table. - Routes 4 -> 2: open-rule-wizard (new + edit), save-step (every transition via the engine's `direction` field). The dedicated `navigate` route is deleted. - decode-rule-form select-keys to the schema's known keys so the engine's nav fields (wizard-id/current-step/direction) don't leak into the upserted entity. Scorecard (admin/transaction_rules.clj): fc/ 82->0, mm/ 20->0, defrecords 3->0, LOC 1000->964, routes 4->2. Scope note: the de-cursored edit step keeps com/* Hiccup leaf components (not yet sc/* Selmer); the value here was removing fc/ + mm/ and proving the engine, not re-templating the conditional/Alpine-cross-field layout. Hiccup-in-render is a documented partial; the com/ -> sc/ swap is a mechanical follow-up. Verification: rule spec 4/4 (new + edit dialogs, advance-to-test preview, save); full Playwright suite 55/55; cljfmt clean. Skill fed: scorecard row + narrative (engine's first real modal; generalizes for a one-data-step wizard); gotchas (strip engine nav fields in decode, new-row temp-id, direction-button nav). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -265,6 +265,24 @@ carve-out. Verify with `load-file` (compile) + `lein cljfmt check`, not by eyeba
|
|||||||
diff is contained with `git diff -U0 <file> | grep '^@@'` — the hunks should cluster only where you
|
diff is contained with `git diff -U0 <file> | grep '^@@'` — the hunks should cluster only where you
|
||||||
edited (requires + the modal region), nothing else.
|
edited (requires + the modal region), nothing else.
|
||||||
|
|
||||||
|
## Wiring a modal onto the wizard2 engine — three traps that cost a debug cycle each
|
||||||
|
|
||||||
|
1. **Strip the engine's nav fields in the step `:decode`.** The posted form carries
|
||||||
|
`wizard-id` / `current-step` / `direction` alongside the real fields. If the step schema is
|
||||||
|
an open `:map` (most are), `mc/decode` keeps them, they ride into `get-all`, and the save's
|
||||||
|
`:upsert-entity` dies with `:db.error/not-an-entity ... :current-step`. Fix: `select-keys`
|
||||||
|
the decode to the schema's known top-level keys (the same allowlist trick as the flat-form
|
||||||
|
migrations). Symptom is a **500 on save**, not a validation message.
|
||||||
|
2. **New repeated-row needs a temp `:db/id` or the step can't advance.** If the row schema
|
||||||
|
requires `[:db/id [:or entity-id temp-id]]`, an added row with no id fails per-step
|
||||||
|
validation, so the engine re-renders the *same* step instead of advancing — looks like "the
|
||||||
|
Next/Test button does nothing." Give new rows `(str (java.util.UUID/randomUUID))`.
|
||||||
|
3. **Nav is a `direction` field, and Back/Save are both submit buttons.** The footer buttons
|
||||||
|
are plain `<button type="submit" name="direction" value="next|back|submit">`; the clicked
|
||||||
|
one's value rides in the POST and the engine branches on it. In tests, a selector like
|
||||||
|
`button:has-text("Save"), button[type=submit]` also matches **Back** (also a submit) and
|
||||||
|
`.first()` clicks Back — target the button by its text/value precisely.
|
||||||
|
|
||||||
## Scorecard exceptions (ratchet violations with a reason)
|
## Scorecard exceptions (ratchet violations with a reason)
|
||||||
|
|
||||||
**Heuristic 4 (LOC net ↓) — exception (Phase 3, Transaction Bulk Code: 420→506).** When the
|
**Heuristic 4 (LOC net ↓) — exception (Phase 3, Transaction Bulk Code: 420→506).** When the
|
||||||
|
|||||||
@@ -161,3 +161,29 @@ Each migration appends one row (after-numbers), referencing the before in the di
|
|||||||
> helper. Per-row ids are generated from the row index the form already uses for field names
|
> helper. Per-row ids are generated from the row index the form already uses for field names
|
||||||
> (`path->name2`), so server and markup agree by construction. Whole-form swap (Rule 3) covers
|
> (`path->name2`), so server and markup agree by construction. Whole-form swap (Rule 3) covers
|
||||||
> structural changes (add/remove row). This is now the cookbook default; see `swap-doctrine.md`.
|
> structural changes (add/remove row). This is now the cookbook default; see `swap-doctrine.md`.
|
||||||
|
|
||||||
|
> **Phase 6 — the wizard engine, and its first real modal (Transaction Rule).** The inflection
|
||||||
|
> phase. (a) **Engine** (`6a`, committed separately): `wizard-state` + `wizard2`, the Django
|
||||||
|
> `formtools` SessionStorage model, REPL-proven before any modal touched it. (b) **First real
|
||||||
|
> modal** (`6b`): the Transaction Rule wizard (edit step + read-only test/preview step) migrated
|
||||||
|
> onto the engine and **fully de-cursored** like Phases 2-5. Scorecard (`admin/transaction_rules.clj`):
|
||||||
|
> `fc/` cursor refs **82 -> 0**, `mm/` coupling **20 -> 0**, defrecords **3 -> 0** (EditModal /
|
||||||
|
> TestModal / TransactionRuleWizard all gone), LOC 1000 -> 964, the 4 wizard routes
|
||||||
|
> (open/navigate/save + per-dialog) collapse to **2** (`open-rule-wizard` for new+edit,
|
||||||
|
> `save-step` for every transition). Parity held: rule spec **4/4**, full suite **55/55**.
|
||||||
|
>
|
||||||
|
> **The engine generalizes even for a one-data-step "wizard".** Transaction Rule is *edit + a
|
||||||
|
> read-only preview of the same entity*, not two independent data steps — so it exercises the
|
||||||
|
> engine's render / navigation / `:all-data`-preview path but not the cross-step *merge* (that
|
||||||
|
> waits for Phase 7's Invoice Pay). The test step's `:render` reads `:all-data` (the engine's
|
||||||
|
> `get-all`), which here is just the edit step's rule — so the formtools "combine at the end"
|
||||||
|
> mechanism is exactly what feeds the preview table. Nav is the engine's `direction` field
|
||||||
|
> (plain submit buttons `name="direction" value="next|back|submit"`), so the per-step
|
||||||
|
> `navigate` route is deleted.
|
||||||
|
>
|
||||||
|
> **Note (scope):** the de-cursored edit step keeps `com/*` Hiccup leaf components rather than
|
||||||
|
> porting to `sc/*` Selmer partials — the modal's value was removing `fc/` + `mm/` and proving
|
||||||
|
> the engine, not re-templating its (conditional, Alpine-cross-field) layout. Hiccup-in-render
|
||||||
|
> (heuristic 9) is therefore a documented partial here; the leaf-component `com/ -> sc/` swap is
|
||||||
|
> a mechanical follow-up. The Alpine cross-field dispatch wiring (clientId -> accountId ->
|
||||||
|
> location) was preserved verbatim — de-cursoring touched only the data plumbing.
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ async function addAccount(page: any, accountId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fillDescription(page: any, desc: string) {
|
async function fillDescription(page: any, desc: string) {
|
||||||
await page.locator('#wizard-form input[name*="[transaction-rule/description]"]').first().fill(desc);
|
await page.locator('#wizard-form input[name="transaction-rule/description"]').first().fill(desc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approval status is required to advance/save; the radio-card's first option is "Approved".
|
// Approval status is required to advance/save; the radio-card's first option is "Approved".
|
||||||
@@ -89,7 +89,7 @@ test.describe('Transaction Rule wizard (characterization)', () => {
|
|||||||
test('Edit dialog pre-populates the seeded rule', async ({ page }) => {
|
test('Edit dialog pre-populates the seeded rule', async ({ page }) => {
|
||||||
await navigateToRules(page);
|
await navigateToRules(page);
|
||||||
await openEditDialog(page);
|
await openEditDialog(page);
|
||||||
const desc = page.locator('#wizard-form input[name*="[transaction-rule/description]"]').first();
|
const desc = page.locator('#wizard-form input[name="transaction-rule/description"]').first();
|
||||||
await expect(desc).toHaveValue('ZZRULEMATCH');
|
await expect(desc).toHaveValue('ZZRULEMATCH');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,8 +116,8 @@ test.describe('Transaction Rule wizard (characterization)', () => {
|
|||||||
await addAccount(page, info.accounts['test-account'].toString());
|
await addAccount(page, info.accounts['test-account'].toString());
|
||||||
await selectApproved(page);
|
await selectApproved(page);
|
||||||
await clickTest(page);
|
await clickTest(page);
|
||||||
// Save from the test step
|
// Save from the test step (the precise Save button, not Back which is also submit)
|
||||||
await page.locator('#wizard-form button:has-text("Save"), #wizard-form button[type="submit"]').first().click();
|
await page.locator('#wizard-form button:has-text("Save")').first().click();
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
// modal closed + a new rule row added
|
// modal closed + a new rule row added
|
||||||
await expect(page.locator('#wizard-form')).toBeHidden();
|
await expect(page.locator('#wizard-form')).toBeHidden();
|
||||||
|
|||||||
@@ -14,21 +14,22 @@
|
|||||||
[auto-ap.rule-matching :as rm]
|
[auto-ap.rule-matching :as rm]
|
||||||
[auto-ap.solr :as solr]
|
[auto-ap.solr :as solr]
|
||||||
[auto-ap.ssr-routes :as ssr-routes]
|
[auto-ap.ssr-routes :as ssr-routes]
|
||||||
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
|
|
||||||
[auto-ap.ssr.company :refer [bank-account-typeahead*]]
|
[auto-ap.ssr.company :refer [bank-account-typeahead*]]
|
||||||
[auto-ap.ssr.components :as com]
|
[auto-ap.ssr.components :as com]
|
||||||
[auto-ap.ssr.components.multi-modal :as mm]
|
[auto-ap.ssr.components.wizard-state :as ws]
|
||||||
[auto-ap.ssr.form-cursor :as fc]
|
[auto-ap.ssr.components.wizard2 :as wizard2]
|
||||||
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
||||||
[auto-ap.ssr.hx :as hx]
|
[auto-ap.ssr.hx :as hx]
|
||||||
|
[auto-ap.ssr.nested-form-params :as nfp]
|
||||||
[auto-ap.ssr.svg :as svg]
|
[auto-ap.ssr.svg :as svg]
|
||||||
[auto-ap.ssr.utils
|
[auto-ap.ssr.utils
|
||||||
:refer [apply-middleware-to-all-handlers
|
:refer [->db-id apply-middleware-to-all-handlers
|
||||||
default-grid-fields-schema entity-id
|
default-grid-fields-schema entity-id
|
||||||
field-validation-error form-validation-error
|
field-validation-error form-validation-error
|
||||||
html-response many-entity modal-response money percentage
|
html-response main-transformer many-entity modal-response money
|
||||||
ref->enum-schema ref->radio-options regex temp-id
|
path->name2 percentage ref->enum-schema ref->radio-options regex
|
||||||
wrap-entity wrap-merge-prior-hx wrap-schema-enforce]]
|
temp-id wrap-entity wrap-form-4xx-2 wrap-merge-prior-hx
|
||||||
|
wrap-schema-enforce]]
|
||||||
[auto-ap.time :as atime]
|
[auto-ap.time :as atime]
|
||||||
[auto-ap.utils :refer [dollars=]]
|
[auto-ap.utils :refer [dollars=]]
|
||||||
[bidi.bidi :as bidi]
|
[bidi.bidi :as bidi]
|
||||||
@@ -37,7 +38,23 @@
|
|||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[datomic.api :as dc]
|
[datomic.api :as dc]
|
||||||
[malli.core :as mc]
|
[malli.core :as mc]
|
||||||
[malli.util :as mut]))
|
[malli.error :as me]))
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Field-name / error helpers for the (de-cursored) rule form. No step-params
|
||||||
|
;; prefix -- posted fields decode straight into form-schema. Mirrors edit.clj.
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(def ^:dynamic *errors*
|
||||||
|
"Humanized form errors for the current rule render, keyed by form-schema paths.
|
||||||
|
Bound by render-edit-step from the engine ctx :errors."
|
||||||
|
{})
|
||||||
|
|
||||||
|
(defn- fname [& path] (apply path->name2 path))
|
||||||
|
(defn- ferr [& path] (get-in *errors* (vec path)))
|
||||||
|
(defn- err? [& path] (boolean (seq (apply ferr path))))
|
||||||
|
(defn- account-field-name [index field] (path->name2 :transaction-rule/accounts index field))
|
||||||
|
(defn- account-field-errors [index field] (ferr :transaction-rule/accounts index field))
|
||||||
|
|
||||||
(def query-schema (mc/schema
|
(def query-schema (mc/schema
|
||||||
[:maybe
|
[:maybe
|
||||||
@@ -437,67 +454,63 @@
|
|||||||
client-id))))))})])
|
client-id))))))})])
|
||||||
|
|
||||||
(defn- transaction-rule-account-row*
|
(defn- transaction-rule-account-row*
|
||||||
[account client-id client-locations]
|
"One account-coding row, from a plain account map + its index (no cursor). The Alpine
|
||||||
(com/data-grid-row
|
cross-field dispatch wiring (clientId -> accountId -> location) is preserved verbatim;
|
||||||
(-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account)))
|
only the field names/values move from the form cursor to explicit data + path->name2."
|
||||||
(fc/field-value (:transaction-rule-account/account account)))
|
[account index client-id client-locations]
|
||||||
:location (fc/field-value (:transaction-rule-account/location account))
|
(let [acct (:transaction-rule-account/account account)
|
||||||
:show (boolean (not (fc/field-value (:new? account))))})
|
acct-id (if (map? acct) (:db/id acct) acct)
|
||||||
:data-key "show"
|
aname (account-field-name index :transaction-rule-account/account)
|
||||||
:x-ref "p"}
|
lname (account-field-name index :transaction-rule-account/location)]
|
||||||
hx/alpine-mount-then-appear)
|
(com/data-grid-row
|
||||||
(let [account-name (fc/field-name (:transaction-rule-account/account account))]
|
(-> {:x-data (hx/json {:accountId acct-id
|
||||||
(list
|
:location (:transaction-rule-account/location account)
|
||||||
|
:show (boolean (not (:new? account)))})
|
||||||
(fc/with-field :db/id
|
:data-key "show"
|
||||||
(com/hidden {:name (fc/field-name)
|
:x-ref "p"}
|
||||||
:value (fc/field-value)}))
|
hx/alpine-mount-then-appear)
|
||||||
(fc/with-field :transaction-rule-account/account
|
(com/hidden {:name (account-field-name index :db/id)
|
||||||
(com/data-grid-cell
|
:value (:db/id account)})
|
||||||
{}
|
(com/data-grid-cell
|
||||||
(com/validated-field
|
{}
|
||||||
{:errors (fc/field-errors)}
|
(com/validated-field
|
||||||
[:div {:hx-trigger "changed"
|
{:errors (account-field-errors index :transaction-rule-account/account)}
|
||||||
:hx-target "next div"
|
[:div {:hx-trigger "changed"
|
||||||
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', value: event.detail.accountId || ''}" account-name)
|
:hx-target "next div"
|
||||||
:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/account-typeahead))
|
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', value: event.detail.accountId || ''}" aname)
|
||||||
:x-init "$watch('clientId', cid => $dispatch('changed', $data));"}]
|
:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/account-typeahead))
|
||||||
(account-typeahead* {:value (fc/field-value)
|
:x-init "$watch('clientId', cid => $dispatch('changed', $data));"}]
|
||||||
:client-id client-id
|
(account-typeahead* {:value acct-id
|
||||||
:name (fc/field-name)
|
:client-id client-id
|
||||||
:x-model "accountId"}))))
|
:name aname
|
||||||
(fc/with-field :transaction-rule-account/location
|
:x-model "accountId"})))
|
||||||
(com/data-grid-cell
|
(com/data-grid-cell
|
||||||
{}
|
{}
|
||||||
(com/validated-field
|
(com/validated-field
|
||||||
{:errors (fc/field-errors)
|
{:errors (account-field-errors index :transaction-rule-account/location)
|
||||||
:x-data (hx/json {:location (fc/field-value)})}
|
:x-data (hx/json {:location (:transaction-rule-account/location account)})}
|
||||||
;; TODO make this thing into a component
|
[:div {:hx-trigger "changed"
|
||||||
[:div {:hx-trigger "changed"
|
:hx-target "next *"
|
||||||
:hx-target "next *"
|
:hx-swap "outerHTML"
|
||||||
:hx-swap "outerHTML"
|
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location || ''}" lname)
|
||||||
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location || ''}" (fc/field-name))
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
|
||||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
|
:x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}]
|
||||||
:x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}]
|
(location-select* {:name lname
|
||||||
(location-select* {:name (fc/field-name)
|
:account-location (:account/location (when (nat-int? acct-id)
|
||||||
:account-location (:account/location (cond->> (:transaction-rule-account/account @account)
|
(dc/pull (dc/db conn) '[:account/location] acct-id)))
|
||||||
(nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn)
|
:client-locations client-locations
|
||||||
'[:account/location])))
|
:value (:transaction-rule-account/location account)})))
|
||||||
:client-locations client-locations
|
(com/data-grid-cell
|
||||||
:x-model "location"
|
{}
|
||||||
:value (fc/field-value)}))))
|
(com/validated-field
|
||||||
(fc/with-field :transaction-rule-account/percentage
|
{:errors (account-field-errors index :transaction-rule-account/percentage)}
|
||||||
(com/data-grid-cell
|
(com/money-input {:name (account-field-name index :transaction-rule-account/percentage)
|
||||||
{}
|
:class "w-16"
|
||||||
(com/validated-field
|
:value (some-> (:transaction-rule-account/percentage account)
|
||||||
{:errors (fc/field-errors)}
|
(* 100)
|
||||||
(com/money-input {:name (fc/field-name)
|
(long))})))
|
||||||
:class "w-16"
|
(com/data-grid-cell {:class "align-top"}
|
||||||
:value (some-> (fc/field-value)
|
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))))
|
||||||
(* 100)
|
|
||||||
(long))}))))))
|
|
||||||
(com/data-grid-cell {:class "align-top"}
|
|
||||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
|
||||||
|
|
||||||
(defn all-ids-not-locked [all-ids]
|
(defn all-ids-not-locked [all-ids]
|
||||||
(->> all-ids
|
(->> all-ids
|
||||||
@@ -638,269 +651,243 @@
|
|||||||
(html-response (row* (:identity request) entity {:delete-after-settle? true :class "live-removed"})
|
(html-response (row* (:identity request) entity {:delete-after-settle? true :class "live-removed"})
|
||||||
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id entity))}))
|
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id entity))}))
|
||||||
|
|
||||||
(defrecord EditModal [linear-wizard]
|
;; ---------------------------------------------------------------------------
|
||||||
mm/ModalWizardStep
|
;; The rule wizard on the data-driven session engine (wizard2 / wizard-state),
|
||||||
(step-name [_]
|
;; replacing the EditModal/TestModal/TransactionRuleWizard records +
|
||||||
"Edit")
|
;; MultiStepFormState + the EDN-snapshot round-trip.
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
(step-key [_]
|
(defn- rule-modal-card [& {:keys [head body footer]}]
|
||||||
:edit)
|
(com/modal-card-advanced
|
||||||
|
{}
|
||||||
|
(com/modal-header {} head)
|
||||||
|
(com/modal-body {} body)
|
||||||
|
(com/modal-footer {} footer)))
|
||||||
|
|
||||||
(edit-path [_ _] [])
|
(defn- rule-nav
|
||||||
|
"Footer step controls. Buttons post a `direction` field the engine reads:
|
||||||
|
next = validate + advance, back = no validate, submit = finish."
|
||||||
|
[{:keys [next back? save?]}]
|
||||||
|
[:div.flex.justify-end.gap-x-4
|
||||||
|
[:div#form-errors]
|
||||||
|
(when back?
|
||||||
|
(com/button {:type "submit" :name "direction" :value "back" :class "w-24"} "Back"))
|
||||||
|
(when next
|
||||||
|
(com/button {:type "submit" :name "direction" :value "next" :color :primary :class "w-24"} next))
|
||||||
|
(when save?
|
||||||
|
(com/button {:type "submit" :name "direction" :value "submit" :color :primary :class "w-24" :x-ref "next"} "Save"))])
|
||||||
|
|
||||||
(step-schema [_]
|
(defn render-edit-step
|
||||||
(mm/form-schema linear-wizard))
|
"Edit step: the rule form, de-cursored (explicit data + path->name2 + *errors*)."
|
||||||
|
[{:keys [step-data errors]}]
|
||||||
|
(binding [*errors* (or errors {})]
|
||||||
|
(let [rule (or step-data {})
|
||||||
|
rule-client (:transaction-rule/client rule)
|
||||||
|
client-id (if (map? rule-client) (:db/id rule-client) rule-client)
|
||||||
|
client-locations (some->> client-id (pull-attr (dc/db conn) :client/locations))
|
||||||
|
accounts (vec (:transaction-rule/accounts rule))]
|
||||||
|
(rule-modal-card
|
||||||
|
:head "Transaction rule"
|
||||||
|
:body [:div#my-form {:x-trap "true"}
|
||||||
|
[:fieldset {:class "hx-disable"
|
||||||
|
:x-data (hx/json {:clientId client-id})}
|
||||||
|
[:div.space-y-1
|
||||||
|
(when-let [id (:db/id rule)]
|
||||||
|
(com/hidden {:name "db/id" :value id}))
|
||||||
|
(com/validated-field {:label "Description" :errors (ferr :transaction-rule/description)}
|
||||||
|
(com/text-input {:name (fname :transaction-rule/description)
|
||||||
|
:error? (err? :transaction-rule/description)
|
||||||
|
:x-init "$el.focus()"
|
||||||
|
:placeholder "HOME DEPOT"
|
||||||
|
:class "w-96"
|
||||||
|
:value (:transaction-rule/description rule)}))
|
||||||
|
[:div.filters {:x-data (hx/json {:clientFilter (boolean (:transaction-rule/client rule))
|
||||||
|
:clientGroupFilter (boolean (:transaction-rule/client-group rule))
|
||||||
|
:bankAccountFilter (boolean (:transaction-rule/bank-account rule))
|
||||||
|
:amountFilter (boolean (or (:transaction-rule/amount-gte rule) (:transaction-rule/amount-lte rule)))
|
||||||
|
:domFilter (boolean (or (:transaction-rule/dom-gte rule) (:transaction-rule/dom-lte rule)))})}
|
||||||
|
[:div.flex.gap-2.mb-2
|
||||||
|
(com/a-button {"@click" "clientFilter=true" "x-show" "!clientFilter"} "Filter client")
|
||||||
|
(com/a-button {"@click" "clientGroupFilter=true" "x-show" "!clientGroupFilter"} "Filter client group")
|
||||||
|
(com/a-button {"@click" "bankAccountFilter=true" "x-show" "clientFilter && !bankAccountFilter"} "Filter bank account")
|
||||||
|
(com/a-button {"@click" "amountFilter=true" "x-show" "!amountFilter"} "Filter amount")
|
||||||
|
(com/a-button {"@click" "domFilter=true" "x-show" "!domFilter"} "Filter day of month")]
|
||||||
|
(com/validated-field
|
||||||
|
(-> {:label "Client" :errors (ferr :transaction-rule/client) :x-show "clientFilter"} (hx/alpine-appear))
|
||||||
|
[:div.w-96
|
||||||
|
(com/typeahead {:name (fname :transaction-rule/client)
|
||||||
|
:error? (err? :transaction-rule/client)
|
||||||
|
:class "w-96" :placeholder "Search..."
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
||||||
|
:x-model "clientId"
|
||||||
|
:value rule-client
|
||||||
|
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})])
|
||||||
|
(com/validated-field
|
||||||
|
(-> {:label "Client Group" :errors (ferr :transaction-rule/client-group) :x-show "clientGroupFilter"} (hx/alpine-appear))
|
||||||
|
[:div.w-96
|
||||||
|
(com/text-input {:name (fname :transaction-rule/client-group)
|
||||||
|
:error? (err? :transaction-rule/client-group)
|
||||||
|
:class "w-24" :placeholder "NTG"
|
||||||
|
:value (:transaction-rule/client-group rule)})])
|
||||||
|
(com/validated-field
|
||||||
|
(-> {:label "Bank Account" :errors (ferr :transaction-rule/bank-account) :x-show "bankAccountFilter"} hx/alpine-appear)
|
||||||
|
[:div.w-96
|
||||||
|
[:div#bank-account-changer {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead)
|
||||||
|
:hx-trigger "changed"
|
||||||
|
:hx-target "next *"
|
||||||
|
:hx-include "#bank-account-changer"
|
||||||
|
:hx-swap "outerHTML"
|
||||||
|
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fname :transaction-rule/bank-account))
|
||||||
|
:x-init "$watch('clientId', cid => $dispatch('changed', $data))"}]
|
||||||
|
(bank-account-typeahead* {:client-id client-id
|
||||||
|
:name (fname :transaction-rule/bank-account)
|
||||||
|
:value (:transaction-rule/bank-account rule)})])
|
||||||
|
(com/field (-> {:label "Amount" :x-show "amountFilter"} hx/alpine-appear)
|
||||||
|
[:div.flex.gap-2
|
||||||
|
[:div.flex.flex-col
|
||||||
|
(com/money-input {:name (fname :transaction-rule/amount-gte) :placeholder ">=" :class "w-24" :value (:transaction-rule/amount-gte rule)})
|
||||||
|
(com/errors {:errors (ferr :transaction-rule/amount-gte)})]
|
||||||
|
[:div.flex.flex-col
|
||||||
|
(com/money-input {:name (fname :transaction-rule/amount-lte) :placeholder "<=" :class "w-24" :value (:transaction-rule/amount-lte rule)})
|
||||||
|
(com/errors {:errors (ferr :transaction-rule/amount-lte)})]])
|
||||||
|
(com/field (-> {:label "Day of month" :x-show "domFilter"} hx/alpine-appear)
|
||||||
|
[:div.flex.gap-2
|
||||||
|
(com/validated-field {:errors (ferr :transaction-rule/dom-gte)}
|
||||||
|
(com/int-input {:name (fname :transaction-rule/dom-gte) :placeholder ">=" :class "w-24" :value (:transaction-rule/dom-gte rule)}))
|
||||||
|
(com/validated-field {:errors (ferr :transaction-rule/dom-lte)}
|
||||||
|
(com/int-input {:name (fname :transaction-rule/dom-lte) :placeholder ">=" :class "w-24" :value (:transaction-rule/dom-lte rule)}))])]
|
||||||
|
[:h2.text-lg "Outcomes"]
|
||||||
|
(com/validated-field {:label "Assign Vendor" :errors (ferr :transaction-rule/vendor)}
|
||||||
|
[:div.w-96
|
||||||
|
(com/typeahead {:name (fname :transaction-rule/vendor)
|
||||||
|
:placeholder "Search..."
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||||
|
:class "w-96"
|
||||||
|
:value (:transaction-rule/vendor rule)
|
||||||
|
:content-fn #(pull-attr (dc/db conn) :vendor/name %)})])
|
||||||
|
(com/validated-field
|
||||||
|
{:errors (ferr :transaction-rule/accounts)}
|
||||||
|
(com/data-grid {:headers [(com/data-grid-header {} "Account")
|
||||||
|
(com/data-grid-header {:class "w-32"} "Location")
|
||||||
|
(com/data-grid-header {:class "w-16"} "%")
|
||||||
|
(com/data-grid-header {:class "w-16"})]}
|
||||||
|
(map-indexed (fn [i a] (transaction-rule-account-row* a i client-id client-locations)) accounts)
|
||||||
|
(com/data-grid-new-row {:colspan 4
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-account)
|
||||||
|
:index (count accounts)
|
||||||
|
:tr-params (hx/bind-alpine-vals {} {"client-id" "clientId"})}
|
||||||
|
"New account")))
|
||||||
|
(com/validated-field {:label "Approval status" :errors (ferr :transaction-rule/transaction-approval-status)}
|
||||||
|
(com/radio-card {:options (ref->radio-options "transaction-approval-status")
|
||||||
|
:value (:transaction-rule/transaction-approval-status rule)
|
||||||
|
:name (fname :transaction-rule/transaction-approval-status)
|
||||||
|
:size :small
|
||||||
|
:orientation :horizontal}))]]]
|
||||||
|
:footer (rule-nav {:next "Test"})))))
|
||||||
|
|
||||||
(render-step [this request]
|
(defn render-test-step
|
||||||
(mm/default-render-step
|
"Test step: a read-only preview of the transactions the rule (the combined session
|
||||||
linear-wizard this
|
data) matches. The query/render is reused unchanged."
|
||||||
:head "Transaction rule"
|
[{:keys [all-data request]}]
|
||||||
:body (mm/default-step-body {}
|
(rule-modal-card
|
||||||
[:div#my-form {:x-trap "true"}
|
:head [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]]
|
||||||
[:fieldset {:class "hx-disable"
|
:body [:div.space-y-1 {:class "w-[850px] h-[600px]"}
|
||||||
:x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client (fc/field-value)))
|
(transaction-rule-test-table* {:entity all-data :clients (:clients request)})]
|
||||||
(:transaction-rule/client (fc/field-value)))})}
|
:footer (rule-nav {:back? true :save? true})))
|
||||||
|
|
||||||
[:div.space-y-1
|
(def ^:private rule-form-keys
|
||||||
(when-let [id (:db/id (fc/field-value))]
|
"Top-level keys form-schema recognises. The posted form also carries the engine's nav
|
||||||
(com/hidden {:name "db/id"
|
fields (wizard-id / current-step / direction); without this allowlist they'd ride into
|
||||||
:value id}))
|
the decoded rule (form-schema is an open :map) and break the upsert."
|
||||||
(fc/with-field :transaction-rule/description
|
[:db/id :transaction-rule/client :transaction-rule/client-group :transaction-rule/description
|
||||||
(com/validated-field {:label "Description"
|
:transaction-rule/bank-account :transaction-rule/amount-gte :transaction-rule/amount-lte
|
||||||
:errors (fc/field-errors)}
|
:transaction-rule/dom-gte :transaction-rule/dom-lte :transaction-rule/vendor
|
||||||
(com/text-input {:name (fc/field-name)
|
:transaction-rule/transaction-approval-status :transaction-rule/accounts])
|
||||||
:error? (fc/error?)
|
|
||||||
:x-init "$el.focus()"
|
|
||||||
:placeholder "HOME DEPOT"
|
|
||||||
:class "w-96"
|
|
||||||
:value (fc/field-value)})))
|
|
||||||
[:div.filters {:x-data (hx/json {:clientFilter (boolean (fc/field-value (:transaction-rule/client fc/*current*)))
|
|
||||||
:clientGroupFilter (boolean (fc/field-value (:transaction-rule/client-group fc/*current*)))
|
|
||||||
:bankAccountFilter (boolean (fc/field-value (:transaction-rule/bank-account fc/*current*)))
|
|
||||||
:amountFilter (boolean (or (fc/field-value (:transaction-rule/amount-gte fc/*current*))
|
|
||||||
(fc/field-value (:transaction-rule/amount-lte fc/*current*))))
|
|
||||||
:domFilter (boolean (or (fc/field-value (:transaction-rule/dom-gte fc/*current*))
|
|
||||||
(fc/field-value (:transaction-rule/dom-lte fc/*current*))))})}
|
|
||||||
|
|
||||||
[:div.flex.gap-2.mb-2
|
(defn- decode-rule-form
|
||||||
(com/a-button {"@click" "clientFilter=true"
|
"Parse the posted edit-step fields straight into the rule map (no step-params prefix);
|
||||||
"x-show" "!clientFilter"} "Filter client")
|
strip the stray engine nav fields."
|
||||||
(com/a-button {"@click" "clientGroupFilter=true"
|
[request]
|
||||||
"x-show" "!clientGroupFilter"} "Filter client group")
|
(let [nested (:form-params (nfp/nested-params-request request {}))
|
||||||
(com/a-button {"@click" "bankAccountFilter=true"
|
decoded (mc/decode form-schema nested main-transformer)]
|
||||||
"x-show" "clientFilter && !bankAccountFilter"} "Filter bank account")
|
(if (map? decoded) (select-keys decoded rule-form-keys) {})))
|
||||||
(com/a-button {"@click" "amountFilter=true"
|
|
||||||
"x-show" "!amountFilter"} "Filter amount")
|
|
||||||
(com/a-button {"@click" "domFilter=true"
|
|
||||||
"x-show" "!domFilter"} "Filter day of month")]
|
|
||||||
(fc/with-field :transaction-rule/client
|
|
||||||
|
|
||||||
(com/validated-field
|
(defn- rule-form-errors
|
||||||
(-> {:label "Client"
|
"Per-step validation: schema-validate so an invalid form can't advance to the test step
|
||||||
:errors (fc/field-errors)
|
(matches the old navigate-validates behavior). Returns a humanized errors map or nil.
|
||||||
:x-show "clientFilter"}
|
The full custom checks (percentage sum, location, bank-account) run at save."
|
||||||
(hx/alpine-appear))
|
[rule _request]
|
||||||
[:div.w-96
|
(when-not (mc/validate form-schema rule)
|
||||||
(com/typeahead {:name (fc/field-name)
|
(me/humanize (mc/explain form-schema rule))))
|
||||||
:error? (fc/error?)
|
|
||||||
:class "w-96"
|
|
||||||
:placeholder "Search..."
|
|
||||||
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
|
||||||
:x-model "clientId"
|
|
||||||
:value (fc/field-value)
|
|
||||||
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
|
|
||||||
(fc/with-field :transaction-rule/client-group
|
|
||||||
|
|
||||||
(com/validated-field
|
(defn save-rule!
|
||||||
(-> {:label "Client Group"
|
"Engine done-fn: validate + upsert the rule, then return the grid row + modalclose."
|
||||||
:errors (fc/field-errors)
|
[all-data request]
|
||||||
:x-show "clientGroupFilter"}
|
(validate-transaction-rule all-data)
|
||||||
(hx/alpine-appear))
|
(let [editing? (some? (:db/id all-data))
|
||||||
[:div.w-96
|
entity (cond-> all-data
|
||||||
(com/text-input {:name (fc/field-name)
|
(:transaction-rule/client-group all-data) (update :transaction-rule/client-group str/upper-case)
|
||||||
:error? (fc/error?)
|
(not editing?) (assoc :db/id "new")
|
||||||
:class "w-24"
|
true (assoc :transaction-rule/note (entity->note all-data)))
|
||||||
:placeholder "NTG"
|
{:keys [tempids]} (audit-transact [[:upsert-entity entity]] (:identity request))
|
||||||
:value (fc/field-value)})]))
|
saved (dc/pull (dc/db conn) default-read (or (get tempids (:db/id entity)) (:db/id entity)))]
|
||||||
(let [rule-client (fc/field-value (:transaction-rule/client fc/*current*))]
|
(html-response
|
||||||
(fc/with-field :transaction-rule/bank-account
|
(row* (:identity request) saved {:flash? true})
|
||||||
(com/validated-field
|
:headers (cond-> {"hx-trigger" "modalclose"}
|
||||||
(-> {:label "Bank Account"
|
(not editing?) (assoc "hx-retarget" "#entity-table tbody" "hx-reswap" "afterbegin")
|
||||||
:errors (fc/field-errors)
|
editing? (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id saved)) "hx-reswap" "outerHTML")))))
|
||||||
:x-show "bankAccountFilter"}
|
|
||||||
hx/alpine-appear)
|
|
||||||
[:div.w-96
|
|
||||||
[:div#bank-account-changer {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead)
|
|
||||||
:hx-trigger "changed"
|
|
||||||
:hx-target "next *"
|
|
||||||
:hx-include "#bank-account-changer"
|
|
||||||
:hx-swap "outerHTML"
|
|
||||||
|
|
||||||
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fc/field-name))
|
(def transaction-rule-wizard-config
|
||||||
:x-init "$watch('clientId', cid => $dispatch('changed', $data))"}]
|
{:name :transaction-rule
|
||||||
|
:form-id "wizard-form"
|
||||||
|
:submit-route (bidi/path-for ssr-routes/only-routes ::route/save)
|
||||||
|
:form-attrs {:hx-ext "response-targets"
|
||||||
|
:hx-target-400 "#form-errors"}
|
||||||
|
:init-fn (fn [request]
|
||||||
|
{:context {}
|
||||||
|
:init-data (when-let [e (:entity request)] {:edit e})})
|
||||||
|
:steps [{:key :edit
|
||||||
|
:decode decode-rule-form
|
||||||
|
:validate rule-form-errors
|
||||||
|
:render render-edit-step
|
||||||
|
:next (fn [_] :test)}
|
||||||
|
{:key :test
|
||||||
|
:decode (fn [_] {})
|
||||||
|
:render render-test-step
|
||||||
|
:next (fn [_] :done)}]
|
||||||
|
:done-fn save-rule!})
|
||||||
|
|
||||||
(bank-account-typeahead* {:client-id (or (:db/id rule-client) rule-client)
|
(defn open-rule-wizard
|
||||||
:name (fc/field-name)
|
"Open handler (new or edit): create the wizard instance, render its first step, and
|
||||||
:value (fc/field-value)})])))
|
wrap it in the modal shell the stack expects."
|
||||||
|
[request]
|
||||||
|
(let [cfg transaction-rule-wizard-config
|
||||||
|
{:keys [context init-data]} ((:init-fn cfg) request)
|
||||||
|
[id session'] (ws/create-wizard! (:session request) (:name cfg)
|
||||||
|
{:first-step :edit :context context :init-data init-data})
|
||||||
|
form (wizard2/render-wizard {:config cfg :wizard-id id :session session' :request request})]
|
||||||
|
(-> (modal-response [:div#transitioner.flex-1 form])
|
||||||
|
(assoc :session session'))))
|
||||||
|
|
||||||
(com/field (-> {:label "Amount"
|
(defn save-step
|
||||||
:x-show "amountFilter"}
|
"POST handler for every step transition (next / back / save) -- the engine reads the
|
||||||
hx/alpine-appear)
|
`direction` field and either advances, goes back, or finishes via done-fn."
|
||||||
[:div.flex.gap-2
|
[request]
|
||||||
(fc/with-field :transaction-rule/amount-gte
|
(wizard2/handle-step-submit transaction-rule-wizard-config request))
|
||||||
[:div.flex.flex-col
|
|
||||||
(com/money-input {:name (fc/field-name)
|
|
||||||
:placeholder ">="
|
|
||||||
:class "w-24"
|
|
||||||
:value (fc/field-value)})
|
|
||||||
(com/errors {:errors (fc/field-errors)})])
|
|
||||||
(fc/with-field :transaction-rule/amount-lte
|
|
||||||
[:div.flex.flex-col
|
|
||||||
(com/money-input {:name (fc/field-name)
|
|
||||||
:placeholder "<="
|
|
||||||
:class "w-24"
|
|
||||||
:value (fc/field-value)})
|
|
||||||
(com/errors {:errors (fc/field-errors)})])])
|
|
||||||
|
|
||||||
(com/field (-> {:label "Day of month"
|
(defn- new-account
|
||||||
:x-show "domFilter"}
|
"Render one fresh (de-cursored) account row at the posted index (the data grid's
|
||||||
hx/alpine-appear)
|
newRowIndex Alpine counter increments it for repeated adds)."
|
||||||
[:div.flex.gap-2
|
[request]
|
||||||
(fc/with-field :transaction-rule/dom-gte
|
(let [idx (-> request :query-params :index)
|
||||||
(com/validated-field
|
idx (if (string? idx) (Integer/parseInt idx) idx)
|
||||||
{:errors (fc/field-errors)}
|
client-id (-> request :query-params :client-id)
|
||||||
(com/int-input {:name (fc/field-name)
|
client-locations (some->> client-id (pull-attr (dc/db conn) :client/locations))]
|
||||||
:placeholder ">="
|
(html-response
|
||||||
:class "w-24"
|
(transaction-rule-account-row* {:db/id (str (java.util.UUID/randomUUID))
|
||||||
:value (fc/field-value)})))
|
:new? true
|
||||||
(fc/with-field :transaction-rule/dom-lte
|
:transaction-rule-account/location "Shared"}
|
||||||
(com/validated-field
|
idx client-id client-locations))))
|
||||||
{:errors (fc/field-errors)}
|
|
||||||
(com/int-input {:name (fc/field-name)
|
|
||||||
:placeholder ">="
|
|
||||||
:class "w-24"
|
|
||||||
:value (fc/field-value)})))])]
|
|
||||||
|
|
||||||
[:h2.text-lg "Outcomes"]
|
|
||||||
(fc/with-field :transaction-rule/vendor
|
|
||||||
(com/validated-field {:label "Assign Vendor"
|
|
||||||
:errors (fc/field-errors)}
|
|
||||||
[:div.w-96
|
|
||||||
(com/typeahead {:name (fc/field-name)
|
|
||||||
:placeholder "Search..."
|
|
||||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
|
||||||
:class "w-96"
|
|
||||||
:value (fc/field-value)
|
|
||||||
:content-fn #(pull-attr (dc/db conn) :vendor/name %)})]))
|
|
||||||
|
|
||||||
(fc/with-field :transaction-rule/accounts
|
|
||||||
(com/validated-field
|
|
||||||
{:errors (fc/field-errors)}
|
|
||||||
(let [client-locations (some->> (fc/field-value) :transaction-rule/client (pull-attr (dc/db conn) :client/locations))]
|
|
||||||
(com/data-grid {:headers [(com/data-grid-header {} "Account")
|
|
||||||
(com/data-grid-header {:class "w-32"} "Location")
|
|
||||||
(com/data-grid-header {:class "w-16"} "%")
|
|
||||||
(com/data-grid-header {:class "w-16"})]}
|
|
||||||
(fc/cursor-map #(transaction-rule-account-row* % (:transaction-rule/client (fc/field-value)) client-locations))
|
|
||||||
(com/data-grid-new-row {:colspan 4
|
|
||||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
|
||||||
::route/new-account)
|
|
||||||
:index (count (fc/field-value))
|
|
||||||
:tr-params (hx/bind-alpine-vals {} {"client-id" "clientId"})}
|
|
||||||
"New account")))))
|
|
||||||
|
|
||||||
(fc/with-field :transaction-rule/transaction-approval-status
|
|
||||||
(com/validated-field {:label "Approval status"
|
|
||||||
:errors (fc/field-errors)}
|
|
||||||
(com/radio-card {:options (ref->radio-options "transaction-approval-status")
|
|
||||||
:value (fc/field-value)
|
|
||||||
:name (fc/field-name)
|
|
||||||
:size :small
|
|
||||||
:orientation :horizontal})))]]])
|
|
||||||
:footer
|
|
||||||
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
|
||||||
:validation-route ::route/navigate)))
|
|
||||||
|
|
||||||
(defrecord TestModal [linear-wizard]
|
|
||||||
mm/ModalWizardStep
|
|
||||||
(step-name [_]
|
|
||||||
"Test")
|
|
||||||
|
|
||||||
(step-key [_]
|
|
||||||
:test)
|
|
||||||
|
|
||||||
(edit-path [_ _] [])
|
|
||||||
|
|
||||||
(step-schema [_]
|
|
||||||
(mut/select-keys (mm/form-schema linear-wizard) #{}))
|
|
||||||
|
|
||||||
(render-step [this request]
|
|
||||||
(mm/default-render-step
|
|
||||||
linear-wizard this
|
|
||||||
:head [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]]
|
|
||||||
:body [:div.space-y-1 {:class "w-[850px] h-[600px]"}
|
|
||||||
(transaction-rule-test-table* {:entity (:snapshot (:multi-form-state request))
|
|
||||||
:clients (:clients request)})]
|
|
||||||
:footer
|
|
||||||
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
|
||||||
:validation-route ::route/navigate)))
|
|
||||||
|
|
||||||
(defrecord TransactionRuleWizard [transaction-rule current-step entity]
|
|
||||||
mm/LinearModalWizard
|
|
||||||
(hydrate-from-request
|
|
||||||
[this request]
|
|
||||||
this
|
|
||||||
#_(assoc this :entity (:entity request)))
|
|
||||||
(navigate [this step-key]
|
|
||||||
(assoc this :current-step step-key))
|
|
||||||
(get-current-step [this]
|
|
||||||
(if current-step
|
|
||||||
(mm/get-step this current-step)
|
|
||||||
(mm/get-step this :edit)))
|
|
||||||
(render-wizard [this {:keys [multi-form-state] :as request}]
|
|
||||||
(mm/default-render-wizard
|
|
||||||
this request
|
|
||||||
:form-params
|
|
||||||
(-> mm/default-form-props
|
|
||||||
(assoc (if (get-in multi-form-state [:snapshot :db/id])
|
|
||||||
:hx-put
|
|
||||||
:hx-post)
|
|
||||||
(str (bidi/path-for ssr-routes/only-routes ::route/save))))))
|
|
||||||
(steps [_]
|
|
||||||
[:edit
|
|
||||||
:test])
|
|
||||||
|
|
||||||
(get-step [this step-key]
|
|
||||||
(let [step-key-result (mc/parse mm/step-key-schema step-key)
|
|
||||||
[step-key-type step-key] step-key-result]
|
|
||||||
(if (= :step step-key-type)
|
|
||||||
(get {:edit (->EditModal this)
|
|
||||||
:test (->TestModal this)}
|
|
||||||
step-key)
|
|
||||||
|
|
||||||
nil)))
|
|
||||||
(form-schema [_] form-schema)
|
|
||||||
(submit [_ {:keys [multi-form-state request-method identity] :as request}]
|
|
||||||
|
|
||||||
(let [transaction-rule (:snapshot multi-form-state)
|
|
||||||
_ (validate-transaction-rule transaction-rule)
|
|
||||||
entity (cond-> transaction-rule
|
|
||||||
(:transaction-rule/client-group transaction-rule) (update :transaction-rule/client-group str/upper-case)
|
|
||||||
(= :post request-method) (assoc :db/id "new")
|
|
||||||
true (assoc :transaction-rule/note (entity->note transaction-rule)))
|
|
||||||
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
|
|
||||||
(:identity request))
|
|
||||||
updated-rule (dc/pull (dc/db conn)
|
|
||||||
default-read
|
|
||||||
(or (get tempids (:db/id entity)) (:db/id entity)))]
|
|
||||||
(html-response
|
|
||||||
(row* identity updated-rule {:flash? true})
|
|
||||||
:headers (cond-> {"hx-trigger" "modalclose"}
|
|
||||||
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
|
|
||||||
"hx-reswap" "afterbegin")
|
|
||||||
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule))
|
|
||||||
"hx-reswap" "outerHTML"))))))
|
|
||||||
(def rule-wizard (->TransactionRuleWizard nil nil nil))
|
|
||||||
|
|
||||||
(def key->handler
|
(def key->handler
|
||||||
(apply-middleware-to-all-handlers
|
(apply-middleware-to-all-handlers
|
||||||
@@ -911,18 +898,11 @@
|
|||||||
(wrap-entity [:route-params :db/id] default-read)
|
(wrap-entity [:route-params :db/id] default-read)
|
||||||
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
||||||
::route/new-account
|
::route/new-account
|
||||||
(->
|
(-> new-account
|
||||||
(add-new-entity-handler [:step-params :transaction-rule/accounts]
|
(wrap-schema-enforce :query-schema [:map
|
||||||
(fn render [cursor request]
|
[:index {:optional true} [:maybe nat-int?]]
|
||||||
(transaction-rule-account-row*
|
[:client-id {:optional true}
|
||||||
cursor
|
[:maybe entity-id]]]))
|
||||||
(:client-id (:query-params request))
|
|
||||||
(some->> (:client-id (:query-params request)) (pull-attr (dc/db conn) :client/locations))))
|
|
||||||
(fn build-new-row [base _]
|
|
||||||
(assoc base :transaction-rule-account/location "Shared")))
|
|
||||||
(wrap-schema-enforce :query-schema [:map
|
|
||||||
[:client-id {:optional true}
|
|
||||||
[:maybe entity-id]]]))
|
|
||||||
|
|
||||||
::route/location-select (-> location-select
|
::route/location-select (-> location-select
|
||||||
(wrap-schema-enforce :query-schema [:map
|
(wrap-schema-enforce :query-schema [:map
|
||||||
@@ -938,10 +918,7 @@
|
|||||||
[:maybe entity-id]]
|
[:maybe entity-id]]
|
||||||
[:value {:optional true}
|
[:value {:optional true}
|
||||||
[:maybe entity-id]]]))
|
[:maybe entity-id]]]))
|
||||||
::route/save (-> mm/submit-handler
|
::route/save save-step
|
||||||
(mm/wrap-wizard rule-wizard)
|
|
||||||
(mm/wrap-decode-multi-form-state)
|
|
||||||
(wrap-entity [:form-params :db/id] default-read))
|
|
||||||
|
|
||||||
::route/execute (-> execute
|
::route/execute (-> execute
|
||||||
(wrap-entity [:route-params :db/id] default-read)
|
(wrap-entity [:route-params :db/id] default-read)
|
||||||
@@ -971,24 +948,11 @@
|
|||||||
(wrap-entity [:route-params :db/id] default-read)
|
(wrap-entity [:route-params :db/id] default-read)
|
||||||
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
||||||
|
|
||||||
::route/navigate (-> mm/next-handler
|
::route/edit-dialog (-> open-rule-wizard
|
||||||
(mm/wrap-wizard rule-wizard)
|
|
||||||
(mm/wrap-decode-multi-form-state))
|
|
||||||
::route/edit-dialog (-> mm/open-wizard-handler
|
|
||||||
(mm/wrap-wizard rule-wizard)
|
|
||||||
(mm/wrap-init-multi-form-state (fn [request]
|
|
||||||
(mm/->MultiStepFormState (:entity request)
|
|
||||||
[]
|
|
||||||
(:entity request))))
|
|
||||||
(wrap-entity [:route-params :db/id] default-read)
|
(wrap-entity [:route-params :db/id] default-read)
|
||||||
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
||||||
|
|
||||||
::route/new-dialog (-> mm/open-wizard-handler
|
::route/new-dialog open-rule-wizard})
|
||||||
(mm/wrap-wizard rule-wizard)
|
|
||||||
(mm/wrap-init-multi-form-state (fn [_]
|
|
||||||
(mm/->MultiStepFormState {}
|
|
||||||
[]
|
|
||||||
{}))))})
|
|
||||||
(fn [h]
|
(fn [h]
|
||||||
(-> h
|
(-> h
|
||||||
(wrap-copy-qp-pqp)
|
(wrap-copy-qp-pqp)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"/account/typeahead" ::account-typeahead
|
"/account/typeahead" ::account-typeahead
|
||||||
"/test" ::test
|
"/test" ::test
|
||||||
"/new" {:get ::new-dialog}
|
"/new" {:get ::new-dialog}
|
||||||
"/navigate" ::navigate
|
|
||||||
["/" [#"\d+" :db/id] "/edit"] ::edit-dialog
|
["/" [#"\d+" :db/id] "/edit"] ::edit-dialog
|
||||||
["/" [#"\d+" :db/id] "/delete"] ::delete
|
["/" [#"\d+" :db/id] "/delete"] ::delete
|
||||||
["/" [#"\d+" :db/id] "/run"] {:get ::execute-dialog
|
["/" [#"\d+" :db/id] "/run"] {:get ::execute-dialog
|
||||||
|
|||||||
Reference in New Issue
Block a user