ccount modal

This commit is contained in:
2023-10-24 16:59:43 -07:00
parent d7edf0221c
commit f5fd532a31
5 changed files with 167 additions and 180 deletions

View File

@@ -29,8 +29,10 @@
html-response
main-transformer
many-entity
modal-response
ref->enum-schema
ref->select-options
strip
temp-id
wrap-entity
wrap-form-4xx-2
@@ -44,8 +46,8 @@
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes
:admin-account-table)
"hx-target" "#account-table"
"hx-indicator" "#account-table"}
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
[:fieldset.space-y-6
(com/field {:label "Name"}
@@ -129,7 +131,7 @@
matching-count]))
(def grid-page
(helper/build {:id "account-table"
(helper/build {:id "entity-table"
:nav (com/admin-aside-nav)
:page-specific-nav filters
:fetch-page fetch-page
@@ -139,16 +141,12 @@
:action-buttons (fn [_]
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-account-new-dialog))
:hx-target "#modal-content"
:hx-swap "innerHTML"
:color :primary}
"New Account")])
:row-buttons (fn [_ entity]
[(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-account-edit-dialog
:db/id (:db/id entity)))
:hx-target "#modal-content"
:hx-swap "innerHTML"}
:db/id (:db/id entity)))}
svg/pencil)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
@@ -181,31 +179,29 @@
(def table* (partial helper/table* grid-page))
(defn account-save [{:keys [form-params request-method] :as request}]
(let [entity (cond-> form-params
(= :post request-method) (assoc :db/id "new"))
_ (cond (= :post request-method)
(when (seq (dc/q '[:find ?x :in $ ?nc :where [?x :account/numeric-code ?nc]] (dc/db conn) (:account/numeric-code entity)))
(field-validation-error (format "The code %d is already in use." (:account/numeric-code entity))
[:account/numeric-code]
:form form-params))
)
_ (some->> form-params
:account/client-overrides
(group-by :account-client-override/client)
(filter (fn [[_ overrides]]
(> (count overrides) 1)))
(map first)
seq
(#(form-validation-error (format "Client(s) %s have more than one override."
(str/join ", "
(map (fn [client]
(format "'%s'" (pull-attr (dc/db conn)
:client/name
(-> client)))
) %)))
:form form-params))
)
(let [entity (cond-> form-params
(= :post request-method) (assoc :db/id "new"))
_ (cond (= :post request-method)
(when (seq (dc/q '[:find ?x :in $ ?nc :where [?x :account/numeric-code ?nc]] (dc/db conn) (:account/numeric-code entity)))
(field-validation-error (format "The code %d is already in use." (:account/numeric-code entity))
[:account/numeric-code]
:form form-params)))
_ (some->> form-params
:account/client-overrides
(group-by :account-client-override/client)
(filter (fn [[_ overrides]]
(> (count overrides) 1)))
(map first)
seq
(#(form-validation-error (format "Client(s) %s have more than one override."
(str/join ", "
(map (fn [client]
(format "'%s'" (pull-attr (dc/db conn)
:client/name
(-> client)))
) %)))
:form form-params)) ;; TODO shouldnt need to bubble this through. See if we can eliminate the passing of form and last-form.
)
{:keys [tempids]} (audit-transact [[:upsert-entity (cond-> entity
(:account/numeric-code entity) (assoc :account/code (str (:account/numeric-code entity))))]]
(:identity request))
@@ -231,8 +227,10 @@
"account_client_override_id" (:db/id o)})))
(html-response
(row* identity updated-account {:flash? true})
:headers {"hx-trigger" "modalclose"
"hx-retarget" (format "#account-table tr[data-id=\"%d\"]" (:db/id updated-account))})))
:headers (cond-> {"hx-trigger" "modalclose"}
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
"hx-reswap" "afterbegin")
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-account)))))))
;; TODO decide when cursors are used. other cases it's not, some are
(defn client-override* [override]
@@ -377,14 +375,14 @@
:index (count (fc/field-value))
:hx-get (bidi/path-for ssr-routes/only-routes
:admin-account-client-override-new)}
"New override"))))
]
"New override"))))]
[:div
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors (seq form-errors)}
"Save account")])]])]))
;; TODO saving new row should att it to the tbody
(defn new-client-override [{ {:keys [index]} :query-params}]
(html-response
(fc/start-form-with-prefix
@@ -398,8 +396,8 @@
[:map
[:db/id {:optional true} [:maybe entity-id]]
[:account/numeric-code {:optional true} [:maybe :int]]
[:account/name [:string {:min 1}]]
[:account/location [:maybe :string]]
[:account/name [:string {:min 1 :decode/string strip}]]
[:account/location {:optional true} [:maybe [:string {:decode/string strip}]]]
[:account/type (ref->enum-schema "account-type")]
[:account/applicability (ref->enum-schema "account-applicability")] ;
[:account/invoice-allowance (ref->enum-schema "allowance")]
@@ -409,17 +407,16 @@
(many-entity {}
[:db/id [:or entity-id temp-id]]
[:account-client-override/client entity-id]
[:account-client-override/name [:string {:min 2}]])]]]))
[:account-client-override/name [:string {:min 2 :decode/string strip}]])]]]))
(defn account-dialog [{:keys [entity form-params form-errors]}]
(html-response (dialog* {:entity entity
(modal-response (dialog* {:entity entity
:form-params (or (when (seq form-params)
form-params)
(when entity
(mc/decode form-schema entity main-transformer))
{})
:form-errors form-errors})
:headers {"hx-trigger" "modalopen"}))
:form-errors form-errors})))

