Manage empty property values in the combobox in design tab (#6574)

*  Manage empty property values in the combobox in design tab

* 📎 PR changes
This commit is contained in:
luisδμ 2025-05-28 12:41:04 +02:00 committed by GitHub
parent 878952f7b5
commit 46b0e4f0e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 168 additions and 101 deletions

View file

@ -8,8 +8,7 @@
[app.common.data.macros :as dm]
[app.common.types.component :as ctc]
[app.common.types.components-list :as ctcl]
[app.common.types.variant :as ctv]
[cuerdas.core :as str]))
[app.common.types.variant :as ctv]))
(defn find-variant-components
@ -21,11 +20,6 @@
(map #(ctcl/get-component data % true))
reverse))
(defn- dashes-to-end
[property-values]
(let [dashes (if (some #(= % "--") property-values) ["--"] [])]
(concat (remove #(= % "--") property-values) dashes)))
(defn extract-properties-names
[shape data]
@ -42,10 +36,7 @@
(group-by :name)
(map (fn [[k v]]
{:name k
:value (->> v
(map #(if (str/empty? (:value %)) "--" (:value %)))
distinct
dashes-to-end)}))))
:value (->> v (map :value) distinct)}))))
(defn get-variant-mains
[component data]

View file

@ -59,11 +59,12 @@
[:disabled {:optional true} :boolean]
[:default-selected {:optional true} :string]
[:on-change {:optional true} fn?]
[:empty-to-end {:optional true} :boolean]
[:has-error {:optional true} :boolean]])
(mf/defc combobox*
{::mf/schema schema:combobox}
[{:keys [id options class placeholder disabled has-error default-selected max-length on-change] :rest props}]
[{:keys [id options class placeholder disabled has-error default-selected max-length empty-to-end on-change] :rest props}]
(let [is-open* (mf/use-state false)
is-open (deref is-open*)
@ -187,7 +188,6 @@
(kbd/enter? event)
(do
#_(handle-selection focused-value* selected-value* is-open*)
(reset! selected-value* focused-value)
(reset! is-open* false)
(reset! focused-value* nil)
@ -286,4 +286,5 @@
:focused focused-value
:set-ref set-option-ref
:id listbox-id
:empty-to-end empty-to-end
:data-testid "combobox-options"}])]))

View file

