(ns auto-ap.ssr.grid-page-helper (:require [auto-ap.graphql.utils :refer [extract-client-ids]] [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.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.utils :refer [html-response]] [auto-ap.time :as atime] [malli.core :as m] [bidi.bidi :as bidi] [cemerick.url :as url] [clojure.string :as str] [hiccup2.core :as hiccup] [malli.transform :as mt2] [auto-ap.ssr.hiccup-helper :as hh])) (defn row* [gridspec user entity {:keys [flash? delete-after-settle? request class] :as options}] (let [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))))) 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 {:href (str (bidi/path-for ssr-routes/only-routes (:route grid-spec)) "?remove-sort=" sort-key) :hx-boost "true" :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 (:title grid-spec) :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 ((:action-buttons grid-spec) request) :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 [])) (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}] (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 (unparse-query-params (:parsed-query-params request))))} :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 (:session request)) :clients (:clients request) :client (:client request) :identity (:identity request)} (apply com/breadcrumbs {} (:breadcrumbs grid-spec)) (table* grid-spec identity request)) (:title grid-spec))) (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] [: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 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 :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})))