finally sunset old binding

This commit is contained in:
2022-07-23 10:17:41 -07:00
parent 03b5846d82
commit 3628f1e018
12 changed files with 159 additions and 484 deletions

View File

@@ -220,7 +220,7 @@ nav.navbar .navbar-item.is-active {
margin: 0 -50px;
padding-left: 50px;
}
.aside .main .icon {
.aside .main .menu-item .icon {
font-size: 19px;
padding-right: 30px;
color: #A0A0A0;

View File

@@ -201,7 +201,7 @@
" is-danger"
value
"is-success"
" is-success"
:else
""))))))))))))))

View File

@@ -1,55 +1,51 @@
(ns auto-ap.views.components.date-range-filter
(:require
[auto-ap.views.utils :refer [bind-field date-picker date->str local-now standard]]
[auto-ap.views.utils :refer [date-picker date->str local-now standard]]
[cljs-time.core :as t]
[re-frame.core :as re-frame]))
[re-frame.core :as re-frame]
[auto-ap.forms.builder :as form-builder]))
(defn dispatch-change [on-change-event start end]
(fn [_]
(re-frame/dispatch (into on-change-event [[:start] start]) )
(re-frame/dispatch (into on-change-event [[:end] end]))))
(defn set-value [on-change-event v]
(re-frame/dispatch (conj on-change-event v)))
(defn date-range-filter [{:keys [value on-change-event]}]
[:div
[:div.field.has-addons
[:p.control [:a.button.is-small {:on-click
(dispatch-change on-change-event
(date->str (t/minus (local-now) (t/period :days 7)) standard)
(date->str (local-now) standard))}
"Week" ]]
[:p.control [:a.button.is-small {:on-click
(dispatch-change on-change-event
(date->str (t/minus (local-now) (t/period :months 1)) standard)
(date->str (local-now) standard))}
"Month" ]]
[:p.control [:a.button.is-small {:on-click
(dispatch-change on-change-event
(date->str (t/minus (local-now) (t/period :years 1)) standard)
(date->str (local-now) standard))}
"Year"]]
[:p.control [:a.button.is-small {:on-click
(dispatch-change on-change-event
nil
nil)}
"All"]]]
[:div.field.has-addons
[:div.control
[bind-field
[date-picker
{:event on-change-event
:type "date"
:placeholder "Start"
:class "is-small"
:field [:start]
:subscription value
:output :text}]]]
[:div.control
[bind-field
[date-picker
{:event on-change-event
:type "date"
:class "is-small"
:placeholder "End"
:field [:end]
:subscription value
:output :text}]]]]])
[form-builder/virtual-builder {:value (or value {})
:on-change (fn [v]
(set-value on-change-event v))}
[:div
[:div.field.has-addons
[:p.control [:a.button.is-small {:on-click
#(set-value on-change-event
{:start (date->str (t/minus (local-now) (t/period :days 7)) standard)
:end (date->str (local-now) standard)})}
"Week" ]]
[:p.control [:a.button.is-small {:on-click
#(set-value on-change-event
{:start (date->str (t/minus (local-now) (t/period :months 1)) standard)
:end (date->str (local-now) standard)})}
"Month" ]]
[:p.control [:a.button.is-small {:on-click
#(set-value on-change-event
{:start (date->str (t/minus (local-now) (t/period :years 1)) standard)
:end (date->str (local-now) standard)})}
"Year"]]
[:p.control [:a.button.is-small {:on-click
#(set-value on-change-event nil)}
"All"]]]
[:div.field.has-addons
[:div.control
[form-builder/raw-field-v2 {:field :start}
[date-picker
{:placeholder "Start"
:class "is-small"
:output :text}]]]
[:div.control
[form-builder/raw-field-v2 {:field :end}
[date-picker
{:class "is-small"
:placeholder "End"
:output :text}]]]]]])

View File

