(ns auto-ap.ssr.components.wizard-state "Session-backed storage for multi-step wizards — the Django formtools `SessionStorage` model. Each wizard instance's per-step *validated* data lives in the Ring session under [:wizards :step-data ] and the steps are combined only at the very end via `get-all`. This replaces the EDN-snapshot-in-a-hidden-field round-trip (and its custom readers + merge logic): no data about other steps ever rides through the page — only an opaque `wizard-id` token. State is namespaced by `wizard-id` (a random uuid), so concurrent wizards and browser tabs don't collide, and a completed/abandoned wizard is discarded with `forget`. These functions are pure: each takes a session map and returns a new session map (or a read). The engine (`wizard2`) threads the returned session into the Ring response; the session store (cookie / durable) then persists it. Nothing here touches global state." (:require [clojure.string :as str])) (defn create-wizard! "Seed a fresh wizard instance. Returns `[wizard-id session']`. `opts`: :first-step the step key the wizard opens on (required) :context read-only data the steps need but don't edit (e.g. an entity id) — kept out of :step-data so it never gets merged into the combined result :init-data optional pre-filled per-step data ({step-key data}), e.g. when editing an existing entity so step 1 opens populated. Despite the bang, this only *computes* the next session — it doesn't mutate anything; the caller threads `session'` into its response." [session config-name {:keys [first-step context init-data]}] (let [id (str (java.util.UUID/randomUUID))] [id (assoc-in session [:wizards id] {:config-name config-name :current-step first-step :context (or context {}) :step-data (or init-data {})})])) (defn instance [session id] (get-in session [:wizards id])) (defn exists? [session id] (boolean (and id (get-in session [:wizards id])))) (defn current-step [session id] (get-in session [:wizards id :current-step])) (defn context [session id] (get-in session [:wizards id :context])) (defn step-data [session id step-key] (get-in session [:wizards id :step-data step-key])) (defn put-step "Store (REPLACE, never merge) a step's validated data. Replacing is the whole point — re-submitting a step overwrites that step only; other steps are untouched." [session id step-key data] (assoc-in session [:wizards id :step-data step-key] data)) (defn set-step "Move the wizard's current step (navigation)." [session id step-key] (assoc-in session [:wizards id :current-step] step-key)) (defn get-all "Combine every stored step's data into one map (the formtools `get_all_cleaned_data`). Combined only here, at the end — later steps win on key collisions (steps order)." [session id] (->> (get-in session [:wizards id :step-data]) vals (apply merge {}))) (defn forget "Discard the wizard instance (on completion or abandonment) so the session doesn't grow unbounded. Call from the done-fn's response." [session id] (update session :wizards dissoc id))