diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj index 3fd9557f..6dad6db6 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -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}))) diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index 383a2f18..f3c1a92b 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -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}))) diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index 3cccfa40..6ab04c9e 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -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"}]] diff --git a/src/clj/auto_ap/ssr/users.clj b/src/clj/auto_ap/ssr/users.clj index c7f5fec4..0f650dd0 100644 --- a/src/clj/auto_ap/ssr/users.clj +++ b/src/clj/auto_ap/ssr/users.clj @@ -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 diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index ca664017..27685076 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -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]