Files
integreat/docs/superpowers/plans/2025-01-15-wizard-phase-i-trial.md
2026-06-01 09:40:49 -07:00

8.8 KiB

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

(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

(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

(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):

{::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?