Builds client SSR approach, sunsets old cljs.

This commit is contained in:
2024-01-09 21:40:43 -08:00
parent d824cdfff4
commit 8063a8fcbd
74 changed files with 4603 additions and 4047 deletions

View File

@@ -8,8 +8,10 @@
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
[auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.routes.admin.import-batch :as ib-routes]
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.excel-invoices :as ei-routes]
[auto-ap.routes.admin.vendors :as v-routes]))
[auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.graphql.clients :as clients]))
(defn menu-button- [params & children]
[:div
@@ -199,7 +201,7 @@
[:li
(menu-button- {:icon svg/restaurant
:href (bidi/path-for client-routes/routes :admin-clients)
:href (bidi/path-for ssr-routes/only-routes ::ac-routes/page)
:target "_new"}
"Clients")]
[:li

View File

@@ -176,7 +176,7 @@
(defn validated-save-button- [{:keys [errors class] :as params} & children]
(button- (-> {:color (or (:color params) :primary)
(button- (-> {:color (or (:color params) :primary)
:type "submit" :class (cond-> (or class "")
true (hh/add-class "w-32")
(seq errors) (hh/add-class "animate-shake"))}

View File

@@ -1,13 +1,17 @@
(ns auto-ap.ssr.components.card
(:require [auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx]))
[auto-ap.ssr.hx :as hx]
[clojure.string :as str]))
(defn card- [params & children]
(into [:div (update params :class str " shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white overflow-hidden")]
(into [:div (update params :class
#(cond-> (or % "")
(not (str/includes? % "bg-")) (hh/add-class "dark:bg-gray-800 bg-white ")
true (hh/add-class "shadow-md sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 overflow-hidden")))]
children))
(defn content-card- [params & children]
[:section {:class (hh/add-class " py-3 sm:py-5" (:class params))}
[:section (merge params {:class (hh/add-class " py-3 sm:py-5" (:class params))})
[:div {:class "max-w-screen-2xl"}
(into
[:div {:class "relative overflow-hidden shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}]

View File

@@ -3,36 +3,19 @@
[auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx]))
(defn modal- [params & children]
(defn modal-
"This modal function is used to create a modal window with a stack that allows for transitioning between modals.
:params should include the following keys:
- :handle-unexpected-error? (default: true) - A boolean indicating whether to handle unexpected errors.
- :class (optional) - A string representing additional CSS classes to add to the modal.
&children should include the child components to be rendered within the modal."
[{:as params} & children]
[:div (-> params
(assoc "@click.outside" "open=false"
:x-data (hx/json {:index 0 :hidingIndex -1 :unexpectedError false :transitioning false})
"x-on:htmx:response-error" "unexpectedError=true"
"x-on:htmx:before-request" "unexpectedError=false"
:x-ref "modalStack"
"@modalnext.window"
" $refs.modalStack.children[index].setAttribute('x-transition:leave-end', '-translate-x-full scale-0 opacity-0' );
$refs.modalStack.children[index + 1].setAttribute('x-transition:enter-start', 'translate-x-full scale-0 opacity-0' );
hidingIndex = index;
setTimeout(() => {index ++; transitioning=true; hidingIndex = -1; }, 150);
setTimeout(() => transitioning=false, 320)"
"@modalprevious.window"
" $refs.modalStack.children[index].setAttribute('x-transition:leave-end', 'translate-x-full scale-0 opacity-0' );
$refs.modalStack.children[index - 1].setAttribute('x-transition:enter-start', '-translate-x-full scale-0 opacity-0' );
hidingIndex = index;
setTimeout(() => { index --; hidingIndex = -1; transitioning=true; }, 150);
setTimeout(() => transitioning=false, 320)"
"@modalpop.window"
" $refs.modalStack.children[index].setAttribute('x-transition:leave-end', 'translate-x-full scale-0 opacity-0' );
$refs.modalStack.children[index - 1].setAttribute('x-transition:enter-start', '-translate-x-full scale-0 opacity-0' );
hidingIndex = index;
setTimeout(() => {index --; transitioning=true;}, 150);
setTimeout(() => { $refs.modalStack.removeChild($refs.modalStack.children[index+1]); hidingIndex=-1; }, 300);
setTimeout(() => transitioning=false, 320)
"
)
(assoc "@click.outside" "open=false")
(dissoc :handle-unexpected-error?)
(update :class (fnil hh/add-class "") "w-full h-full modal-stack"))
children])
@@ -40,11 +23,10 @@
[:div (update params
:class (fn [c] (-> c
(or "")
(hh/add-class "w-full p-4 h-full modal-card")
)))
(hh/add-class "w-full p-4 h-full modal-card"))))
[:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col h-full"}
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header]
[:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"}
[:div {:class "px-6 py-2 space-y-6 overflow-y-scroll w-full shrink"}
#_[:div.bg-green-300.w-full.h-64
"hello"]
content]
@@ -55,28 +37,6 @@
[:div {:class "shrink-0"}
footer]])]])
(defn stacked-modal-card- [index params header content footer]
[:div (merge params
{:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params ""))
:x-data (hx/json {:i index})
:x-show "index == i && hidingIndex != i"
"x-trap" "index == i && hidingIndex == -1 && !transitioning"
"x-transition:enter" "transition duration-150",
"x-transition:enter-end" "translate-x-0 scale-100 opacity-100",
"x-transition:leave" "transition duration-150",
"x-transition:leave-start" "translate-x-0 scale-100 opacity-100",
})
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header] ;; todo componentize these
[:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"}
content] ;; TODO componentize
(when footer [:div {:class "p-4"}
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
[:span {:class "w-2 h-2 bg-red-500 rounded-full"}]
[:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]]
[:div {:class "shrink-0"}]footer])])
(defn modal-header- [params & children]
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"}
children])
@@ -92,21 +52,14 @@
(defn modal-footer- [params & children]
[:div {:class "p-4"}
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex
(hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
[:span {:class "w-2 h-2 bg-red-500 rounded-full"}]
[:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]]
[:div {:class "shrink-0"}]
children])
(defn stacked-modal-card-2- [index params & children]
(defn modal-card-advanced- [params & children]
[:div (merge params
{:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params ""))
:x-data (hx/json {:i index})
:x-show "index == i && hidingIndex != i"
"x-trap" "index == i && hidingIndex == -1 && !transitioning"
"x-transition:enter" "transition duration-150",
"x-transition:enter-end" "translate-x-0 scale-100 opacity-100",
"x-transition:leave" "transition duration-150",
"x-transition:leave-start" "translate-x-0 scale-100 opacity-100",
})
{:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params "")) })
children])