@ -15,6 +15,7 @@
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[clojure.string :as str]
[rumext.v2 :as mf]))
(def listbox-id-index (atom 0))
@ -66,13 +67,23 @@
[:class {:optional true} :string]
[:disabled {:optional true} :boolean]
[:default-selected {:optional true} :string]
[:empty-to-end {:optional true} :boolean]
[:on-change {:optional true} fn?]])
(mf/defc select*
{::mf/schema schema:select}
[{:keys [options class disabled default-selected on-change] :rest props}]
(let [open* (mf/use-state false)
open (deref open*)
[{:keys [options class disabled default-selected empty-to-end on-change] :rest props}]
(let [is-open* (mf/use-state false)
is-open (deref is-open*)
selected-value* (mf/use-state #(get-selected-option-id options default-selected))
selected-value (deref selected-value*)
focused-value* (mf/use-state nil)
focused-value (deref focused-value*)
has-focus* (mf/use-state false)
has-focus (deref has-focus*)
listbox-id-ref (mf/use-ref (dm/str "select-listbox-" (swap! listbox-id-index inc)))
options-nodes-refs (mf/use-ref nil)
@ -80,38 +91,11 @@
select-ref (mf/use-ref nil)
listbox-id (mf/ref-val listbox-id-ref)
selected* (mf/use-state #(get-selected-option-id options default-selected))
selected (deref selected*)
empty-selected-value? (str/blank? selected-value)
focused* (mf/use-state nil)
focused (deref focused*)
has-focus* (mf/use-state false)
has-focus (deref has-focus*)
on-click
(mf/use-fn
(mf/deps disabled)
(fn [event]
(dom/stop-propagation event)
(reset! has-focus* true)
(when-not disabled
(swap! open* not))))
on-option-click
(mf/use-fn
(mf/deps on-change)
(fn [event]
(let [node (dom/get-current-target event)
id (dom/get-data node "id")]
(reset! selected* id)
(reset! focused* nil)
(reset! open* false)
(when (fn? on-change)
(on-change id)))))
set-ref
set-option-ref
(mf/use-fn
(mf/deps options-nodes-refs)
(fn [node id]
(let [refs (or (mf/ref-val options-nodes-refs) #js {})
refs (if node
@ -119,47 +103,69 @@
(obj/unset! refs id))]
(mf/set-ref-val! options-nodes-refs refs))))
on-blur
on-option-click
(mf/use-fn
(mf/deps on-change)
(fn [event]
(let [node (dom/get-current-target event)
id (dom/get-data node "id")]
(reset! selected-value* id)
(reset! focused-value* nil)
(reset! is-open* false)
(when (fn? on-change)
(on-change id)))))
on-component-click
(mf/use-fn
(mf/deps disabled)
(fn [event]
(dom/stop-propagation event)
(reset! has-focus* true)
(when-not disabled
(swap! is-open* not))))
on-component-blur
(mf/use-fn
(fn [event]
(let [target (.-relatedTarget event)
outside? (not (.contains (mf/ref-val select-ref) target))]
(when outside?
(reset! focused* nil)
(reset! open* false)
(reset! focused-value* nil)
(reset! is-open* false)
(reset! has-focus* false)))))
on-key-down
on-component-focus
(mf/use-fn
(mf/deps focused disabled)
(fn [_]
(reset! has-focus* true)))
on-button-key-down
(mf/use-fn
(mf/deps focused-value disabled)
(fn [event]
(dom/stop-propagation event)
(when-not disabled
(let [options (mf/ref-val options-ref)
len (alength options)
index (array/find-index #(= (deref focused*) (obj/get % "id")) options)]
(dom/stop-propagation event)
index (array/find-index #(= (deref focused-value*) (obj/get % "id")) options)]
(cond
(kbd/home? event)
(handle-focus-change options focused* 0 options-nodes-refs)
(handle-focus-change options focused-value* 0 options-nodes-refs)
(kbd/up-arrow? event)
(handle-focus-change options focused* (mod (- index 1) len) options-nodes-refs)
(handle-focus-change options focused-value* (mod (- index 1) len) options-nodes-refs)
(kbd/down-arrow? event)
(handle-focus-change options focused* (mod (+ index 1) len) options-nodes-refs)
(handle-focus-change options focused-value* (mod (+ index 1) len) options-nodes-refs)
(or (kbd/space? event) (kbd/enter? event))
(when (deref open*)
(when (deref is-open*)
(dom/prevent-default event)
(handle-selection focused* selected* open*))
(handle-selection focused-value* selected-value* is-open*))
(kbd/esc? event)
(do (reset! open* false)
(reset! focused* nil)))))))
on-focus
(mf/use-fn
(fn [_] (reset! has-focus* true)))
(do (reset! is-open* false)
(reset! focused-value* nil)))))))
class (dm/str class " " (stl/css-case :select true
:focused has-focus))
@ -168,13 +174,13 @@
:role "combobox"
:aria-controls listbox-id
:aria-haspopup "listbox"
:aria-activedescendant focused
:aria-expanded open
:on-key-down on-key-down
:aria-activedescendant focused-value
:aria-expanded is-open
:on-key-down on-button-key-down
:disabled disabled
:on-click on-click})
:on-click on-component-click})
selected-option (get-option options selected)
selected-option (get-option options selected-value)
label (obj/get selected-option "label")
icon (obj/get selected-option "icon")]
@ -182,10 +188,11 @@
(mf/set-ref-val! options-ref options))
[:div {:class (stl/css :select-wrapper)
:on-click on-click
:on-focus on-focus
:on-click on-component-click
:on-focus on-component-focus
:ref select-ref
:on-blur on-blur}
:on-blur on-component-blur}
[:> :button props
[:span {:class (stl/css-case :select-header true
:header-icon (some? icon))}
@ -193,16 +200,19 @@
[:> icon* {:icon-id icon
:size "s"
:aria-hidden true}])
[:span {:class (stl/css :header-label)}
label]]
[:span {:class (stl/css-case :header-label true
:header-label-dimmed empty-selected-value?)}
(if empty-selected-value? "--" label)]]
[:> icon* {:icon-id i/arrow
:class (stl/css :arrow)
:size "m"
:aria-hidden true}]]
(when open
(when is-open
[:> options-dropdown* {:on-click on-option-click
:id listbox-id
:options options
:selected selected
:focused focused
:set-ref set-ref}])]))
:selected selected-value
:focused focused-value
:empty-to-end empty-to-end
:set-ref set-option-ref}])]))

