critique of wizard design.
This commit is contained in:
298
docs/superpowers/plans/2025-01-15-wizard-phase-i-trial.md
Normal file
298
docs/superpowers/plans/2025-01-15-wizard-phase-i-trial.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# 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?**
|
||||
1283
docs/superpowers/plans/2025-01-15-wizard-refactor.md
Normal file
1283
docs/superpowers/plans/2025-01-15-wizard-refactor.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user