298 lines
8.8 KiB
Markdown
298 lines
8.8 KiB
Markdown
# Phase I Trial: Django-Formtools Style Wizard (Server-Side Storage)
|
|
|
|
## Goal
|
|
Create a **copy** of one existing wizard using Approach B (server-side session storage) without modifying any existing multi_modal.clj code. This is an isolated trial to validate the pattern.
|
|
|
|
## Trial Subject: Transaction Bulk Code Wizard
|
|
**Why this one:**
|
|
- Single-step form (simplest case)
|
|
- Currently uses wizard protocols unnecessarily
|
|
- ~420 lines in `transaction/bulk_code.clj`
|
|
- Well-contained with clear inputs/outputs
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Browser Server
|
|
| |
|
|
|-- GET /bulk-code-trial -->|
|
|
| |-- Create session entry
|
|
| |-- Return form with wizard-id
|
|
|<-- HTML + wizard-id -----|
|
|
| |
|
|
|-- POST (wizard-id + -->|
|
|
| current-step data) |-- Validate step
|
|
| |-- Store in session
|
|
| |-- Return next step or done
|
|
|<-- HTML -----------------|
|
|
```
|
|
|
|
**Key difference from current approach:**
|
|
- Current: EDN snapshot serialized in hidden form fields
|
|
- Trial: Only `wizard-id` and current step fields in form. State lives server-side in atom.
|
|
|
|
---
|
|
|
|
## Files (All New - No Existing Files Modified)
|
|
|
|
```
|
|
src/clj/auto_ap/ssr/components/wizard_trial/
|
|
state.clj - Session storage backend
|
|
core.clj - Trial wizard engine (minimal)
|
|
|
|
src/clj/auto_ap/ssr/transaction/
|
|
bulk_code_trial.clj - Trial implementation of bulk code
|
|
|
|
test/clj/auto_ap/ssr/transaction/
|
|
bulk_code_trial_test.clj - Tests for trial
|
|
```
|
|
|
|
---
|
|
|
|
## Phase I: Trial Implementation (This Week)
|
|
|
|
### Step 1: Create Session Storage
|
|
|
|
**File:** `src/clj/auto_ap/ssr/components/wizard_trial/state.clj`
|
|
|
|
```clojure
|
|
(ns auto-ap.ssr.components.wizard-trial.state)
|
|
|
|
(defonce ^:private store (atom {}))
|
|
|
|
(defn create!
|
|
"Creates new wizard session. Returns wizard-id."
|
|
[initial-data]
|
|
(let [id (str (java.util.UUID/randomUUID))]
|
|
(swap! store assoc id {:data initial-data
|
|
:created-at (java.util.Date.)})
|
|
id))
|
|
|
|
(defn get-wizard
|
|
"Retrieves wizard data by id."
|
|
[id]
|
|
(get @store id))
|
|
|
|
(defn update-step!
|
|
"Merges step data into wizard session."
|
|
[id step-key step-data]
|
|
(swap! store assoc-in [id :data step-key] step-data))
|
|
|
|
(defn get-all-data
|
|
"Returns merged data from all steps."
|
|
[id]
|
|
(-> (get-wizard id)
|
|
:data
|
|
vals
|
|
(apply merge)))
|
|
|
|
(defn destroy!
|
|
"Removes wizard session."
|
|
[id]
|
|
(swap! store dissoc id))
|
|
```
|
|
|
|
### Step 2: Create Minimal Wizard Engine
|
|
|
|
**File:** `src/clj/auto_ap/ssr/components/wizard_trial/core.clj`
|
|
|
|
```clojure
|
|
(ns auto-ap.ssr.components.wizard-trial.core
|
|
(:require [auto-ap.ssr.components.wizard-trial.state :as ws]
|
|
[malli.core :as mc]))
|
|
|
|
(defn render-step
|
|
"Renders a single step form."
|
|
[{:keys [wizard-id step-config request]}]
|
|
(let [step-data (get-in (ws/get-wizard wizard-id) [:data (:key step-config)])]
|
|
[:form {:hx-post (:submit-route step-config)
|
|
:hx-target "this"
|
|
:hx-swap "outerHTML"}
|
|
[:input {:type "hidden" :name "wizard-id" :value wizard-id}]
|
|
[:input {:type "hidden" :name "step-key" :value (name (:key step-config))}]
|
|
((:render step-config) (assoc request :step-data step-data))]))
|
|
|
|
(defn handle-submit
|
|
"Handles step submission."
|
|
[step-config request]
|
|
(let [{:keys [wizard-id step-key]} (:form-params request)
|
|
wizard-id (str wizard-id)
|
|
step-key (keyword step-key)
|
|
step-data (select-keys (:form-params request) (:fields step-config))]
|
|
|
|
(if-let [errors (mc/explain (:schema step-config) step-data)]
|
|
;; Validation failed - re-render with errors
|
|
(render-step {:wizard-id wizard-id
|
|
:step-config step-config
|
|
:request (assoc request :errors errors)})
|
|
|
|
;; Success - save and done (single step for trial)
|
|
(let [all-data (ws/get-all-data wizard-id)]
|
|
(ws/destroy! wizard-id)
|
|
((:done-fn step-config) all-data request)))))
|
|
```
|
|
|
|
### Step 3: Create Trial Bulk Code Form
|
|
|
|
**File:** `src/clj/auto_ap/ssr/transaction/bulk_code_trial.clj`
|
|
|
|
```clojure
|
|
(ns auto-ap.ssr.transaction.bulk-code-trial
|
|
(:require [auto-ap.ssr.components.wizard-trial.core :as wt]
|
|
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
|
[auto-ap.ssr.components :as com]
|
|
[auto-ap.ssr.utils :refer [html-response]]
|
|
[auto-ap.datomic :refer [conn pull-attr]]
|
|
[datomic.api :as dc]))
|
|
|
|
(def bulk-code-schema
|
|
[:map
|
|
[:vendor {:optional true} [:maybe int?]]
|
|
[:approval-status {:optional true} [:maybe keyword?]]
|
|
[:accounts {:optional true}
|
|
[:vector {:coerce? true}
|
|
[:map
|
|
[:account int?]
|
|
[:location :string]
|
|
[:percentage :double]]]]]])
|
|
|
|
(defn render-bulk-code-form
|
|
"Pure function - renders the form given data."
|
|
[{:keys [step-data errors]}]
|
|
[:div.bulk-code-trial
|
|
[:h2 "Bulk Code (Trial - Phase I)"]
|
|
|
|
[:div.space-y-4
|
|
;; Vendor field
|
|
[:div
|
|
[:label "Vendor"]
|
|
(com/typeahead {:name "vendor"
|
|
:value (:vendor step-data)
|
|
:url "/api/vendors/search"})]
|
|
|
|
;; Accounts table
|
|
[:div
|
|
[:h3 "Accounts"]
|
|
[:table
|
|
[:thead
|
|
[:tr
|
|
[:th "Account"]
|
|
[:th "Location"]
|
|
[:th "%"]]]
|
|
[:tbody
|
|
(for [[idx account] (map-indexed vector (:accounts step-data []))]
|
|
[:tr {:key idx}
|
|
[:td (com/typeahead {:name (str "accounts[" idx "][account]")
|
|
:value (:account account)})]
|
|
[:td (com/text-input {:name (str "accounts[" idx "][location]")
|
|
:value (:location account)})]
|
|
[:td (com/money-input {:name (str "accounts[" idx "][percentage]")
|
|
:value (:percentage account)})]])]]]
|
|
|
|
;; Submit
|
|
[:button {:type "submit"} "Apply Bulk Code"]]])
|
|
|
|
(def trial-step-config
|
|
{:key :bulk-code
|
|
:schema bulk-code-schema
|
|
:fields [:vendor :approval-status :accounts]
|
|
:render render-bulk-code-form
|
|
:submit-route "/transaction/bulk-code-trial"
|
|
:done-fn (fn [data request]
|
|
;; Apply bulk coding logic here
|
|
(html-response [:div.success "Bulk code applied! (Trial)"]))})
|
|
|
|
;; Route handlers
|
|
(defn open-trial [request]
|
|
(let [wizard-id (ws/create! {:accounts []})]
|
|
(wt/render-step {:wizard-id wizard-id
|
|
:step-config trial-step-config
|
|
:request request})))
|
|
|
|
(defn submit-trial [request]
|
|
(wt/handle-submit trial-step-config request))
|
|
```
|
|
|
|
### Step 4: Add Routes
|
|
|
|
In your routes file (new entries, don't modify existing):
|
|
|
|
```clojure
|
|
{::route/bulk-code-trial open-trial
|
|
::route/bulk-code-trial-submit submit-trial}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase II: Expand Trial (If Phase I Works)
|
|
|
|
**Goal:** Test with a multi-step wizard
|
|
|
|
**Subject:** New Invoice Wizard (2-3 steps)
|
|
- Step 1: Basic details
|
|
- Step 2: Accounts (conditional)
|
|
- Step 3: Submit
|
|
|
|
**New additions to trial engine:**
|
|
- Step navigation (next/prev)
|
|
- Conditional steps (skip accounts if not customizing)
|
|
- Step validation per-step
|
|
- Progress indicator
|
|
|
|
**Files to create:**
|
|
```
|
|
src/clj/auto_ap/ssr/invoice/
|
|
new_invoice_trial.clj
|
|
```
|
|
|
|
---
|
|
|
|
## Phase III: Full Migration Decision (If Phase II Works)
|
|
|
|
**Goal:** Decide whether to migrate all wizards or keep both systems
|
|
|
|
**Evaluation criteria:**
|
|
1. ✅ Line count reduction (target: 50%+)
|
|
2. ✅ Testability (pure functions easier to test)
|
|
3. ✅ Performance (server-side storage vs EDN serialization)
|
|
4. ✅ Complexity (fewer protocols/middleware)
|
|
5. ⚠️ Session handling (what happens on server restart?)
|
|
6. ⚠️ Multiple tabs (can user have two wizards open?)
|
|
|
|
**Decision matrix:**
|
|
|
|
| Criteria | Current | Trial | Winner |
|
|
|----------|---------|-------|--------|
|
|
| Lines of code | ~8,200 | ~3,000 (est.) | Trial |
|
|
| Server restarts | Survives (state in form) | Loses state | Current |
|
|
| Multiple tabs | Works (independent forms) | Needs separate IDs | Tie |
|
|
| Testability | Hard (cursor context) | Easy (pure functions) | Trial |
|
|
| Complex merges | Painful | Simple (keyed steps) | Trial |
|
|
|
|
**If trial wins:** Migrate all wizards using Phase II pattern
|
|
**If mixed:** Use trial for simple forms, keep current for complex multi-step
|
|
|
|
---
|
|
|
|
## How to Run the Trial
|
|
|
|
1. **Start server:** `lein run`
|
|
2. **Navigate to:** `/transaction/bulk-code-trial`
|
|
3. **Test:** Fill form, submit, verify state handling
|
|
4. **Compare:** Open existing `/transaction/bulk-code` in another tab
|
|
5. **Evaluate:** Which feels simpler? Which is easier to debug?
|
|
|
|
---
|
|
|
|
## Success Criteria for Phase I
|
|
|
|
- [ ] Trial renders without errors
|
|
- [ ] Form submission validates correctly
|
|
- [ ] Server-side state persists across requests
|
|
- [ ] No modifications to existing multi_modal.clj
|
|
- [ ] Code is < 200 lines (vs 420 original)
|
|
- [ ] Developer can understand flow in 5 minutes
|
|
|
|
**Ready to implement Phase I?** |