diff --git a/resources/public/output.css b/resources/public/output.css index 6b7c19ec..96983899 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1348,6 +1348,19 @@ input:checked + .toggle-bg { height: 100vh; } +.h-64 { + height: 16rem; +} + +.h-min { + height: -moz-min-content; + height: min-content; +} + +.h-\[90vh\] { + height: 90vh; +} + .max-h-96 { max-height: 24rem; } @@ -1360,6 +1373,14 @@ input:checked + .toggle-bg { max-height: 90vh; } +.max-h-\[100vh\] { + max-height: 100vh; +} + +.max-h-\[80vh\] { + max-height: 80vh; +} + .w-1\/2 { width: 50%; } @@ -1433,6 +1454,23 @@ 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%; +} + .max-w-2xl { max-width: 42rem; } @@ -1457,6 +1495,10 @@ input:checked + .toggle-bg { max-width: 1024px; } +.max-w-xs { + max-width: 20rem; +} + .flex-1 { flex: 1 1 0%; } @@ -1481,6 +1523,10 @@ input:checked + .toggle-bg { flex-grow: 1; } +.grow { + flex-grow: 1; +} + .basis-1\/4 { flex-basis: 25%; } @@ -1671,6 +1717,10 @@ input:checked + .toggle-bg { place-items: center; } +.content-center { + align-content: center; +} + .items-start { align-items: flex-start; } @@ -1819,6 +1869,10 @@ input:checked + .toggle-bg { overflow: hidden; } +.overflow-visible { + overflow: visible; +} + .overflow-scroll { overflow: scroll; } @@ -1831,6 +1885,10 @@ input:checked + .toggle-bg { overflow-y: auto; } +.overflow-y-scroll { + overflow-y: scroll; +} + .truncate { overflow: hidden; text-overflow: ellipsis; @@ -2208,6 +2266,11 @@ input:checked + .toggle-bg { padding-bottom: 1.25rem; } +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + .pb-2 { padding-bottom: 0.5rem; } @@ -3543,6 +3606,10 @@ input:checked + .toggle-bg { } @media (min-width: 640px) { + .sm\:m-12 { + margin: 3rem; + } + .sm\:block { display: block; } @@ -3563,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 895921b0..f3657e02 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -21,8 +21,8 @@ [auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers entity-id - forced-vector html-response + many-entity map->db-id-decoder ref->enum-schema ref->select-options @@ -135,16 +135,16 @@ :action-buttons (fn [request] [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-account-new-dialog)) - :hx-target "#modal-holder" - :hx-swap "outerHTML" + :hx-target "#modal-content" + :hx-swap "innerHTML" :color :primary} "New Account")]) :row-buttons (fn [request entity] [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-account-edit-dialog :db/id (:db/id entity))) - :hx-target "#modal-holder" - :hx-swap "outerHTML"} + :hx-target "#modal-content" + :hx-swap "innerHTML"} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} @@ -207,22 +207,26 @@ "account_client_override_id" (:db/id o)}))) (html-response (row* identity updated-account {:flash? true}) - :headers {"hx-trigger" "closeModal" + :headers {"hx-trigger" "modalclose" "hx-retarget" (format "#account-table tr[data-id=\"%d\"]" (:db/id updated-account))}))) +;; TODO use cursor +;; TODO index based list not dbid -(defn client-override* [override] +(defn client-override* [override index] [:div.flex.gap-2.mb-2.client-override [:div.w-96 - (com/typeahead {:name (format "account/client-overrides[%s][account-client-override/client]" (:db/id override)) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :id (str "account-client-override-" (:db/id override)) - :value [(:db/id (:account-client-override/client override)) - (:client/name (:account-client-override/client override))]})] + (com/hidden {:name (format "account/client-overrides[%s][db/id]" index) + :value (:db/id override)}) + (com/typeahead-2 {:name (format "account/client-overrides[%s][account-client-override/client]" index) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :id (str "account-client-override-" (:db/id override)) + :value [(:db/id (:account-client-override/client override)) + (:client/name (:account-client-override/client override))]})] [:div.w-96 - (com/text-input {:name (format "account/client-overrides[%s][account-client-override/name]" (:db/id override)) + (com/text-input {:name (format "account/client-overrides[%s][account-client-override/name]" index) :class "w-full" :value (:account-client-override/name override)})] [: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)]]) @@ -230,7 +234,7 @@ (defn dialog* [& {:keys [ account form-params]}] (com/modal - {:modal-class "max-w-4xl"} + {} [:form#edit-form (merge {:hx-ext "response-targets" :hx-swap "outerHTML swap:300ms" :hx-target-400 "#form-errors .error-content"} @@ -282,33 +286,37 @@ :options (ref->select-options "account-applicability")})) (com/field {:label "Client Overrides" :id "client-overrides"} - (for [override (:account/client-overrides account)] - (client-override* override))) + (for [[override index] (map vector (:account/client-overrides account) (range))] + (client-override* override index))) (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes - :admin-account-client-override-new) - :hx-target "#client-overrides" - :hx-swap "beforeend"} + :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/button {:color :primary :form "edit-form" :type "submit"} - "Save")] - [:div])]])) + ] + (com/validated-save-button {:errors []} + "Save account"))]])) -(defn new-client-override [_] +(defn new-client-override [{ {:keys [index]} :query-params}] (html-response - (client-override* {:db/id (str (java.util.UUID/randomUUID))}))) + (client-override* {:db/id (str (java.util.UUID/randomUUID))} + index))) (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 :form-params {:hx-put (str (bidi/path-for ssr-routes/only-routes - :admin-account-edit-save))})))) + :admin-account-edit-save))}) + :headers {"hx-trigger" "modalopen"}))) (defn account-new-dialog [_] (html-response (dialog* :account nil :form-params {:hx-post (str (bidi/path-for ssr-routes/only-routes - :admin-account-new-save))}))) + :admin-account-new-save))}) + :headers {"hx-trigger" "modalopen"})) (def account-schema (mc/schema [:map @@ -320,20 +328,23 @@ [:account/applicability (ref->enum-schema "account-applicability")] [:account/invoice-allowance (ref->enum-schema "allowance")] [:account/vendor-allowance (ref->enum-schema "allowance")] - [:account/client-overrides {:decode/json map->db-id-decoder - :optional true} + [:account/client-overrides {:optional true} [:maybe - (forced-vector [:map - [:db/id [:or entity-id temp-id]] - [:account-client-override/client [:or entity-id :string]] - [:account-client-override/name :string]])]]])) + (many-entity {} + [:db/id [:or entity-id temp-id]] + [:account-client-override/client [:or entity-id :string]] + [:account-client-override/name :string])]]])) (def key->handler (apply-middleware-to-all-handlers (->> {:admin-accounts (helper/page-route grid-page) :admin-account-table (helper/table-route grid-page) - :admin-account-client-override-new (-> new-client-override wrap-admin wrap-client-redirect-unauthenticated) + :admin-account-client-override-new (-> new-client-override + (wrap-schema-decode :query-schema [:map + [:index {:optional true + :default 0} [nat-int? {:default 0}]]]) + wrap-admin wrap-client-redirect-unauthenticated) :admin-account-save (-> account-save (wrap-schema-decode :form-schema account-schema) (wrap-nested-form-params) diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index 40911e8a..ffe7a84f 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -193,7 +193,7 @@ [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-new-dialog)) :hx-target "#modal-content" - :hx-swap "outerHTML" + :hx-swap "innerHTML" :color :primary} "New Transaction Rule")]) :row-buttons (fn [request entity] @@ -201,7 +201,7 @@ :admin-transaction-rule-edit-dialog :db/id (:db/id entity))) :hx-target "#modal-content" - :hx-swap "outerHTML"} + :hx-swap "innerHTML"} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} @@ -581,10 +581,7 @@ [:div#form-errors (when (:errors fc/*form-errors*) [:span.error-content (com/errors {:errors (:errors fc/*form-errors*)})])]])] - [:div (com/button {:color :primary :form "edit-form" :type "submit" :class (cond-> "w-32" - (seq form-errors) (-> - (hh/add-class "animate-shake")))} - "Save")])])) + (com/validated-save-button {:errors form-errors} "Save rule"))])) ;; TODO Should forms have some kind of helper to render an individual field? saving diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index 41fff258..cee70ef2 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -15,6 +15,7 @@ (def breadcrumbs breadcrumbs/breadcrumbs-) (def button buttons/button-) +(def validated-save-button buttons/validated-save-button-) (def a-button buttons/a-button-) (def button-icon buttons/button-icon-) (def icon-button buttons/icon-button-) diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index 9edd6529..076945f9 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -1,5 +1,6 @@ (ns auto-ap.ssr.components.buttons - (:require [auto-ap.ssr.svg :as svg])) + (:require [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.hiccup-helper :as hh])) (defn button-icon- [_ i] [:div.h-4.w-4 i]) @@ -170,3 +171,16 @@ :hx-on:click "this.querySelector(\"input\").value = event.target.value; this.querySelector(\"input\").dispatchEvent(new Event('change', {bubbles: true}));"} [:input {:type "hidden" :name name}]] children))) + + +(defn validated-save-button- [{:keys [errors class] :as params} & children] + (button- (-> {:color :primary :form "edit-form" + :type "submit" :class (cond-> (or class "") + true (hh/add-class "w-32") + (seq errors) (hh/add-class "animate-shake"))} + (merge params) + (dissoc :errors)) + (if (seq children) + children + "Save")) + ) diff --git a/src/clj/auto_ap/ssr/components/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index f4ae762d..d2109e35 100644 --- a/src/clj/auto_ap/ssr/components/dialog.clj +++ b/src/clj/auto_ap/ssr/components/dialog.clj @@ -1,17 +1,26 @@ (ns auto-ap.ssr.components.dialog (:require [hiccup2.core :as hiccup] - [auto-ap.ssr.hx :as hx])) + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.hiccup-helper :as hh])) (defn modal- [params & children] - [:div#modal-content {:class ""} - children]) + [:div {:class (-> (:class params) + (or "max-w-4xl w-1/4 overflow-visible") + (hh/add-class "h-min")) + "@click.outside" "open=false" + } children]) (defn modal-card- [params header content footer] - [:div#modal-card params - [:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full"} - [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"} header] - [:div {:class "p-6 space-y-6 overflow-scroll "} + [:div#modal-card (update params + :class (fn [c] (-> c + (or "w-full") + ))) + [:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col max-h-[80vh]"} + [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header] + [:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"} + #_[:div.bg-green-300.w-full.h-64 + "hello"] content] - (when footer [:div {:class "p-4 "} footer])]]) + (when footer [:div {:class "p-4 shrink-0"} footer])]]) ;; fade-in-settle slide-up-settle duration-300 transition-all diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index 480854f1..2704c2b6 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -82,7 +82,8 @@ c.clearChoices(); [{:value ((:value-fn params first) (:value params)) :label ((:content-fn params second) (:value params))}] [])}) :x-modelable "value.value" - :x-model (:x-model params)} + :x-model (:x-model params) + :class "relative"} [:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes) (hh/add-class "cursor-pointer")) "@click.prevent" "open = !open;" diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index d2ef40b1..3cccfa40 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -77,17 +77,17 @@ input[type=number] { "x-transition:leave-start" "!bg-opacity-50" "x-transition:leave-end" "!bg-opacity-0"} - [:div.flex.place-items-center.place-content-center.w-full.h-full.p-12 - [:div {:class (str "w-full sm:max-w-2xl sm:rounded-lg overflow-scroll max-h-full sm:max-h-[90vh]") - "@click.outside" "open=false" - "x-trap.inert.noscroll" "open" - "x-trap.inert" "open" - "x-show" "open" - "x-transition:enter" "ease-out duration-300" - "x-transition:enter-start" "!bg-opacity-0 !translate-y-32" - "x-transition:enter-end" "!bg-opacity-100 !translate-y-0" - "x-transition:leave" "duration-300" - "x-transition:leave-start" "!opacity-100 !translate-y-0" - "x-transition:leave-end" "!opacity-0 !translate-y-32"} - - [:div#modal-content {:class "flex items-center justify-between pb-2"}]]]]]]])) + ;; TODO to get this right i think what needs to happen is to just set this up as having a single + ;; div that is forced to the maximum allowed size. inside that will be a div that just centers + ;; the elements, allowing it to grow as necessar. Then make the modal on the inside of this + ;; div just use flexbox to make the inside part be the part that scrolls + [:div#modal-content {:class (str "inset-0 max-h-[80vh] sm:m-12 flex justify-center items-center shrink h-full") + "x-trap.inert.noscroll" "open" + "x-trap.inert" "open" + "x-show" "open" + "x-transition:enter" "ease-out duration-300" + "x-transition:enter-start" "!bg-opacity-0 !translate-y-32" + "x-transition:enter-end" "!bg-opacity-100 !translate-y-0" + "x-transition:leave" "duration-300" + "x-transition:leave-start" "!opacity-100 !translate-y-0" + "x-transition:leave-end" "!opacity-0 !translate-y-32"}]]]]])) diff --git a/src/clj/auto_ap/ssr/users.clj b/src/clj/auto_ap/ssr/users.clj index 2e4df1ac..aad8b8f5 100644 --- a/src/clj/auto_ap/ssr/users.clj +++ b/src/clj/auto_ap/ssr/users.clj @@ -71,13 +71,13 @@ {:value "none" :content "None"}]})) (com/field {:label "Client"} - (com/typeahead {:name "client" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :id (str "client-search") - :value [(:db/id (:client (:parsed-query-params request))) - (:client/name (:client (:parsed-query-params request)))]}))]]) + (com/typeahead-2 {:name "client" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :id (str "client-search") + :value [(:db/id (:client (:parsed-query-params request))) + (:client/name (:client (:parsed-query-params request)))]}))]]) (def default-read '[:db/id :user/name @@ -207,7 +207,7 @@ :user-edit-dialog :db/id (:db/id entity))) :hx-target "#modal-content" - :hx-swap "outerHTML"} + :hx-swap "innerHTML"} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} @@ -281,7 +281,8 @@ :user-edit-save :request-method :put)) :hx-swap "outerHTML swap:300ms" - :hx-target-400 "#form-errors .error-content"} + :hx-target-400 "#form-errors .error-content" + :class "w-full"} [:fieldset {:class "hx-disable"} (com/modal-card {}