View File

@@ -0,0 +1,349 @@
(ns auto-ap.ssr.components.multi-modal
(:require [auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.utils
:refer [ html-response
assert-schema
main-transformer
modal-response
wrap-form-4xx-2
wrap-schema-enforce]]
[auto-ap.ssr.components.timeline :as timeline]
[bidi.bidi :as bidi]
[hiccup.util :as hu]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.hx :as hx]
[malli.core :as mc]
[hiccup2.core :as hiccup2]
[hiccup2.core :as hiccup]
[auto-ap.cursor :as cursor]
[malli.core :as m]
[auto-ap.logging :as alog])
(:import [auto_ap.cursor VecCursor]))
(def default-form-props {:hx-ext "response-targets"
:hx-swap "outerHTML"
:hx-target-400 "#form-errors .error-content"
:hx-trigger "submit"
:hx-target "this"
"x-trap" "true"
:class "h-full w-full" })
(defprotocol ModalWizardStep
(step-key [this])
(edit-path [this request])
(render-step [this request])
(step-schema [this])
(step-name [this]))
(defprotocol Initializable
(init-step-params [this request]))
(defprotocol Discardable
(can-discard? [this step-params])
(discard-changes [this request]))
(defn- init-step-params- [step request]
(if (satisfies? Initializable step)
(init-step-params step request)
{}))
(defprotocol LinearModalWizard
(hydrate-from-request [this request])
(get-current-step [this])
(navigate [this step-key])
(form-schema [this])
(steps [this])
(get-step [this step-key])
(render-wizard [this request])
(submit [this request]))
(defrecord MultiStepFormState [snapshot edit-path step-params])
(defn select-state [multi-form-state edit-path default]
(->MultiStepFormState (:snapshot multi-form-state)
edit-path
(or (get-in (:snapshot multi-form-state) edit-path)
default)))
(defn merge-multi-form-state [{:keys [snapshot edit-path step-params] :as multi-form-state}]
(let [cursor (cursor/cursor (or snapshot {}))
;; this hack makes sure that, in the event of a missing vector entry, will make sure to add it first
edit-cursor (cond-> cursor
(seq edit-path) (cursor/ensure-path! edit-path {})
(seq edit-path) (get-in edit-path {}))
_ (cursor/transact! edit-cursor (fn [spot]
(merge spot step-params)))]
(assoc multi-form-state
:snapshot @cursor
:edit-path []
:step-params @cursor)))
(def step-key-schema (mc/schema [:orn {:decode/arbitrary clojure.edn/read-string
:encode/arbitrary pr-str}
[:sub-step [:cat :keyword [:or :int :string]]]
[:step :keyword]]))
(def encode-step-key
(m/-instrument {:schema [:=> [:cat step-key-schema] :any]}
(fn encode-step-key [sk]
(mc/encode step-key-schema sk main-transformer))))
(defn render-timeline [linear-wizard current-step validation-route]
(let [step-names (map #(step-name (get-step linear-wizard %)) (steps linear-wizard))
active-index (.indexOf step-names (step-name current-step))]
(timeline/vertical-timeline
{}
(for [[n i] (map vector (steps linear-wizard) (range))]
(timeline/vertical-timeline-step (cond-> {}
(= i active-index) (assoc :active? true)
(< i active-index) (assoc :visited? true)
(= i (dec (count step-names))) (assoc :last? true))
[:a.cursor-pointer.whitespace-nowrap {:x-data (hx/json {:timelineIndex i})
:hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route)
{:from (encode-step-key (step-key current-step))
:to (encode-step-key (step-key (get-step linear-wizard n)))})}
(step-name (get-step linear-wizard n))])))))
(defn back-button [linear-wizard step validation-route]
[:a.cursor-pointer.whitespace-nowrap {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route)
{:from (encode-step-key (step-key step))
:to (encode-step-key (->> (partition-all 2 1 (steps linear-wizard))
(filter (fn [[from to]]
(= to (step-key step))))
ffirst))})}
"Back"])
(defn default-next-button [linear-wizard step validation-route]
(let [steps (steps linear-wizard)
last? (= (step-key step) (last steps))
next-step (when-not last? (->> steps
(drop-while #(not= (step-key step)
%))
(drop 1)
first
(get-step linear-wizard)))]
(com/validated-save-button (cond-> {:errors (seq fc/*form-errors*)
;;:x-data (hx/json {})
:x-ref "next"
:class "w-48"}
(not last?) (assoc :hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route)
{:from (encode-step-key (step-key step))
:to (encode-step-key (step-key next-step))})))
(if next-step
(step-name next-step)
"Save")
(when-not last?
[:div.w-5.h-5 svg/arrow-right]))))
(defn default-step-body [params & children]
[:div.space-y-1 {:class "w-[600px] h-[700px]"}
children])
(defn default-step-footer [linear-wizard step & {:keys [validation-route
discard-button
next-button]}]
[:div.flex.justify-end
[:div.flex.items-baseline.gap-x-4
(com/form-errors {:errors (:errors (:step-params fc/*form-errors*))})
(when (not= (first (steps linear-wizard))
(step-key step))
(when validation-route
(back-button linear-wizard step validation-route)))
(when (and (satisfies? Discardable step) (can-discard? step @fc/*current*))
discard-button)
(cond next-button
next-button
validation-route
(default-next-button linear-wizard step validation-route)
:else
[:div "No action possible."])]])
(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route]}]
(let [is-last? (= (step-key step) (last (steps linear-wizard)))]
(com/modal-card-advanced
{"@keydown.enter.prevent.stop" "$refs.next.click()"
:class (str (when is-last? "last-modal-step")
" transition duration-300 ease-in-out
")
":class" (hiccup/raw "{
\"htmx-swapping:-translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='forward',
\"htmx-swapping:translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:-translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='backward'
}
")
"x-data" ""}
(com/modal-header {}
head)
#_(com/modal-header-attachment {})
[:div.flex.shrink
[:div.grow-0.pr-6.pt-2.bg-gray-100.self-stretch #_{:style "margin-left:-20px"} (render-timeline linear-wizard step validation-route)]
(com/modal-body {}
body)]
(com/modal-footer {}
footer))))
(defn wrap-ensure-step [handler]
(->
(fn [{:keys [wizard multi-form-state] :as request}]
(assert-schema (step-schema (get-current-step wizard)) (:step-params multi-form-state))
(handler request))
(wrap-form-4xx-2 (fn [{:keys [wizard] :as request}] ;; THIS MAY BE BETTER TO JUST MAKE THE LINEAR WIZARD POPULATE FROM THE REQUEST
(html-response
(render-wizard wizard request)
:headers {"x-transition-type" "none"
"HX-reswap" "outerHTML"})))))
(defn get-transition-type [wizard from-step-key to-step-key]
(let [to-step-index (.indexOf (steps wizard) to-step-key)
from-step-index (.indexOf (steps wizard)
from-step-key)]
(cond (= -1 to-step-index)
nil
(= -1 from-step-index)
nil
(= from-step-index to-step-index)
nil
(> from-step-index to-step-index)
"backward"
:else
"forward")))
(def next-handler
(-> (fn [{:keys [wizard] :as request}]
(let [current-step (get-current-step wizard)
to-step (:to (:query-params request))
wizard (navigate wizard to-step)
new-step (get-current-step wizard)
transition-type (get-transition-type wizard (step-key current-step) to-step)]
(html-response
(render-wizard wizard
(-> request
(assoc :multi-form-state (-> (:multi-form-state request)
(merge-multi-form-state)
(select-state
(edit-path new-step request)
(init-step-params- new-step request))))))
:headers {"HX-reswap" (when transition-type "outerHTML swap:0.15s")
"x-transition-type" (or transition-type "none")})))
(wrap-ensure-step)
(wrap-schema-enforce :query-schema
[:map
[:to step-key-schema]])))
(def discard-handler
(->
(fn [{:keys [wizard multi-form-state] :as request}]
(let [current-step (get-current-step wizard)
to-step (:to (:query-params request))
wizard (navigate wizard to-step)
transition-type (get-transition-type wizard (step-key current-step) to-step)]
(html-response
(render-wizard wizard
(-> request
(assoc :multi-form-state (discard-changes current-step multi-form-state))))
:headers {"HX-reswap" (when transition-type "outerHTML swap:0.15s")
"x-transition-type" (or transition-type "none")})))
(wrap-schema-enforce :query-schema
[:map
[:to step-key-schema]])))
(def submit-handler
(-> (fn [{:keys [wizard multi-form-state] :as request}]
(submit wizard (-> request
(assoc :multi-form-state (merge-multi-form-state multi-form-state)))))
(wrap-ensure-step)))
(defn default-render-wizard [linear-wizard {:keys [multi-form-state form-errors snapshot current-step] :as request} & {:keys [form-params]}]
(let [current-step (get-current-step linear-wizard)
edit-path (edit-path current-step request)]
[:form#wizard-form form-params
(fc/start-form multi-form-state (when form-errors {:step-params form-errors})
(list
(fc/with-field :snapshot
(com/hidden {:name (fc/field-name)
:value (pr-str (fc/field-value))}))
(fc/with-field :edit-path
(com/hidden {:name (fc/field-name)
:value (pr-str (or edit-path []))}))
(com/hidden {:name "current-step"
:value (pr-str (step-key current-step))})
(fc/with-field :step-params
(com/modal
{:id "wizardmodal"}
(render-step current-step request)))))]))
(defn wrap-wizard [handler linear-wizard]
(fn [request]
(let [current-step-key (if-let [current-step (get (:form-params request) "current-step")]
(mc/decode step-key-schema current-step main-transformer)
(first (steps linear-wizard)))
current-step (get-step linear-wizard current-step-key)
multi-form-state (-> (:multi-form-state request)
(update :snapshot (fn [snapshot]
(mc/decode (form-schema linear-wizard)
snapshot
main-transformer)))
(update :step-params (fn [step-params]
(or
(mc/decode (step-schema current-step)
step-params
main-transformer)
{} ;; Todo add a defaultable
))))
request (-> request
(assoc :multi-form-state multi-form-state))
linear-wizard (navigate linear-wizard current-step-key)]
(handler
(assoc request :wizard (hydrate-from-request linear-wizard request))))))
(defn open-wizard-handler [{:keys [wizard current-step] :as request}]
(modal-response
[:div {:x-data (hx/json {"transitionType" "none"
}
)
"@htmx:after-request" "if(event.detail.xhr.getResponseHeader('x-transition-type')) { $data.transitionType = event.detail.xhr.getResponseHeader('x-transition-type');}"
}
(render-wizard wizard request)]))
(defn wrap-init-multi-form-state [handler get-multi-form-state]
(->
(fn init-multi-form [request]
(handler (assoc request :multi-form-state (get-multi-form-state request))))
(wrap-nested-form-params)))
(defn wrap-decode-multi-form-state [handler]
(wrap-init-multi-form-state
handler
(fn parse-multi-form-state [request]
(map->MultiStepFormState (mc/decode [:map
[:snapshot {:optional true
:decode/arbitrary
#(clojure.edn/read-string {:readers clj-time.coerce/data-readers
:eof nil}
%)}
[:maybe :any]]
[:edit-path {:optional true :decode/arbitrary (fn [z]
(clojure.edn/read-string z))} [:maybe [:sequential {:min 0} any?]]]
[:step-params {:optional true}
[:maybe
:any]]]
(:form-params request)
main-transformer)))))

View File

@@ -2,9 +2,9 @@
(:require [auto-ap.ssr.hiccup-helper :as hh]))
(defn timeline-step [{:keys [active? visited? last?]} & children]
(if active?
(if active?
[:li {:class "flex items-center text-primary-600 font-medium dark:text-primary-500"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border-2 border-primary-600 rounded-full shrink-0 dark:border-primary-500"} ]
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border-2 border-primary-600 rounded-full shrink-0 dark:border-primary-500"}]
children
(when-not last?
[:svg {:class "w-3 h-3 ml-2 sm:ml-4", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 12 10"}
@@ -24,5 +24,24 @@
[:ol {:class "flex items-center w-full space-x-2 text-xs text-center text-gray-500 bg-white dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-x-4 px-2"}
children
#_[:li {:class "flex items-center"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"} ]]])
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"}]]])
(defn vertical-timeline-step [{:keys [active? visited? last?]} & children]
(if active?
[:li {:class "flex items-center text-primary-600 font-medium dark:text-primary-500"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border-2 border-primary-600 rounded-full shrink-0 dark:border-primary-500"}]
children ]
[:li {:class (cond-> "flex items-center"
(not visited?) (hh/add-class "text-gray-400"))}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"}
(when visited?
[:svg {:class "w-3 h-3 text-primary-600 dark:text-primary-500", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 16 12"}
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M1 5.917 5.724 10.5 15 1.5"}]])]
children ]))
(defn vertical-timeline [params & children]
[:ol {:class "flex flex-col items-start space-y-2 text-xs text-center text-gray-500 bg-gray-100 dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-y-4 px-2"}
children
#_[:li {:class "flex items-center"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"}]]])