(ns auto-ap.ssr.users (:require [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query pull-many query2]] [auto-ap.query-params :as query-params] [auto-ap.routes.auth :as auth] [auto-ap.routes.utils :refer [wrap-admin]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [html-response]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [buddy.sign.jwt :as jwt] [clojure.string :as str] [config.core :refer [env]] [datomic.api :as dc] [malli.core :as mc])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes :user-table) "hx-target" "#user-table" "hx-indicator" "#user-table"} [:fieldset.space-y-6 (com/field {:label "Name"} (com/text-input {:name "name" :id "name" :class "hot-filter" :value (:name (:parsed-query-params request)) :placeholder "Johnny Testerson" :size :small})) (com/field {:label "Email"} (com/text-input {:name "email" :id "email" :class "hot-filter" :value (:name (:parsed-query-params request)) :placeholder "hello@friend.com" :size :small})) (com/field {:label "Role"} (com/radio {:size :small :name "role" :options [{:value "" :content "All"} {:value "admin" :content "Admin"} {:value "power-user" :content "Power user"} {:value "manager" :content "Manager"} {:value "user" :content "User"} {:value "none" :content "None"}]}))]]) (def default-read '[:db/id :user/name :user/email :user/profile-image-url [:user/last-login :xform clj-time.coerce/from-date] {[:user/role :xform iol-ion.query/ident] [:db/ident] :user/clients [:client/code :db/id :client/locations :client/name]}]) (defn fetch-ids [db request] (let [query-params (:parsed-query-params request) query (cond-> {:query {:find [] :in '[$ ] :where '[]} :args [db ]} (:sort query-params) (add-sorter-fields {"name" ['[?e :user/name ?un] '[(clojure.string/upper-case ?un) ?sort-name]] "email" ['[(get-else $ ?e :user/email "") ?sort-email]] "role" ['[?e :user/role ?r] '[?r :db/ident ?ri] '[(name ?ri) ?sort-role]] "last-login" ['[?e :user/last-login ?sort-last-login]]} query-params) (some->> query-params :name not-empty) (merge-query {:query {:find [] :in ['?ns] :where ['[?e :user/name ?sn] '[(clojure.string/upper-case ?sn) ?upper-sn] '[(clojure.string/includes? ?upper-sn ?ns)]]} :args [(str/upper-case (:name query-params))]}) (some->> query-params :email not-empty) (merge-query {:query {:find [] :in ['?es] :where ['[?e :user/email ?se] '[(clojure.string/upper-case ?se) ?upper-se] '[(clojure.string/includes? ?upper-se ?es)]]} :args [(str/upper-case (:email query-params))]}) (some->> query-params :role) (merge-query {:query {:find [] :in ['?r] :where ['[?e :user/role ?r] '[?r :db/ident ?ri]]} :args [(some->> query-params :role)]}) true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :user/name ?un] '[(clojure.string/upper-case ?un) ?sort-default]]}}))] (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) (defn hydrate-results [ids db _] (let [results (->> (pull-many db default-read ids) (group-by :db/id)) refunds (->> ids (map results) (map first))] refunds)) (defn fetch-page [request] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)] [(->> (hydrate-results ids-to-retrieve db request)) matching-count])) (defn role->pill [role] (com/pill {:color (cond (= :user-role/admin role) :primary (= :user-role/manager role) :secondary (= :user-role/power-user role) :secondary (= :user-role/user role) :yellow :else :red)} (name role))) (defn user->client-pills [user] [:div.flex.space-x-2 (for [{:client/keys [code]} (take 3 (:user/clients user))] (com/pill {:color :primary} code) ) (let [remainder (- (count (:user/clients user)) 3)] (when (> remainder 0) (com/pill {:color :white} (format "%d more" remainder))))]) (def grid-page (helper/build {:id "user-table" :nav (com/admin-aside-nav) :page-specific-nav filters :fetch-page fetch-page :parse-query-params (comp (query-params/parse-key :role #(query-params/parse-keyword "user-role" %)) (query-params/parse-key :total-gte query-params/parse-double) (query-params/parse-key :total-lte query-params/parse-double) (helper/default-parse-query-params grid-page)) :row-buttons (fn [request entity] [(com/button {:hx-post (str (bidi/path-for ssr-routes/only-routes :user-impersonate)) :hx-vals (format "{\"user-id\": \"%s\"}" (:db/id entity))} "Impersonate") (com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes :user-edit-dialog :user-id (:db/id entity))) :hx-target "#modal-holder" :hx-swap "outerHTML"} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} "Admin"] [:a {:href (bidi/path-for ssr-routes/only-routes :users)} "Users"]] :title "Users" :entity-name "User" :route :user-table :headers [{:key "name" :name "Name" :sort-key "name" :render (fn [user] [:div.flex.space-x-2.place-items-center (when-let [profile-image (:user/profile-image-url user) ] [:div.rounded-full.overflow-hidden.w-8.h-8.display-inline [:img {:src profile-image }]]) [:span.inline-block ](:user/name user)])} {:key "email" :name "Email" :sort-key "email" :render #(-> % :user/email)} {:key "role" :name "Role" :sort-key "role" :render #(some-> % :user/role role->pill)} {:key "last-login" :name "Last login" :sort-key "last-login" :render #(some-> % (:user/last-login) (atime/unparse-local atime/standard-time))} {:key "clients" :name "Clients" :render user->client-pills} ]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) (defn impersonate [request] (let [user (some-> request :form-params (get "user-id") not-empty Long/parseLong (#(dc/pull (dc/db conn) default-read %))) ] {:status 200 :headers {"hx-redirect" (str "/?jwt=" (jwt/sign (auth/user->jwt user "FAKE_TOKEN") (:jwt-secret env) {:alg :hs512})) } :session {:identity (dissoc (auth/user->jwt user "FAKE_TOKEN") :exp)}})) (defn user-edit-save [{:keys [form-params] :as request}] (let [user (some-> request :params :user-id not-empty Long/parseLong (#(dc/pull (dc/db conn) default-read %))) new-clients (map #(Long/parseLong %) (cond-> (get form-params "clients") (string? (get form-params "clients")) vector)) _ @(dc/transact conn [ [:upsert-entity {:db/id (:db/id user) :user/role (keyword "user-role" (get form-params "role")) :user/clients new-clients}]]) user (some-> request :params :user-id not-empty Long/parseLong (#(dc/pull (dc/db conn) default-read %)))] (html-response (row* identity user {:flash? true}) :headers {"hx-trigger" "closeModal" "hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))}))) (defn user-edit-dialog [request] (let [user (some-> request :params :user-id not-empty Long/parseLong (#(dc/pull (dc/db conn) default-read %)))] (html-response (com/modal {} [:form {:hx-post (str (bidi/path-for ssr-routes/only-routes :user-edit-save :request-method :post :user-id (:db/id user ))) :hx-swap "outerHTML swap:300ms"} [:fieldset {:class "hx-disable"} (com/modal-card {} [:div.flex [:div.p-2 "User"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:user/name user)]] [:div.space-y-6 (com/field {:label "Role"} (com/select {:name "role" :class "w-36" :autofocus true :id "role" :value (name (:user/role user)) :options [["none" "None"] ["power-user" "Power user"] ["manager" "Manager"] ["admin" "Admin"] ["user" "User"]] :size :small})) (com/field {:label "Clients"} (com/typeahead {:name "clients" :class "w-full" :multiple "multiple" :url (bidi/path-for ssr-routes/only-routes :company-search) :id "clients" :value (map (fn [client] [(:db/id client) (:client/name client)]) (:user/clients user)) #_#_:value (name (:user/role user)) #_#_:options [["none" "None"] ["power-user" "Power user"] ["manager" "Manager"] ["admin" "Admin"] ["user" "User"]] :size :small})) (com/button {:color :primary} "Save") ] [:div])]])))) (def key->handler {:users (wrap-admin (helper/page-route grid-page)) :user-table (wrap-admin (helper/table-route grid-page)) :user-edit-save (wrap-client-redirect-unauthenticated (wrap-admin user-edit-save)) :user-edit-dialog (wrap-client-redirect-unauthenticated (wrap-admin user-edit-dialog)) :user-impersonate (wrap-client-redirect-unauthenticated (wrap-admin impersonate))})