@@ -1,25 +1,21 @@
(ns auto-ap.views.components.number-filter
(:require
[auto-ap.views.utils :refer [bind-field]]
[re-frame.core :as re-frame]))
[re-frame.core :as re-frame]
[auto-ap.forms.builder :as form-builder]
[auto-ap.views.components :as com]))
(defn number-filter [{:keys [value on-change-event]}]
[:div.field
[:div.control
[:div.columns
[:div.column
[bind-field
[:input.input {:type "number"
:placeholder ">="
:field [:amount-gte]
:step "0.01"
:event on-change-event
:subscription value}]]]
[:div.column
[bind-field
[:input.input {:type "number"
:placeholder "<="
:field [:amount-lte]
:event on-change-event
:step "0.01"
:subscription value}]]]]]])
[form-builder/virtual-builder {:value (or value {})
:on-change (fn [v]
(re-frame/dispatch (conj on-change-event v)))}
[:div.columns
[:div.column
[:div.control
[form-builder/raw-field-v2 {:field :amount-gte}
[com/money-input {:placeholder ">="}]]]]
[:div.column
[:div.control
[form-builder/raw-field-v2 {:field :amount-lte}
[com/money-input {:placeholder "<="}]]]]]]
)

View File

@@ -1,29 +1,25 @@
(ns auto-ap.views.components.vendor-dialog
(:require
[auto-ap.entities.contact :as contact]
[auto-ap.entities.vendors :as entity]
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.schema :as schema]
[auto-ap.status :as status]
[auto-ap.views.components.level :refer [left-stack]]
[auto-ap.subs :as subs]
[auto-ap.views.components :as com]
[auto-ap.views.components.address :refer [address2-field]]
[auto-ap.views.components.level :refer [left-stack]]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.components.multi :refer [multi-field-v2]]
[auto-ap.views.components.number :refer [number-input]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]]
[auto-ap.views.pages.admin.vendors.common :as common]
[auto-ap.views.components.multi :refer [multi-field-v2]]
[auto-ap.views.utils
:refer [dispatch-event multi-field str->int with-is-admin? with-user]]
[clojure.spec.alpha :as s]
[re-frame.core :as re-frame]
[reagent.core :as r]
:refer [dispatch-event str->int with-is-admin? with-user]]
[malli.core :as m]
[auto-ap.schema :as schema]
[malli.error :as me]))
[re-frame.core :as re-frame]
[reagent.core :as r]))
;; Remaining cleanup todos:
;; test minification

View File

@@ -1,18 +1,17 @@
(ns auto-ap.views.pages.admin.accounts.form
(:require [auto-ap.entities.account :as entity]
[auto-ap.forms :as forms]
[auto-ap.subs :as subs]
[auto-ap.views.components.layouts :refer [side-bar]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.utils :refer [dispatch-event multi-field with-user]]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[re-frame.core :as re-frame]
[auto-ap.forms.builder :as form-builder]
[vimsical.re-frame.cofx.inject :as inject]
[auto-ap.views.components :as com]
[malli.core :as m]
[auto-ap.schema :as schema]))
(:require
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.schema :as schema]
[auto-ap.subs :as subs]
[auto-ap.views.components :as com]
[auto-ap.views.components.layouts :refer [side-bar]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.utils :refer [dispatch-event with-user]]
[clojure.string :as str]
[malli.core :as m]
[re-frame.core :as re-frame]
[vimsical.re-frame.cofx.inject :as inject]))
(def types [:dividend :expense :asset :liability :equity :revenue])
(def applicabilities [:global :optional :customized])

View File

@@ -15,13 +15,10 @@
:refer [search-backed-typeahead]]
[auto-ap.views.utils
:refer [date-picker
dispatch-event
horizontal-field
multi-field]]
dispatch-event]]
[bidi.bidi :as bidi]
[cljs-time.coerce :as coerce]
[cljs-time.core :as t]
[clojure.string :as str]
[re-frame.core :as re-frame]
[reagent.core :as r]
[react-signature-canvas]

View File

