diff --git a/resources/public/js/htmx-disable.js b/resources/public/js/htmx-disable.js
index d3784e32..ec0bc0b1 100644
--- a/resources/public/js/htmx-disable.js
+++ b/resources/public/js/htmx-disable.js
@@ -122,3 +122,9 @@ htmx.defineExtension('trigger-filter', {
initDatepicker = function(elem) {
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");
+ return rows.length;
+}
diff --git a/src/clj/auto_ap/cursor.clj b/src/clj/auto_ap/cursor.clj
new file mode 100644
index 00000000..0ff1448d
--- /dev/null
+++ b/src/clj/auto_ap/cursor.clj
@@ -0,0 +1,137 @@
+(ns auto-ap.cursor
+ (:import (clojure.lang IDeref Atom ILookup Counted IFn AFn Indexed ISeq Seqable)))
+
+; TODO not sure if these methods are needed at all; ICursor is used solely as a marker right now
+(defprotocol ICursor
+ (path [cursor])
+ (state [cursor]))
+
+
+(defprotocol ITransact
+ (-transact! [cursor f]))
+
+
+(declare to-cursor cursor?)
+
+
+(deftype ValCursor [value state path]
+ IDeref
+ (deref [_]
+ (get-in @state path value))
+ ICursor
+ (path [_] path)
+ (state [_] state)
+ ITransact
+ (-transact! [_ f]
+ (get-in
+ (swap! state (if (empty? path) f #(update-in % path f)))
+ path)))
+
+
+(deftype MapCursor [value state path]
+ Counted
+ (count [_]
+ (count (get-in @state path value)))
+ ICursor
+ (path [_] path)
+ (state [_] state)
+ IDeref
+ (deref [_]
+ (get-in @state path value))
+ IFn
+ (invoke [this key]
+ (get this key))
+ (invoke [this key defval]
+ (get this key defval))
+ (applyTo [this args]
+ (AFn/applyToHelper this args))
+ ILookup
+ (valAt [obj key]
+ (.valAt obj key nil))
+ (valAt [_ key defv]
+ (let [value (get-in @state path value)]
+ (to-cursor (get value key defv) state (conj path key) defv)))
+ ITransact
+ (-transact! [cursor f]
+ (get-in
+ (swap! state (if (empty? path) f #(update-in % path f)))
+ path))
+ Seqable
+ (seq [this]
+ (for [[k v] @this]
+ [k (to-cursor v state (conj path k) nil)])))
+
+
+(deftype VecCursor [value state path]
+ Counted
+ (count [_]
+ (count (get-in @state path)))
+ ICursor
+ (path [_] path)
+ (state [_] state)
+ IDeref
+ (deref [_]
+ (get-in @state path))
+ IFn
+ (invoke [this i]
+ (nth this i))
+ (applyTo [this args]
+ (AFn/applyToHelper this args))
+ ILookup
+ (valAt [this i]
+ (nth this i))
+ (valAt [this i not-found]
+ (nth this i not-found))
+ Indexed
+ (nth [_ i]
+ (let [value (get-in @state path value)]
+ (to-cursor (nth value i) state (conj path i) nil)))
+ (nth [_ i not-found]
+ (let [value (get-in @state path value)]
+ (to-cursor (nth value i not-found) state (conj path i) not-found)))
+ ITransact
+ (-transact! [cursor f]
+ (get-in
+ (swap! state (if (empty? path) f #(update-in % path f)))
+ path))
+ Seqable
+ (seq [this]
+ (for [[v i] (map vector @this (range))]
+ (to-cursor v state (conj path i) nil))))
+
+
+(defn- to-cursor
+ ([v state path value]
+ (cond
+ (cursor? v) v
+ (map? v) (MapCursor. value state path)
+ (vector? v) (VecCursor. value state path)
+ :else (ValCursor. value state path)
+ )))
+
+
+(defn cursor? [c]
+ "Returns true if c is a cursor."
+ (satisfies? ICursor c))
+
+
+(defn cursor [v]
+ "Creates cursor from supplied value v. If v is an ordinary
+ data structure, it is wrapped into atom. If v is an atom,
+ it is used directly, so all changes by cursor modification
+ functions are reflected in supplied atom reference."
+ (to-cursor (if (instance? Atom v) @v v)
+ (if (instance? Atom v) v (atom v))
+ [] nil))
+
+
+(defn transact! [cursor f]
+ "Changes value beneath cursor by passing it to a single-argument
+ function f. Old value will be passed as function argument. Function
+ result will be the new value."
+ (-transact! cursor f))
+
+
+(defn update! [cursor v]
+ "Replaces value supplied by cursor with value v."
+ (-transact! cursor (constantly v)))
diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj
index 172efbe0..21a0b39a 100644
--- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj
+++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj
@@ -45,7 +45,8 @@
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[iol-ion.query :refer [ident]]
- [malli.core :as mc]))
+ [malli.core :as mc]
+ [auto-ap.cursor :as cursor]))
;; 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.
@@ -57,6 +58,27 @@
;; 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"
@@ -271,21 +293,21 @@
bank-account-id))
(defn transaction-rule-save [{:keys [form-params request-method identity] :as request}]
- (let [entity (cond-> form-params
- (= :post request-method) (assoc :db/id "new")
- true (assoc :transaction-rule/note (entity->note form-params)))
- _ (doseq [[{:transaction-rule-account/keys [account location]} i] (map vector (:transaction-rule/accounts entity) (range))
- :let [account-location (pull-attr (dc/db conn) :account/location account)]
- :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))
+ (let [entity (cond-> form-params
+ (= :post request-method) (assoc :db/id "new")
+ true (assoc :transaction-rule/note (entity->note form-params)))
+ _ (doseq [[{:transaction-rule-account/keys [account location]} i] (map vector (:transaction-rule/accounts entity) (range))
+ :let [account-location (pull-attr (dc/db conn) :account/location account)]
+ :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))
total (reduce +
0.0
(map :transaction-rule-account/percentage
(:transaction-rule/accounts entity)))
- _ (when-not (dollars= 1.0 total)
+ _ (when-not (dollars= 1.0 total)
(form-validation-error (format "Expense accounts total (%d%%) must add to 100%%" (int (* 100.0 total)))
:form entity))
@@ -344,64 +366,65 @@
(defn- transaction-rule-account-row*
[transaction-rule account]
(com/data-grid-row {}
- (let [account-name (path->name2 :transaction-rule/accounts (:db/id account) :transaction-rule-account/account)
- location-name (path->name2 :transaction-rule/accounts (:db/id account) :transaction-rule-account/location)]
+ (let [account-name (with-cursor (:transaction-rule-account/account account)
+ (cursor-name))]
(list
- (com/data-grid-cell {}
- [:div {:hx-trigger (hx/trigger-field-change :name "transaction-rule/client"
- :from "#edit-form")
- :hx-include "#edit-form"
- :hx-vals (hx/vals {:name account-name})
- :hx-ext "rename-params"
- :hx-rename-params-ex (hx/json {:transaction-rule/client "client-id"
- :name "name"
- account-name "value"})
- :hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead))
- :hx-swap "innerHTML"}
- (account-typeahead* {:value (:transaction-rule-account/account account)
- :client-id (:db/id (:transaction-rule/client transaction-rule))
- :name account-name})
- (com/field-errors {:source account
- :key :transaction-rule-account/account})])
- (com/data-grid-cell {}
- [:div [:div {:hx-trigger (hx/triggers
- (hx/trigger-field-change :name "transaction-rule/client"
- :from "#edit-form")
- (hx/trigger-field-change :name account-name
- :from "#edit-form"))
- :hx-include "#edit-form"
- :hx-vals (hx/vals {:name location-name})
- :hx-ext "rename-params"
- :hx-rename-params-ex (hx/json {"transaction-rule/client" "client-id"
- account-name "account-id"
- "name" "name"
- location-name "value"})
- :hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select)
- :hx-swap "innerHTML"}
- (location-select* {:name location-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 (:transaction-rule-account/location account)})]
- (com/field-errors {:source account
- :key :transaction-rule-account/location})])
- (com/data-grid-cell (com/money-input {:name (format "transaction-rule/accounts[%s][transaction-rule-account/percentage]" (:db/id account))
- :class "w-16"
- :value (some-> account
- :transaction-rule-account/percentage
- (* 100 )
- (long ))})
- (com/field-errors {:source account
- :key :transaction-rule-account/percentage}))))
+ (with-cursor (:db/id account)
+ (com/hidden {:name (cursor-name)
+ :value (cursor-value)}))
+ (with-cursor (:transaction-rule-account/account account)
+ (com/data-grid-cell {}
+ [:div {:hx-trigger (hx/trigger-field-change :name "transaction-rule/client"
+ :from "#edit-form")
+ :hx-include "#edit-form"
+ :hx-vals (hx/vals {:name account-name})
+ :hx-ext "rename-params"
+ :hx-rename-params-ex (hx/json {:transaction-rule/client "client-id"
+ :name "name"
+ 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)
+ :client-id (:db/id (:transaction-rule/client transaction-rule))
+ :name (cursor-name)})
+ (com/errors {:errors (cursor-errors)})]))
+ (with-cursor (:transaction-rule-account/location account)
+ (com/data-grid-cell {}
+ [:div [:div {:hx-trigger (hx/triggers
+ (hx/trigger-field-change :name "transaction-rule/client"
+ :from "#edit-form")
+ (hx/trigger-field-change :name account-name
+ :from "#edit-form"))
+ :hx-include "#edit-form"
+ :hx-vals (hx/vals {:name (cursor-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"})
+ :hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select)
+ :hx-swap "innerHTML"}
+ (location-select* {:name (cursor-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)})]
+ ))
+ (with-cursor (:transaction-rule-account/percentage account)
+ (com/data-grid-cell (com/money-input {:name (cursor-name)
+ :class "w-16"
+ :value (some-> (cursor-value)
+ (* 100 )
+ (long ))})
+ (com/errors {:errors (cursor-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]}]
(com/modal
{:modal-class "max-w-4xl"}
@@ -415,121 +438,151 @@
form-params)
[:fieldset {:class "hx-disable" :hx-disinherit "hx-target"}
- [:div.space-y-6
- (when-let [id (:db/id entity)]
- (com/hidden {:name "db/id"
- :value id}))
- (com/field {:label "Client"}
- [:div.w-96
- (com/typeahead {:name "transaction-rule/client"
- :class "w-96"
- :placeholder "Search..."
- :url (bidi/path-for ssr-routes/only-routes :company-search)
- :id (str "form-client-search")
- :value (:transaction-rule/client entity)
- :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 (cursor/cursor entity)
+ [:div.space-y-2
+ (when-let [id (:db/id entity)]
+ (com/hidden {:name "db/id"
+ :value id}))
+ (with-cursor (:transaction-rule/client *cursor*)
+ (com/validated-field
+ {:label "Client"
+ :errors (cursor-errors)}
+ [:div.w-96
+ (com/typeahead {:name (cursor-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-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/field {:label "Bank Account"}
- [: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 "transaction-rule/bank-account"})
- :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 "transaction-rule/bank-account"
- :value (:transaction-rule/bank-account entity)})
- (com/field-errors {:source entity
- :key :transaction-rule/bank-account})])
+ (with-cursor (:transaction-rule/bank-account *cursor*)
+ (com/validated-field {:label "Bank Account"
+ :errors (cursor-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-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)})]))
- (com/field {:label "Description"
- :error-source entity
- :error-key :transaction-rule/description}
- (com/text-input {:name "transaction-rule/description"
- "_" (hiccup/raw "on load call me.focus()")
- :placeholder "HOME DEPOT"
- :class "w-96"
- :value (:transaction-rule/description entity)}))
+ (with-cursor (:transaction-rule/description *cursor*)
+ (com/validated-field {:label "Description"
+ :errors (cursor-errors)}
+ (com/text-input {:name (cursor-name)
+ :placeholder "HOME DEPOT"
+ :class "w-96"
+ :value (cursor-value)})))
+ (com/field {:label "Amount"}
+ [:div.flex.gap-2
+ (with-cursor (:transaction-rule/amount-gte *cursor*)
+ [:div.flex.flex-col
+ (com/money-input {:name (cursor-name)
+ :placeholder ">="
+ :class "w-24"
+ :value (cursor-value)})
+ (com/errors {:errors (cursor-errors)})])
+ (with-cursor (:transaction-rule/amount-lte *cursor*)
+ [:div.flex.flex-col
+ (com/money-input {:name (cursor-name)
+ :placeholder "<="
+ :class "w-24"
+ :value (cursor-value)})
+ (com/errors {:errors (cursor-errors)})])])
+ (com/field {:label "Day of month"}
+ [:div.flex.gap-2
+ (with-cursor (:transaction-rule/dom-gte *cursor*)
+ [:div.flex.flex-col
+ (com/int-input {:name (cursor-name)
+ :placeholder ">="
+ :class "w-24"
+ :value (cursor-value)})
+ (com/errors {:errors (cursor-errors)})])
+ (with-cursor (:transaction-rule/dom-lte *cursor*)
+ [:div.flex.flex-col
+ (com/int-input {:name (cursor-name)
+ :placeholder ">="
+ :class "w-24"
+ :value (cursor-value)})
+ (com/errors {:errors (cursor-errors)})])])
- (com/field {:label "Amount"}
- [:div.flex.gap-2
- (com/money-input {:name "transaction-rule/amount-gte"
- :placeholder ">="
- :class "w-24"
- :value (:transaction-rule/amount-gte entity)})
- (com/money-input {:name "transaction-rule/amount-lte"
- :placeholder "<="
- :class "w-24"
- :value (:transaction-rule/amount-lte entity)})])
+ [:h2.text-lg "Outcomes"]
+ (with-cursor (:transaction-rule/vendor *cursor*)
+ (com/validated-field {:label "Assign Vendor"
+ :errors (cursor-errors)}
+ [:div.w-96
+ (com/typeahead {:name (cursor-name)
+ :placeholder "Search..."
+ :url (bidi/path-for ssr-routes/only-routes :vendor-search)
+ :id (str "form-vendor-search")
+ :value (cursor-value)
+ :value-fn (some-fn :db/id identity)
+ :content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})]))
- (com/field {:label "Day of month"}
- [:div.flex.gap-2
- (com/int-input {:name "transaction-rule/dom-gte"
- :placeholder ">="
- :class "w-24"
- :value (:transaction-rule/dom-gte entity)})
- (com/int-input {:name "transaction-rule/dom-lte"
- :placeholder "<="
- :class "w-24"
- :value (:transaction-rule/dom-lte entity)})])
-
- [:h2.text-lg "Outcomes"]
- (com/field {:label "Assign Vendor"
- :error-source entity
- :error-key :transaction-rule/vendor}
- [:div.w-96
- (com/typeahead {:name "transaction-rule/vendor"
- :placeholder "Search..."
- :url (bidi/path-for ssr-routes/only-routes :vendor-search)
- :id (str "form-vendor-search")
- :value (:transaction-rule/vendor entity)
- :value-fn (some-fn :db/id identity)
- :content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})])
-
- (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"}
- (for [tra (:transaction-rule/accounts entity)]
- (transaction-rule-account-row* entity tra)))
- (com/field-errors {:source entity
- :key :transaction-rule/accounts})
- (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes
- :admin-transaction-rule-new-account)
- :hx-include "#edit-form"
- :hx-ext "rename-params"
- :hx-rename-params-ex (cheshire/generate-string {"transaction-rule/client" "client-id"})
- :hx-target "#transaction-rule-account-table tbody"
- :hx-swap "beforeend"}
- "New account")
- (com/radio {:options (ref->radio-options "transaction-approval-status")
- :value (:transaction-rule/transaction-approval-status entity)
- :name (path->name2 :transaction-rule/transaction-approval-status)})
-
- [:div#form-errors [:span.error-content
- (com/field-errors {:source entity})]]
- (com/button {:color :primary :form "edit-form" :type "submit"}
- "Save")]]]
+ (with-cursor (:transaction-rule/accounts *cursor*)
+ (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"}
+ (for [tra *cursor*]
+ (with-cursor tra
+ (transaction-rule-account-row* entity tra))))
+ (com/errors {:errors (cursor-errors)})))
+ (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes
+ :admin-transaction-rule-new-account)
+ :hx-include "#edit-form"
+ :hx-ext "rename-params"
+ :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 "#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)}))
+
+ [:div#form-errors [:span.error-content
+ (com/errors {:errors (:errors (meta entity))})]]
+ (com/button {:color :primary :form "edit-form" :type "submit"}
+ "Save")])]]
[:div])))
-(defn new-account [{{:keys [client-id]} :query-params}]
- (html-response
- (transaction-rule-account-row*
- {:transaction-rule/client (dc/pull (dc/db conn) '[:client/name :client/locations :db/id]
- client-id)}
- {:db/id (str (java.util.UUID/randomUUID))
- :transaction-rule-account/location "shared"})))
+(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]
+ 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*
+ )))))))
+
+
+;; TODO check to see if it should be called "Shared" or "shared" for the value
(defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}]
(html-response (location-select* {:name name
@@ -574,7 +627,7 @@
(def transaction-rule-schema (mc/schema
[:map
[:db/id {:optional true} [:maybe entity-id]]
- [:transaction-rule/client {:optional true} [:maybe entity-id]]
+ [:transaction-rule/client entity-id]
[:transaction-rule/description [:and regex
[:string {:min 3}]]]
[:transaction-rule/bank-account [:maybe entity-id]]
@@ -599,7 +652,8 @@
:admin-transaction-rule-new-account (-> new-account
(wrap-schema-decode :query-schema [:map
[:client-id {:optional true}
- [:maybe entity-id]]])
+ [:maybe entity-id]]
+ [:index nat-int?]])
wrap-admin wrap-client-redirect-unauthenticated)
:admin-transaction-rule-location-select (-> location-select
(wrap-schema-decode :query-schema [:map
diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj
index eed6f5f4..e7f6782d 100644
--- a/src/clj/auto_ap/ssr/components.clj
+++ b/src/clj/auto_ap/ssr/components.clj
@@ -33,6 +33,8 @@
(def typeahead inputs/typeahead-)
(def field-errors inputs/field-errors-)
(def field inputs/field-)
+(def validated-field inputs/validated-field-)
+(def errors inputs/errors-)
(def left-aside aside/left-aside-)
(def company-aside-nav aside/company-aside-nav-)
diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj
index e8b9b0af..97b3e8e3 100644
--- a/src/clj/auto_ap/ssr/components/inputs.clj
+++ b/src/clj/auto_ap/ssr/components/inputs.clj
@@ -122,5 +122,13 @@ c.clearChoices();
(field-errors- {:source (:error-source params)
: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)])
+
+(defn validated-field- [params & rest]
+ (field- (dissoc params :errors)
+ rest
+ (errors- {:errors (:errors params)})))
+
(defn hidden- [{:keys [name value]}]
[:input {:type "hidden" :value value :name name}])
diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj
index 11ec3da9..33540125 100644
--- a/src/clj/auto_ap/ssr/utils.clj
+++ b/src/clj/auto_ap/ssr/utils.clj
@@ -117,8 +117,9 @@
(def map->db-id-decoder
{:enter (fn [x]
(into []
- (for [[k v] x]
- (assoc v :db/id (cond (and (string? k) (re-find #"^\d+$" k))
+ (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)