From be9d777a1778d917e691b706d7f39b5d58b5a218 Mon Sep 17 00:00:00 2001 From: Bryce Date: Mon, 23 Oct 2023 11:59:14 -0700 Subject: [PATCH] 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}}