From e3443a3dd8e2ea7420b37a9a281a929a007deac0 Mon Sep 17 00:00:00 2001 From: Bryce Date: Fri, 20 Oct 2023 08:54:00 -0700 Subject: [PATCH] uses cursors instead, much clearer experience. --- resources/public/js/htmx-disable.js | 1 + .../auto_ap/ssr/admin/transaction_rules.clj | 217 +++++++++--------- src/clj/auto_ap/ssr/components/inputs.clj | 4 +- src/clj/auto_ap/ssr/form_cursor.clj | 38 +++ src/clj/auto_ap/ssr/utils.clj | 20 +- 5 files changed, 161 insertions(+), 119 deletions(-) create mode 100644 src/clj/auto_ap/ssr/form_cursor.clj diff --git a/resources/public/js/htmx-disable.js b/resources/public/js/htmx-disable.js index ec0bc0b1..4a63b0d0 100644 --- a/resources/public/js/htmx-disable.js +++ b/resources/public/js/htmx-disable.js @@ -126,5 +126,6 @@ elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true}); countRows = function(id) { var table = document.querySelector(id); var rows = table.querySelectorAll("tbody tr"); + console.log("ROWS", rows.length); return rows.length; } diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index 21a0b39a..ed4a4622 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -20,6 +20,7 @@ [auto-ap.ssr.components :as com] [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils @@ -58,34 +59,13 @@ ;; TODO better generation of names? -(def ^:dynamic *errors*) -(def ^:dynamic *prev-cursor* nil) -(def ^:dynamic *cursor* nil) - -(defmacro with-cursor [cursor & rest] - `(binding [*prev-cursor* (or *cursor* ~cursor)] - (binding [*cursor* ~cursor] - ~@rest))) - -(defn cursor-name [] - (apply path->name2 (cursor/path *cursor*))) - -(defn cursor-value [] - @*cursor*) - -(defn cursor-errors [] - (:errors (get - (meta @*prev-cursor*) - (last (cursor/path *cursor*))))) - - (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"} + [: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"} [:fieldset.space-y-6 (com/field {:label "Vendor"} @@ -301,7 +281,7 @@ :when (and account-location (not= account-location location))] (field-validation-error (str "must be " account-location) [:transaction-rule/accounts i :transaction-rule-account/location] - :form entity)) + :form form-params)) total (reduce + 0.0 @@ -309,14 +289,14 @@ (:transaction-rule/accounts entity))) _ (when-not (dollars= 1.0 total) (form-validation-error (format "Expense accounts total (%d%%) must add to 100%%" (int (* 100.0 total))) - :form entity)) + :form form-params)) _ (when (and (:transaction-rule/bank-account entity) (not (bank-account-belongs-to-client? (:transaction-rule/bank-account entity) (:transaction-rule/client entity)))) (field-validation-error "does not belong to client" [:transaction-rule/bank-account] - :form entity)) + :form form-params)) {:keys [tempids]} (audit-transact [[:upsert-entity entity]] @@ -366,13 +346,12 @@ (defn- transaction-rule-account-row* [transaction-rule account] (com/data-grid-row {} - (let [account-name (with-cursor (:transaction-rule-account/account account) - (cursor-name))] + (let [account-name (fc/field-name (:transaction-rule-account/account account))] (list - (with-cursor (:db/id account) - (com/hidden {:name (cursor-name) - :value (cursor-value)})) - (with-cursor (:transaction-rule-account/account account) + (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 {} [:div {:hx-trigger (hx/trigger-field-change :name "transaction-rule/client" :from "#edit-form") @@ -384,11 +363,15 @@ account-name "value"}) :hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead)) :hx-swap "innerHTML"} - (account-typeahead* {:value (cursor-value) + (account-typeahead* {:value (fc/field-value) :client-id (:db/id (:transaction-rule/client transaction-rule)) - :name (cursor-name)}) - (com/errors {:errors (cursor-errors)})])) - (with-cursor (:transaction-rule-account/location account) + :name (fc/field-name)}) + (println "HERE" (cursor/path fc/*current*)) + (println "DATA "fc/*form-data*) + (println "ERR" fc/*form-errors*) + (println (fc/field-errors)) + (com/errors {:errors (fc/field-errors)})])) + (fc/with-field :transaction-rule-account/location (com/data-grid-cell {} [:div [:div {:hx-trigger (hx/triggers (hx/trigger-field-change :name "transaction-rule/client" @@ -396,36 +379,36 @@ (hx/trigger-field-change :name account-name :from "#edit-form")) :hx-include "#edit-form" - :hx-vals (hx/vals {:name (cursor-name)}) + :hx-vals (hx/vals {:name (fc/field-name)}) :hx-ext "rename-params" :hx-rename-params-ex (hx/json {"transaction-rule/client" "client-id" account-name "account-id" "name" "name" - (cursor-name) "value"}) + (fc/field-name) "value"}) :hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select) :hx-swap "innerHTML"} - (location-select* {:name (cursor-name) + (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)) - :value (cursor-value)})] - (com/errors {:errors (cursor-errors)})] + :value (fc/field-value)})] + (com/errors {:errors (fc/field-errors)})] )) - (with-cursor (:transaction-rule-account/percentage account) - (com/data-grid-cell (com/money-input {:name (cursor-name) + (fc/with-field :transaction-rule-account/percentage + (com/data-grid-cell (com/money-input {:name (fc/field-name) :class "w-16" - :value (some-> (cursor-value) + :value (some-> (fc/field-value) (* 100 ) (long ))}) - (com/errors {:errors (cursor-errors)}))))) + (com/errors {:errors (fc/field-errors)}))))) (com/data-grid-cell (com/a-icon-button {"_" (hiccup/raw "on click halt the event then transition the closest 's opacity to 0 then remove closest ") :href "#"} svg/x)))) -(defn dialog* [& {:keys [ entity form-params]}] +(defn dialog* [& {:keys [ entity form-params form-errors]}] (com/modal {:modal-class "max-w-4xl"} (com/modal-card @@ -438,100 +421,102 @@ form-params) [:fieldset {:class "hx-disable" :hx-disinherit "hx-target"} - (with-cursor (cursor/cursor entity) + (fc/start-form entity form-errors [:div.space-y-2 (when-let [id (:db/id entity)] (com/hidden {:name "db/id" :value id})) - (with-cursor (:transaction-rule/client *cursor*) + + (fc/with-field :transaction-rule/client + (com/validated-field {:label "Client" - :errors (cursor-errors)} + :errors (fc/field-errors)} [:div.w-96 - (com/typeahead {:name (cursor-name) + (com/typeahead {:name (fc/field-name) :class "w-96" :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :company-search) :id (str "form-client-search") - :value (cursor-value) + :value (fc/field-value) :value-fn (some-fn :db/id identity) :content-fn (fn [c] (cond->> c (nat-int? c) (dc/pull (dc/db conn) '[:client/name]) true :client/name))})])) - (with-cursor (:transaction-rule/bank-account *cursor*) + (fc/with-field :transaction-rule/bank-account (com/validated-field {:label "Bank Account" - :errors (cursor-errors)} + :errors (fc/field-errors)} [:div#bank-account-spot.w-96 {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead) :hx-trigger (hx/trigger-field-change :name "transaction-rule/client" :from "#edit-form") :hx-swap "innerHTML" :hx-ext "rename-params" :hx-include "#edit-form" - :hx-vals (hx/vals {:name (cursor-name)}) + :hx-vals (hx/vals {:name (fc/field-name)}) :hx-rename-params-ex (cheshire/generate-string {"transaction-rule/client" "client-id" "name" "name"})} (bank-account-typeahead* {:client-id ((some-fn :db/id identity) (:transaction-rule/client entity)) - :name (cursor-name) - :value (cursor-value)})])) + :name (fc/field-name) + :value (fc/field-value)})])) - (with-cursor (:transaction-rule/description *cursor*) + (fc/with-field :transaction-rule/description (com/validated-field {:label "Description" - :errors (cursor-errors)} - (com/text-input {:name (cursor-name) + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) :placeholder "HOME DEPOT" :class "w-96" - :value (cursor-value)}))) + :value (fc/field-value)}))) (com/field {:label "Amount"} [:div.flex.gap-2 - (with-cursor (:transaction-rule/amount-gte *cursor*) + (fc/with-field :transaction-rule/amount-gte [:div.flex.flex-col - (com/money-input {:name (cursor-name) + (com/money-input {:name (fc/field-name) :placeholder ">=" :class "w-24" - :value (cursor-value)}) - (com/errors {:errors (cursor-errors)})]) - (with-cursor (:transaction-rule/amount-lte *cursor*) + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})]) + (fc/with-field :transaction-rule/amount-lte [:div.flex.flex-col - (com/money-input {:name (cursor-name) + (com/money-input {:name (fc/field-name) :placeholder "<=" :class "w-24" - :value (cursor-value)}) - (com/errors {:errors (cursor-errors)})])]) + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})])]) (com/field {:label "Day of month"} [:div.flex.gap-2 - (with-cursor (:transaction-rule/dom-gte *cursor*) + (fc/with-field :transaction-rule/dom-gte [:div.flex.flex-col - (com/int-input {:name (cursor-name) + (com/int-input {:name (fc/field-name) :placeholder ">=" :class "w-24" - :value (cursor-value)}) - (com/errors {:errors (cursor-errors)})]) - (with-cursor (:transaction-rule/dom-lte *cursor*) + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})]) + (fc/with-field :transaction-rule/dom-lte [:div.flex.flex-col - (com/int-input {:name (cursor-name) + (com/int-input {:name (fc/field-name) :placeholder ">=" :class "w-24" - :value (cursor-value)}) - (com/errors {:errors (cursor-errors)})])]) + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})])]) [:h2.text-lg "Outcomes"] - (with-cursor (:transaction-rule/vendor *cursor*) + (fc/with-field :transaction-rule/vendor (com/validated-field {:label "Assign Vendor" - :errors (cursor-errors)} + :errors (fc/field-errors)} [:div.w-96 - (com/typeahead {:name (cursor-name) + (com/typeahead {:name (fc/field-name) :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :vendor-search) :id (str "form-vendor-search") - :value (cursor-value) + :value (fc/field-value) :value-fn (some-fn :db/id identity) :content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})])) - (with-cursor (:transaction-rule/accounts *cursor*) + (fc/with-field :transaction-rule/accounts (list (com/data-grid {:headers [(com/data-grid-header {} "Account") @@ -539,10 +524,11 @@ (com/data-grid-header {:class "w-16"} "%") (com/data-grid-header {:class "w-16"})] :id "transaction-rule-account-table"} - (for [tra *cursor*] - (with-cursor tra - (transaction-rule-account-row* entity tra)))) - (com/errors {:errors (cursor-errors)}))) + (when @fc/*current* + (doall (for [tra fc/*current*] + (fc/with-cursor tra + (transaction-rule-account-row* entity tra)))))) + (com/errors {:errors (fc/field-errors)}))) (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-new-account) :hx-include "#edit-form" @@ -553,36 +539,41 @@ :hx-target "#transaction-rule-account-table tbody" :hx-swap "beforeend"} "New account") - (with-cursor (:transaction-rule/transaction-approval-status *cursor*) - (com/radio {:options (ref->radio-options "transaction-approval-status") - :value (cursor-value) - :name (cursor-name)})) + (fc/with-field :transaction-rule/transaction-approval-status + (com/validated-field {:label "Approval status" + :errors (fc/field-errors)} + (com/radio {:options (ref->radio-options "transaction-approval-status") + :value (fc/field-value) + :name (fc/field-name)}))) [:div#form-errors [:span.error-content - (com/errors {:errors (:errors (meta entity))})]] + (com/errors {:errors (:errors fc/*form-errors*)})]] (com/button {:color :primary :form "edit-form" :type "submit"} "Save")])]] [:div]))) (defn new-account [{{:keys [client-id index]} :query-params}] - (let [transaction-rule {:transaction-rule/client (dc/pull (dc/db conn) '[:client/name :client/locations :db/id] + (let [index (or index 0) ;; TODO schema decode is not working + transaction-rule {:transaction-rule/client (dc/pull (dc/db conn) '[:client/name :client/locations :db/id] client-id) :transaction-rule/accounts (conj (into [] (repeat (inc index) {} )) {:db/id (str (java.util.UUID/randomUUID)) :transaction-rule-account/location "Shared"})}] (html-response - (with-cursor (cursor/cursor transaction-rule) - (with-cursor (:transaction-rule/accounts *cursor*) - (with-cursor (nth *cursor* index) - (transaction-rule-account-row* - ;; TODO store a pointer to the "head " cursor for errors instead of nesting them - ;; makes it so you don't have to do this - transaction-rule - *cursor* - ))))))) + (fc/start-form transaction-rule [] + (fc/with-cursor (get-in fc/*current* [:transaction-rule/accounts index]) + (transaction-rule-account-row* + ;; TODO store a pointer to the "head " cursor for errors instead of nesting them + ;; makes it so you don't have to do this + transaction-rule + fc/*current*)))))) ;; TODO check to see if it should be called "Shared" or "shared" for the value +;; TODO blank location is being allowed +;; TODO hydrate nested types more easily. make it easy to hydrate so you don't do weird sometimes pulls +;; TODO is it possible to make it easy to get a virtual cursor in the case of adding a new row? setting up +;; fake data doesn't feel right - maybe have a "prelude" that's dynamic (defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}] (html-response (location-select* {:name name @@ -613,15 +604,22 @@ (defn transaction-rule-error [request] (let [entity (some-> request :last-form)] - (html-response (dialog* :entity entity) + (html-response (dialog* :entity entity + :form-errors (:form-errors request) + :form-params (if (:db/id entity) + {:hx-put (str (bidi/path-for ssr-routes/only-routes + :admin-transaction-rule-edit-save))} + {:hx-post (str (bidi/path-for ssr-routes/only-routes + :admin-transaction-rule-edit-save))})) :headers {"hx-retarget" "#edit-form fieldset" "hx-reselect" "#edit-form fieldset"}))) -(defn account-new-dialog [_] - (html-response (dialog* :account nil +(defn transaction-rule-new-dialog [_] + (html-response (dialog* :entity {} + :form-errors {} :form-params {:hx-post (str (bidi/path-for ssr-routes/only-routes - :admin-account-new-save))}) + :admin-transaction-rule-edit-save))}) :headers {"hx-trigger" "modalOpening"})) (def transaction-rule-schema (mc/schema @@ -653,7 +651,8 @@ (wrap-schema-decode :query-schema [:map [:client-id {:optional true} [:maybe entity-id]] - [:index nat-int?]]) + [:index {:optional true + :default 0} [nat-int? {:default 0}]]]) wrap-admin wrap-client-redirect-unauthenticated) :admin-transaction-rule-location-select (-> location-select (wrap-schema-decode :query-schema [:map @@ -675,7 +674,7 @@ (wrap-form-4xx-2 transaction-rule-error)) :admin-transaction-rule-edit-dialog (-> transaction-rule-edit-dialog (wrap-schema-decode :route-schema [:map [:db/id entity-id]])) - :admin-transaction-rule-new-dialog account-new-dialog}) + :admin-transaction-rule-new-dialog transaction-rule-new-dialog}) (fn [h] (-> h (wrap-admin) diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index 97b3e8e3..069d14ea 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -123,7 +123,9 @@ c.clearChoices(); :key (:error-key params)}))]) (defn errors- [{:keys [errors]}] - [:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4 (str/join ", " errors)]) + [:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4 + (when (sequential? errors) + (str/join ", " (filter string? errors)))]) (defn validated-field- [params & rest] (field- (dissoc params :errors) diff --git a/src/clj/auto_ap/ssr/form_cursor.clj b/src/clj/auto_ap/ssr/form_cursor.clj new file mode 100644 index 00000000..537dcc78 --- /dev/null +++ b/src/clj/auto_ap/ssr/form_cursor.clj @@ -0,0 +1,38 @@ +(ns auto-ap.ssr.form-cursor + (:require [auto-ap.ssr.utils :refer [path->name2]] + [auto-ap.cursor :as cursor])) + +(def ^:dynamic *form-data*) +(def ^:dynamic *form-errors*) +(def ^:dynamic *prev-cursor* nil) +(def ^:dynamic *current* nil) + +(defmacro start-form [form-data errors & rest] + `(binding [*form-data* ~form-data + *form-errors* (or ~errors {})] + (binding [*current* (cursor/cursor *form-data*)] + ~@rest))) + +(defmacro with-cursor [cursor & rest] + `(binding [*current* ~cursor] + ~@rest)) + +(defmacro with-field [field & rest] + `(with-cursor (get *current* ~field ) + ~@rest)) + +(defn field-name + ([] (field-name *current*)) + ([cursor] + (apply path->name2 (cursor/path cursor)))) + +(defn field-value [] + @*current*) + +(defn field-errors + ([] + (field-errors *current*)) + ([cursor] + (get-in *form-errors* (cursor/path cursor)))) + + diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index 33540125..6529aebb 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -151,8 +151,7 @@ (defn field-validation-error [m path & {:as data}] (throw+ (ex-info m (merge data {:type :field-validation - :field-validation-errors [{:path path - :message [m]}]})))) + :form-errors (assoc-in {} path [m])})))) (defn form-validation-error [m & {:as data}] (throw+ (ex-info m (merge data {:type :form-validation @@ -164,7 +163,8 @@ (mt2/key-transformer {:encode keyword->str :decode str->keyword}) mt2/string-transformer mt2/json-transformer - (mt2/transformer {:name :arbitrary}))) + (mt2/transformer {:name :arbitrary}) + mt2/default-value-transformer)) (defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}] (fn [{:keys [form-params query-params params] :as request}] @@ -290,18 +290,20 @@ (:errors (:explain (:error e))))] (alog/warn ::form-4xx :errors errors) (form-handler (assoc request - :last-form (assoc-errors-into-meta (:decoded e) errors) - :field-validation-errors errors))) + :last-form (:decoded e) + :field-validation-errors errors + :form-errors humanized))) #_(html-response [:span.error-content.text-red-500 (:message &throw-context)] :status 400)) (catch [:type :field-validation] e (form-handler (assoc request - :last-form (assoc-errors-into-meta (:form e) (:field-validation-errors e)) - :field-validation-errors (:field-validation-errors e)))) + :last-form (:form e) + :form-errors (:form-errors e)))) (catch [:type :form-validation] e (form-handler (assoc request - :last-form (with-meta (:form e) {:errors (:form-validation-errors e)}) - :form-validation-errors (:form-validation-errors e))))))) + :last-form (:form e) + :form-validation-errors (:form-validation-errors e) + :form-errors {:errors (:form-validation-errors e)})))))) (defn apply-middleware-to-all-handlers [key->handler f]