View File

@@ -31,6 +31,7 @@
html-response
main-transformer
many-entity
modal-response
money
percentage
ref->enum-schema
@@ -44,21 +45,14 @@
[bidi.bidi :as bidi]
[clojure.string :as str]
[datomic.api :as dc]
[iol-ion.query :refer [ident]]
[malli.core :as mc]))
;; TODO with dependencies, I really don't like that you have to be ultra specific in what
;; you want to include, and generating the routes and interconnection is weird too.
;; I'm tempted to say to include a full snapshot of the form, and the indicator
;; as to which one to generate.
(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
:admin-transaction-rule-table)
"hx-target" "#transaction-rule-table"
"hx-indicator" "#transaction-rule-table"}
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
[:fieldset.space-y-6
(com/field {:label "Vendor"}
@@ -180,7 +174,7 @@
matching-count]))
(def grid-page
(helper/build {:id "transaction-rule-table"
(helper/build {:id "entity-table"
:nav (com/admin-aside-nav)
:page-specific-nav filters
:fetch-page fetch-page
@@ -190,16 +184,12 @@
:action-buttons (fn [request]
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-new-dialog))
:hx-target "#modal-content"
:hx-swap "innerHTML"
:color :primary}
"New Transaction Rule")])
:row-buttons (fn [request entity]
[(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-dialog
:db/id (:db/id entity)))
:hx-target "#modal-content"
:hx-swap "innerHTML"}
:db/id (:db/id entity)))}
svg/pencil)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
@@ -269,6 +259,7 @@
bank-account-id))
(defn transaction-rule-save [{:keys [form-params request-method identity] :as request}]
(clojure.pprint/pprint form-params)
(let [entity (cond-> form-params
(= :post request-method) (assoc :db/id "new")
true (assoc :transaction-rule/note (entity->note form-params)))
@@ -297,13 +288,15 @@
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
(:identity request))
updated-account (dc/pull (dc/db conn)
updated-rule (dc/pull (dc/db conn)
default-read
(or (get tempids (:db/id entity)) (:db/id entity)))]
(html-response
(row* identity updated-account {:flash? true})
:headers {"hx-trigger" "modalclose"
"hx-retarget" (format "#transaction-rule-table tr[data-id=\"%d\"]" (:db/id updated-account))})))
(row* identity updated-rule {:flash? true})
:headers (cond-> {"hx-trigger" "modalclose"}
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
"hx-reswap" "afterbegin")
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule)))))))
@@ -327,81 +320,84 @@
[{:keys [name value client-id x-model]}]
[:div.flex.flex-col
(com/typeahead-2 {:name name
:placeholder "Search..."
:url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id)
:id name
:x-model x-model
:value value
:content-fn (fn [value]
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
client-id)))})])
:placeholder "Search..."
:url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id)
:id name
:x-model x-model
:value value
:content-fn (fn [value]
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
client-id)))})])
;; TODO something is making the accountId not change the location for only existing ones?
(defn- transaction-rule-account-row*
[transaction-rule account]
(com/data-grid-row (-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account)))
(fc/field-value (:transaction-rule-account/account account)))
:show (boolean (not (fc/field-value (:new? account))))})
:data-key "show"
:x-ref "p"}
hx/alpine-mount-then-appear)
(let [account-name (fc/field-name (:transaction-rule-account/account account))]
(list
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :transaction-rule-account/account
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
[:div {:hx-trigger "changed"
:hx-target "next div"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" account-name)
:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead))
:x-init "$watch('clientId', cid => $dispatch('changed', $data))"}]
(account-typeahead* {:value (fc/field-value)
:client-id (:db/id (:transaction-rule/client transaction-rule))
:name (fc/field-name)
:x-model "accountId"}))))
(fc/with-field :transaction-rule-account/location
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)
:x-data (hx/json {:location (fc/field-value)})}
[:div {:hx-trigger "changed"
:hx-target "next *"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || ''}" (fc/field-name) )
:hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select)
:x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data))"}]
(location-select* {:name (fc/field-name)
:account-location (:account/location (cond->> (:transaction-rule-account/account @account)
(nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn)
'[:account/location])))
:client-locations (:client/locations (:transaction-rule/client transaction-rule))
:hx-model "location"
:value (fc/field-value)}))))
(fc/with-field :transaction-rule-account/percentage
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
(com/money-input {:name (fc/field-name)
:class "w-16"
:value (some-> (fc/field-value)
(* 100 )
(long ))}))))))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
(com/data-grid-row
(-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account)))
(fc/field-value (:transaction-rule-account/account account)))
:show (boolean (not (fc/field-value (:new? account))))})
:data-key "show"
:x-ref "p"}
hx/alpine-mount-then-appear)
(let [account-name (fc/field-name (:transaction-rule-account/account account))]
(list
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :transaction-rule-account/account
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
[:div {:hx-trigger "changed"
:hx-target "next div"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" account-name)
:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead))
:x-init "$watch('clientId', cid => $dispatch('changed', $data))"}]
(account-typeahead* {:value (fc/field-value)
:client-id (:db/id (:transaction-rule/client transaction-rule))
:name (fc/field-name)
:x-model "accountId"}))))
(fc/with-field :transaction-rule-account/location
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)
:x-data (hx/json {:location (fc/field-value)})}
[:div {:hx-trigger "changed"
:hx-target "next *"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || ''}" (fc/field-name) )
:hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select)
:x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}]
(location-select* {:name (fc/field-name)
:account-location (:account/location (cond->> (:transaction-rule-account/account @account)
(nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn)
'[:account/location])))
:client-locations (:client/locations (:transaction-rule/client transaction-rule))
:hx-model "location"
:value (fc/field-value)}))))
(fc/with-field :transaction-rule-account/percentage
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
(println "FIELD VALUE IS" (fc/field-value) (some-> (fc/field-value)
(* 100 )
(long )))
(com/money-input {:name (fc/field-name)
:class "w-16"
:value (some-> (fc/field-value)
(* 100 )
(long ))}))))))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
;; TODO background jobs and company 1099
(defn dialog* [{:keys [entity form-params form-errors]}]
(fc/start-form form-params form-errors
(com/modal
{:modal-class "max-w-2xl"
:hx-target "this"}
:hx-target "this"}
[:form {:hx-ext "response-targets"
:hx-swap "outerHTML swap:300ms"
@@ -415,7 +411,6 @@
{}
[:div.flex [:div.p-2 "Transaction Rule"]]
[:fieldset {:class "hx-disable"
:hx-disinherit "hx-target" ;; TODO why disinherit
:x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client form-params))
(:transaction-rule/client form-params)
(:db/id (:transaction-rule/client entity)))})}
@@ -542,8 +537,7 @@
(com/data-grid {:headers [(com/data-grid-header {} "Account")
(com/data-grid-header {:class "w-32"} "Location")
(com/data-grid-header {:class "w-16"} "%")
(com/data-grid-header {:class "w-16"})]
:id "transaction-rule-account-table"}
(com/data-grid-header {:class "w-16"})]}
(fc/cursor-map #(transaction-rule-account-row* form-params %))
(com/data-grid-new-row {:colspan 4
:hx-get (bidi/path-for ssr-routes/only-routes
@@ -604,7 +598,8 @@
client-id client-id]
(html-response (account-typeahead* {:name name
:value account
:client-id client-id}))))
:client-id client-id
:x-model "accountId"}))))
(def form-schema (mc/schema
[:map
@@ -627,14 +622,13 @@
[:transaction-rule-account/percentage percentage])]]))
(defn transaction-dialog [{:keys [entity form-params form-errors]}]
(html-response (dialog* {:entity entity
(modal-response (dialog* {:entity entity
:form-params (or (when (seq form-params)
form-params)
(when entity
(mc/decode form-schema entity main-transformer)) ;; TODO coerce into form params
(mc/decode form-schema entity main-transformer))
{})
:form-errors form-errors})
:headers {"hx-trigger" "modalopen"}))
:form-errors form-errors})))

