From dd5063c72d04526b10eda9301eae07dfac066eb8 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sun, 22 Oct 2023 21:47:36 -0700 Subject: [PATCH 1/5] account new override --- resources/public/output.css | 33 ++++++++++++++++++++++++++ src/clj/auto_ap/ssr/admin/accounts.clj | 12 +++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/resources/public/output.css b/resources/public/output.css index 73530dec..96983899 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1357,6 +1357,10 @@ input:checked + .toggle-bg { height: min-content; } +.h-\[90vh\] { + height: 90vh; +} + .max-h-96 { max-height: 24rem; } @@ -1369,6 +1373,10 @@ input:checked + .toggle-bg { max-height: 90vh; } +.max-h-\[100vh\] { + max-height: 100vh; +} + .max-h-\[80vh\] { max-height: 80vh; } @@ -1446,6 +1454,19 @@ input:checked + .toggle-bg { width: 100vw; } +.w-min { + width: -moz-min-content; + width: min-content; +} + +.w-8\/12 { + width: 66.666667%; +} + +.w-6\/12 { + width: 50%; +} + .w-1\/4 { width: 25%; } @@ -1474,6 +1495,10 @@ input:checked + .toggle-bg { max-width: 1024px; } +.max-w-xs { + max-width: 20rem; +} + .flex-1 { flex: 1 1 0%; } @@ -1692,6 +1717,10 @@ input:checked + .toggle-bg { place-items: center; } +.content-center { + align-content: center; +} + .items-start { align-items: flex-start; } @@ -3601,6 +3630,10 @@ input:checked + .toggle-bg { padding: 1.5rem; } + .sm\:p-12 { + padding: 3rem; + } + .sm\:py-5 { padding-top: 1.25rem; padding-bottom: 1.25rem; diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj index 28ddfc5e..35eca1b0 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -35,7 +35,8 @@ [datomic.api :as dc] [hiccup2.core :as hiccup] [malli.core :as mc] - [auto-ap.ssr.form-cursor :as fc])) + [auto-ap.ssr.form-cursor :as fc] + [clj-time.format :as f])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" @@ -328,8 +329,13 @@ "Save account"))]])) (defn new-client-override [{ {:keys [index]} :query-params}] - (html-response - (client-override* {:db/id (str (java.util.UUID/randomUUID))}))) + (let [index (or index 0) + account {:account/client-overrides (conj (into [] (repeat index {})) + {:db/id (str (java.util.UUID/randomUUID))})}] ;; TODO schema decode is not working + (html-response + (fc/start-form account [] + (fc/with-cursor (get-in fc/*current* [:account/client-overrides index]) + (client-override* fc/*current*)))))) (defn account-edit-dialog [request] (let [account (some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %)))] From 2f05197f5bf641500258222982014acb60d288c7 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sun, 22 Oct 2023 21:53:47 -0700 Subject: [PATCH 2/5] progress. --- src/clj/auto_ap/ssr/admin/accounts.clj | 75 ++++++++++++++++---------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj index 35eca1b0..e10eb667 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -15,6 +15,7 @@ [auto-ap.solr :as solr] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] @@ -23,20 +24,17 @@ entity-id html-response many-entity - map->db-id-decoder ref->enum-schema ref->select-options temp-id validation-error - wrap-form-4xx + wrap-form-4xx-2 wrap-schema-decode]] [bidi.bidi :as bidi] [clojure.string :as str] [datomic.api :as dc] [hiccup2.core :as hiccup] - [malli.core :as mc] - [auto-ap.ssr.form-cursor :as fc] - [clj-time.format :as f])) + [malli.core :as mc])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" @@ -222,18 +220,20 @@ (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (fc/with-field :account-client-override/client - (com/typeahead-2 {:name (fc/field-name) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :value (fc/field-value) - :value-fn :db/id ;; TODO hydration something here - :content-fn :client/name}))] + (com/validated-field {:errors (fc/field-errors)} + (com/typeahead-2 {:name (fc/field-name) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :value (fc/field-value) + :value-fn :db/id ;; TODO hydration something here + :content-fn :client/name})))] (fc/with-field :account-client-override/name [:div.w-96 - (com/text-input {:name (fc/field-name) - :class "w-full" - :value (fc/field-value)})]) + (com/validated-field {:errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value)}))]) [:div (com/a-icon-button {"_" (hiccup/raw "on click halt the event then transition the closest <.client-override />'s opacity to 0 then remove closest <.client-override />")} svg/x)]]) ;; TODO each form: @@ -269,43 +269,50 @@ (fc/with-field :account/numeric-code (when (nil? (fc/field-value)) - (com/field {:label "Numeric code"} - (com/text-input {:name (fc/field-name) - :autofocus true - :class "w-32"})))) + (com/validated-field {:label "Numeric code" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :autofocus true + :class "w-32"})))) (fc/with-field :account/name - (com/field {:label "Name"} + (com/validated-field {:label "Name" + :errors (fc/field-errors)} (com/text-input {:name (fc/field-name) :autofocus true :class "w-32" :value (fc/field-value)}))) (fc/with-field :account/type - (com/field {:label "Account Type"} + (com/validated-field {:label "Account Type" + :errors (fc/field-errors)} (com/select {:name (fc/field-name) :class "w-36" :id "type" :value (some-> (fc/field-value) name) :options (ref->select-options "account-type")}))) (fc/with-field :account/location - (com/field {:label "Location"} + (com/validated-field {:label "Location" + :errors (fc/field-errors)} (com/text-input {:name (fc/field-name) :class "w-16" :value (fc/field-value)}))) (fc/with-field :account/invoice-allowance - (com/field {:label "Invoice Allowance"} + (com/validated-field {:label "Invoice Allowance" + :errors (fc/field-errors)} (com/select {:name (fc/field-name) :value (some-> (fc/field-value) name) :class "w-36" :options (ref->select-options "allowance")}))) (fc/with-field :account/vendor-allowance - (com/field {:label "Vendor Allowance"} + (com/validated-field {:label "Vendor Allowance" + :errors (fc/field-errors)} (com/select {:name (fc/field-name) :class "w-36" :value (some-> (fc/field-value) name) :options (ref->select-options "allowance")}))) (fc/with-field :account/applicability - (com/field {:label "Applicability"} + (com/validated-field {:label "Applicability" + :errors (fc/field-errors)} (com/select {:name (fc/field-name) :class "w-36" :value (some-> (fc/field-value) name) @@ -351,6 +358,20 @@ :admin-account-new-save))}) :headers {"hx-trigger" "modalopen"})) +(defn account-save-error [request] + ;; TODO hydration + ;; TODO consistency of error handling and passing, on all form examples + (let [entity (some-> request :last-form)] + (html-response (dialog* :account 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"}))) + (def account-schema (mc/schema [:map [:db/id {:optional true} [:maybe entity-id]] @@ -366,7 +387,7 @@ (many-entity {} [:db/id [:or entity-id temp-id]] [:account-client-override/client [:or entity-id :string]] - [:account-client-override/name :string])]]])) + [:account-client-override/name [:string {:min 2}]])]]])) (def key->handler (apply-middleware-to-all-handlers @@ -381,7 +402,7 @@ :admin-account-save (-> account-save (wrap-schema-decode :form-schema account-schema) (wrap-nested-form-params) - (wrap-form-4xx)) + (wrap-form-4xx-2 account-save-error)) :admin-account-edit-dialog (-> account-edit-dialog (wrap-schema-decode :route-schema [:map [:db/id entity-id]])) :admin-account-new-dialog account-new-dialog}) From 825443ef2c4724fc5478a9f306a69222ef7be6b8 Mon Sep 17 00:00:00 2001 From: Bryce Date: Mon, 23 Oct 2023 10:57:35 -0700 Subject: [PATCH 3/5] Made accounts page look great --- src/clj/auto_ap/ssr/admin/accounts.clj | 235 +++++++----- .../auto_ap/ssr/admin/transaction_rules.clj | 347 +++++++++--------- src/clj/auto_ap/ssr/components/buttons.clj | 4 +- src/clj/auto_ap/ssr/hx.clj | 14 + src/clj/auto_ap/ssr/utils.clj | 2 + 5 files changed, 337 insertions(+), 265 deletions(-) diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj index e10eb667..ca2af3b1 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -7,6 +7,7 @@ audit-transact conn merge-query + pull-attr pull-many query2]] [auto-ap.query-params :as query-params] @@ -17,17 +18,19 @@ [auto-ap.ssr.components :as com] [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.hx :as hx] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers entity-id + field-validation-error + form-validation-error html-response many-entity ref->enum-schema ref->select-options temp-id - validation-error wrap-form-4xx-2 wrap-schema-decode]] [bidi.bidi :as bidi] @@ -181,7 +184,28 @@ (= :post request-method) (assoc :db/id "new")) _ (cond (= :post request-method) (when-let [extant (seq (dc/q '[:find ?x :in $ ?nc :where [?x :account/numeric-code ?nc]] (dc/db conn) (:account/numeric-code entity)))] - (validation-error (format "The code %d is already in use." (: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)) + ) + ;; TODO the following would work better if the schema was hydrated automatically with needed values + _ (some->> form-params + :account/client-overrides + (group-by :account-client-override/client) + (filter (fn [[client 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)) + + ) {:keys [tempids]} (audit-transact [[:upsert-entity (cond-> entity (:account/numeric-code entity) (assoc :account/code (str (:account/numeric-code entity))))]] (:identity request)) @@ -213,11 +237,21 @@ ;; TODO use cursor ;; TODO index based list not dbid +;; TODO lots of weird edge cases with indexes +;; for example, what happens when you have 0, 1, 2, but then delete 1? +;; what happens when you delete 2 but then add another one? +;; preference is that the field parsing logic does better grouping of what +;; goes with what, building index on the server side +;; not needing to pass index in + (defn client-override* [override] - [:div.flex.gap-2.mb-2.client-override + [:div.flex.gap-2.mb-2.client-override (-> {"x-ref" "p" + :data-key "show" + } + hx/alpine-mount-then-appear) [:div.w-96 (fc/with-field :db/id - (com/hidden {:name (fc/field-name) + (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (fc/with-field :account-client-override/client (com/validated-field {:errors (fc/field-errors)} @@ -226,15 +260,17 @@ :url (bidi/path-for ssr-routes/only-routes :company-search) :value (fc/field-value) - :value-fn :db/id ;; TODO hydration something here - :content-fn :client/name})))] + :value-fn (some-fn :db/id identity) ;; TODO better hydration + :content-fn (fn [value] + (:client/name (cond->> value + (nat-int? value) (dc/pull (dc/db conn) [:client/name]))))})))] (fc/with-field :account-client-override/name [:div.w-96 (com/validated-field {:errors (fc/field-errors)} (com/text-input {:name (fc/field-name) :class "w-full" :value (fc/field-value)}))]) - [:div (com/a-icon-button {"_" (hiccup/raw "on click halt the event then transition the closest <.client-override />'s opacity to 0 then remove closest <.client-override />")} svg/x)]]) + [:div (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)]]) ;; TODO each form: ;; elimante typeahead1 @@ -246,94 +282,108 @@ ;; componentize ;; ensure all dependency oriented stuff works the same way ;; make sure that "new row index" stuff works ok +;; TODO figure out when hx-targets are decided +;; ensure that adding a new one results in a new row -(defn dialog* [& {:keys [account form-params form-errors]}] - (com/modal - {} - [:form#edit-form (merge {:hx-ext "response-targets" - :hx-swap "outerHTML swap:300ms" - :hx-target-400 "#form-errors .error-content"} - form-params) - [:fieldset {:class "hx-disable"} - (com/modal-card - {} - [:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:account/numeric-code account) " - " (:account/name account)]] +(defn dialog* [& {:keys [entity form-params form-errors]}] + (fc/start-form entity form-errors + [:div {:x-data (hx/json {"accountName" (:account/name entity) + "accountCode" (:account/numeric-code entity)})} + (com/modal + {} + [:form#edit-form (merge {:hx-ext "response-targets" + :hx-swap "outerHTML swap:300ms" + :hx-target-400 "#form-errors .error-content"} + form-params) + [:fieldset {:class "hx-disable"} + (com/modal-card + {} + [:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 + [:span {:x-text "accountCode"}] + " - " + [:span {:x-text "accountName"}]]] + [:div.space-y-1 + (when-let [id (:db/id entity)] + (com/hidden {:name "db/id" + :value id})) - (fc/start-form - account form-errors - [:div.space-y-1 - (when-let [id (:db/id account)] - (com/hidden {:name "db/id" - :value id})) + (fc/with-field :account/numeric-code + (if (nil? (:db/id entity)) + (com/validated-field {:label "Numeric code" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :x-model "accountCode" + :autofocus true + :class "w-32"})) + (com/hidden {:name (fc/field-name) + :value (fc/field-value)}))) + (fc/with-field :account/name + (com/validated-field {:label "Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :x-model "accountName" - (fc/with-field :account/numeric-code - (when (nil? (fc/field-value)) - (com/validated-field {:label "Numeric code" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :autofocus true - :class "w-32"})))) - (fc/with-field :account/name - (com/validated-field {:label "Name" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :autofocus true - :class "w-32" - :value (fc/field-value)}))) - (fc/with-field :account/type - (com/validated-field {:label "Account Type" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :class "w-36" - :id "type" - :value (some-> (fc/field-value) name) - :options (ref->select-options "account-type")}))) - (fc/with-field :account/location - (com/validated-field {:label "Location" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-16" - :value (fc/field-value)}))) + :class "w-64" + :value (fc/field-value)}))) + (fc/with-field :account/type + (com/validated-field {:label "Account Type" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :id "type" + :value (some-> (fc/field-value) name) + :options (ref->select-options "account-type")}))) + (fc/with-field :account/location + (com/validated-field {:label "Location" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-16" + :value (fc/field-value)}))) - (fc/with-field :account/invoice-allowance - (com/validated-field {:label "Invoice Allowance" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :value (some-> (fc/field-value) name) - :class "w-36" - :options (ref->select-options "allowance")}))) - (fc/with-field :account/vendor-allowance - (com/validated-field {:label "Vendor Allowance" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :class "w-36" - :value (some-> (fc/field-value) name) - :options (ref->select-options "allowance")}))) - (fc/with-field :account/applicability - (com/validated-field {:label "Applicability" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :class "w-36" - :value (some-> (fc/field-value) name) - :options (ref->select-options "account-applicability")}))) + [:div.flex.flex-wrap.gap-4 + (fc/with-field :account/invoice-allowance + (com/validated-field {:label "Invoice Allowance" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :value (some-> (fc/field-value) name) + :class "w-36" + :options (ref->select-options "allowance")}))) + (fc/with-field :account/vendor-allowance + (com/validated-field {:label "Vendor Allowance" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :value (some-> (fc/field-value) name) + :options (ref->select-options "allowance")})))] + (fc/with-field :account/applicability + (com/validated-field {:label "Applicability" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :value (some-> (fc/field-value) name) + :options (ref->select-options "account-applicability")}))) - (fc/with-field :account/client-overrides - (com/field {:label "Client Overrides" :id "client-overrides"} - (when (fc/field-value) - (doall - (for [override fc/*current*] - (fc/with-cursor override - (client-override* override))))))) - (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes - :admin-account-client-override-new) - :hx-vals (hiccup/raw "js:{index: document.getElementById('client-overrides').children.length - 1}") - :hx-target "#client-overrides" - :hx-swap "beforeend"} - "New override") - [:div#form-errors [:span.error-content]]]) - (com/validated-save-button {:errors []} ;; TODO - "Save account"))]])) + (fc/with-field :account/client-overrides + (com/field {:label "Client Overrides" :id "client-overrides"} + (when (fc/field-value) + (doall + (for [override fc/*current*] + (fc/with-cursor override + (client-override* override))))))) + (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes + :admin-account-client-override-new) + :hx-vals (hiccup/raw "js:{index: document.getElementById('client-overrides').children.length - 1}") + :hx-target "#client-overrides" + :hx-swap "beforeend"} + "New override") + ] + [:div + [:div [:div#form-errors (when (:errors fc/*form-errors*) + [:span.error-content + (com/errors {:errors (:errors fc/*form-errors*)})])]] + (com/validated-save-button {:errors (seq form-errors)} + "Save account")])]])])) (defn new-client-override [{ {:keys [index]} :query-params}] (let [index (or index 0) @@ -346,14 +396,15 @@ (defn account-edit-dialog [request] (let [account (some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %)))] - (html-response (dialog* :account account + (html-response (dialog* :entity account :form-params {:hx-put (str (bidi/path-for ssr-routes/only-routes :admin-account-edit-save))}) :headers {"hx-trigger" "modalopen"}))) (defn account-new-dialog [_] - (html-response (dialog* :account nil + (html-response (dialog* :entity {} + :form-errors {} :form-params {:hx-post (str (bidi/path-for ssr-routes/only-routes :admin-account-new-save))}) :headers {"hx-trigger" "modalopen"})) @@ -362,7 +413,7 @@ ;; TODO hydration ;; TODO consistency of error handling and passing, on all form examples (let [entity (some-> request :last-form)] - (html-response (dialog* :account 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 diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index 972ec8a6..93ceadb2 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -401,189 +401,192 @@ ;; TODO dialog is no longer closeable (defn dialog* [& {:keys [entity form-params form-errors]}] - (com/modal - {:modal-class "max-w-2xl"} + (fc/start-form entity form-errors + (com/modal + {:modal-class "max-w-2xl"} - [:form#edit-form (merge {:hx-ext "response-targets" - :hx-swap "outerHTML swap:300ms" - :hx-target "#modal-holder" ;; TODO sort - :hx-target-400 "#form-errors .error-content" - :x-trap "true" - :class "group/form"} - form-params) - (com/modal-card - {} - [: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 entity)) - (:transaction-rule/client entity))})} + [:form#edit-form (merge {:hx-ext "response-targets" + :hx-swap "outerHTML swap:300ms" + :hx-target "#modal-holder" ;; TODO sort + :hx-target-400 "#form-errors .error-content" + :x-trap "true" + :class "group/form"} + form-params) + (com/modal-card + {} + [: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 entity)) + (:transaction-rule/client entity))})} - (fc/start-form entity form-errors - [:div.space-y-1 - (when-let [id (:db/id entity)] - (com/hidden {:name "db/id" - :value id})) - (fc/with-field :transaction-rule/description - (com/validated-field {:label "Description" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :error? (fc/error?) - :x-init "$el.focus()" - :placeholder "HOME DEPOT" - :class "w-96" - :value (fc/field-value)}))) - [:div.filters {:x-data (hx/json {:clientFilter (boolean (fc/field-value (:transaction-rule/client fc/*current*))) - :bankAccountFilter (boolean (fc/field-value (:transaction-rule/bank-account fc/*current*))) - :amountFilter (boolean (or (fc/field-value (:transaction-rule/amount-gte fc/*current*)) - (fc/field-value (:transaction-rule/amount-lte fc/*current*)))) - :domFilter (boolean (or (fc/field-value (:transaction-rule/dom-gte fc/*current*)) - (fc/field-value (:transaction-rule/dom-lte fc/*current*))))})} + [:div.space-y-1 + (when-let [id (:db/id entity)] + (com/hidden {:name "db/id" + :value id})) + (fc/with-field :transaction-rule/description + (com/validated-field {:label "Description" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :error? (fc/error?) + :x-init "$el.focus()" + :placeholder "HOME DEPOT" + :class "w-96" + :value (fc/field-value)}))) + [:div.filters {:x-data (hx/json {:clientFilter (boolean (fc/field-value (:transaction-rule/client fc/*current*))) + :bankAccountFilter (boolean (fc/field-value (:transaction-rule/bank-account fc/*current*))) + :amountFilter (boolean (or (fc/field-value (:transaction-rule/amount-gte fc/*current*)) + (fc/field-value (:transaction-rule/amount-lte fc/*current*)))) + :domFilter (boolean (or (fc/field-value (:transaction-rule/dom-gte fc/*current*)) + (fc/field-value (:transaction-rule/dom-lte fc/*current*))))})} - [:div.flex.gap-2.mb-2 - (com/a-button {"@click" "clientFilter=true" - "x-show" "!clientFilter"} "Filter client") - (com/a-button {"@click" "bankAccountFilter=true" - "x-show" "clientFilter && !bankAccountFilter"} "Filter bank account") - (com/a-button {"@click" "amountFilter=true" - "x-show" "!amountFilter"} "Filter amount") - (com/a-button {"@click" "domFilter=true" - "x-show" "!domFilter"} "Filter day of month")] - (fc/with-field :transaction-rule/client + [:div.flex.gap-2.mb-2 + (com/a-button {"@click" "clientFilter=true" + "x-show" "!clientFilter"} "Filter client") + (com/a-button {"@click" "bankAccountFilter=true" + "x-show" "clientFilter && !bankAccountFilter"} "Filter bank account") + (com/a-button {"@click" "amountFilter=true" + "x-show" "!amountFilter"} "Filter amount") + (com/a-button {"@click" "domFilter=true" + "x-show" "!domFilter"} "Filter day of month")] + (fc/with-field :transaction-rule/client - (com/validated-field - (-> {:label "Client" - :errors (fc/field-errors) - :x-show "clientFilter"} - (hx/alpine-appear)) - [:div.w-96 - (com/typeahead-2 {:name (fc/field-name) - :error? (fc/error?) - :class "w-96" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :company-search) - :x-model "clientId" - :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))})])) + (com/validated-field + (-> {:label "Client" + :errors (fc/field-errors) + :x-show "clientFilter"} + (hx/alpine-appear)) + [:div.w-96 + (com/typeahead-2 {:name (fc/field-name) + :error? (fc/error?) + :class "w-96" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :company-search) + :x-model "clientId" + :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))})])) - (fc/with-field :transaction-rule/bank-account - (com/validated-field - (-> {:label "Bank Account" - :errors (fc/field-errors) - :x-show "bankAccountFilter"} - hx/alpine-appear) - [:div.w-96 - [:div#bank-account-changer {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead) - :hx-trigger "changed" - :hx-target "next *" - :hx-include "#bank-account-changer" - :hx-swap "innerHTML" + (fc/with-field :transaction-rule/bank-account + (com/validated-field + (-> {:label "Bank Account" + :errors (fc/field-errors) + :x-show "bankAccountFilter"} + hx/alpine-appear) + [:div.w-96 + [:div#bank-account-changer {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead) + :hx-trigger "changed" + :hx-target "next *" + :hx-include "#bank-account-changer" + :hx-swap "innerHTML" - :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fc/field-name)) - :x-init "$watch('clientId', cid => $dispatch('changed', $data))"}] + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fc/field-name)) + :x-init "$watch('clientId', cid => $dispatch('changed', $data))"}] - (bank-account-typeahead* {:client-id ((some-fn :db/id identity) (:transaction-rule/client entity)) - :name (fc/field-name) - :value (fc/field-value)})])) + (bank-account-typeahead* {:client-id ((some-fn :db/id identity) (:transaction-rule/client entity)) + :name (fc/field-name) + :value (fc/field-value)})])) - (com/field (-> {:label "Amount" - :x-show "amountFilter"} - hx/alpine-appear) - [:div.flex.gap-2 - (fc/with-field :transaction-rule/amount-gte - [:div.flex.flex-col - (com/money-input {:name (fc/field-name) - :placeholder ">=" - :class "w-24" - :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 (fc/field-name) - :placeholder "<=" - :class "w-24" - :value (fc/field-value)}) - (com/errors {:errors (fc/field-errors)})])]) + (com/field (-> {:label "Amount" + :x-show "amountFilter"} + hx/alpine-appear) + [:div.flex.gap-2 + (fc/with-field :transaction-rule/amount-gte + [:div.flex.flex-col + (com/money-input {:name (fc/field-name) + :placeholder ">=" + :class "w-24" + :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 (fc/field-name) + :placeholder "<=" + :class "w-24" + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})])]) - (com/field (-> {:label "Day of month" - :x-show "domFilter"} - hx/alpine-appear) - [:div.flex.gap-2 - (fc/with-field :transaction-rule/dom-gte - (com/validated-field - {:errors (fc/field-errors)} - (com/int-input {:name (fc/field-name) - :placeholder ">=" - :class "w-24" - :value (fc/field-value)}))) - (fc/with-field :transaction-rule/dom-lte - (com/validated-field - {:errors (fc/field-errors)} - (com/int-input {:name (fc/field-name) - :placeholder ">=" - :class "w-24" - :value (fc/field-value)})))])] + (com/field (-> {:label "Day of month" + :x-show "domFilter"} + hx/alpine-appear) + [:div.flex.gap-2 + (fc/with-field :transaction-rule/dom-gte + (com/validated-field + {:errors (fc/field-errors)} + (com/int-input {:name (fc/field-name) + :placeholder ">=" + :class "w-24" + :value (fc/field-value)}))) + (fc/with-field :transaction-rule/dom-lte + (com/validated-field + {:errors (fc/field-errors)} + (com/int-input {:name (fc/field-name) + :placeholder ">=" + :class "w-24" + :value (fc/field-value)})))])] - [:h2.text-lg "Outcomes"] - (fc/with-field :transaction-rule/vendor - (com/validated-field {:label "Assign Vendor" - :errors (fc/field-errors)} - [:div.w-96 - (com/typeahead-2 {:name (fc/field-name) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :vendor-search) - :id (str "form-vendor-search") - :class "w-96" - :value (fc/field-value) - :value-fn (some-fn :db/id identity) - :content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})])) + [:h2.text-lg "Outcomes"] + (fc/with-field :transaction-rule/vendor + (com/validated-field {:label "Assign Vendor" + :errors (fc/field-errors)} + [:div.w-96 + (com/typeahead-2 {:name (fc/field-name) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :id (str "form-vendor-search") + :class "w-96" + :value (fc/field-value) + :value-fn (some-fn :db/id identity) + :content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})])) - (fc/with-field :transaction-rule/accounts - (list - (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"} - (when @fc/*current* - (doall (for [tra fc/*current*] - (fc/with-cursor tra - (transaction-rule-account-row* entity tra))))) - (com/data-grid-row - {:class "new-row"} - (com/data-grid-cell {:colspan 4 - :class "bg-gray-100"} - [:div.flex.justify-center - (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes - :admin-transaction-rule-new-account) - :color :secondary - :hx-include "#edit-form" - :hx-ext "rename-params" - ;; TODO - :hx-rename-params-ex (cheshire/generate-string {"transaction-rule/client" "client-id" - "index" "index"}) - :hx-vals (hiccup/raw "js:{index: countRows(\"#transaction-rule-account-table\")}") - :hx-target "#edit-form .new-row" - :hx-swap "beforebegin"} - "New account")]))) - (com/errors {:errors (fc/field-errors)}))) + (fc/with-field :transaction-rule/accounts + (list + (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"} + (when @fc/*current* + (doall (for [tra fc/*current*] + (fc/with-cursor tra + (transaction-rule-account-row* entity tra))))) + (com/data-grid-row + {:class "new-row"} + (com/data-grid-cell {:colspan 4 + :class "bg-gray-100"} + [:div.flex.justify-center + (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes + :admin-transaction-rule-new-account) + :color :secondary + :hx-include "#edit-form" + :hx-ext "rename-params" + ;; TODO + :hx-rename-params-ex (cheshire/generate-string {"transaction-rule/client" "client-id" + "index" "index"}) + :hx-vals (hiccup/raw "js:{index: countRows(\"#transaction-rule-account-table\")}") + :hx-target "#edit-form .new-row" + :hx-swap "beforebegin"} + "New account")]))) + (com/errors {:errors (fc/field-errors)}))) - (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) - :size :small - :orientation :horizontal}))) + (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) + :size :small + :orientation :horizontal}))) - [:div#form-errors (when (:errors fc/*form-errors*) - [:span.error-content - (com/errors {:errors (:errors fc/*form-errors*)})])]])] - (com/validated-save-button {:errors form-errors} "Save rule"))])) + ;; TODO componentize + ]] + [:div + [:div#form-errors (when (:errors fc/*form-errors*) + [:span.error-content + (com/errors {:errors (:errors fc/*form-errors*)})])] + (com/validated-save-button {:errors form-errors} "Save rule")])]))) ;; TODO Should forms have some kind of helper to render an individual field? saving @@ -592,8 +595,8 @@ ;; pull out the single field to swap (defn new-account [{{:keys [client-id index]} :query-params}] - (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] + (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 index {} )) {:db/id (str (java.util.UUID/randomUUID)) diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index 076945f9..9df54b67 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -134,7 +134,9 @@ (defn a-icon-button- [params & children] (into - [:a (update params :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100") + [:a (-> params (update :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100" + ) + (update :href #(or % ""))) [:div.h-4.w-4 children]])) (defn save-button- [params & children] diff --git a/src/clj/auto_ap/ssr/hx.clj b/src/clj/auto_ap/ssr/hx.clj index cb5e5c00..895d024e 100644 --- a/src/clj/auto_ap/ssr/hx.clj +++ b/src/clj/auto_ap/ssr/hx.clj @@ -24,3 +24,17 @@ "x-transition:enter" "transition duration-500" "x-transition:enter-start" "opacity-0" "x-transition:enter-end" "opacity-100")) + +(defn alpine-disappear [m] + (assoc m + "x-transition:leave" "transition duration-500" + "x-transition:leave-start" "opacity-100" + "x-transition:leave-end" "opacity-0")) + +(defn alpine-mount-then-appear [{:keys [data-key] :as params}] + (merge (-> {"x-data" (json {data-key false}) + "x-init" (format "$nextTick(() => %s=true)" (name data-key)) + "x-show" (name data-key)} + alpine-appear + alpine-disappear) + (dissoc params :data-key))) diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index 6529aebb..b80c08bd 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -146,6 +146,8 @@ (defn keyword->str [k] (subs (str k) 1)) + +;; TODO need to remove or at least remove usages as the form is not included (defn validation-error [m & {:as data}] (throw+ (ex-info m (merge data {:type :validation})))) From dc1fefd30c007c28388f875c009bf15521c0e796 Mon Sep 17 00:00:00 2001 From: Bryce Date: Mon, 23 Oct 2023 11:08:05 -0700 Subject: [PATCH 4/5] accounts and transactions work the same way --- src/clj/auto_ap/ssr/admin/accounts.clj | 10 ++++++---- src/clj/auto_ap/ssr/admin/transaction_rules.clj | 16 +++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj index ca2af3b1..87f83db0 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -244,11 +244,12 @@ ;; goes with what, building index on the server side ;; not needing to pass index in +;; TODO decide when cursors are used. other cases it's not, some are (defn client-override* [override] - [:div.flex.gap-2.mb-2.client-override (-> {"x-ref" "p" + [:div.flex.gap-2.mb-2.client-override (-> {:x-ref "p" :data-key "show" - } - hx/alpine-mount-then-appear) + :x-data (hx/json {:show (boolean (not (:new? override)))})} + hx/alpine-mount-then-appear) [:div.w-96 (fc/with-field :db/id (com/hidden {:name (fc/field-name) @@ -388,7 +389,8 @@ (defn new-client-override [{ {:keys [index]} :query-params}] (let [index (or index 0) account {:account/client-overrides (conj (into [] (repeat index {})) - {:db/id (str (java.util.UUID/randomUUID))})}] ;; TODO schema decode is not working + {:db/id (str (java.util.UUID/randomUUID)) + :new? true})}] ;; TODO schema decode is not working (html-response (fc/start-form account [] (fc/with-cursor (get-in fc/*current* [:account/client-overrides index]) diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index 93ceadb2..fa27b0e5 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -342,8 +342,12 @@ (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)))})} + (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 @@ -394,10 +398,7 @@ (* 100 ) (long ))})))))) (com/data-grid-cell {:class "align-top"} - (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)))) + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) ;; TODO dialog is no longer closeable (defn dialog* [& {:keys [entity form-params form-errors]}] @@ -600,7 +601,8 @@ client-id) :transaction-rule/accounts (conj (into [] (repeat index {} )) {:db/id (str (java.util.UUID/randomUUID)) - :transaction-rule-account/location "Shared"})}] + :transaction-rule-account/location "Shared" + :new? true})}] (html-response (fc/start-form transaction-rule [] (fc/with-cursor (get-in fc/*current* [:transaction-rule/accounts index]) From be9d777a1778d917e691b706d7f39b5d58b5a218 Mon Sep 17 00:00:00 2001 From: Bryce Date: Mon, 23 Oct 2023 11:59:14 -0700 Subject: [PATCH 5/5] consistent user experience with new client --- src/clj/auto_ap/ssr/admin/accounts.clj | 91 +++++---- .../auto_ap/ssr/admin/transaction_rules.clj | 2 +- src/clj/auto_ap/ssr/users.clj | 182 ++++++++++++------ src/cljc/auto_ap/ssr_routes.cljc | 1 + 4 files changed, 184 insertions(+), 92 deletions(-) diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj index 87f83db0..6146ef91 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -246,32 +246,35 @@ ;; TODO decide when cursors are used. other cases it's not, some are (defn client-override* [override] - [:div.flex.gap-2.mb-2.client-override (-> {:x-ref "p" - :data-key "show" - :x-data (hx/json {:show (boolean (not (:new? override)))})} - hx/alpine-mount-then-appear) - [:div.w-96 - (fc/with-field :db/id - (com/hidden {:name (fc/field-name) - :value (fc/field-value)})) - (fc/with-field :account-client-override/client - (com/validated-field {:errors (fc/field-errors)} - (com/typeahead-2 {:name (fc/field-name) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :value (fc/field-value) - :value-fn (some-fn :db/id identity) ;; TODO better hydration - :content-fn (fn [value] - (:client/name (cond->> value - (nat-int? value) (dc/pull (dc/db conn) [:client/name]))))})))] - (fc/with-field :account-client-override/name - [:div.w-96 - (com/validated-field {:errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-full" - :value (fc/field-value)}))]) - [:div (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)]]) + (com/data-grid-row (-> {:x-ref "p" + :data-key "show" + :x-data (hx/json {:show (boolean (not (:new? override)))})} + hx/alpine-mount-then-appear) + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + (fc/with-field :account-client-override/client + (com/data-grid-cell {} + (com/validated-field {:errors (fc/field-errors)} + (com/typeahead-2 {:name (fc/field-name) + :placeholder "Search..." + :class "w-96" + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :value (fc/field-value) + :value-fn (some-fn :db/id identity) ;; TODO better hydration + :content-fn (fn [value] + (:client/name (cond->> value + (nat-int? value) (dc/pull (dc/db conn) [:client/name]))))})))) + (fc/with-field :account-client-override/name + (com/data-grid-cell + {} + (com/validated-field {:errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-96" + :value (fc/field-value)})))) + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) ;; TODO each form: ;; elimante typeahead1 @@ -366,18 +369,32 @@ :options (ref->select-options "account-applicability")}))) (fc/with-field :account/client-overrides + (com/field {:label "Client Overrides" :id "client-overrides"} - (when (fc/field-value) - (doall - (for [override fc/*current*] - (fc/with-cursor override - (client-override* override))))))) - (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes - :admin-account-client-override-new) - :hx-vals (hiccup/raw "js:{index: document.getElementById('client-overrides').children.length - 1}") - :hx-target "#client-overrides" - :hx-swap "beforeend"} - "New override") + + (com/data-grid {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} "Account name") + (com/data-grid-header {})] + :id "client-override-table"} + (when (fc/field-value) + (doall + (for [override fc/*current*] + (fc/with-cursor override + (client-override* override))))) + (com/data-grid-row + {:class "new-row"} + (com/data-grid-cell {:colspan 3 + :class "bg-gray-100"} + [:div.flex.justify-center + (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes + :admin-account-client-override-new) + :color :secondary + :hx-include "#edit-form" + :hx-vals (hiccup/raw "js:{index: countRows(\"#client-override-table\")}") + :hx-target "#edit-form .new-row" + :hx-swap "beforebegin"} + "New override")]))))) + ] [:div [:div [:div#form-errors (when (:errors fc/*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 fa27b0e5..5cd9dcf9 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -549,7 +549,7 @@ (com/data-grid-header {:class "w-16"} "%") (com/data-grid-header {:class "w-16"})] :id "transaction-rule-account-table"} - (when @fc/*current* + (when (fc/field-value) (doall (for [tra fc/*current*] (fc/with-cursor tra (transaction-rule-account-row* entity tra))))) diff --git a/src/clj/auto_ap/ssr/users.clj b/src/clj/auto_ap/ssr/users.clj index 2b327f95..46c5ca19 100644 --- a/src/clj/auto_ap/ssr/users.clj +++ b/src/clj/auto_ap/ssr/users.clj @@ -14,14 +14,19 @@ :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers entity-id - forced-vector html-response + many-entity ref->enum-schema + ref->select-options + wrap-form-4xx-2 wrap-schema-decode]] [auto-ap.time :as atime] [bidi.bidi :as bidi] @@ -29,7 +34,9 @@ [clojure.string :as str] [config.core :refer [env]] [datomic.api :as dc] - [malli.core :as mc])) + [hiccup2.core :as hiccup] + [malli.core :as mc] + [clj-time.format :as f])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" @@ -258,79 +265,146 @@ } :session {:identity (dissoc (auth/user->jwt user "FAKE_TOKEN") :exp)}})) +(defn client-row* [client] + (com/data-grid-row (-> {:x-ref "p" + :data-key "show" + :x-data (hx/json {:show (boolean (not (fc/field-value (:new? client))))})} + hx/alpine-mount-then-appear) + (com/data-grid-cell {} + (com/validated-field {:errors (fc/field-errors (:db/id fc/*current*))} + (com/typeahead-2 {:name (fc/field-name (:db/id fc/*current*)) + :class "w-full" + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :value (fc/field-value) + + :value-fn :db/id ;; TODO better hydration + :content-fn (fn [value] + (:client/name (dc/pull (dc/db conn) [:client/name] + (or (:db/id value) + value)))) + :size :small}))) + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) + +(defn dialog* [{:keys [entity form-params form-errors]}] + (fc/start-form + entity form-errors + (com/modal + {} + [:form#edit-form (merge {:hx-ext "response-targets" + :hx-put (str (bidi/path-for ssr-routes/only-routes + :user-edit-save + :request-method :put)) + :hx-swap "outerHTML swap:300ms" + :hx-target-400 "#form-errors .error-content" + :class "w-full"} + form-params) + [: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 entity)]] + [:div.space-y-6 + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + (fc/with-field :user/role + (com/validated-field {:label "Role" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :autofocus true + :value (some->> (fc/field-value) name) + :options (ref->select-options "user-role")}))) + (fc/with-field :user/clients + (com/validated-field {:label "Clients"} + (com/data-grid {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} )] + :id "client-table"} + (doall (for [client fc/*current*] + (fc/with-cursor client + (client-row* client)))) + (com/data-grid-row + {:class "new-row"} + (com/data-grid-cell {:colspan 2 + :class "bg-gray-100"} + [:div.flex.justify-center + (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes + :user-client-new) + :color :secondary + :hx-include "#edit-form" + :hx-vals (hiccup/raw "js:{index: countRows(\"#client-table\")}") + :hx-target "#edit-form .new-row" + :hx-swap "beforebegin"} + "New override")]))) + )) + [:div#form-errors [:span.error-content]]] + [:div + [:div [:div#form-errors (when (:errors fc/*form-errors*) + [:span.error-content + (com/errors {:errors (:errors fc/*form-errors*)})])]] + (com/validated-save-button {:errors (seq form-errors)} + "Save user")])]]))) + +;; TODO rename edit-form or make it generic (defn user-edit-save [{:keys [form-params identity] :as request}] - (let [_ @(dc/transact conn [[:upsert-entity form-params]]) + (let [_ @(dc/transact conn [[:upsert-entity form-params]]) user (some-> form-params :db/id (#(dc/pull (dc/db conn) default-read %)))] (html-response (row* identity user {:flash? true}) - :headers {"hx-trigger" "modalclose" + :headers {"hx-trigger" "modalclose" "hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))}))) +(defn user-save-error [request] + ;; TODO hydration + ;; TODO consistency of error handling and passing, on all form examples + (let [entity (some-> request :last-form)] + (html-response (dialog* {:entity entity + :form-errors (:form-errors request)}) + :headers {"hx-retarget" "#edit-form fieldset" + "hx-reselect" "#edit-form fieldset"}))) + (defn user-edit-dialog [request] (let [user (some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %)))] (html-response - (com/modal - {} - [:form {:hx-ext "response-targets" - :hx-put (str (bidi/path-for ssr-routes/only-routes - :user-edit-save - :request-method :put)) - :hx-swap "outerHTML swap:300ms" - :hx-target-400 "#form-errors .error-content" - :class "w-full"} - [: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/hidden {:name "db/id" - :value (:db/id user)}) - (com/field {:label "Role"} - (com/select {:name "user/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"]]})) - (com/field {:label "Clients"} - (com/typeahead {:name "user/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)) - :size :small})) - [:div#form-errors [:span.error-content]]] - (com/validated-save-button {:errors []} ;; TODO - "Save user"))]]) + (dialog* {:entity user + :form-errors {}}) + :headers {"hx-trigger" "modalopen"}))) +(defn new-client [{ {:keys [index]} :query-params}] + (let [index (or index 0) + account {:user/clients (conj (into [] (repeat index {})) + {:db/id nil + :new? true})}] ;; TODO schema decode is not working + (html-response + (fc/start-form account [] + (fc/with-cursor (get-in fc/*current* [:user/clients index]) + (client-row* fc/*current*)))))) + (def key->handler (apply-middleware-to-all-handlers {:users (helper/page-route grid-page) :user-table (helper/table-route grid-page) :user-edit-save (-> user-edit-save - (wrap-schema-decode - :form-schema (mc/schema - [:map - [:db/id entity-id] - [:user/clients {:optional true} - [:maybe - (forced-vector entity-id)]] - [:user/role (ref->enum-schema "user-role")]]))) + (wrap-schema-decode :form-schema (mc/schema + [:map + [:db/id entity-id] + [:user/clients {:optional true} + [:maybe + (many-entity {} [:db/id entity-id])]] + [:user/role (ref->enum-schema "user-role")]])) + (wrap-nested-form-params) + (wrap-form-4xx-2 user-save-error)) + :user-client-new (-> new-client + (wrap-schema-decode :query-schema [:map + [:index {:optional true + :default 0} [nat-int? {:default 0}]]])) :user-edit-dialog (-> user-edit-dialog (wrap-schema-decode :route-schema (mc/schema [:map [:db/id entity-id]]))) diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index cad4d193..7da78c5e 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -16,6 +16,7 @@ ["/inspect/" [#"\d+" :entity-id] #"/?"] :admin-history-inspect} "/user" {"" {:get :users :put :user-edit-save} + "/client/new" :user-client-new "/table" :user-table "/impersonate" :user-impersonate [[#"\d+" :db/id] "/edit"] {:get :user-edit-dialog}}