View file

@ -11,6 +11,7 @@
.select-wrapper {
--select-icon-fg-color: var(--color-foreground-secondary);
--select-fg-color: var(--color-foreground-primary);
--select-fg-color-dimmed: var(--color-foreground-secondary);
--select-bg-color: var(--color-background-tertiary);
--select-outline-color: none;
--select-border-color: none;
@ -80,6 +81,10 @@
color: var(--select-fg-color);
}
.header-label-dimmed {
color: var(--select-fg-color-dimmed);
}
.header-icon {
grid-template-columns: auto 1fr;
color: var(--select-icon-fg-color);

View file

@ -9,12 +9,14 @@
[app.main.style :as stl])
(:require
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.util.array :as array]
[app.util.object :as obj]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc option*
{::mf/private true}
[{:keys [id label icon aria-label on-click selected set-ref focused] :rest props}]
[{:keys [id label icon aria-label on-click selected set-ref focused dimmed] :rest props}]
[:> :li {:value id
:class (stl/css-case :option true
@ -38,7 +40,8 @@
:aria-hidden (when label true)
:aria-label (when (not label) aria-label)}])
[:span {:class (stl/css :option-text)} label]
[:span {:class (stl/css-case :option-text true
:option-text-dimmed dimmed)} label]
(when selected
[:> icon*
{:icon-id i/tick
@ -48,11 +51,18 @@
(mf/defc options-dropdown*
{::mf/props :obj}
[{:keys [set-ref on-click options selected focused] :rest props}]
[{:keys [set-ref on-click options selected focused empty-to-end] :rest props}]
(let [props (mf/spread-props props
{:class (stl/css :option-list)
:tab-index "-1"
:role "listbox"})]
:role "listbox"})
options-blank (when empty-to-end
(array/filter #(str/blank? (obj/get % "id")) options))
options (if empty-to-end
(array/filter #((complement str/blank?) (obj/get % "id")) options)
options)]
[:> "ul" props
(for [option ^js options]
(let [id (obj/get option "id")
@ -67,4 +77,26 @@
:aria-label aria-label
:set-ref set-ref
:focused (= id focused)
:on-click on-click}]))]))
:dimmed false
:on-click on-click}]))
(when (seq options-blank)
[:*
(when (seq options)
[:hr {:class (stl/css :option-separator)}])
(for [option ^js options-blank]
(let [id (obj/get option "id")
label (obj/get option "label")
aria-label (obj/get option "aria-label")
icon (obj/get option "icon")]
[:> option* {:selected (= id selected)
:key id
:id id
:label label
:icon icon
:aria-label aria-label
:set-ref set-ref
:focused (= id focused)
:dimmed true
:on-click on-click}]))])]))

View file

@ -13,6 +13,7 @@
--options-dropdown-bg-color: var(--color-background-tertiary);
--options-dropdown-outline-color: none;
--options-dropdown-border-color: var(--color-background-quaternary);
--options-dropdown-empty: var(--color-canvas);
position: absolute;
right: 0;
@ -66,6 +67,10 @@
padding-inline-start: var(--sp-xxs);
}
.option-text-dimmed {
color: var(--options-dropdown-empty);
}
.option-icon {
color: var(--options-dropdown-icon-fg-color);
}
@ -79,3 +84,9 @@
--options-dropdown-fg-color: var(--color-accent-primary);
--options-dropdown-icon-fg-color: var(--color-accent-primary);
}
.option-separator {
border: $b-1 solid var(--options-dropdown-border-color);
margin-top: var(--sp-xs);
margin-bottom: var(--sp-xs);
}

View file