View File

@@ -1,7 +1,8 @@
(ns auto-ap.ssr.ui
(:require
[hiccup2.core :as hiccup]
[auto-ap.ssr.hx :as hx]))
[auto-ap.ssr.hx :as hx]
[config.core :refer [env]]
[hiccup2.core :as hiccup]))
(defn html-page [hiccup]
{:status 200
@@ -27,8 +28,12 @@
[:script {:src "https://unpkg.com/hyperscript.org@0.9.7/dist/_hyperscript.min.js"}]
[:script {:src "https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js"}]
[:script {:src "https://cdn.plaid.com/link/v2/stable/link-initialize.js"}]
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js"
:crossorigin= "anonymous"}]
(if (= "dev" (:dd-env env))
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.js"
:crossorigin= "anonymous"}]
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js"
:crossorigin= "anonymous"}])
[:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}]
[:script {:src "/js/htmx-disable.js"}]
[:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]]

View File

@@ -26,6 +26,7 @@
html-response
main-transformer
many-entity
modal-response
ref->enum-schema
ref->select-options
wrap-entity
@@ -213,9 +214,7 @@
:hx-vals (format "{\"db/id\": \"%s\"}" (:db/id entity))} "Impersonate")
(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:user-edit-dialog
:db/id (:db/id entity)))
:hx-target "#modal-content"
:hx-swap "innerHTML"}
:db/id (:db/id entity)))}
svg/pencil)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
@@ -352,16 +351,14 @@
[:user/role (ref->enum-schema "user-role")]]))
(defn user-dialog [{:keys [form-params entity form-errors]}]
(html-response
(modal-response
(dialog* {:form-params (or (when (seq form-params)
form-params)
(when entity
(mc/decode form-schema entity main-transformer))
{})
:entity entity
:form-errors form-errors})
:headers {"hx-trigger" "modalopen"}))
:form-errors form-errors})))
(defn new-client [{ {:keys [index]} :query-params}]
(html-response

View File

@@ -27,6 +27,16 @@
o))
oob)))})
(defn modal-response [hiccup & {:as opts}]
(apply html-response
(into
[hiccup]
(mapcat identity
(-> opts
(assoc-in [:headers "hx-trigger"] "modalopen")
(assoc-in [:headers "hx-retarget"] "#modal-content")
(assoc-in [:headers "hx-reswap"] "innerHTML"))))))
(defn wrap-error-response [handler]
(fn [request]
(try
@@ -105,8 +115,11 @@
(def temp-id (mc/schema [:string {:min 1}]))
(def money (mc/schema [:double]))
(def percentage (mc/schema [:double {:decode/arbitrary (fn [x] (some-> x (* 0.01)))
:max 1.0
(def percentage (mc/schema [:double {:decode/string {:enter (fn [x]
(if (and (string? x) (re-find #"^\d+(\.\d+)?$" x))
(-> x (Double/parseDouble) (* 0.01))
x))}
:max 1.0
:error/message "1-100"}]))
(def regex (mc/schema [:fn {:error/message "not a regex"}
@@ -124,13 +137,7 @@
x
(into []
(for [[k v] (sort-by (comp #(Long/parseLong %) name first) x)]
v
#_(assoc v :db/id (cond (and (string? k) (re-find #"^\d+$" k))
(Long/parseLong k)
(keyword? k)
(name k)
:else
k))))))})
v))))})
(defn many-entity [params & keys]
(mc/schema
@@ -174,6 +181,16 @@
(mt2/transformer {:name :arbitrary})
mt2/default-value-transformer))
(defn strip [s]
(cond (and (string? s) (str/blank? s))
nil
(string? s)
(str/trim s)
:else
s))
(defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}]
(fn [{:keys [form-params query-params params] :as request}]
(let [request (try
@@ -207,7 +224,6 @@
main-transformer)))
(catch Exception e
(alog/warn ::validation-error :error e)
(throw (ex-info (->> (-> e
(ex-data )
:data
@@ -251,17 +267,6 @@
#_(defn namespaceize-decoder [n]
{:exit (fn [m]
(when m
(reduce
(fn [m [k v]]
(if (= k "id")
(assoc m :db/id v)
(assoc m (keyword n (name k)) v)))
m
m)))})
(defn wrap-form-4xx [handler]
(fn [request]
@@ -274,17 +279,6 @@
(html-response [:span.error-content.text-red-500 (:message &throw-context)]
:status 400)))))
(defn assoc-errors-into-meta [entity errors]
(reduce
(fn add-error [entity {:keys [path message] :as se}]
(if (= (count path) 1)
(with-meta entity (assoc (meta entity) (last path) {:errors message}))
(update-in entity (butlast path)
(fn [terminal]
(with-meta terminal (assoc (meta terminal) (last path) {:errors message}))))))
entity
errors))
(defn wrap-form-4xx-2 [handler form-handler]
(fn [request]