-
-
Save awkay/023c974212ec3c928695b6f299565b00 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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