@ -31,6 +31,7 @@
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
[app.main.ui.hooks :as h]
@ -236,7 +237,9 @@
[:div {:class (stl/css :counter)} (str size "/300")])]])))
(defn- get-variant-error-message [errors]
(defn- get-variant-error-message
"Generate error message depending on the selected variants"
[errors]
(cond
(and (= (count errors) 1) (some? (first errors)))
(tr "workspace.options.component.variant.malformed.single.one")
@ -250,6 +253,16 @@
:else nil))
(defn- get-variant-options
"Get variant options for a given property name"
[prop-name prop-vals]
(->> (filter #(= (:name %) prop-name) prop-vals)
first
:value
(map (fn [val] {:id val
:label (if (str/blank? val) (str "(" (tr "labels.empty") ")") val)}))))
(mf/defc component-variant-main-instance*
[{:keys [components shapes data]}]
(let [component (first components)
@ -271,23 +284,17 @@
variant-errors (mapv :variant-error shapes)
variant-error-msg (get-variant-error-message variant-errors)
empty-indicator "--"
get-options
(mf/use-fn
(mf/deps prop-vals)
(fn [prop-name]
(->> (filter #(= (:name %) prop-name) prop-vals)
first
:value
(map (fn [val] {:label val :id val})))))
(get-variant-options prop-name prop-vals)))
update-property-value
(mf/use-fn
(mf/deps component-ids)
(fn [pos value]
(let [value (str/trim value)
value (if (= value empty-indicator) "" value)]
(let [value (d/nilv (str/trim value) "")]
(doseq [id component-ids]
(st/emit! (dwv/update-property-value id pos value))
(st/emit! (dwv/update-error id nil))))))
@ -315,9 +322,10 @@
(let [mixed-value? (= (:value prop) false)]
[:> combobox* {:id (str "variant-prop-" variant-id "-" pos)
:placeholder (if mixed-value? (tr "settings.multiple") empty-indicator)
:placeholder (if mixed-value? (tr "settings.multiple") "--")
:default-selected (if mixed-value? "" (:value prop))
:options (clj->js (get-options (:name prop)))
:empty-to-end true
:on-change (partial update-property-value pos)}])]])]
(when variant-error-msg
@ -329,6 +337,7 @@
[:div {:class (stl/css :variant-error-darken)}
(tr "workspace.options.component.variant.malformed.structure.example")]])]))
(mf/defc component-variant*
[{:keys [component shape data]}]
(let [component-id (:id component)
@ -342,13 +351,11 @@
prop-vals (mf/with-memo [data objects variant-id]
(cfv/extract-properties-values data objects variant-id))
get-options-vals
get-options
(mf/use-fn
(mf/deps prop-vals)
(fn [prop-name]
(->> (filter #(= (:name %) prop-name) prop-vals)
first
:value)))
(get-variant-options prop-name prop-vals)))
switch-component
(mf/use-fn
@ -360,7 +367,7 @@
valid-comps (->> variant-components
(remove #(= (:id %) component-id))
(filter #(= (dm/get-in % [:variant-properties pos :value]) val)))
nearest-comp (apply min-key #(ctv/distance target-props (:variant-properties %)) valid-comps)]
nearest-comp (apply min-key #(ctv/distance target-props (:variant-properties %)) valid-comps)]
(when nearest-comp
(st/emit! (dwl/component-swap shape (:component-file shape) (:id nearest-comp) true)))))))]
@ -370,9 +377,11 @@
[:*
[:span {:class (stl/css :variant-property-name)}
(:name prop)]
[:& select {:default-value (if (str/empty? (:value prop)) "--" (:value prop))
:options (clj->js (get-options-vals (:name prop)))
:on-change #(switch-component pos %)}]]])]))
[:> select* {:default-selected (:value prop)
:options (clj->js (get-options (:name prop)))
:empty-to-end true
:on-change (partial switch-component pos)}]]])]))
(mf/defc component-swap-item
{::mf/props :obj}

View file

@ -2051,6 +2051,10 @@ msgstr "Edit file"
msgid "labels.editor"
msgstr "Editor"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:263
msgid "labels.empty"
msgstr "Empty"
#: src/app/main/ui/dashboard/import.cljs:294
msgid "labels.error"
msgstr "Error"

View file

@ -2088,6 +2088,10 @@ msgstr "Editar archivo"
msgid "labels.editor"
msgstr "Edición"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:263
msgid "labels.empty"
msgstr "Vacío"
#: src/app/main/ui/dashboard/import.cljs:294
msgid "labels.error"
msgstr "Error"