Files
integreat/src/clj/auto_ap/ssr/grid_page_helper.clj
2024-04-10 09:58:05 -07:00

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