347 lines
19 KiB
Clojure
347 lines
19 KiB
Clojure
(ns auto-ap.ssr.grid-page-helper
|
|
(:require [auto-ap.graphql.utils :refer [extract-client-ids]]
|
|
[auto-ap.logging :as alog]
|
|
[auto-ap.query-params :as query-params]
|
|
[auto-ap.routes.utils
|
|
:refer [wrap-client-redirect-unauthenticated wrap-secure]]
|
|
[auto-ap.ssr-routes :as ssr-routes]
|
|
[auto-ap.ssr.components :as com]
|
|
[auto-ap.ssr.hiccup-helper :as hh]
|
|
[auto-ap.ssr.hx :as hx]
|
|
[auto-ap.ssr.svg :as svg]
|
|
[auto-ap.ssr.ui :refer [base-page]]
|
|
[auto-ap.ssr.utils :refer [html-response main-transformer]]
|
|
[auto-ap.time :as atime]
|
|
[bidi.bidi :as bidi]
|
|
[cemerick.url :as url]
|
|
[clojure.string :as str]
|
|
[hiccup2.core :as hiccup]
|
|
[malli.core :as m]
|
|
[malli.transform :as mt2]
|
|
[malli.transform :as mt]
|
|
[taoensso.encore :refer [filter-vals]]))
|
|
|
|
(defn row* [gridspec user entity {:keys [flash? delete-after-settle? request class] :as options}]
|
|
(let [cells (if (:check-boxes? gridspec)
|
|
[(com/data-grid-cell {} (com/checkbox {:name "id" :value ((:id-fn gridspec) entity)
|
|
:x-model "selected"}))]
|
|
[])
|
|
cells (->> gridspec
|
|
:headers
|
|
(filter (fn [h]
|
|
(if (and (:hide? h)
|
|
((:hide? h) request))
|
|
nil
|
|
h)))
|
|
(mapv (fn [header]
|
|
(com/data-grid-cell {:class (if-let [show-starting (:show-starting header)]
|
|
(format "hidden %s:table-cell" show-starting)
|
|
(:class header))}
|
|
((:render header) entity))))
|
|
(into cells))
|
|
cells (conj cells (com/data-grid-right-stack-cell {}
|
|
(into [:form.flex.space-x-2
|
|
[:input {:type :hidden :name "id" :value ((:id-fn gridspec) entity)}]]
|
|
((:row-buttons gridspec) request entity))))] ;; TODO double check usage of row buttons user and identity in callers
|
|
(apply com/data-grid-row
|
|
{:class (cond-> (or class "")
|
|
flash? (hh/add-class "live-added"))
|
|
"_" (hiccup/raw (when delete-after-settle?
|
|
" on htmx:afterSettle wait 400ms then remove me"))
|
|
|
|
:data-id ((:id-fn gridspec) entity)}
|
|
cells)))
|
|
|
|
(defn sort-icon [sort key]
|
|
(->> sort
|
|
(filter (comp #(= key %) :sort-key))
|
|
first
|
|
:sort-icon))
|
|
|
|
(defn sort-by-list [grid-spec sort]
|
|
(if (seq sort)
|
|
(into
|
|
[:div.flex.gap-2.items-center
|
|
|
|
"sorted by"]
|
|
(for [{:keys [name sort-icon sort-key]} sort]
|
|
[:div.py-1.px-3.text-sm.rounded.bg-gray-100.dark:bg-gray-600.flex.items-center.gap-2.relative name [:div.h-4.w-4.mr-3 sort-icon]
|
|
[:div {:class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white hover:scale-110 transition-all duration-300 bg-gray-400 border-2 border-white rounded-full -top-2 -right-2 dark:border-gray-900"}
|
|
[:a {:hx-get (str (bidi/path-for ssr-routes/only-routes
|
|
(:route grid-spec)) "?remove-sort=" sort-key)
|
|
:href "#"
|
|
:hx-target (str "#" (:id grid-spec))}
|
|
[:div.h-4.w-4 svg/x]]]]))
|
|
"default sort"))
|
|
|
|
(defn table* [grid-spec user {{:keys [start per-page flash-id sort]} :parsed-query-params :as request}]
|
|
(let [start (or start 0)
|
|
per-page (or per-page 25)
|
|
[entities total] ((:fetch-page grid-spec)
|
|
request)]
|
|
|
|
(com/data-grid-card {:id (:id grid-spec)
|
|
:title (if (string? (:title grid-spec))
|
|
(:title grid-spec)
|
|
((:title grid-spec) request))
|
|
:route (:route grid-spec)
|
|
:start start
|
|
:per-page per-page
|
|
:total total
|
|
:subtitle [:div.flex.items-center.gap-2
|
|
[:span (format "Total %s: %d, " (:entity-name grid-spec) total)]
|
|
(sort-by-list grid-spec sort)]
|
|
:action-buttons (cond->> ((:action-buttons grid-spec) request)
|
|
(:check-boxes? grid-spec) (into [(com/pill {:color :primary
|
|
:x-show "selected.length > 0"}
|
|
[:div.flex.space-x-2.items-center
|
|
[:div
|
|
|
|
[:span {:x-text "selected.length" :x-show "!all_selected"}]
|
|
[:span {:x-show "all_selected"} "All"]
|
|
" selected"]
|
|
[:div.w-3.h-3
|
|
(com/link {"@click" "selected=[]; all_selected=false"}
|
|
svg/x)]])]))
|
|
:rows (for [entity entities]
|
|
(row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity)) :request request}))
|
|
:thead-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
|
(:route grid-spec))
|
|
:hx-target (str "#" (:id grid-spec))
|
|
:hx-indicator (str "#" (:id grid-spec))
|
|
:hx-trigger "sorted once"
|
|
:hx-vals "js:{\"toggle-sort\": event.detail.key || \"\"}"}
|
|
:headers
|
|
(conj
|
|
(->> grid-spec
|
|
:headers
|
|
(map
|
|
(fn [h]
|
|
(cond
|
|
(and (:hide? h)
|
|
((:hide? h) request))
|
|
nil
|
|
|
|
(:sort-key h)
|
|
(com/data-grid-sort-header {:class (if-let [show-starting (:show-starting h)]
|
|
(format "hidden %s:table-cell" show-starting)
|
|
(:class h))
|
|
:sort-key (:sort-key h)}
|
|
|
|
[:div.flex.gap-4.items-center
|
|
(:name h)
|
|
[:div.h-6.w-6.text-gray-400.dark:text-gray-500 (sort-icon sort (:sort-key h))]])
|
|
|
|
:else
|
|
(com/data-grid-header {:class (if-let [show-starting (:show-starting h)]
|
|
(format "hidden %s:table-cell" show-starting)
|
|
(:class h))
|
|
:sort-key (:sort-key h)}
|
|
(:name h)))))
|
|
(filter identity)
|
|
(into (if (:check-boxes? grid-spec)
|
|
[(com/data-grid-checkbox-header {:name "all" :value "all" :x-model "all_selected"})]
|
|
[])))
|
|
(com/data-grid-header {}))})))
|
|
|
|
|
|
(defn sort->query [s]
|
|
(str/join "," (map (fn [k] (format "%s:%s" (:sort-key k) (if (= true (:asc k))
|
|
"asc"
|
|
"desc")))
|
|
s)))
|
|
|
|
(defn default-unparse-query-params [query-params]
|
|
(reduce
|
|
(fn [query-params [k value]]
|
|
(assoc query-params k
|
|
(cond (= k :sort)
|
|
(sort->query value)
|
|
|
|
(instance? org.joda.time.base.AbstractInstant value)
|
|
(atime/unparse-local value atime/normal-date)
|
|
|
|
(instance? Long value)
|
|
(str value)
|
|
|
|
(instance? Double value)
|
|
(format "%.2f" value)
|
|
|
|
(instance? Float value)
|
|
(format "%.2f" value)
|
|
|
|
(keyword? value)
|
|
(name value)
|
|
|
|
(and (map? value)
|
|
(:db/id value))
|
|
(:db/id value)
|
|
|
|
:else
|
|
value)))
|
|
query-params
|
|
query-params))
|
|
|
|
(defn default-parse-query-params [grid-spec]
|
|
(comp
|
|
(query-params/apply-remove-sort)
|
|
(query-params/apply-toggle-sort grid-spec)
|
|
(query-params/apply-date-range :date-range :start-date :end-date)
|
|
(query-params/parse-key :exact-match-id query-params/parse-long)
|
|
(query-params/parse-key :sort #(query-params/parse-sort grid-spec %))
|
|
(query-params/parse-key :per-page query-params/parse-long)
|
|
(query-params/parse-key :start query-params/parse-long)
|
|
(query-params/parse-key :start-date query-params/parse-date)
|
|
(query-params/parse-key :end-date query-params/parse-date)))
|
|
|
|
(defn wrap-trim-client-ids [handler]
|
|
(fn trim-client-ids [request]
|
|
(let [valid-clients (extract-client-ids (:clients request)
|
|
(:client request)
|
|
(:client-id (:parsed-query-params request))
|
|
(when (:client-code (:parsed-query-params request))
|
|
[:client/code (:client-code (:parsed-query-params request))]))
|
|
valid-clients (->> valid-clients
|
|
(take 20)
|
|
set)]
|
|
(handler (assoc request :trimmed-clients valid-clients)))))
|
|
|
|
(defn table-route [grid-spec]
|
|
(-> (fn table [{:keys [identity] :as request}]
|
|
(alog/peek ::TABLE-QP (:parsed-query-params request))
|
|
(let [unparse-query-params (or (:unparse-query grid-spec)
|
|
default-unparse-query-params)]
|
|
(html-response (table*
|
|
grid-spec
|
|
identity
|
|
request)
|
|
:headers {"hx-push-url" (str "?" (url/map->query
|
|
(dissoc (if (:query-schema grid-spec)
|
|
(do
|
|
(alog/peek ::setup4
|
|
(pr-str (update (filter-vals #(not (nil? %))
|
|
(m/encode (:query-schema grid-spec)
|
|
(:query-params request)
|
|
main-transformer))
|
|
"sort" sort->query)))
|
|
(update (filter-vals #(not (nil? %))
|
|
(m/encode (:query-schema grid-spec)
|
|
(:query-params request)
|
|
main-transformer))
|
|
"sort" sort->query))
|
|
(unparse-query-params (:parsed-query-params request)))
|
|
"selected" "all-selected")))} ;; TODO seems hacky to special case selected and all-selected here
|
|
:oob (when-let [oob-render (:oob-render grid-spec)]
|
|
(oob-render request)))))
|
|
(wrap-trim-client-ids)
|
|
(query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
|
|
(default-parse-query-params grid-spec)))
|
|
(wrap-secure)
|
|
(wrap-client-redirect-unauthenticated)))
|
|
|
|
(defn page-route [grid-spec]
|
|
(-> (fn page [{:keys [identity] :as request}]
|
|
(base-page
|
|
request
|
|
(com/page {:nav (:nav grid-spec)
|
|
:page-specific (when-let [page-specific-nav (:page-specific-nav grid-spec)]
|
|
[:div#page-specific-nav (page-specific-nav request)])
|
|
:client-selection (:client-selection request)
|
|
:clients (:clients request)
|
|
:client (:client request)
|
|
:identity (:identity request)
|
|
:request request}
|
|
(apply com/breadcrumbs {} (:breadcrumbs grid-spec))
|
|
[:div {:x-data (hx/json {:selected [] :all_selected false})
|
|
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
|
:x-init "$watch('selected', s=> $dispatch('selectedChanged', {selected: s, all_selected: all_selected}) );
|
|
$watch('all_selected', a=>$dispatch('selectedChanged', {selected: selected, all_selected: a}))"}
|
|
|
|
(table* grid-spec
|
|
identity
|
|
request)])
|
|
(if (string? (:title grid-spec))
|
|
(:title grid-spec)
|
|
((:title grid-spec) request))))
|
|
(wrap-trim-client-ids)
|
|
(query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
|
|
(default-parse-query-params grid-spec)))
|
|
(wrap-secure)
|
|
(wrap-client-redirect-unauthenticated)))
|
|
|
|
(def request-spec (m/schema [:map]))
|
|
(def entity-spec (m/schema [:map]))
|
|
(def header-spec (m/schema [:map
|
|
[:key :string]
|
|
[:name :string]
|
|
[:header-class {:optional true} [:maybe :string]]
|
|
[:sort-key {:optional true} :string]
|
|
[:render [:=> [:cat entity-spec] :any]]
|
|
[:hide? {:optional true} [:=> [:cat entity-spec] :boolean]]]))
|
|
(def grid-spec (m/schema [:map
|
|
[:id :string]
|
|
[:nav [:=>
|
|
[:cat request-spec]
|
|
vector?]]
|
|
[:page-specific-nav
|
|
{:optional true
|
|
:default (fn [request])}
|
|
[:maybe [:=>
|
|
[:cat request-spec]
|
|
vector?]]]
|
|
[:id-fn {:default :db/id
|
|
:optional true}
|
|
[:=>
|
|
[:cat map?]
|
|
nat-int?]]
|
|
[:fetch-page [:=>
|
|
[:cat request-spec]
|
|
[:cat [:vector entity-spec] :int]]]
|
|
[:parse-query-params
|
|
{:optional true}
|
|
[:=>
|
|
[:cat [:map-of :keyword :any]]
|
|
[:map-of :keyword :any]]]
|
|
[:oob-render
|
|
{:optional true
|
|
:default (fn [request])}
|
|
[:=>
|
|
[:cat request-spec]
|
|
vector?]]
|
|
[:breadcrumbs [:vector vector?]]
|
|
[:title [:or :string
|
|
[:=> [:cat [:map-of :keyword :any]]
|
|
:string]]]
|
|
[:entity-name :string]
|
|
[:route :keyword]
|
|
[:action-buttons
|
|
{:default (fn [request])
|
|
:optional true}
|
|
[:=>
|
|
[:cat request-spec]
|
|
[:maybe [:vector vector?]]]]
|
|
[:row-buttons
|
|
{:default (fn [request entity])
|
|
:optional true}
|
|
[:=>
|
|
[:cat request-spec entity-spec]
|
|
[:maybe [:vector vector?]]]]
|
|
[:headers [:vector header-spec]]]))
|
|
|
|
(defn build [grid-page]
|
|
(when-not (m/validate grid-spec grid-page)
|
|
(throw (ex-info "Could not validate grid"
|
|
(m/explain grid-spec grid-page))))
|
|
(m/decode grid-spec grid-page (mt2/default-value-transformer {::mt2/add-optional-keys true})))
|
|
|
|
(defn wrap-apply-sort [handler grid-spec]
|
|
(fn apply-sort [request]
|
|
(handler (update request :query-params
|
|
(fn [qp]
|
|
((comp
|
|
(query-params/apply-remove-sort)
|
|
(query-params/apply-toggle-sort grid-spec)
|
|
(query-params/parse-key :sort #(query-params/parse-sort grid-spec %)))
|
|
qp))))))
|
|
|