In the beginning there was darkness
commit
c614cdbff3
@ -0,0 +1,26 @@
|
||||
*~
|
||||
*.iml
|
||||
*.log
|
||||
*.sw?
|
||||
*.swp
|
||||
*jar
|
||||
.DS_Store
|
||||
.cljs_rhino_repl
|
||||
.idea
|
||||
.nrepl*
|
||||
.nrepl-port
|
||||
.repl
|
||||
checkouts
|
||||
classes
|
||||
compiled
|
||||
datahub.log*
|
||||
node_modules
|
||||
out
|
||||
resources/public/js
|
||||
target
|
||||
.cpcache
|
||||
.shadow-cljs
|
||||
.clj-kondo/.cache
|
||||
DevelopersGuide.html
|
||||
docs/assets
|
||||
docs/.asciidoctor
|
@ -0,0 +1,15 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2017-2019, Fulcrologic, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -0,0 +1,37 @@
|
||||
= Fulcro RAD Carbon Rendering Plugin
|
||||
|
||||
WARNING: This library is alpha.
|
||||
|
||||
== Usage
|
||||
|
||||
image:https://img.shields.io/clojars/v/ch.lyrion/fulcro-rad-carbon.svg[link=https://clojars.org/ch.lyrion/fulcro-rad-carbon]
|
||||
|
||||
To use this library simply install the controls on your RAD application:
|
||||
|
||||
[source]
|
||||
-----
|
||||
(ns com.example.main
|
||||
(:require
|
||||
[com.fulcrologic.rad.application :as rad-app]))
|
||||
|
||||
(defonce app (-> (rad-app/fulcro-rad-app)
|
||||
(rad-app/install-ui-controls! sui/all-controls)))
|
||||
-----
|
||||
|
||||
== LICENSE
|
||||
|
||||
The MIT License (MIT)
|
||||
Copyright (c), Fulcrologic, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -0,0 +1,9 @@
|
||||
{:paths ["src/main"]
|
||||
|
||||
:deps {com.fulcrologic/fulcro-rad {:mvn/version "0.0.9-alpha"}
|
||||
com.fulcrologic/fulcro-i18n {:mvn/version "0.0.3-alpha"}
|
||||
ch.lyrion/carbon-wrapper {:mvn/version "1.0.3-SNAPSHOT"}
|
||||
com.fulcrologic/semantic-ui-wrapper {:mvn/version "1.0.1"}
|
||||
|
||||
org.clojure/clojure {:mvn/version "1.10.1" :scope "provided"}
|
||||
org.clojure/clojurescript {:mvn/version "1.10.764" :scope "provided"}}}
|
@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.fulcrologic</groupId>
|
||||
<artifactId>fulcro-rad-semantic-ui</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.0.10-alpha-SNAPSHOT</version>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
<name>Fulcro RAD Semantic UI</name>
|
||||
<description>A UI plugin for Fulcro RAD that renders to web/electron using Semantic UI CSS</description>
|
||||
<url>https://github.com/fulcrologic/fulcro-rad-semantic-ui</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
<url>https://opensource.org/licenses/MIT</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<scm>
|
||||
<url>https://github.com/fulcrologic/fulcro-rad-semantic-ui</url>
|
||||
<connection>scm:git:git://github.com/fulcrologic/fulcro-rad-semantic-ui.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com/fulcrologic/fulcro-rad-semantic-ui.git</developerConnection>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>clojars</id>
|
||||
<name>Clojars repository</name>
|
||||
<url>https://clojars.org/repo</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
<build>
|
||||
<sourceDirectory>src/main</sourceDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>1.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>clojars</id>
|
||||
<url>https://repo.clojars.org/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.clojure</groupId>
|
||||
<artifactId>clojure</artifactId>
|
||||
<version>1.10.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fulcrologic</groupId>
|
||||
<artifactId>fulcro-rad</artifactId>
|
||||
<version>0.0.9-alpha</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fulcrologic</groupId>
|
||||
<artifactId>fulcro-i18n</artifactId>
|
||||
<version>0.0.3-alpha</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.clojure</groupId>
|
||||
<artifactId>clojurescript</artifactId>
|
||||
<version>1.10.764</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fulcrologic</groupId>
|
||||
<artifactId>semantic-ui-wrapper</artifactId>
|
||||
<version>1.0.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -0,0 +1,154 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.autocomplete
|
||||
(:require
|
||||
#?@(:cljs
|
||||
[[com.fulcrologic.fulcro.dom :as dom :refer [div label input]]
|
||||
[goog.object :as gobj]
|
||||
[cljs.reader :refer [read-string]]
|
||||
[com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]]
|
||||
:clj
|
||||
[[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]])
|
||||
[com.fulcrologic.rad.ids :as ids]
|
||||
[com.fulcrologic.rad.ui-validation :as validation]
|
||||
[com.fulcrologic.fulcro.rendering.multiple-roots-renderer :as mroot]
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.fulcro.data-fetch :as df]
|
||||
[com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
|
||||
[com.fulcrologic.fulcro.algorithms.merge :as merge]
|
||||
[com.fulcrologic.rad.options-util :as opts]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]]
|
||||
[com.fulcrologic.rad.attributes :as attr]
|
||||
[clojure.string :as str]
|
||||
[taoensso.timbre :as log]
|
||||
[com.fulcrologic.rad.form :as form]
|
||||
[com.fulcrologic.fulcro.algorithms.normalized-state :as fns]))
|
||||
|
||||
(defsc AutocompleteQuery [_ _] {:query [:text :value]})
|
||||
|
||||
(defn to-js [v]
|
||||
#?(:clj v
|
||||
:cljs (clj->js v)))
|
||||
|
||||
(defmutation normalize-options [{:keys [source target]}]
|
||||
(action [{:keys [state]}]
|
||||
#?(:clj true
|
||||
:cljs
|
||||
(let [options (get @state source)
|
||||
normalized-options (apply array
|
||||
(map (fn [{:keys [text value]}]
|
||||
#js {:text text :value (pr-str value)}) options))]
|
||||
(fns/swap!-> state
|
||||
(dissoc source)
|
||||
(assoc-in target normalized-options))))))
|
||||
|
||||
(defsc AutocompleteField [this {:ui/keys [search-string options] :as props} {:keys [value label onChange
|
||||
invalid? validation-message
|
||||
read-only?]}]
|
||||
{:initLocalState (fn [this]
|
||||
;; TASK: props not making it...fix that, or debounce isn't configurable.
|
||||
(let [{:autocomplete/keys [debounce-ms]} (comp/props this)]
|
||||
{:load! (opts/debounce
|
||||
(fn [s]
|
||||
(let [{id ::autocomplete-id
|
||||
:autocomplete/keys [search-key]} (comp/props this)]
|
||||
(df/load! this search-key AutocompleteQuery
|
||||
{:params {:search-string s}
|
||||
:post-mutation `normalize-options
|
||||
:post-mutation-params {:source search-key
|
||||
:target [::autocomplete-id id :ui/options]}})))
|
||||
(or debounce-ms 200))}))
|
||||
:componentDidMount (fn [this]
|
||||
(let [{id ::autocomplete-id
|
||||
:autocomplete/keys [search-key]} (comp/props this)
|
||||
value (comp/get-computed this :value)]
|
||||
(when (and search-key value)
|
||||
(df/load! this search-key AutocompleteQuery
|
||||
{:params {:only value}
|
||||
:post-mutation `normalize-options
|
||||
:post-mutation-params {:source search-key
|
||||
:target [::autocomplete-id id :ui/options]}}))))
|
||||
:query [::autocomplete-id :ui/search-string :ui/options :autocomplete/search-key
|
||||
:autocomplete/debounce-ms :autocomplete/minimum-input]
|
||||
:ident ::autocomplete-id}
|
||||
(let [load! (comp/get-state this :load!)]
|
||||
#?(:clj
|
||||
(dom/div "")
|
||||
:cljs
|
||||
(dom/div :.field {:classes [(when invalid? "error")]}
|
||||
(dom/label label (when invalid? (str " " validation-message)))
|
||||
(if read-only?
|
||||
(gobj/getValueByKeys options 0 "text")
|
||||
(ui-dropdown #js {:search true
|
||||
:options (if options options #js [])
|
||||
:value (pr-str value)
|
||||
:selection true
|
||||
:closeOnBlur true
|
||||
:openOnFocus true
|
||||
:selectOnBlur true
|
||||
:selectOnNavigation true
|
||||
:onSearchChange (fn [_ v]
|
||||
(let [query (comp/isoget v "searchQuery")]
|
||||
(load! query)))
|
||||
:onChange (fn [_ v]
|
||||
(when onChange
|
||||
(onChange (some-> (comp/isoget v "value")
|
||||
read-string))))}))))))
|
||||
|
||||
(def ui-autocomplete-field (comp/computed-factory AutocompleteField {:keyfn ::autocomplete-id}))
|
||||
|
||||
(defmutation gc-autocomplete [{:keys [id]}]
|
||||
(action [{:keys [state]}]
|
||||
(when id
|
||||
(swap! state fns/remove-entity [::autocomplete-id id]))))
|
||||
|
||||
(defsc AutocompleteFieldRoot [this props {:keys [env attribute]}]
|
||||
{:initLocalState (fn [this] {:field-id (ids/new-uuid)})
|
||||
:componentDidMount (fn [this]
|
||||
(let [id (comp/get-state this :field-id)
|
||||
{:keys [attribute]} (comp/get-computed this)
|
||||
{:autocomplete/keys [search-key debounce-ms minimum-input]} (::form/field-options attribute)]
|
||||
(merge/merge-component! this AutocompleteField {::autocomplete-id id
|
||||
:autocomplete/search-key search-key
|
||||
:autocomplete/debounce-ms debounce-ms
|
||||
:autocomplete/minimum-input minimum-input
|
||||
:ui/search-string ""
|
||||
:ui/options #js []}))
|
||||
(mroot/register-root! this {:initialize? true}))
|
||||
:shouldComponentUpdate (fn [_ _] true)
|
||||
:initial-state {::autocomplete-id {}}
|
||||
:componentWillUnmount (fn [this]
|
||||
(comp/transact! this [(gc-autocomplete {:id (comp/get-state this :field-id)})])
|
||||
(mroot/deregister-root! this))
|
||||
:query [::autocomplete-id]}
|
||||
(let [{:autocomplete/keys [debounce-ms search-key]} (::form/field-options attribute)
|
||||
k (::attr/qualified-key attribute)
|
||||
{::form/keys [form-instance]} env
|
||||
value (-> (comp/props form-instance) (get k))
|
||||
id (comp/get-state this :field-id)
|
||||
label (form/field-label env attribute)
|
||||
read-only? (form/read-only? form-instance attribute)
|
||||
invalid? (validation/invalid-attribute-value? env attribute)
|
||||
validation-message (when invalid? (validation/validation-error-message env attribute))
|
||||
field (get-in props [::autocomplete-id id])]
|
||||
;; Have to pass the id and debounce early since the merge in mount won't happen until after, which is too late for initial
|
||||
;; state
|
||||
(ui-autocomplete-field (assoc field
|
||||
::autocomplete-id id
|
||||
:autocomplete/search-key search-key
|
||||
:autocomplete/debounce-ms debounce-ms)
|
||||
{:value value
|
||||
:invalid? invalid?
|
||||
:validation-message validation-message
|
||||
:label label
|
||||
:read-only? read-only?
|
||||
:onChange (fn [normalized-value]
|
||||
#?(:cljs
|
||||
(when normalized-value (form/input-changed! env k normalized-value))))})))
|
||||
|
||||
(def ui-autocomplete-field-root (mroot/floating-root-factory AutocompleteFieldRoot
|
||||
{:keyfn (fn [props] (-> props :attribute ::attr/qualified-key))}))
|
||||
|
||||
(defn render-autocomplete-field [env {::attr/keys [cardinality] :or {cardinality :one} :as attribute}]
|
||||
(if (= :many cardinality)
|
||||
(log/error "Cannot autocomplete to-many attributes with renderer" `render-autocomplete-field)
|
||||
(ui-autocomplete-field-root {:env env :attribute attribute})))
|
||||
|
@ -0,0 +1,121 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.blob-field
|
||||
(:require
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
#?@(:cljs [[com.fulcrologic.fulcro.dom :as dom :refer [div input]]
|
||||
[goog.object :as gobj]
|
||||
[com.fulcrologic.fulcro.networking.file-upload :as file-upload]]
|
||||
:clj [[com.fulcrologic.fulcro.dom-server :as dom :refer [div input]]])
|
||||
[com.fulcrologic.rad.form :as form]
|
||||
[com.fulcrologic.rad.attributes :as attr]
|
||||
[taoensso.timbre :as log]
|
||||
[com.fulcrologic.rad.ui-validation :as validation]
|
||||
[com.fulcrologic.rad.blob :as blob]
|
||||
[com.fulcrologic.rad.options-util :refer [?! narrow-keyword]]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]]
|
||||
[com.fulcrologic.fulcro.algorithms.form-state :as fs]))
|
||||
|
||||
(defn evt->js-files [evt]
|
||||
#?(:cljs
|
||||
(let [js-file-list (.. evt -target -files)]
|
||||
(map (fn [file-idx]
|
||||
(let [js-file (.item js-file-list file-idx)
|
||||
name (.-name js-file)]
|
||||
js-file))
|
||||
(range (.-length js-file-list))))))
|
||||
|
||||
#_(defsc ImageUploadField [this
|
||||
{::form/keys [form-instance] :as env}
|
||||
{::blob/keys [accept-file-types]
|
||||
::attr/keys [qualified-key] :as attribute}]
|
||||
{:initLocalState (fn [this]
|
||||
#?(:cljs
|
||||
{:save-ref (fn [r] (gobj/set this "fileinput" r))
|
||||
:on-click (fn [evt] (when-let [i (gobj/get this "fileinput")]
|
||||
(.click i)))
|
||||
:on-change (fn [evt]
|
||||
(let [env (comp/props this)
|
||||
attribute (comp/get-computed this)
|
||||
file (-> evt evt->js-files first)]
|
||||
(blob/upload-file! env attribute file)))}))}
|
||||
(let [props (comp/props form-instance)
|
||||
url-key (narrow-keyword qualified-key "url")
|
||||
current-sha (get props qualified-key)
|
||||
url (get props url-key)
|
||||
has-current-value? (seq current-sha)
|
||||
{:keys [save-ref on-change on-click]} (comp/get-state this)
|
||||
upload-complete? false
|
||||
label (form/field-label env attribute)
|
||||
valid? (and upload-complete? has-current-value?)]
|
||||
(div :.field {:key (str qualified-key)}
|
||||
(dom/label label)
|
||||
(div :.ui.tiny.image
|
||||
(dom/img {:src url :width "100"
|
||||
:onClick on-click})
|
||||
(dom/input (cond-> {:id (str qualified-key)
|
||||
:ref save-ref
|
||||
:style {:position "absolute"
|
||||
:opacity 0
|
||||
:top 0
|
||||
:right 0}
|
||||
:onChange on-change
|
||||
:type "file"}
|
||||
accept-file-types (assoc :allow (?! accept-file-types))))))))
|
||||
|
||||
#_(def ui-image-upload-field (comp/computed-factory ImageUploadField
|
||||
{:keyfn (fn [props] (some-> props comp/get-computed ::attr/qualified-key))}))
|
||||
|
||||
#_(defn render-image-upload [env attribute]
|
||||
(ui-image-upload-field env attribute))
|
||||
|
||||
(defsc FileUploadField [this
|
||||
{::form/keys [form-instance master-form] :as env}
|
||||
{::blob/keys [accept-file-types can-change?]
|
||||
::attr/keys [qualified-key] :as attribute}]
|
||||
{:componentDidMount (fn [this]
|
||||
(comment "TRIGGER UPLOAD IF CONFIG SAYS TO?"))
|
||||
:initLocalState (fn [this]
|
||||
#?(:cljs
|
||||
{:save-ref (fn [r] (gobj/set this "fileinput" r))
|
||||
:on-click (fn [evt] (when-let [i (gobj/get this "fileinput")]
|
||||
(.click i)))
|
||||
:on-change (fn [evt]
|
||||
(let [env (comp/props this)
|
||||
attribute (comp/get-computed this)
|
||||
file (-> evt evt->js-files first)]
|
||||
(blob/upload-file! this attribute file {:file-ident []})))}))}
|
||||
(let [props (comp/props form-instance)
|
||||
read-only? (or
|
||||
(form/read-only? master-form attribute)
|
||||
(form/read-only? form-instance attribute))
|
||||
can-change? (if read-only? false (?! can-change? env attribute))
|
||||
url-key (blob/url-key qualified-key)
|
||||
name-key (blob/filename-key qualified-key)
|
||||
current-sha (get props qualified-key)
|
||||
url (get props url-key)
|
||||
filename (get props name-key)
|
||||
pct (blob/upload-percentage props qualified-key)
|
||||
has-current-value? (seq current-sha)
|
||||
{:keys [save-ref on-change on-click]} (comp/get-state this)
|
||||
dirty? (if read-only? false (fs/dirty? props qualified-key))
|
||||
label (form/field-label env attribute)
|
||||
invalid? (if read-only? false (validation/invalid-attribute-value? env attribute))
|
||||
validation-message (when invalid? (validation/validation-error-message env attribute))]
|
||||
(div :.field {:key (str qualified-key)}
|
||||
(dom/label label)
|
||||
(cond
|
||||
(blob/blob-downloadable? props qualified-key)
|
||||
(dom/a {:href (str url "?filename=" filename)} "Download")
|
||||
|
||||
(blob/uploading? props qualified-key)
|
||||
(dom/div :.ui.small.blue.progress
|
||||
(div :.bar {:style {:transitionDuration "300ms"
|
||||
:display "block"
|
||||
:width pct}}
|
||||
(div :.progress pct)))))))
|
||||
|
||||
(def ui-file-upload-field (comp/computed-factory FileUploadField
|
||||
{:keyfn (fn [props] (some-> props comp/get-computed ::attr/qualified-key))}))
|
||||
|
||||
(defn render-file-upload [env attribute]
|
||||
(ui-file-upload-field env attribute))
|
@ -0,0 +1,34 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.boolean-field
|
||||
(:require
|
||||
#?(:cljs
|
||||
[com.fulcrologic.fulcro.dom :as dom :refer [div label input]]
|
||||
:clj
|
||||
[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]])
|
||||
[com.fulcrologic.rad.attributes :as attr]
|
||||
[com.fulcrologic.fulcro.components :as comp]
|
||||
[clojure.string :as str]
|
||||
[com.fulcrologic.rad.form :as form]
|
||||
[com.fulcrologic.fulcro.dom.events :as evt]))
|
||||
|
||||
(defn render-field [{::form/keys [form-instance] :as env} attribute]
|
||||
(let [k (::attr/qualified-key attribute)
|
||||
props (comp/props form-instance)
|
||||
user-props (form/field-style-config env attribute :input/props)
|
||||
field-label (form/field-label env attribute)
|
||||
read-only? (form/read-only? form-instance attribute)
|
||||
value (get props k false)]
|
||||
(div :.ui.field {:key (str k)}
|
||||
(if read-only?
|
||||
(label field-label " " (if value "Yes" "No"))
|
||||
(div :.ui.checkbox
|
||||
(input (merge
|
||||
{:checked value
|
||||
:type "checkbox"
|
||||
:disabled (boolean read-only?)
|
||||
:onChange (fn [evt]
|
||||
(let [v (not value)]
|
||||
(form/input-blur! env k v)
|
||||
(form/input-changed! env k v)))}
|
||||
user-props))
|
||||
(label field-label))))))
|
||||
|
@ -0,0 +1,92 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.components
|
||||
(:require
|
||||
#?@(:cljs
|
||||
[[com.fulcrologic.fulcro.dom :as dom :refer [div label input]]
|
||||
[com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]]
|
||||
:clj
|
||||
[[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]])
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.fulcro.algorithms.transit :as ftransit]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defn sui-format->user-format
|
||||
"Converts transit encoded value(s), used by Semantic UI, into CLJS datastructure."
|
||||
[{:keys [multiple]} value]
|
||||
(if multiple
|
||||
(into [] (map ftransit/transit-str->clj value))
|
||||
(ftransit/transit-str->clj value)))
|
||||
|
||||
(defn user-format->sui-format [{:keys [multiple]} value]
|
||||
"Converts CLJS datastructure into transit encoded string(s), usable by Semantic UI."
|
||||
(if multiple
|
||||
(if value
|
||||
(to-array (map ftransit/transit-clj->str value))
|
||||
#js [])
|
||||
(if (or value (boolean? value))
|
||||
(ftransit/transit-clj->str value)
|
||||
"")))
|
||||
|
||||
(defn wrapped-onChange
|
||||
"Wraps userOnChange fn with try/catch and sui-form->user-format conversion."
|
||||
[props userOnChange]
|
||||
(fn [_ v]
|
||||
#?(:cljs
|
||||
(try
|
||||
(if (and (.-value v) (seq (.-value v)))
|
||||
(let [value (sui-format->user-format props (.-value v))]
|
||||
(when (and value userOnChange) (userOnChange value)))
|
||||
(userOnChange nil))
|
||||
(catch :default e
|
||||
(log/error e "Unable to read dropdown value " (when v (.-value v))))))))
|
||||
|
||||
(defsc WrappedDropdown [this {:keys [onChange value multiple] :as props}]
|
||||
{:initLocalState (fn [this]
|
||||
#?(:cljs
|
||||
(let [xform-options (memoize (fn [options]
|
||||
(clj->js (mapv (fn [{:keys [text value]}]
|
||||
#js {:text text :value (some-> value (ftransit/transit-clj->str))})
|
||||
options))))
|
||||
xform-value (fn [multiple? value]
|
||||
(user-format->sui-format {:multiple multiple?} value))]
|
||||
{:get-options (fn [props] (xform-options (:options props)))
|
||||
:format-value (fn [props value] (xform-value (:multiple props) value))})))}
|
||||
#?(:cljs
|
||||
(let [{:keys [get-options format-value]} (comp/get-state this)
|
||||
userOnChange onChange
|
||||
options (get-options props)
|
||||
value (format-value props value)
|
||||
props (merge
|
||||
{:search true
|
||||
:selection true
|
||||
:closeOnBlur true
|
||||
:openOnFocus true
|
||||
:selectOnBlur true
|
||||
:selectOnNavigation true
|
||||
:multiple (boolean multiple)}
|
||||
props
|
||||
{:value value
|
||||
:options options
|
||||
:onChange (fn [e v]
|
||||
(try
|
||||
(let [string-value (.-value v)
|
||||
value (if multiple
|
||||
(mapv #(when (seq %) (ftransit/transit-str->clj %)) string-value)
|
||||
(when (seq string-value) (ftransit/transit-str->clj string-value)))]
|
||||
(when userOnChange
|
||||
(userOnChange value)))
|
||||
(catch :default e
|
||||
(log/error "Unable to read dropdown value " e (when v (.-value v))))))})]
|
||||
(ui-dropdown props))
|
||||
:clj
|
||||
(dom/div :.ui.selection.dropdown
|
||||
(dom/input {:type "hidden"})
|
||||
(dom/i :.dropdown.icon)
|
||||
(dom/div :.default.text "")
|
||||
(dom/div :.menu))))
|
||||
|
||||
(def ui-wrapped-dropdown
|
||||
"Draw a SUI dropdown with the given props. The arguments are identical to sui/ui-dropdown, but options and onChange
|
||||
are auto-wrapped so that clojure data (e.g. keywords) can be used for the option :value fields. It also defaults
|
||||
a number of things (:search, :closeOnBlue, openOnFocus, selectOnBlue, and :selectOnNavigation) to true, but you can"
|
||||
(comp/factory WrappedDropdown))
|
||||
|
@ -0,0 +1,85 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.container
|
||||
(:require
|
||||
#?@(:cljs
|
||||
[[com.fulcrologic.fulcro.dom :as dom :refer [div]]]
|
||||
:clj
|
||||
[[com.fulcrologic.fulcro.dom-server :as dom :refer [div]]])
|
||||
[com.fulcrologic.fulcro.components :as comp]
|
||||
[com.fulcrologic.rad.container :as container]
|
||||
[com.fulcrologic.rad.control :as control]
|
||||
[com.fulcrologic.rad.options-util :refer [?!]]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.form :as sui-form]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(comp/defsc StandardContainerControls [_ {:keys [instance]}]
|
||||
{:shouldComponentUpdate (fn [_ _ _] true)}
|
||||
(let [controls (control/component-controls instance)
|
||||
{:keys [input-layout action-layout]} (control/standard-control-layout instance)]
|
||||
(div :.ui.top.attached.compact.basic.segment
|
||||
(dom/h3 :.ui.header
|
||||
(or (some-> instance comp/component-options ::container/title (?! instance)) "")
|
||||
(div :.ui.right.floated.buttons
|
||||
(keep (fn [k] (control/render-control instance k (get controls k))) action-layout)))
|
||||
(div :.ui.form
|
||||
(map-indexed
|
||||
(fn [idx row]
|
||||
(div {:key idx :className (sui-form/n-fields-string (count row))}
|
||||
(map #(if-let [c (get controls %)]
|
||||
(control/render-control instance % c)
|
||||
(dom/div :.ui.field {:key (str %)} "")) row)))
|
||||
input-layout)))))
|
||||
|
||||
(let [ui-standard-container-controls (comp/factory StandardContainerControls)]
|
||||
(defn render-standard-controls [instance]
|
||||
(ui-standard-container-controls {:instance instance})))
|
||||
|
||||
(def n-string {0 "zero"
|
||||
1 "one"
|
||||
2 "two"
|
||||
3 "three"
|
||||
4 "four"
|
||||
5 "five"
|
||||
6 "six"
|
||||
7 "seven"
|
||||
8 "eight"
|
||||
9 "nine"
|
||||
10 "ten"
|
||||
11 "eleven"
|
||||
12 "twelve"
|
||||
13 "thirteen"
|
||||
14 "fourteen"
|
||||
15 "fifteen"
|
||||
16 "sixteen"})
|
||||
|
||||
(defn render-container-layout [container-instance]
|
||||
(let [{::container/keys [children layout]} (comp/component-options container-instance)]
|
||||
;; TODO: Custom controls rendering as a separate config?
|
||||
(let [container-props (comp/props container-instance)
|
||||
render-cls (fn [id cls]
|
||||
(let [factory (comp/computed-factory cls)
|
||||
props (get container-props id {})]
|
||||
(factory props {::container/controlled? true})))]
|
||||
(dom/div :.ui.basic.segments
|
||||
(render-standard-controls container-instance)
|
||||
(dom/div :.ui.basic.segment
|
||||
(if layout
|
||||
(dom/div :.ui.container.centered.grid
|
||||
(map-indexed
|
||||
(fn *render-row [idx row]
|
||||
(let [cols (count row)]
|
||||
(dom/div :.row {:key idx}
|
||||
(map
|
||||
(fn *render-col [entry]
|
||||
(let [id (if (keyword? entry) entry (:id entry))
|
||||
width (or
|
||||
(and (map? entry) (:width entry))
|
||||
(int (/ 16 cols)))
|
||||
cls (get children id)]
|
||||
(dom/div {:key id :classes [(when width (str (n-string width) " wide")) "column"]}
|
||||
(render-cls id cls))))
|
||||
row))))
|
||||
layout))
|
||||
(map
|
||||
(fn [[id cls]]
|
||||
(dom/div {:key id}
|
||||
(render-cls id cls))) (container/id-child-pairs children))))))))
|
@ -0,0 +1,40 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.controls.action-button
|
||||
(:require
|
||||
[com.fulcrologic.rad.report :as report]
|
||||
[com.fulcrologic.rad.options-util :refer [?!]]
|
||||
[com.fulcrologic.fulcro.data-fetch :as df]
|
||||
[com.fulcrologic.rad.control :as control]
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :as dom]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :as dom])
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
#?@(:cljs [[ch.lyrion.carbon.button.ui-button :refer [ui-button]]])))
|
||||
|
||||
(defsc ActionButton [_ {:keys [instance control-key]}]
|
||||
{:shouldComponentUpdate (fn [_ _ _] true)}
|
||||
#?(:cljs
|
||||
(let [controls (control/component-controls instance)
|
||||
props (comp/props instance)
|
||||
{:keys [label icon class action disabled? visible?] :as control} (get controls control-key)]
|
||||
(when control
|
||||
(let [label (?! label instance)
|
||||
class (?! class instance)
|
||||
loading? (df/loading? (get-in props [df/marker-table (comp/get-ident instance)]))
|
||||
disabled? (or loading? (?! disabled? instance))
|
||||
visible? (or (nil? visible?) (?! visible? instance))]
|
||||
(when visible?
|
||||
(ui-button
|
||||
{:id (str control-key)
|
||||
:className (when loading? "bx--skeleton")
|
||||
:disabled (boolean disabled?)
|
||||
:onClick (fn [] (when action (action instance control-key)))}
|
||||
(when icon (dom/i {:className (str icon " icon")}))
|
||||
(when label label))
|
||||
#_(dom/button :.bx--btn.bx--btn--sm.bx--skeleton
|
||||
{:key (str control-key)
|
||||
:classes [(when class class)]
|
||||
:disabled (boolean disabled?)
|
||||
:onClick (fn [] (when action (action instance control-key)))}
|
||||
(when icon (dom/i {:className (str icon " icon")}))
|
||||
(when label label))))))))
|
||||
|
||||
(def render-control (comp/factory ActionButton {:keyfn :control-key}))
|
@ -0,0 +1,35 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.controls.boolean-control
|
||||
(:require
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.fulcro.ui-state-machines :as uism]
|
||||
[com.fulcrologic.rad.options-util :refer [?!]]
|
||||
[com.fulcrologic.rad.report :as report]
|
||||
[taoensso.timbre :as log]
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :as dom]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :as dom])
|
||||
[com.fulcrologic.rad.control :as control]
|
||||
#?@(:cljs [[ch.lyrion.carbon.toggle.ui-toggle :refer [ui-toggle]]])))
|
||||
|
||||
(defsc BooleanControl [_ {:keys [instance control-key]}]
|
||||
{:shouldComponentUpdate (fn [_ _ _] true)}
|
||||
(let [controls (control/component-controls instance)
|
||||
{:keys [label onChange disabled? visible?] :as control} (get controls control-key)]
|
||||
(if control
|
||||
(let [label (or (?! label instance))
|
||||
disabled? (?! disabled? instance)
|
||||
visible? (or (nil? visible?) (?! visible? instance))
|
||||
value (control/current-value instance control-key)]
|
||||
(when visible?
|
||||
#?(:cljs
|
||||
(ui-toggle
|
||||
{:id (str (hash control))
|
||||
:defaultToggled (boolean value)
|
||||
:labelA label
|
||||
:labelB label
|
||||
:onToggle (fn [_]
|
||||
(control/set-parameter! instance control-key (not value))
|
||||
(when onChange
|
||||
(onChange instance (not value))))}))))
|
||||
(log/error "Could not find control definition for " control-key))))
|
||||
|
||||
(def render-control (comp/factory BooleanControl {:keyfn :control-key}))
|
@ -0,0 +1,35 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.controls.control
|
||||
(:require
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.fulcro.ui-state-machines :as uism]
|
||||
[com.fulcrologic.rad.options-util :refer [?!]]
|
||||
[com.fulcrologic.guardrails.core :refer [>defn =>]]
|
||||
[com.fulcrologic.rad.report :as report]
|
||||
[com.fulcrologic.rad.control :as control]
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :as dom]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :as dom])
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defsc Control [_ {:keys [instance control control-key input-factory] :as report-env}]
|
||||
{:shouldComponentUpdate (fn [_ _ _] true)}
|
||||
(let [controls (control/component-controls instance)
|
||||
{:keys [label onChange disabled? visible? user-props] :as control} (get controls control-key control)]
|
||||
(if (and input-factory control)
|
||||
(let [label (or (?! label instance))
|
||||
disabled? (?! disabled? instance)
|
||||
visible? (or (nil? visible?) (?! visible? instance))
|
||||
value (control/current-value instance control-key)
|
||||
onChange (fn [new-value]
|
||||
(control/set-parameter! instance control-key new-value)
|
||||
(when onChange
|
||||
(onChange instance new-value)))]
|
||||
(when visible?
|
||||
(dom/div :.ui.field {:key (str control-key)}
|
||||
(dom/label label)
|
||||
(input-factory report-env (merge user-props
|
||||
{:disabled? disabled?
|
||||
:value value
|
||||
:onChange onChange})))))
|
||||
(log/error "Cannot render control. Missing input factory or control definition."))))
|
||||
|
||||
(def ui-control (comp/factory Control {:keyfn :control-key}))
|
@ -0,0 +1,87 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.controls.instant-inputs
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[com.fulcrologic.guardrails.core :refer [>defn => ?]]
|
||||
[com.fulcrologic.rad.type-support.date-time :as dt]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.controls.control :as control]
|
||||
[cljc.java-time.local-time :as lt]
|
||||
[com.fulcrologic.fulcro.dom.events :as evt]
|
||||
#?(:clj [com.fulcrologic.fulcro.dom-server :as dom]
|
||||
:cljs [com.fulcrologic.fulcro.dom :as dom])
|
||||
[cljc.java-time.local-date-time :as ldt]
|
||||
[cljc.java-time.local-date :as ld]
|
||||
#?@(:cljs [[ch.lyrion.carbon.date-picker.ui-date-picker :refer [ui-date-picker]]
|
||||
[ch.lyrion.carbon.date-picker-input.ui-date-picker-input :refer [ui-date-picker-input]]])))
|
||||
|
||||
(defn ui-date-instant-input [{::keys [default-local-time]} {:keys [value onChange local-time] :as props}]
|
||||
#?(:cljs
|
||||
(let [value (dt/inst->html-date (or value (dt/now)))
|
||||
local-time (or local-time default-local-time)]
|
||||
(ui-date-picker
|
||||
{:id (str (hash props) "-picker")
|
||||
:datePickerType "single"
|
||||
:dateFormat "Y-m-d"
|
||||
:value value
|
||||
:onChange (fn [evt]
|
||||
(when onChange
|
||||
(let [date (.toJSON (first evt))
|
||||
date-string (first (str/split date "T"))
|
||||
instant (dt/html-date->inst date-string local-time)]
|
||||
(onChange instant))))}
|
||||
(ui-date-picker-input
|
||||
{:id (str (dt/now) "ui-date-instant-input")})))))
|
||||
|
||||
(defn ui-ending-date-instant-input
|
||||
"Display the date the user selects, but control a value that is midnight on the next date. Used for generating ending
|
||||
instants that can be used for a proper non-inclusive end date."
|
||||
[_ {:keys [value onChange] :as props}]
|
||||
#?(:cljs
|
||||
(let [today (dt/inst->local-datetime (or value (dt/now)))
|
||||
display-date (ldt/to-local-date (ldt/minus-days today 1))
|
||||
value (dt/local-date->html-date-string display-date)]
|
||||
(ui-date-picker
|
||||
{:id (str (hash props) "-picker")
|
||||
:datePickerType "single"
|
||||
:dateFormat "Y-m-d"
|
||||
:value value
|
||||
:onChange (fn [evt]
|
||||
(when onChange
|
||||
(let [date (.toJSON (first evt))
|
||||
date-string (first (str/split date "T"))
|
||||
tomorrow (ld/at-time (ld/plus-days (dt/html-date-string->local-date date-string) 1)
|
||||
lt/midnight)
|
||||
instant (dt/local-datetime->inst tomorrow)]
|
||||
(onChange instant))))}
|
||||
(ui-date-picker-input
|
||||
{:id (str (hash props) "-picker-input")})))))
|
||||
|
||||
(defn ui-date-time-instant-input [_ {:keys [disabled? value onChange] :as props}]
|
||||
(let [value (dt/inst->html-datetime-string (or value (dt/now)))]
|
||||
(dom/input
|
||||
(merge props
|
||||
(cond->
|
||||
{:value value
|
||||
:type "date"
|
||||
:onChange (fn [evt]
|
||||
(when onChange
|
||||
(let [date-time-string (evt/target-value evt)
|
||||
instant (dt/html-datetime-string->inst date-time-string)]
|
||||
(onChange instant))))}
|
||||
disabled? (assoc :readOnly true))))))
|
||||
|
||||
(defn date-time-control [render-env]
|
||||
(control/ui-control (assoc render-env :input-factory ui-date-time-instant-input)))
|
||||
|
||||
(defn midnight-on-date-control [render-env]
|
||||
(control/ui-control (assoc render-env
|
||||
:input-factory ui-date-instant-input
|
||||
::default-local-time lt/midnight)))
|
||||
|
||||
(defn midnight-next-date-control [render-env]
|
||||
(control/ui-control (assoc render-env
|
||||
:input-factory ui-ending-date-instant-input)))
|
||||
|
||||
(defn date-at-noon-control [render-env]
|
||||
(control/ui-control (assoc render-env
|
||||
::default-local-time lt/noon
|
||||
:input-factory ui-date-instant-input)))
|
@ -0,0 +1,72 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.controls.pickers
|
||||
(:require
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.rad.picker-options :as po]
|
||||
[com.fulcrologic.rad.control :as control]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]]
|
||||
#?(:cljs [ch.lyrion.carbon.combo-box.ui-combo-box :refer [ui-combo-box]])
|
||||
[com.fulcrologic.rad.options-util :refer [?!]]
|
||||
[taoensso.timbre :as log]
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :as dom]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :as dom])))
|
||||
|
||||
(defsc SimplePicker [_ {:keys [instance control-key]}]
|
||||
{:shouldComponentUpdate (fn [_ _ _] true)
|
||||
:componentDidMount (fn [this]
|
||||
(let [{:keys [instance control-key] :as props} (comp/props this)
|
||||
controls (control/component-controls instance)
|
||||
{::po/keys [query-key] :as picker-options} (get controls control-key)]
|
||||
(when query-key
|
||||
(po/load-picker-options! instance (comp/react-type instance) props picker-options))))}
|
||||
#?(:cljs
|
||||
(let [controls (control/component-controls instance)
|
||||
props (comp/props instance)
|
||||
{::po/keys [query-key cache-key]
|
||||
:keys [label onChange disabled? visible? action placeholder options user-props] :as control} (get controls control-key)
|
||||
options (or options (get-in props [::po/options-cache (or cache-key query-key) :options]))]
|
||||
(when control
|
||||
(let [label (or (?! label instance))
|
||||
disabled? (?! disabled? instance)
|
||||
placeholder (?! placeholder instance)
|
||||
visible? (or (nil? visible?) (?! visible? instance))
|
||||
value (control/current-value instance control-key)]
|
||||
(when visible?
|
||||
(dom/div
|
||||
(ui-combo-box
|
||||
{:id (or (str control-key) (str (hash options)))
|
||||
;;:titleText label
|
||||
:disabled (or disabled? false)
|
||||
:placeholder (str placeholder)
|
||||
:items (or options [])
|
||||
:itemToString (fn [item] (.-text item))
|
||||
:initialSelectedItem (or value "")
|
||||
:onChange (fn [v]
|
||||
(let [selectedItem (.-selectedItem v)
|
||||
selected-item (if (nil? selectedItem)
|
||||
(or (first (filter #(= "" (:value %)) options))
|
||||
{:text "" :value ""})
|
||||
{:text (.-text selectedItem) :value (.-value selectedItem)})]
|
||||
(js/console.log "Event:" v)
|
||||
(control/set-parameter! instance control-key (:value selected-item))
|
||||
(binding [comp/*after-render* true]
|
||||
(when onChange
|
||||
(onChange instance v))
|
||||
(when action
|
||||
(action instance)))))}))
|
||||
#_(dom/div :.ui.field {:key (str control-key)}
|
||||
(dom/label label)
|
||||
(ui-wrapped-dropdown (merge
|
||||
user-props
|
||||
{:disabled disabled?
|
||||
:placeholder (str placeholder)
|
||||
:options options
|
||||
:value value
|
||||
:onChange (fn [v]
|
||||
(control/set-parameter! instance control-key v)
|
||||
(binding [comp/*after-render* true]
|
||||
(when onChange
|
||||
(onChange instance v))
|
||||
(when action
|
||||
(action instance))))})))))))))
|
||||
|
||||
(def render-control (comp/factory SimplePicker {:keyfn :control-key}))
|
@ -0,0 +1,59 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.controls.text-input
|
||||
(:require
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.fulcro.ui-state-machines :as uism]
|
||||
[com.fulcrologic.fulcro.dom.events :as evt]
|
||||
[com.fulcrologic.rad.control :as control]
|
||||
[com.fulcrologic.rad.options-util :refer [?! debounce]]
|
||||
[taoensso.timbre :as log]
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :as dom]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :as dom])
|
||||
#?@(:cljs [[ch.lyrion.carbon.text-input.ui-text-input :refer [ui-text-input]]])))
|
||||
|
||||
(defsc TextControl [this {:keys [instance control-key]}]
|
||||
{:shouldComponentUpdate (fn [_ _ _] true)}
|
||||
#?(:cljs
|
||||
(let [controls (control/component-controls instance)
|
||||
props (comp/props instance)
|
||||
{:keys [label onChange icon placeholder disabled? visible?] :as control} (get controls control-key)]
|
||||
(when control
|
||||
(let [label (?! label instance)
|
||||
disabled? (?! disabled? instance)
|
||||
placeholder (?! placeholder)
|
||||
visible? (or (nil? visible?) (?! visible? instance))
|
||||
chg! #(control/set-parameter! instance control-key (evt/target-value %))
|
||||
run! (fn [evt] (let [v (evt/target-value evt)]
|
||||
(when onChange (onChange instance v))))
|
||||
value (control/current-value instance control-key)]
|
||||
(when visible?
|
||||
(dom/div
|
||||
(when icon
|
||||
(dom/i {:className (str icon " icon")}))
|
||||
(ui-text-input
|
||||
{:id (str control-key)
|
||||
:disabled (boolean disabled?)
|
||||
:labelText label
|
||||
:placeholder (str placeholder)
|
||||
:onChange chg!
|
||||
:onBlur run!
|
||||
:onKeyDown (fn [evt] (when (evt/enter? evt) (run! evt)))
|
||||
:value (str value)}))
|
||||
#_(dom/div :.ui.field {:key (str control-key)}
|
||||
(dom/label label)
|
||||
(if icon
|
||||
(dom/div :.ui.icon.input
|
||||
(dom/i {:className (str icon " icon")})
|
||||
(dom/input {:readOnly (boolean disabled?)
|
||||
:placeholder (str placeholder)
|
||||
:onChange chg!
|
||||
:onBlur run!
|
||||
:onKeyDown (fn [evt] (when (evt/enter? evt) (run! evt)))
|
||||
:value (str value)}))
|
||||
(dom/input {:readOnly (boolean disabled?)
|
||||
:placeholder (str placeholder)
|
||||
:onChange chg!
|
||||
:onBlur run!
|
||||
:onKeyDown (fn [evt] (when (evt/enter? evt) (run! evt)))
|
||||
:value (str value)})))))))))
|
||||
|
||||
(def render-control (comp/factory TextControl {:keyfn :control-key}))
|
@ -0,0 +1,15 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.decimal-field
|
||||
(:require
|
||||
[com.fulcrologic.fulcro.components :as comp]
|
||||
[com.fulcrologic.fulcro.dom.inputs :as inputs]
|
||||
[clojure.string :as str]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]]
|
||||
[com.fulcrologic.rad.type-support.decimal :as math]))
|
||||
|
||||
(def ui-decimal-input
|
||||
(comp/factory (inputs/StringBufferedInput ::DecimalInput
|
||||
{:model->string (fn [n] (if (math/numeric? n) (math/numeric->str n) ""))
|
||||
:string->model (fn [s] (math/numeric s))
|
||||
:string-filter (fn [s] (str/replace s #"[^\d.]" ""))})))
|
||||
|
||||
(def render-field (render-field-factory {} ui-decimal-input))
|
@ -0,0 +1,115 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.entity-picker
|
||||
(:require
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :as dom :refer [div h3 button i span]]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :as dom :refer [div h3 button i span]])
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]]
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.rad.form :as form]
|
||||
[com.fulcrologic.rad.attributes :as attr]
|
||||
[com.fulcrologic.rad.picker-options :as picker-options]
|
||||
[com.fulcrologic.rad.ui-validation :as validation]
|
||||
[com.fulcrologic.rad.options-util :refer [?!]]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defsc ToOnePicker [this {:keys [env attr]}]
|
||||
{:componentDidMount (fn [this]
|
||||
(let [{:keys [env attr]} (comp/props this)
|
||||
form-instance (::form/form-instance env)
|
||||
props (comp/props form-instance)
|
||||
form-class (comp/react-type form-instance)]
|
||||
(picker-options/load-options! form-instance form-class props attr)))}
|
||||
(let [{::form/keys [master-form form-instance]} env
|
||||
{::form/keys [attributes field-options]} (comp/component-options form-instance)
|
||||
{::attr/keys [qualified-key required?]} attr
|
||||
field-options (get field-options qualified-key)
|
||||
target-id-key (first (keep (fn [{k ::attr/qualified-key ::attr/keys [target]}]
|
||||
(when (= k qualified-key) target)) attributes))
|
||||
{::picker-options/keys [cache-key query-key]} (merge attr field-options)
|
||||
cache-key (or (?! cache-key (comp/react-type form-instance) (comp/props form-instance)) query-key)
|
||||
cache-key (or cache-key query-key (log/error "Ref field MUST have either a ::picker-options/cache-key or ::picker-options/query-key in attribute " qualified-key))
|
||||
props (comp/props form-instance)
|
||||
options (get-in props [::picker-options/options-cache cache-key :options])
|
||||
value [target-id-key (get-in props [qualified-key target-id-key])]
|
||||
field-label (form/field-label env attr)
|
||||
read-only? (or (form/read-only? master-form attr) (form/read-only? form-instance attr))
|
||||
invalid? (and (not read-only?) (validation/invalid-attribute-value? env attr))
|
||||
onSelect (fn [v]
|
||||
(form/input-changed! env qualified-key v))]
|
||||
(div :.ui.field {:classes [(when invalid? "error")]}
|
||||
(dom/label field-label (when invalid? " (Required)"))
|
||||
(if read-only?
|
||||
(let [value (first (filter #(= value (:value %)) options))]
|
||||
(:text value))
|
||||
(ui-wrapped-dropdown (cond->
|
||||
{:onChange (fn [v] (onSelect v))
|
||||
:value value
|
||||
:clearable (not required?)
|
||||
:disabled read-only?
|
||||
:options options}))))))
|
||||
|
||||
(let [ui-to-one-picker (comp/factory ToOnePicker {:keyfn (fn [{:keys [attr]}] (::attr/qualified-key attr))})]
|
||||
(defn to-one-picker [env attribute]
|
||||
(ui-to-one-picker {:env env
|
||||
:attr attribute})))
|
||||
|
||||
(defsc ToManyPicker [this {:keys [env attr]}]
|
||||
{:componentDidMount (fn [this]
|
||||
(let [{:keys [env attr]} (comp/props this)
|
||||
form-instance (::form/form-instance env)
|
||||
props (comp/props form-instance)
|
||||
form-class (comp/react-type form-instance)]
|
||||
(picker-options/load-options! form-instance form-class props attr)))}
|
||||
(let [{::form/keys [form-instance]} env
|
||||
visible? (form/field-visible? form-instance attr)]
|
||||
(when visible?
|
||||
(let [{::form/keys [attributes field-options]} (comp/component-options form-instance)
|
||||
{attr-field-options ::form/field-options
|
||||
::attr/keys [qualified-key]} attr
|
||||
field-options (get field-options qualified-key)
|
||||
target-id-key (first (keep (fn [{k ::attr/qualified-key ::attr/keys [target]}]
|
||||
(when (= k qualified-key) target)) attributes))
|
||||
{:keys [style]
|
||||
::picker-options/keys [cache-key query-key]} (merge attr-field-options field-options)
|
||||
cache-key (or (?! cache-key (comp/react-type form-instance) (comp/props form-instance)) query-key)
|
||||
cache-key (or cache-key query-key (log/error "Ref field MUST have either a ::picker-options/cache-key or ::picker-options/query-key in attribute " qualified-key))
|
||||
props (comp/props form-instance)
|
||||
options (get-in props [::picker-options/options-cache cache-key :options])
|
||||
current-selection (into #{}
|
||||
(keep (fn [entity]
|
||||
(when-let [id (get entity target-id-key)]
|
||||
[target-id-key id])))
|
||||
(get props qualified-key))
|
||||
field-label (form/field-label env attr)
|
||||
invalid? (validation/invalid-attribute-value? env attr)
|
||||
read-only? (form/read-only? form-instance attr)
|
||||
validation-message (when invalid? (validation/validation-error-message env attr))]
|
||||
(div :.ui.field {:classes [(when invalid? "error")]}
|
||||
(dom/label field-label " " (when invalid? validation-message))
|
||||
(div :.ui.middle.aligned.celled.list.big
|
||||
{:style {:marginTop "0"}}
|
||||
(if (= style :dropdown)
|
||||
(ui-wrapped-dropdown
|
||||
{:value current-selection
|
||||
:multiple true
|
||||
:disabled read-only?
|
||||
:options options
|
||||
:onChange (fn [v] (form/input-changed! env qualified-key v))})
|
||||
(map (fn [{:keys [text value]}]
|
||||
(let [checked? (contains? current-selection value)]
|
||||
(div :.item {:key value}
|
||||
(div :.content {}
|
||||
(div :.ui.toggle.checkbox {:style {:marginTop "0"}}
|
||||
(dom/input
|
||||
{:type "checkbox"
|
||||
:checked checked?
|
||||
:onChange #(if-not checked?
|
||||
(form/input-changed! env qualified-key (vec (conj current-selection value)))
|
||||
(form/input-changed! env qualified-key (vec (disj current-selection value))))})
|
||||
(dom/label text))))))
|
||||
options))))))))
|
||||
|
||||
(def ui-to-many-picker (comp/factory ToManyPicker {:keyfn :id}))
|
||||
(let [ui-to-many-picker (comp/factory ToManyPicker {:keyfn (fn [{:keys [attr]}] (::attr/qualified-key attr))})]
|
||||
(defn to-many-picker [env attribute]
|
||||
(ui-to-many-picker {:env env
|
||||
:attr attribute})))
|
@ -0,0 +1,79 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.enumerated-field
|
||||
(:require
|
||||
#?@(:cljs
|
||||
[[com.fulcrologic.fulcro.dom :as dom :refer [div label input]]
|
||||
[cljs.reader :refer [read-string]]
|
||||
[com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]]
|
||||
:clj
|
||||
[[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]])
|
||||
[com.fulcrologic.rad.ui-validation :as validation]
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.rad.options-util :refer [?!]]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.components :refer [ui-wrapped-dropdown]]
|
||||
[com.fulcrologic.rad.attributes :as attr]
|
||||
[clojure.string :as str]
|
||||
[com.fulcrologic.rad.form :as form]))
|
||||
|
||||
(defn enumerated-options [{::form/keys [form-instance] :as env} {::attr/keys [qualified-key] :as attribute}]
|
||||
(let [{::attr/keys [enumerated-values]} attribute
|
||||
enumeration-labels (merge
|
||||
(::attr/enumerated-labels attribute)
|
||||
(comp/component-options form-instance ::form/enumerated-labels qualified-key))]
|
||||
;; TODO: Sorting should be something users control
|
||||
(sort-by :text
|
||||
(mapv (fn [k]
|
||||
{:text (?! (get enumeration-labels k (name k)))
|
||||
:value k}) enumerated-values))))
|
||||
|
||||
(defn- render-to-many [{::form/keys [form-instance] :as env} {::form/keys [field-label]
|
||||
::attr/keys [qualified-key] :as attribute}]
|
||||
(when (form/field-visible? form-instance attribute)
|
||||
(let [props (comp/props form-instance)
|
||||
read-only? (form/read-only? form-instance attribute)
|
||||
options (enumerated-options env attribute)
|
||||
selected-ids (set (get props qualified-key))]
|
||||
(div :.ui.field {:key (str qualified-key)}
|
||||
(label (or field-label (some-> qualified-key name str/capitalize)))
|
||||
(div :.ui.middle.aligned.celled.list.big {:style {:marginTop "0"}}
|
||||
(map (fn [{:keys [text value]}]
|
||||
(let [checked? (contains? selected-ids value)]
|
||||
(div :.item {:key value}
|
||||
(div :.content {}
|
||||
(div :.ui.toggle.checkbox {:style {:marginTop "0"}}
|
||||
(dom/input
|
||||
{:type "checkbox"
|
||||
:checked checked?
|
||||
:disabled read-only?
|
||||
:onChange #(let [selection (if-not checked?
|
||||
(conj (set (or selected-ids #{})) value)
|
||||
(disj selected-ids value))]
|
||||
(form/input-changed! env qualified-key selection))})
|
||||
(dom/label text))))))
|
||||
options))))))
|
||||
|
||||
(defn- render-to-one [{::form/keys [form-instance] :as env} {::form/keys [field-label]
|
||||
::attr/keys [qualified-key] :as attribute}]
|
||||
(when (form/field-visible? form-instance attribute)
|
||||
(let [props (comp/props form-instance)
|
||||
read-only? (form/read-only? form-instance attribute)
|
||||
invalid? (validation/invalid-attribute-value? env attribute)
|
||||
user-props (form/field-style-config env attribute :input/props)
|
||||
options (enumerated-options env attribute)
|
||||
value (get props qualified-key)]
|
||||
(div :.ui.field {:key (str qualified-key) :classes [(when invalid? "error")]}
|
||||
(label (str (or field-label (some-> qualified-key name str/capitalize))
|
||||
(when invalid? " (Required)")))
|
||||
(if read-only?
|
||||
(let [value (first (filter #(= value (:value %)) options))]
|
||||
(:text value))
|
||||
(ui-wrapped-dropdown (merge
|
||||
{:disabled read-only?
|
||||
:options options
|
||||
:value value
|
||||
:onChange (fn [v] (form/input-changed! env qualified-key v))}
|
||||
user-props)))))))
|
||||
|
||||
(defn render-field [env {::attr/keys [cardinality] :or {cardinality :one} :as attribute}]
|
||||
(if (= :many cardinality)
|
||||
(render-to-many env attribute)
|
||||
(render-to-one env attribute)))
|
@ -0,0 +1,44 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.field
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[com.fulcrologic.fulcro.components :as comp]
|
||||
[com.fulcrologic.guardrails.core :refer [>defn =>]]
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :refer [div label input span]]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :refer [div label input span]])
|
||||
[com.fulcrologic.rad.attributes :as attr]
|
||||
[com.fulcrologic.fulcro.dom.html-entities :as ent]
|
||||
[com.fulcrologic.rad.form :as form]
|
||||
[com.fulcrologic.rad.ui-validation :as validation]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defn render-field-factory
|
||||
"Create a general field factory using the given input factory as the function to call to draw an input."
|
||||
([input-factory]
|
||||
(render-field-factory {} input-factory))
|
||||
([addl-props input-factory]
|
||||
(fn [{::form/keys [form-instance] :as env} {::attr/keys [type qualified-key] :as attribute}]
|
||||
(let [props (comp/props form-instance)
|
||||
value (or (form/computed-value env attribute)
|
||||
(and attribute (get props qualified-key)))
|
||||
invalid? (validation/invalid-attribute-value? env attribute)
|
||||
validation-message (when invalid? (validation/validation-error-message env attribute))
|
||||
user-props (form/field-style-config env attribute :input/props)
|
||||
field-label (form/field-label env attribute)
|
||||
visible? (form/field-visible? form-instance attribute)
|
||||
read-only? (form/read-only? form-instance attribute)
|
||||
addl-props (if read-only? (assoc addl-props :readOnly "readonly") addl-props)]
|
||||
(when visible?
|
||||
(div :.ui.field {:key (str qualified-key)
|
||||
:classes [(when invalid? "error")]}
|
||||
(label
|
||||
(or field-label (some-> qualified-key name str/capitalize))
|
||||
(when validation-message (str ent/nbsp "(" validation-message ")")))
|
||||
(div :.ui.input {:classes [(when read-only? "transparent")]}
|
||||
(input-factory (merge addl-props
|
||||
{:value value
|
||||
:onBlur (fn [v] (form/input-blur! env qualified-key v))
|
||||
:onChange (fn [v] (form/input-changed! env qualified-key v))}
|
||||
user-props)))
|
||||
#_(when validation-message
|
||||
(div :.ui.error.message
|
||||
(str validation-message)))))))))
|
@ -0,0 +1,391 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.form
|
||||
(:require
|
||||
[com.fulcrologic.rad.attributes :as attr]
|
||||
[com.fulcrologic.rad.options-util :refer [?! narrow-keyword]]
|
||||
[com.fulcrologic.rad.ui-validation :as validation]
|
||||
[com.fulcrologic.rad.form :as form]
|
||||
[com.fulcrologic.rad.control :as control]
|
||||
[com.fulcrologic.rad.blob :as blob]
|
||||
[com.fulcrologic.fulcro.dom.events :as evt]
|
||||
[com.fulcrologic.fulcro-i18n.i18n :refer [tr]]
|
||||
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
|
||||
[com.fulcrologic.fulcro.application :as app]
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :as dom :refer [div h3 button i span]]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :as dom :refer [div h3 button i span]])
|
||||
[com.fulcrologic.fulcro.dom.html-entities :as ent]
|
||||
[com.fulcrologic.fulcro.algorithms.form-state :as fs]
|
||||
[com.fulcrologic.fulcro.algorithms.tempid :as tempid]
|
||||
[com.fulcrologic.fulcro.algorithms.merge :as merge]
|
||||
[taoensso.encore :as enc]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defn render-to-many [{::form/keys [form-instance] :as env} {k ::attr/qualified-key :as attr} {::form/keys [subforms] :as options}]
|
||||
(let [{:semantic-ui/keys [add-position]
|
||||
::form/keys [ui title can-delete? can-add? added-via-upload?]} (get subforms k)
|
||||
form-instance-props (comp/props form-instance)
|
||||
read-only? (form/read-only? form-instance attr)
|
||||
add? (if read-only? false (?! can-add? form-instance attr))
|
||||
delete? (fn [item] (and (not read-only?) (?! can-delete? form-instance item)))
|
||||
items (get form-instance-props k)
|
||||
title (?! (or title (some-> ui (comp/component-options ::form/title)) "") form-instance form-instance-props)
|
||||
invalid? (validation/invalid-attribute-value? env attr)
|
||||
validation-message (validation/validation-error-message env attr)
|
||||
add (when (or (nil? add?) add?)
|
||||
(let [order (if (keyword? add?) add? :append)]
|
||||
(if (?! added-via-upload? env)
|
||||
(dom/input {:type "file"
|
||||
:onChange (fn [evt]
|
||||
(log/info "UPLOAD FILE!!!")
|
||||
(let [new-id (tempid/tempid)
|
||||
js-file (-> evt blob/evt->js-files first)
|
||||
attributes (comp/component-options ui ::form/attributes)
|
||||
id-attr (comp/component-options ui ::form/id)
|
||||
id-key (::attr/qualified-key id-attr)
|
||||
{::attr/keys [qualified-key] :as sha-attr} (first (filter ::blob/store
|
||||
attributes))
|
||||
target (conj (comp/get-ident form-instance) k)
|
||||
new-entity (fs/add-form-config ui
|
||||
{id-key new-id
|
||||
qualified-key ""})]
|
||||
(merge/merge-component! form-instance ui new-entity order target)
|
||||
(blob/upload-file! form-instance sha-attr js-file {:file-ident [id-key new-id]})))})
|
||||
(button :.ui.tiny.icon.button
|
||||
{:onClick (fn [_]
|
||||
(form/add-child! (assoc env
|
||||
::form/order order
|
||||
::form/parent-relation k
|
||||
::form/parent form-instance
|
||||
::form/child-class ui)))}
|
||||
(i :.plus.icon)))))
|
||||
ui-factory (comp/computed-factory ui {:keyfn (fn [item] (-> ui (comp/get-ident item) second str))})]
|
||||
(div :.ui.container {:key (str k)}
|
||||
(h3 title (span ent/nbsp ent/nbsp) (when (or (nil? add-position) (= :top add-position)) add))
|
||||
(when invalid?
|
||||
(div :.ui.error.message
|
||||
validation-message))
|
||||
(if (seq items)
|
||||
(div :.ui.segments
|
||||
(mapv
|
||||
(fn [props]
|
||||
(ui-factory props
|
||||
(merge
|
||||
env
|
||||
{::form/parent form-instance
|
||||
::form/parent-relation k
|
||||
::form/can-delete? (if delete? (delete? props) false)})))
|
||||
items))
|
||||
(div :.ui.message "None."))
|
||||
(when (= :bottom add-position) add))))
|
||||
|
||||
(defn render-to-one [{::form/keys [form-instance] :as env} {k ::attr/qualified-key :as attr} {::form/keys [subforms] :as options}]
|
||||
(let [{::form/keys [ui can-delete? title]} (get subforms k)
|
||||
form-props (comp/props form-instance)
|
||||
props (get form-props k)
|
||||
title (?! (or title (some-> ui (comp/component-options ::form/title)) "") form-instance form-props)
|
||||
ui-factory (comp/computed-factory ui)
|
||||
invalid? (validation/invalid-attribute-value? env attr)
|
||||
validation-message (validation/validation-error-message env attr)
|
||||
std-props {::form/nested? true
|
||||
::form/parent form-instance
|
||||
::form/parent-relation k
|
||||
::form/can-delete? (or
|
||||
(?! can-delete? form-instance form-props)
|
||||
false)}]
|
||||
(cond
|
||||
props
|
||||
(div {:key (str k)}
|
||||
(h3 :.ui.header title)
|
||||
(when invalid?
|
||||
(div :.ui.error.message validation-message))
|
||||
(ui-factory props (merge env std-props)))
|
||||
|
||||
:else
|
||||
(div {:key (str k)}
|
||||
(h3 :.ui.header title)
|
||||
(button {:onClick (fn [] (form/add-child! (assoc env
|
||||
::form/parent-relation k
|
||||
::form/parent form-instance
|
||||
::form/child-class ui)))} "Create")))))
|
||||
|
||||
(defn standard-ref-container [env {::attr/keys [cardinality] :as attr} options]
|
||||
(if (= :many cardinality)
|
||||
(render-to-many env attr options)
|
||||
(render-to-one env attr options)))
|
||||
|
||||
(defn render-single-file [{::form/keys [form-instance] :as env} {k ::attr/qualified-key :as attr} {::form/keys [subforms] :as options}]
|
||||
(let [{::form/keys [ui can-delete?]} (get subforms k)
|
||||
parent (comp/props form-instance)
|
||||
form-props (comp/props form-instance)
|
||||
props (get form-props k)
|
||||
ui-factory (comp/computed-factory ui)
|
||||
label (form/field-label env attr)
|
||||
std-props {::form/nested? true
|
||||
::form/parent form-instance
|
||||
::form/parent-relation k
|
||||
::form/can-delete? (if can-delete?
|
||||
(can-delete? parent props)
|
||||
false)}]
|
||||
(if props
|
||||
(div :.field {:key (str k)}
|
||||
(dom/label label)
|
||||
(ui-factory props (merge env std-props)))
|
||||
(div {:key (str k)}
|
||||
(div "Upload??? (TODO)")))))
|
||||
|
||||
(defsc ManyFiles [this {{::form/keys [form-instance master-form] :as env} :env
|
||||
{k ::attr/qualified-key :as attr} :attribute
|
||||
{::form/keys [subforms] :as options} :options}]
|
||||
{:initLocalState (fn [this] {:input-key (str (rand-int 1000000))})}
|
||||
(let [{:semantic-ui/keys [add-position]
|
||||
::form/keys [ui title can-delete? can-add? sort-children]} (get subforms k)
|
||||
form-instance-props (comp/props form-instance)
|
||||
read-only? (or
|
||||
(form/read-only? master-form attr)
|
||||
(form/read-only? form-instance attr))
|
||||
add? (if read-only? false (?! can-add? form-instance attr))
|
||||
delete? (if read-only? false (fn [item] (?! can-delete? form-instance item)))
|
||||
items (-> form-instance comp/props k
|
||||
(cond->
|
||||
sort-children sort-children))
|
||||
title (?! (or title (some-> ui (comp/component-options ::form/title)) "") form-instance form-instance-props)
|
||||
upload-id (str k "-file-upload")
|
||||
add (when (or (nil? add?) add?)
|
||||
(dom/div
|
||||
(dom/label :.ui.green.button {:htmlFor upload-id}
|
||||
(dom/i :.ui.plus.icon)
|
||||
"Add File")
|
||||
(dom/input {:type "file"
|
||||
;; trick: changing the key on change clears the input, so a failed upload can be retried
|
||||
:key (comp/get-state this :input-key)
|
||||
:id upload-id
|
||||
:style {:zIndex -1
|
||||
:width "1px"
|
||||
:height "1px"
|
||||
:opacity 0}
|
||||
:onChange (fn [evt]
|
||||
(let [new-id (tempid/tempid)
|
||||
js-file (-> evt blob/evt->js-files first)
|
||||
attributes (comp/component-options ui ::form/attributes)
|
||||
id-attr (comp/component-options ui ::form/id)
|
||||
id-key (::attr/qualified-key id-attr)
|
||||
{::attr/keys [qualified-key] :as sha-attr} (first (filter ::blob/store
|
||||
attributes))
|
||||
target (conj (comp/get-ident form-instance) k)
|
||||
new-entity (fs/add-form-config ui
|
||||
{id-key new-id
|
||||
qualified-key ""})]
|
||||
(merge/merge-component! form-instance ui new-entity :append target)
|
||||
(blob/upload-file! form-instance sha-attr js-file {:file-ident [id-key new-id]})
|
||||
(comp/set-state! this {:input-key (str (rand-int 1000000))})))})))
|
||||
ui-factory (comp/computed-factory ui {:keyfn (fn [item] (-> ui (comp/get-ident item) second str))})]
|
||||
(div :.ui.basic.segment {:key (str k)}
|
||||
(dom/h2 :.ui.header title)
|
||||
(when (or (nil? add-position) (= :top add-position)) add)
|
||||
(if (seq items)
|
||||
(div :.ui.very.relaxed.items
|
||||
(mapv
|
||||
(fn [props]
|
||||
(ui-factory props
|
||||
(merge
|
||||
env
|
||||
{::form/parent form-instance
|
||||
::form/parent-relation k
|
||||
::form/can-delete? (if delete? (?! delete? props) false)})))
|
||||
items))
|
||||
(div :.ui.message
|
||||
"None"))
|
||||
|
||||
(when (= :bottom add-position) add))))
|
||||
|
||||
(def ui-many-files (comp/factory ManyFiles {:keyfn (fn [{:keys [attribute]}] (::attr/qualified-key attribute))}))
|
||||
|
||||
(defn file-ref-container
|
||||
[env {::attr/keys [cardinality] :as attr} options]
|
||||
(if (= :many cardinality)
|
||||
(ui-many-files {:env env :attribute attr :options options})
|
||||
(render-single-file env attr options)))
|
||||
|
||||
(defn render-attribute [env attr {::form/keys [subforms] :as options}]
|
||||
(let [{k ::attr/qualified-key} attr]
|
||||
(if (contains? subforms k)
|
||||
(let [render-ref (or (form/ref-container-renderer env attr) standard-ref-container)]
|
||||
(render-ref env attr options))
|
||||
(form/render-field env attr))))
|
||||
|
||||
(def n-fields-string {1 "one field"
|
||||
2 "two fields"
|
||||
3 "three fields"
|
||||
4 "four fields"
|
||||
5 "five fields"
|
||||
6 "six fields"
|
||||
7 "seven fields"})
|
||||
|
||||
(def attribute-map (memoize
|
||||
(fn [attributes]
|
||||
(reduce
|
||||
(fn [m {::attr/keys [qualified-key] :as attr}]
|
||||
(assoc m qualified-key attr))
|
||||
{}
|
||||
attributes))))
|
||||
|
||||
(defn- render-layout* [env options k->attribute layout]
|
||||
(when #?(:clj true :cljs goog.DEBUG)
|
||||
(when-not (and (vector? layout) (every? vector? layout))
|
||||
(log/error "::form/layout must be a vector of vectors!")))
|
||||
(try
|
||||
(into []
|
||||
(map-indexed
|
||||
(fn [idx row]
|
||||
(div {:key idx :className (n-fields-string (count row))}
|
||||
(mapv (fn [col]
|
||||
(enc/if-let [_ k->attribute
|
||||
attr (k->attribute col)]
|
||||
(render-attribute env attr options)
|
||||
(if (some-> options ::control/controls (get col))
|
||||
(control/render-control (::form/form-instance env) col)
|
||||
(log/error "Missing attribute (or lookup) for" col))))
|
||||
row)))
|
||||
layout))
|
||||
(catch #?(:clj Exception :cljs :default) _)))
|
||||
|
||||
(defn render-layout [env {::form/keys [attributes layout] :as options}]
|
||||
(let [k->attribute (attribute-map attributes)]
|
||||
(render-layout* env options k->attribute layout)))
|
||||
|
||||
(defsc TabbedLayout [this env {::form/keys [attributes tabbed-layout] :as options}]
|
||||
{:initLocalState (fn [this]
|
||||
(try
|
||||
{:current-tab 0
|
||||
:tab-details (memoize
|
||||
(fn [attributes tabbed-layout]
|
||||
(let [k->attr (attribute-map attributes)
|
||||
tab-labels (filterv string? tabbed-layout)
|
||||
tab-label->layout (into {}
|
||||
(map vec)
|
||||
(partition 2 (mapv first (partition-by string? tabbed-layout))))]
|
||||
{:k->attr k->attr
|
||||
:tab-labels tab-labels
|
||||
:tab-label->layout tab-label->layout})))}
|
||||
(catch #?(:clj Exception :cljs :default) _
|
||||
(log/error "Cannot build tabs for tabbed layout. Check your tabbed-layout options for" (comp/component-name this)))))}
|
||||
(let [{:keys [tab-details current-tab]} (comp/get-state this)
|
||||
{:keys [k->attr tab-labels tab-label->layout]} (tab-details attributes tabbed-layout)
|
||||
active-layout (some->> current-tab
|
||||
(get tab-labels)
|
||||
(get tab-label->layout))]
|
||||
(div {:key (str current-tab)}
|
||||
(div :.ui.pointing.menu {}
|
||||
(map-indexed
|
||||
(fn [idx title]
|
||||
(dom/a :.item
|
||||
{:key (str idx)
|
||||
:onClick #(comp/set-state! this {:current-tab idx})
|
||||
:classes [(when (= current-tab idx) "active")]}
|
||||
title)) tab-labels))
|
||||
(div :.ui.segment
|
||||
(render-layout* env options k->attr active-layout)))))
|
||||
|
||||
(def ui-tabbed-layout (comp/computed-factory TabbedLayout))
|
||||
|
||||
(declare standard-form-layout-renderer)
|
||||
|
||||
(defsc StandardFormContainer [this {::form/keys [props computed-props form-instance master-form] :as env}]
|
||||
{:shouldComponentUpdate (fn [_ _ _] true)}
|
||||
(let [{::form/keys [can-delete?]} computed-props
|
||||
nested? (not= master-form form-instance)
|
||||
read-only-form? (or
|
||||
(?! (comp/component-options form-instance ::form/read-only?) form-instance)
|
||||
(?! (comp/component-options master-form ::form/read-only?) master-form))
|
||||
invalid? (if read-only-form? false (form/invalid? env))
|
||||
render-fields (or (form/form-layout-renderer env) standard-form-layout-renderer)]
|
||||
(when #?(:cljs goog.DEBUG :clj true)
|
||||
(let [valid? (if read-only-form? true (form/valid? env))
|
||||
dirty? (if read-only-form? false (or (:ui/new? props) (fs/dirty? props)))]
|
||||
(log/debug "Form " (comp/component-name form-instance) " valid? " valid?)
|
||||
(log/debug "Form " (comp/component-name form-instance) " dirty? " dirty?)))
|
||||
(if nested?
|
||||
(div :.ui.segment
|
||||
(div :.ui.form {:classes [(when invalid? "error")]
|
||||
:key (str (comp/get-ident form-instance))}
|
||||
(when can-delete?
|
||||
(button :.ui.icon.primary.right.floated.button {:disabled (not can-delete?)
|
||||
:onClick (fn []
|
||||
(form/delete-child! env))}
|
||||
(i :.times.icon)))
|
||||
(render-fields env)))
|
||||
(let [{::form/keys [title action-buttons controls]} (comp/component-options form-instance)
|
||||
title (?! title form-instance props)
|
||||
action-buttons (if action-buttons action-buttons form/standard-action-buttons)]
|
||||
(div :.ui.container {:key (str (comp/get-ident form-instance))}
|
||||
(div :.ui.top.attached.segment
|
||||
(dom/h3 :.ui.header
|
||||
title
|
||||
(div :.ui.right.floated.buttons
|
||||
(keep #(control/render-control master-form %) action-buttons))))
|
||||
(div :.ui.attached.form {:classes [(when invalid? "error")]}
|
||||
(div :.ui.error.message (tr "The form has errors and cannot be saved."))
|
||||
(div :.ui.attached.segment
|
||||
(render-fields env))))))))
|
||||
|
||||
(def standard-form-container (comp/factory StandardFormContainer))
|
||||
|
||||
(defn standard-form-layout-renderer [{::form/keys [form-instance] :as env}]
|
||||
(let [{::form/keys [attributes layout tabbed-layout] :as options} (comp/component-options form-instance)]
|
||||
(cond
|
||||
(vector? layout) (render-layout env options)
|
||||
(vector? tabbed-layout) (ui-tabbed-layout env options)
|
||||
:else (mapv (fn [attr] (render-attribute env attr options)) attributes))))
|
||||
|
||||
(defn- file-icon-renderer* [{::form/keys [form-instance] :as env}]
|
||||
(let [{::form/keys [attributes] :as options} (comp/component-options form-instance)
|
||||
attribute (first (filter ::blob/store attributes))
|
||||
sha-key (::attr/qualified-key attribute)
|
||||
file-key (blob/filename-key sha-key)
|
||||
url-key (blob/url-key sha-key)
|
||||
props (comp/props form-instance)
|
||||
filename (get props file-key "File")
|
||||
dirty? (fs/dirty? props sha-key)
|
||||
failed? (blob/failed-upload? props sha-key)
|
||||
invalid? (validation/invalid-attribute-value? env attribute)
|
||||
pct (blob/upload-percentage props sha-key)
|
||||
sha (get props sha-key)
|
||||
url (get props url-key)]
|
||||
(if (blob/uploading? props sha-key)
|
||||
(dom/span :.item {:key (str sha)}
|
||||
(dom/div :.ui.tiny.image
|
||||
(dom/i :.huge.file.icon)
|
||||
(dom/div :.ui.active.red.loader {:style {:marginLeft "-10px"}})
|
||||
(dom/div :.ui.bottom.attached.blue.progress {:data-percent pct}
|
||||
(div :.bar {:style {:transitionDuration "300ms"
|
||||
:width pct}}
|
||||
(div :.progress ""))))
|
||||
(div :.middle.aligned.content
|
||||
filename)
|
||||
(dom/button :.ui.red.icon.button {:onClick (fn []
|
||||
(app/abort! form-instance sha)
|
||||
(form/delete-child! env))}
|
||||
(dom/i :.times.icon)))
|
||||
((if dirty? dom/span dom/a) :.item
|
||||
{:target "_blank"
|
||||
:key (str sha)
|
||||
:href (str url "?filename=" filename)
|
||||
:onClick (fn [evt]
|
||||
#?(:cljs (when-not (or (not (blob/blob-downloadable? props sha-key))
|
||||
(js/confirm "View/download?"))
|
||||
(evt/stop-propagation! evt)
|
||||
(evt/prevent-default! evt))))}
|
||||
(dom/div :.ui.tiny.image
|
||||
(if failed?
|
||||
(dom/i :.huge.skull.crossbones.icon)
|
||||
(dom/i :.huge.file.icon)))
|
||||
(div :.middle.aligned.content
|
||||
(str filename (cond failed? " (Upload failed. Delete and try again.)"
|
||||
dirty? " (unsaved)")))
|
||||
(dom/button :.ui.red.icon.button {:onClick (fn [evt]
|
||||
(evt/stop-propagation! evt)
|
||||
(evt/prevent-default! evt)
|
||||
(when #?(:clj true :cljs (js/confirm "Permanently Delete File?"))
|
||||
(form/delete-child! env)))}
|
||||
(dom/i :.times.icon))))))
|
||||
|
||||
(defn file-icon-renderer [env] (file-icon-renderer* env))
|
@ -0,0 +1,34 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.instant-field
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[com.fulcrologic.fulcro.components :as comp]
|
||||
#?(:cljs [com.fulcrologic.fulcro.dom :refer [div label input]]
|
||||
:clj [com.fulcrologic.fulcro.dom-server :refer [div label input]])
|
||||
[com.fulcrologic.fulcro.dom.inputs :as inputs]
|
||||
[taoensso.timbre :as log]
|
||||
[com.fulcrologic.rad.type-support.date-time :as datetime]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]]))
|
||||
|
||||
(def ui-datetime-input
|
||||
(comp/factory (inputs/StringBufferedInput ::DateTimeInput
|
||||
{:model->string (fn [tm]
|
||||
(if tm
|
||||
(datetime/inst->html-datetime-string tm)
|
||||
""))
|
||||
:string->model (fn [s] (some-> s (datetime/html-datetime-string->inst)))})))
|
||||
|
||||
(def ui-date-noon-input
|
||||
(comp/factory (inputs/StringBufferedInput ::DateTimeInput
|
||||
{:model->string (fn [tm]
|
||||
(if tm
|
||||
(str/replace (datetime/inst->html-datetime-string tm) #"T.*$" "")
|
||||
""))
|
||||
:string->model (fn [s] (some-> s (str "T12:00") (datetime/html-datetime-string->inst)))})))
|
||||
|
||||
(def render-field
|
||||
"Uses current timezone and gathers date/time."
|
||||
(render-field-factory {:type "datetime-local"} ui-datetime-input))
|
||||
(def render-date-at-noon-field
|
||||
"Uses current timezone and gathers a local date but saves it as an instant at noon on that date."
|
||||
(render-field-factory {:type "date"} ui-date-noon-input))
|
||||
|
@ -0,0 +1,6 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.int-field
|
||||
(:require
|
||||
[com.fulcrologic.fulcro.dom.inputs :as inputs]
|
||||
[com.fulcrologic.rad.rendering.semantic-ui.field :refer [render-field-factory]]))
|
||||
|
||||
(def render-field (render-field-factory inputs/ui-int-input))
|
@ -0,0 +1,360 @@
|
||||
(ns com.fulcrologic.rad.rendering.semantic-ui.report
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[com.fulcrologic.rad.attributes :as attr]
|
||||
[com.fulcrologic.rad.report :as report]
|
||||
[com.fulcrologic.rad.control :as control]
|
||||
[com.fulcrologic.fulcro.components :as comp]
|
||||
#?@(:cljs
|
||||
[[com.fulcrologic.fulcro.dom :as dom :refer [div]]
|
||||
[com.fulcrologic.semantic-ui.addons.pagination.ui-pagination :as sui-pagination]
|
||||
[ch.lyrion.carbon.data-table-skeleton.ui-data-table-skeleton :refer [ui-data-table-skeleton]]
|
||||
[ch.lyrion.carbon.data-table.ui-data-table :refer [ui-data-table]]
|
||||
[ch.lyrion.carbon.data-table.ui-table :refer [ui-table]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-container :refer [ui-table-container]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-head :refer [ui-table-head]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-header :refer [ui-table-header]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-body :refer [ui-table-body]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-row :refer [ui-table-row]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-cell :refer [ui-table-cell]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-toolbar :refer [ui-table-toolbar]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-toolbar-action :refer [ui-table-toolbar-action]]
|
||||
[ch.lyrion.carbon.data-table.ui-table-toolbar-content :refer [ui-table-toolbar-content]]
|
||||
[ch.lyrion.carbon.link.ui-link :refer [ui-link]]
|
||||
[ch.lyrion.carbon.overflow-menu.ui-overflow-menu :refer [ui-overflow-menu]]
|
||||
[ch.lyrion.carbon.overflow-menu-item.ui-overflow-menu-item :refer [ui-overflow-menu-item]]]
|
||||