@@ -15,8 +15,7 @@
::save
[ with-user (forms/in-form ::form)]
(fn [{:keys [db user]}]
{
:http {:token user
{:http {:token user
:method :post
:body (pr-str {:excel-rows (:excel-rows (:data db))})
:headers {"Content-Type" "application/edn"}

View File

@@ -2,37 +2,28 @@
(:require
[auto-ap.events :as events]
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.views.components :as com]
[auto-ap.views.components.dropdown
:refer [drop-down drop-down-contents]]
[auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
[auto-ap.views.utils :refer [bind-field dispatch-event]]
[auto-ap.views.utils :refer [dispatch-event]]
[clojure.string :as str]
[re-frame.core :as re-frame]
[reagent.core :as r]))
(re-frame/reg-sub
::loading
(fn [db]
(-> db ::loading)))
(re-frame/reg-sub
::can-submit
(fn [db]
true))
(defn line->id [{:keys [source id client-code date vendor-name] :as line}]
(defn line->id [{:keys [source id client-code]}]
(str client-code "-" source "-" id))
(re-frame/reg-sub
::request
:<- [::forms/form ::form]
(fn [{{lines :line-items :as d} :data :as g}]
(fn [{{lines :line-items} :data}]
(into []
(for [[external-id lines] (group-by line->id lines)
:let [{:keys [source id client-code date vendor-name note cleared-against] :as line} (first lines)]]
(for [[_ lines] (group-by line->id lines)
:let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]]
{:source source
:external-id (line->id line)
:client-code client-code
@@ -139,6 +130,8 @@
[:form.form
(if value
[:div
[:a.button {:on-click #(on-change nil)}
"reset"]
[:table.table {:style {:width "100%"}}
[:thead
[:tr
@@ -190,68 +183,72 @@
(def balance-sheet-content
(with-meta
(fn []
(let [current-client @(re-frame/subscribe [::subs/client])
user @(re-frame/subscribe [::subs/user])
status @(re-frame/subscribe [::status/single ::import])
{:keys [data result active? error id]} @(re-frame/subscribe [::forms/form ::form]) ]
[:div
[:div.level
[:div.level-left
[:h1.title "Eternal Import"]]
[:div.level-right
[:button.button.is-primary.is-pulled-right.is-large {:disabled (or (not data)
(= :loading (:state status )))
:on-click (dispatch-event [::importing])} "Import"]]]
[status/status-notification {:statuses [[::status/single ::import]]} ]
(when result
[:div.notification
"Imported with "
(count (:errors result)) " errors, "
(count (:ignored result)) " ignored, "
(count (:success result)) " successful."])
(if (= :loading (:state status ))
[status/big-loader status]
[:div
[:div.is-clearfix
[:div.is-pulled-right
[:label.checkbox
[bind-field
[:input {:type "checkbox"
:event [::forms/change ::form]
:subscription data
:field [:only-show-errors?]}]]
"Only show errors"]]]
[:div
[bind-field
[textarea->table {:type "textarea->table"
:field [:line-items]
:headings [["Id" :id]
["Client" :client-code]
["Source" :source]
["Vendor" :vendor-name]
["Date" :date]
["Account" :account-identifier]
["Location" :location]
["Debit" :debit]
["Credit" :credit]
["Note" :note]
["Cleared against" :cleared-against]]
:read-only-headings
[["status" :status]]
(let [status @(re-frame/subscribe [::status/single ::import])
{:keys [data result]} @(re-frame/subscribe [::forms/form ::form]) ]
[form-builder/builder {:id ::form
:submit-event [::importing]}
[:div
[:div.level
[:div.level-left
[:h1.title "Eternal Import"]]
[:div.level-right
[form-builder/submit-button "Import"]]]
[status/status-notification {:statuses [[::status/single ::import]]} ]
(when result
[:div.notification
"Imported with "
(count (:errors result)) " errors, "
(count (:ignored result)) " ignored, "
(count (:success result)) " successful."])
(if (= :loading (:state status ))
[status/big-loader status]
[:div
[:div.is-clearfix
[:div.is-pulled-right
[form-builder/raw-field-v2 {:field :only-show-errors?}
[com/checkbox {:label "Only show errors"}]]]]
[:div
[form-builder/raw-field-v2 {:field :line-items}
[textarea->table {:headings [["Id" :id]
["Client" :client-code]
["Source" :source]
["Vendor" :vendor-name]
["Date" :date]
["Account" :account-identifier]
["Location" :location]
["Debit" :debit]
["Credit" :credit]
["Note" :note]
["Cleared against" :cleared-against]]
:read-only-headings
[["status" :status]]
:row-filter
(fn [{:keys [status-category]}]
(if (:only-show-errors? data)
(= :error status-category)
true))
:event [::forms/change ::form]
:subscription data}
]]]])]))
:row-filter
(fn [{:keys [status-category]}]
(if (:only-show-errors? data)
(= :error status-category)
true))}]]]])]]))
{}))
(defn external-import-page []
(re-frame/reg-event-fx
::mounted
(fn [_ _]
{:dispatch [::forms/start-form ::form]}))
(re-frame/reg-event-fx
::unmounted
(fn [_ _]
{:dispatch [::forms/form-closing ::form]}))
(defn external-import-page-internal []
[side-bar-layout
{:side-bar [ledger-side-bar]
:main [balance-sheet-content]}])
(defn external-import-page []
(r/create-class
{:display-name "external-import-page"
:component-will-unmount #(re-frame/dispatch-sync [::unmounted])
:component-did-mount #(re-frame/dispatch [::mounted])
:reagent-render external-import-page-internal}))

View File

@@ -10,7 +10,6 @@
:refer [appearing-side-bar side-bar-layout]]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.components.switch-field :refer [switch-field]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
[auto-ap.views.pages.ledger.table :as ledger-table]
@@ -19,7 +18,6 @@
date-picker
dispatch-event
local-today
multi-field
query-params
standard
str->date

View File

@@ -51,7 +51,8 @@
(re-frame/reg-event-fx
::unmounted
(fn [{:keys [db]} _]
{:dispatch [::data-page/dispose :invoices]
{:dispatch-n [[::data-page/dispose :invoices]
[::forms/form-closing ::form/form]]
::forward/dispose [{:id ::updated}
{:id ::checks-printed}]
::track/dispose [{:id ::params}]}))

View File

@@ -106,26 +106,6 @@
(when d
(format/parse f d)))
(defn dispatch-date-change [event]
(fn [e]
(re-frame/dispatch (conj event
(if (str/blank? e)
e
(date->str (t/from-default-time-zone (c/from-date e)) standard))))))
(defn dispatch-cljs-date-change [event]
(fn [e]
(re-frame/dispatch (conj event
(if (str/blank? e)
e
(c/to-local-date e))))))
;; TODO inline on-changes causes each field to be rerendered each time. When we fix this
;; let's make sure that we find away not to trigger a re-render for every component any time any form field
;; changes
(defmulti do-bind (fn [_ {:keys [type]}]
type))
(defn with-keys [children]
(map-indexed (fn [i c] ^{:key i} c) children))
@@ -178,290 +158,6 @@
0.0)}}
child])))])])))
(defn multi-field [{:keys [value]} ]
(let [value-repr (reagent/atom (mapv
(fn [x]
(assoc x :key (random-uuid) :new? false))
value))]
(fn [{:keys [template on-change allow-change? disable-new? disable-remove?]} ]
(let [value @value-repr
already-has-new-row? (= [:key :new?] (keys (last value)))
value (if (or already-has-new-row? disable-new?)
value
(conj value {:key (random-uuid)
:new? true}))]
[:div {:style {:margin-bottom "0.25em"}}
(for [[i override] (map vector (range) value)
:let [is-disabled? (if (= false allow-change?)
(not (boolean (:new? override)))
nil)]
]
^{:key (:key override)}
[:div.level {:style {:margin-bottom "0.25em"}}
[:div.level-left {:style {:padding "0.5em 1em"}
:class (cond
(and (= i (dec (count value)))
(:new? override))
"has-background-light"
(:new? override)
"has-background-info-light"
:else
"")}
(let [template (if (fn? template)
(template override)
template)]
[:<> (for [[idx template] (map vector (range ) template)]
^{:key idx}
[:div.level-item
(update template 1 assoc
:value (let [value (get-in override (get-in template [1 :field])) ;; TODO this is really ugly to support maps or strings
value (if (map? value)
(dissoc value :key :new?)
value)]
(if (= value {})
nil
value))
:disabled (or is-disabled? (get-in template [1 :disabled]))
:on-change (fn [e]
(reset! value-repr
(into []
(filter (fn [r]
(not= [:key :new?] (keys r)))
(assoc-in value
(into [i] (get-in template [1 :field]))
(let [this-value (if (and e (.. e -target))
(.. e -target -value )
e)]
(if (map? this-value)
(update this-value :key (fnil identity (random-uuid)))
this-value))))))
(on-change (mapv
(fn [v]
(dissoc v :new? :key))
@value-repr))))])
])
(when-not disable-remove?
[:div.level-item
[:a.button.level-item
{:disabled is-disabled?
:on-click (fn []
(when-not is-disabled?
(reset! value-repr (into []
(filter (fn [{:keys [key ]}]
(not= key (:key override)))
(filter (fn [r]
(not= [:key :new?] (keys r)))
value))))
(on-change (mapv
(fn [v]
(dissoc v :new? :key))
@value-repr))))}
[:span.icon [:span.icon-remove]]]])
]])]))))
(defmethod do-bind "select" [dom {:keys [field allow-nil? subscription event class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (dispatch-value-change (conj event field))
:value (or (get-in subscription field) "")
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)
options (if allow-nil?
(with-keys (conj rest [:option {:value nil}]))
(with-keys rest))]
(into [dom (dissoc keys :allow-nil?)] options)))
(defmethod do-bind "radio" [dom {:keys [field subscription event class value spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (dispatch-value-change (conj event field))
:checked (= (get-in subscription field) value)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field ))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "checkbox" [dom {:keys [field subscription event class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (dispatch-event (-> event
(conj field)
(conj (not (get-in subscription field)))))
:checked (boolean (get-in subscription field))
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field ))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "multi-field" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [value]
(re-frame/dispatch (conj (conj event field) value)))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "typeahead-v3" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [selected]
(re-frame/dispatch (conj (conj event field) selected))
#_(when text-field
(re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "date" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
selected (get-in subscription field)
keys (assoc keys
:on-change (fn [v]
(re-frame/dispatch (-> event (conj field) (conj v))))
:value selected
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "expense-accounts" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:value (get-in subscription field)
:event (conj event field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "button-radio" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:value (get-in subscription field)
:on-change (fn [v]
(re-frame/dispatch (-> event (conj field) (conj v))))
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :event :subscription :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "number" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [e]
(.preventDefault e)
(re-frame/dispatch (-> event
(conj field)
(conj (let [val (.. e -target -value)]
(cond (and val
(re-matches #"[\-]?(\d+)(\.\d{2})?" val))
(js/parseFloat val)
(str/blank? val )
nil
:else
val))))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "textarea->table" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [x]
(re-frame/dispatch (-> event
(conj field)
(conj x))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "money" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [x]
(re-frame/dispatch (-> event
(conj field)
(conj x))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind :default [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (dispatch-value-change (conj event field))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defn bind-field [all]
(apply do-bind all))
(defn horizontal-field [label & controls]
[:div.field.is-horizontal
(when label
[:div.field-label
label
])
(into
[:div.field-body]
(with-keys (map (fn [x] [:div.field x]) controls)))])
(defn coerce-date [d]
(cond (and (string? d)
(some->> (re-find #"^(\d{4})" d)