(ns auto-ap.ssr.components.data-grid (:require [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components.card :refer [content-card-]] [auto-ap.ssr.components.paginator :refer [paginator-]] [auto-ap.ssr.components.buttons :refer [a-button-]] [bidi.bidi :as bidi] [hiccup2.core :as hiccup] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.components.inputs :as inputs] [auto-ap.ssr.hiccup-helper :as hh] [hiccup.util :as hu])) (defn header- [params & rest] (into [:th.px-4.py-3 {:scope "col" :class (:class params) "_" (hiccup/raw (when (:sort-key params ) (format "on click trigger sorted(key:\"%s\")", (:sort-key params)))) :style (:style params)}] (if (:sort-key params) [(into [:a {:href "#"} ] rest)] rest))) (defn sort-header- [params & rest] [:th.px-4.py-3 {:scope "col" :class (:class params) "_" (hiccup/raw (format "on click trigger sorted(key:\"%s\")", (:sort-key params)))} (into [:a {:href "#"} ] rest)]) (defn row- [params & rest] (into [:tr (update params :class str " border-b dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700")] rest)) (defn cell- [params & rest] (into [:td.px-4.py-2 params ] rest)) (defn right-stack-cell- [params & rest] (cell- params (into [:div.flex.flex-row-reverse.items-center.justify-between rest]))) (defn checkbox-header- [params & rest] [:th {:scope "col", :class "p-4"} [:div {:class "flex items-center"} [:input (merge {:id "checkbox-all", :type "checkbox", :class inputs/default-checkbox-classes :name (:name params) :value (:value params)} params)] [:label {:for "checkbox-all", :class "sr-only"} "checkbox"]]]) (defn data-grid- [{:keys [headers thead-params id] :as params} & rest] [:div.shrink.overflow-y-scroll [:table (merge {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400 shrink"} (dissoc params :headers :thead-params)) [:thead (update thead-params :class #(-> "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 group-[.raw]:sticky group-[.raw]:top-0" (hh/add-class (or % "")))) (into [:tr] headers)] (into [:tbody {}] rest)]]) ;; needed for tailwind ;; lg:table-cell md:table-cell (defn raw-table-card [params & children] [:div params children]) (defn data-grid-card- [{:keys [id route title paginate? action-buttons total subtitle thead-params start per-page flash-id headers raw? rows] :as params} & children] (let [card (if raw? raw-table-card content-card-)] (card (cond-> { :id id :class (cond-> "group" raw? (hh/add-class "raw h-full flex flex-col overflow-hidden"))} route (assoc :hx-get (bidi/path-for ssr-routes/only-routes route :request-method :get) :hx-trigger "clientSelected from:body, invalidated from:body" :hx-swap "outerHTML swap:300ms")) [:div {:class " group-[.raw]:hidden flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"} [:h1.text-2xl.mb-3.font-bold title] [:div {:class "flex items-center flex-1 space-x-4"} [:h5 (when subtitle [:span subtitle])]] (into [:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}] action-buttons)] [:div {:class "overflow-x-auto contents"} (data-grid- {:headers headers :thead-params thead-params} rows)] (when (or paginate? (nil? paginate?)) [:div {:class "contents group-[.raw]:block"} (paginator- {:start start :end (Math/min (+ start per-page) total) :per-page per-page :total total :a-params (fn [page] ;; TODO it might be good to have a more global form defined in the specific page ;; with elements that are part of item ;; that way this is not deeply coupled ;; for example, including filters and pagination is awkward ;; TODO the other way to think about this is that we want the request to include ;; all of the correct parameters, not parameters to merge with the current ones. ;; think sorting, filters, pagination {:hx-get (hu/url (bidi/path-for ssr-routes/only-routes route :request-method :get) {:start (* page per-page)}) :hx-target (str "#" id) :hx-swap "outerHTML show:#app:top" :hx-indicator (str "#" id)}) :per-page-params {:hx-get (hu/url (bidi/path-for ssr-routes/only-routes route :request-method :get)) :hx-trigger "change" :hx-include "this" :hx-target (str "#" id) ; :hx-swap "outerHTML show:#app:top" :hx-indicator (str "#" id)}})]) children [:div {:class "htmx-indicator absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2 overflow-hidden w-full h-full"} [:div {:class "flex items-center justify-center w-full h-full border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 bg-opacity-50" } [:div {:class "px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200"} "loading..."]]]))) (defn new-row- [{:keys [index colspan tr-params row-offset] :as params} & content] (row- (merge {:class "new-row" :x-data (hx/json {:newRowIndex index :offset (or row-offset 0)}) } tr-params) (cell- {:colspan colspan :class "bg-gray-100"} [:div.flex.justify-center (a-button- (merge (dissoc params :index :colspan) { "@click.prevent" "$dispatch('newRow', {index: (newRowIndex++)})" :color :secondary :hx-trigger "newRow" :hx-vals (hiccup/raw "js:{index: event.detail.index }") :hx-target "closest .new-row" :hx-swap "beforebegin"}) content)])))