Skip to content

Instantly share code, notes, and snippets.

@awkay
Created August 11, 2022 18:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save awkay/023c974212ec3c928695b6f299565b00 to your computer and use it in GitHub Desktop.
Save awkay/023c974212ec3c928695b6f299565b00 to your computer and use it in GitHub Desktop.
(ns rad.server-paginated-report
(:require
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
#?(:clj [com.fulcrologic.fulcro.dom-server :as dom :refer [div]]
:cljs [com.fulcrologic.fulcro.dom :as dom :refer [div]])
[com.fulcrologic.fulcro.dom.html-entities :as ent]
[com.fulcrologic.fulcro.raw.components :as rc]
[com.fulcrologic.fulcro.ui-state-machines :as uism :refer [defstatemachine]]
[com.fulcrologic.rad.attributes :as attr]
[com.fulcrologic.rad.control :as control]
[com.fulcrologic.rad.options-util :refer [?!]]
[com.fulcrologic.rad.rendering.semantic-ui.form :as sui-form]
[com.fulcrologic.rad.report :as report]
[com.fulcrologic.rad.report-options :as ro]
[com.fulcrologic.rad.routing :as rroute]
[com.fulcrologic.rad.semantic-ui-options :as suo]
[com.fulcrologic.rad.type-support.date-time :as dt]
[taoensso.timbre :as log]))
(defn report-params [env]
(let [Report (uism/actor-class env :actor/report)
sort-by (uism/alias-value env :sort-by)
ascending? (uism/alias-value env :ascending?)
page-offsets (uism/alias-value env :page-offsets)
all-params (report/current-control-parameters env)
offset (last page-offsets)
limit (or (rc/component-options Report ro/page-size) 20)]
(merge all-params
{:sort-by sort-by
:offset offset
:limit limit
:ascending? (boolean ascending?)})))
(defn- load-more
"Load the next page for the report. If `restart?` is true, we're starting over."
[{::uism/keys [state-map] :as env}]
(let [Report (uism/actor-class env :actor/report)
source (rc/component-options Report ro/source-attribute)
report-ident (uism/actor->ident env :actor/report)
BodyItem (rc/component-options Report ro/BodyItem)
Query (rc/nc [:next-offset
{:results (rc/get-query BodyItem state-map)}])
{:keys [offset] :as params} (report-params env)]
(if (= -1 offset)
env
(-> env
(uism/load source Query
{:params params
:marker report-ident
::uism/target-alias :loaded-data
::uism/ok-event :event/loaded
::uism/error-event :event/failed})
(uism/activate :state/loading)))))
(defn load-more!
"Ask a server-paginated report to load more data at the end of the results. "
[this] (uism/trigger! this (comp/get-ident this) :event/load-more))
(defn clear-report-data [env]
(uism/assoc-aliased env :page-offsets [0] :current-rows [] :loaded-data {} :sorted-rows []
:busy? false :page-count 0 :current-page 1))
(defn reload! [env]
(-> env
(clear-report-data)
(load-more)))
(defn report-cache-expired?
"Helper for state machines. Returns true if the report data looks like it has expired according to configured
caching parameters."
[{::uism/keys [state-map] :as uism-env}]
(let [Report (uism/actor-class uism-env :actor/report)
{::report/keys [load-cache-seconds
load-cache-expired?]} (comp/component-options Report)
now-ms (inst-ms (dt/now))
last-load-time (uism/retrieve uism-env :last-load-time)
cache-expiration-ms (* 1000 (or load-cache-seconds 0))
cache-looks-stale? (or
(nil? last-load-time)
(< last-load-time (- now-ms cache-expiration-ms)))
user-cache-expired? (?! load-cache-expired? uism-env cache-looks-stale?)]
(if (boolean user-cache-expired?)
user-cache-expired?
cache-looks-stale?)))
(letfn [(item [{:keys [on-page-change]} value active-page]
(dom/a :.item {:onClick (fn [] (when on-page-change (on-page-change value)))
:key (str value)
:classes [(when (= active-page value) "active")]}
(str value)))
(cell [{:keys [on-page-change]} n label]
(dom/a :.item {:onClick (fn [] (when on-page-change (on-page-change n)))} label))]
(defsc PaginationControl [this {:keys [total-pages active-page sibling-range size
more-available? on-load-more load-more-element
on-page-change]
:as props}]
{}
(let [sibling-range (or sibling-range 1)
total-pages (or total-pages 0)
active-page (or active-page 1)
width (+ 1 (* 2 sibling-range))
left-transition (+ 2 sibling-range)
right-transition (+ 1 (- total-pages left-transition))
window-left (max 2
(cond
(or
(< total-pages right-transition)
(<= active-page left-transition)) 2
(< left-transition active-page right-transition) (- active-page sibling-range)
:else (dec right-transition)))
window-right (min
(dec total-pages)
(cond
(< total-pages right-transition) (dec total-pages)
(<= active-page left-transition) (inc width)
(< left-transition active-page right-transition) (+ active-page sibling-range)
:else (dec total-pages)))
window? (and (> total-pages 2) (<= window-left window-right))
lower-gap? (> window-left 2)
upper-gap? (< window-right (dec total-pages))]
(dom/div :.ui.pagination.menu {:classes [(when size (str size))]}
(cell props 1 "«")
(cell props (max 1 (dec active-page)) "⟨")
(when (pos? total-pages) (item props 1 active-page))
(when lower-gap? (cell props (dec window-left) "..."))
(when window?
(mapv (fn [page] (item props page active-page)) (range window-left (inc window-right))))
(when upper-gap? (cell props (inc window-right) "..."))
(when (> total-pages 1) (item props total-pages active-page))
(when more-available?
(dom/a :.item {:onClick (fn [] (when on-load-more (on-load-more)))} (or load-more-element "...")))
(cell props (min total-pages (inc active-page)) "⟩")
(cell props total-pages "»")))))
(def ui-pagination-control
"[{:keys [total-pages active-page on-page-change size sibling-range more-available? on-load-more
load-more-element]}]"
(comp/factory PaginationControl))
(defstatemachine server-paginated-report-machine
{::uism/actors #{:actor/report}
::uism/aliases
{:parameters [:actor/report :ui/parameters]
:sort-params [:actor/report :ui/parameters ::report/sort]
:sort-by [:actor/report :ui/parameters ::report/sort :sort-by]
:page-offsets [:actor/report :ui/parameters ::report/page-offsets]
:ascending? [:actor/report :ui/parameters ::report/sort :ascending?]
:filtered-rows [:actor/report :ui/cache :filtered-rows]
:sorted-rows [:actor/report :ui/cache :sorted-rows]
:loaded-data [:actor/report :ui/loaded-data]
:page-items [:actor/report :ui/loaded-data :results]
:next-offset [:actor/report :ui/loaded-data :next-offset]
:current-rows [:actor/report :ui/current-rows]
:current-page [:actor/report :ui/parameters ::report/current-page]
:selected-row [:actor/report :ui/parameters ::report/selected-row]
:page-count [:actor/report :ui/page-count]
:busy? [:actor/report :ui/busy?]}
::uism/states
{:initial
{::uism/handler (fn [env]
(let [{::uism/keys [fulcro-app event-data]} env
{::report/keys [run-on-mount?]} (report/report-options env)]
(-> env
(uism/store :route-params (:route-params event-data))
(clear-report-data)
(report/initialize-parameters)
(cond->
run-on-mount? (load-more)
(not run-on-mount?) (uism/activate :state/gathering-parameters)))))}
:state/loading
{::uism/events
(merge report/global-events
{:event/loaded {::uism/handler (fn [{::uism/keys [state-map] :as env}]
(let [Report (uism/actor-class env :actor/report)
items (uism/alias-value env :page-items)
next-offset (uism/alias-value env :next-offset)
{::keys [row-pk report-loaded]} (comp/component-options Report)
table-name (::attr/qualified-key row-pk)]
(as-> env $
(uism/update-aliased $ :sorted-rows (fnil into []) items)
(uism/update-aliased $ :page-offsets conj next-offset)
(report/populate-current-page $)
(report/goto-page* $ (uism/alias-value $ :page-count))
(uism/store $ :last-load-time (dt/now-ms))
(uism/store $ :raw-items-in-table (count (keys (get state-map table-name))))
(uism/activate $ :state/gathering-parameters)
(cond-> $ report-loaded report-loaded))))}
:event/failed {::uism/handler (fn [env] (log/error "Report failed to load.")
(uism/activate env :state/gathering-parameters))}})}
:state/gathering-parameters
{::uism/events
(merge report/global-events
{:event/goto-page {::uism/handler (fn [{::uism/keys [event-data] :as env}]
(let [{:keys [page]} event-data]
(report/goto-page* env page)))}
:event/load-more {::uism/handler (fn [env] (load-more env))}
:event/next-page {::uism/handler (fn [env]
(let [page (uism/alias-value env :current-page)]
env))}
:event/prior-page {::uism/handler (fn [env]
(let [page (uism/alias-value env :current-page)]
env))}
:event/do-sort {::uism/handler (fn [{::uism/keys [event-data app] :as env}]
(if-let [{::attr/keys [qualified-key]} (get event-data ::attr/attribute)]
(let [sort-by (uism/alias-value env :sort-by)
sort-path (report/route-params-path env ::sort)
ascending? (uism/alias-value env :ascending?)
ascending? (if (= qualified-key sort-by)
(not ascending?)
true)]
(rroute/update-route-params! app update-in sort-path merge
{:ascending? ascending?
:sort-by qualified-key})
(-> env
(uism/assoc-aliased
:busy? false
:sort-by qualified-key
:ascending? ascending?)
(reload!)))
env))}
:event/select-row {::uism/handler (fn [{::uism/keys [app event-data] :as env}] env)}
:event/sort {::uism/handler (fn [{::uism/keys [app event-data] :as env}]
;; this ensures that the do sort doesn't get the CPU until the busy state is rendered
(uism/trigger! app (uism/asm-id env) :event/do-sort event-data)
(uism/assoc-aliased env :busy? true))}
:event/do-filter {::uism/handler (fn [{::uism/keys [event-data] :as env}] env)}
:event/filter {::uism/handler (fn [env]
(log/error "Client-side filtering not supported")
env)}
:event/set-ui-parameters {::uism/handler report/initialize-parameters}
:event/run {::uism/handler reload!}
:event/resume {::uism/handler (fn [env]
(let [env (report/initialize-parameters env)]
(when (report-cache-expired? env)
(-> env
(reload!)))))}})}}})
(defn has-more? [report-instance]
(not= -1 (-> report-instance comp/props :ui/parameters ::report/page-offsets last)))
(comp/defsc SSPReportControls [this {:keys [report-instance] :as env}]
{:shouldComponentUpdate (fn [_ _ _] true)}
(let [controls (control/component-controls report-instance)
{:keys [::report/paginate?]} (comp/component-options report-instance)
{::suo/keys [report-action-button-grouping]} (suo/get-rendering-options report-instance)
{:keys [input-layout action-layout]} (control/standard-control-layout report-instance)
{:com.fulcrologic.rad.container/keys [controlled?]} (comp/get-computed report-instance)]
(comp/fragment
(div {:className (or
(?! (suo/get-rendering-options report-instance suo/controls-class))
"ui top attached compact segment")}
(dom/h3 :.ui.header
(or (some-> report-instance comp/component-options ::report/title (?! report-instance)) ent/nbsp)
(div {:className (or (?! report-action-button-grouping report-instance)
"ui right floated buttons")}
(keep (fn [k]
(let [control (get controls k)]
(when (and (or (not controlled?) (:local? control))
(-> (get control :visible? true)
(?! report-instance)))
(control/render-control report-instance k control))))
action-layout)))
(div :.ui.form
(map-indexed
(fn [idx row]
(div {:key idx
:className (or
(?! (suo/get-rendering-options report-instance suo/report-controls-row-class) report-instance idx)
(sui-form/n-fields-string (count row)))}
(keep #(let [control (get controls %)]
(when (or (not controlled?) (:local? control))
(control/render-control report-instance % control))) row)))
input-layout))
(when paginate?
(let [page-count (report/page-count report-instance)]
(div :.ui.two.column.centered.grid {:style {:clear "both"}}
(div :.two.wide.column
(ui-pagination-control {:active-page (report/current-page report-instance)
:more-available? (has-more? report-instance)
:load-more-element "Mas..."
:on-page-change (fn [page] (report/goto-page! report-instance page))
:total-pages page-count
:on-load-more (fn [] (load-more! report-instance))
:size "tiny"})))))))))
(let [ui-server-paginated-report-controls (comp/factory SSPReportControls)]
(defn render-ssp-controls [report-instance]
(ui-server-paginated-report-controls {:report-instance report-instance})))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment