mirror of
https://github.com/penpot/penpot.git
synced 2026-11-07 10:30:40 +00:00
select* resets its label to `default-selected` whenever its options change identity. The access token dialog builds its options vector inline, so picking an expiration re-rendered the dialog and the label snapped back to "Never", although the form kept the picked value and the token was created with it. form-select* now passes the form value as `default-selected` when it holds a non-blank string, so the reset lands on the picked option. The caller's default still applies while the form value is blank. Closes #11663 AI-assisted-by: claude-opus-5 Signed-off-by: chulgil <2044587+chulgil@users.noreply.github.com> Co-authored-by: chulgil <2044587+chulgil@users.noreply.github.com> Co-authored-by: Andrey Antukh <niwi@niwi.nz>
267 lines
9.7 KiB
Clojure
267 lines
9.7 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
|
|
|
(ns app.main.ui.forms
|
|
(:require-macros [app.main.style :as stl])
|
|
(:require
|
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
|
[app.main.ui.ds.controls.input :refer [input*]]
|
|
[app.main.ui.ds.controls.select :refer [select*]]
|
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
|
[app.util.dom :as dom]
|
|
[app.util.forms :as fm]
|
|
[app.util.i18n :refer [tr]]
|
|
[app.util.keyboard :as k]
|
|
[cuerdas.core :as str]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(def context (mf/create-context nil))
|
|
|
|
(mf/defc form-input*
|
|
[{:keys [name trim] :rest props}]
|
|
|
|
(let [form (mf/use-ctx context)
|
|
input-name name
|
|
|
|
touched? (and (contains? (:data @form) input-name)
|
|
(get-in @form [:touched input-name]))
|
|
|
|
error (get-in @form [:errors input-name])
|
|
extra-error (get-in @form [:extra-errors input-name])
|
|
|
|
value (get-in @form [:data input-name] "")
|
|
|
|
on-change
|
|
(mf/use-fn
|
|
(mf/deps input-name)
|
|
(fn [event]
|
|
(let [value (-> event dom/get-target dom/get-input-value)]
|
|
(fm/on-input-change form input-name value trim))))
|
|
|
|
props
|
|
(mf/spread-props props {:on-change on-change
|
|
:value value})
|
|
|
|
props
|
|
(if (or extra-error (and error touched?))
|
|
(mf/spread-props props {:hint-type "error"
|
|
:hint-message (:message (or error extra-error))})
|
|
props)]
|
|
|
|
[:> input* props]))
|
|
|
|
(defn- conj-dedup
|
|
"Adds item into a vector and removes possible duplicates."
|
|
[coll item]
|
|
(into [] (distinct) (conj coll item)))
|
|
|
|
(mf/defc form-multi-input*
|
|
[{:keys [name trim valid-item-fn caution-item-fn] :rest props}]
|
|
(let [form (mf/use-ctx context)
|
|
|
|
touched? (and (contains? (:data @form) name)
|
|
(get-in @form [:touched name]))
|
|
|
|
value* (mf/use-state "")
|
|
value (deref value*)
|
|
|
|
items* (mf/use-state
|
|
(fn []
|
|
(let [initial (get-in @form [:data name])]
|
|
(if (or (vector? initial) (set? initial))
|
|
(mapv (fn [val]
|
|
{:text val
|
|
:valid (valid-item-fn val)
|
|
:caution (caution-item-fn val)})
|
|
initial)
|
|
[]))))
|
|
items (deref items*)
|
|
|
|
on-change
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(let [content (-> event dom/get-target dom/get-input-value)]
|
|
(reset! value* content))))
|
|
|
|
on-key-down
|
|
(mf/use-fn
|
|
(mf/deps value form name valid-item-fn caution-item-fn trim)
|
|
(fn [event]
|
|
(let [val (cond-> value trim str/trim)]
|
|
(cond
|
|
(or (k/enter? event) (k/comma? event) (k/space? event))
|
|
(do
|
|
(dom/prevent-default event)
|
|
(dom/stop-propagation event)
|
|
(swap! form assoc-in [:touched name] true)
|
|
(when (and (valid-item-fn val) (not (str/empty? value)))
|
|
(reset! value* "")
|
|
(swap! form assoc-in [:touched name] false)
|
|
(doseq [v (str/split val #",|\s+")]
|
|
(let [v (str/trim v)]
|
|
(swap! items* conj-dedup {:text v
|
|
:valid (valid-item-fn v)
|
|
:caution (caution-item-fn v)})))))
|
|
|
|
(and (k/backspace? event) (str/empty? value))
|
|
(do
|
|
(dom/prevent-default event)
|
|
(dom/stop-propagation event)
|
|
(swap! items* (fn [items]
|
|
(if (empty? items) items (pop items)))))))))
|
|
|
|
on-paste
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(when-let [clipboard-data (.-clipboardData event)]
|
|
(let [paste-data (.getData clipboard-data "text")]
|
|
(when (and (string? paste-data)
|
|
(re-find #"[,\s]" paste-data))
|
|
(dom/prevent-default event)
|
|
(dom/stop-propagation event)
|
|
|
|
;; Mark as touched
|
|
(swap! form assoc-in [:touched name] true))
|
|
|
|
;; Split pasted text by commas and/or whitespace, add each valid part
|
|
(let [parts (->> (str/split paste-data #",|\s+")
|
|
(map str/trim)
|
|
(remove str/empty?))]
|
|
(doseq [part parts]
|
|
(when (valid-item-fn part)
|
|
(swap! items* conj-dedup {:text part
|
|
:valid true
|
|
:caution (caution-item-fn part)})))
|
|
|
|
;; Reset input value and mark as untouched after successful paste
|
|
(reset! value* "")
|
|
(swap! form assoc-in [:touched name] false))))))
|
|
|
|
on-blur
|
|
(mf/use-fn
|
|
(fn []
|
|
(when-not (get-in @form [:touched name])
|
|
(swap! form assoc-in [:touched name] true))))
|
|
|
|
on-remove-item
|
|
(mf/use-fn
|
|
(fn [item]
|
|
(swap! items* #(filterv (fn [x] (not= x item)) %))))
|
|
|
|
props
|
|
(mf/spread-props props {:value value
|
|
:on-change on-change
|
|
:on-blur on-blur
|
|
:on-key-down on-key-down
|
|
:on-paste on-paste
|
|
:hint-type (when (and touched?
|
|
(not (str/empty? value))
|
|
(not (valid-item-fn value))) "error")})]
|
|
|
|
;; Sync form data whenever items or input value changes.
|
|
;; This ensures the current (unconfirmed) input value is included in the
|
|
;; form data when the user submits without pressing Enter/Space/Comma.
|
|
(mf/with-effect [items value]
|
|
(let [items-text (mapv :text items)
|
|
val (cond-> value trim str/trim)
|
|
combined (if (and (valid-item-fn val) (not (str/empty? val)))
|
|
(conj items-text val)
|
|
items-text)
|
|
data (str/join " " combined)]
|
|
(fm/update-input-value! form name data)))
|
|
|
|
[:div {:class (stl/css :multi-input)}
|
|
[:> input* props]
|
|
|
|
(when-let [items-seq (seq items)]
|
|
[:div {:class (stl/css :multi-input-chips)}
|
|
(for [item items-seq]
|
|
[:div {:class (stl/css :multi-input-chip)
|
|
:key (:text item)}
|
|
[:span {:class (stl/css :multi-input-chip-text)}
|
|
(:text item)]
|
|
[:> icon-button* {:variant "ghost"
|
|
:class (stl/css :multi-input-chip-icon)
|
|
:icon i/close
|
|
:icon-size "s"
|
|
:aria-label (tr "labels.remove")
|
|
:on-click (partial on-remove-item item)}]])])]))
|
|
|
|
(mf/defc form-select*
|
|
[{:keys [name] :rest props}]
|
|
(let [select-name name
|
|
form (mf/use-ctx context)
|
|
value (get-in @form [:data select-name] "")
|
|
|
|
handle-change
|
|
(fn [event]
|
|
(let [value (if (string? event) event (dom/get-target-val event))]
|
|
(fm/on-input-change form select-name value)))
|
|
|
|
props
|
|
(mf/spread-props props {:on-change handle-change
|
|
:value value})
|
|
|
|
;; select* resets its label to `default-selected` whenever its
|
|
;; options change identity, and callers usually build the options
|
|
;; inline, so every re-render snapped the label back to the default
|
|
;; while the form kept the picked value. Pass the form value as the
|
|
;; default so that reset lands on the picked option.
|
|
props
|
|
(if (and (string? value) (not (str/blank? value)))
|
|
(mf/spread-props props {:default-selected value})
|
|
props)]
|
|
|
|
[:> select* props]))
|
|
|
|
(mf/defc form-submit*
|
|
[{:keys [disabled on-submit] :rest props}]
|
|
|
|
(let [form (mf/use-ctx context)
|
|
form-state (when form @form)
|
|
|
|
disabled? (mf/use-memo
|
|
(mf/deps form form-state disabled)
|
|
(fn []
|
|
(boolean
|
|
(or (nil? form)
|
|
(true? disabled)
|
|
(not (:valid form-state))
|
|
(seq (:extra-errors form-state))))))
|
|
|
|
handle-key-down-save
|
|
(mf/use-fn
|
|
(mf/deps on-submit form disabled?)
|
|
(fn [e]
|
|
(when (and (or (k/enter? e) (k/space? e)) (not disabled?))
|
|
(dom/prevent-default e)
|
|
(on-submit form e))))
|
|
|
|
props
|
|
(mf/spread-props props {:on-key-down handle-key-down-save
|
|
:type "submit"})
|
|
|
|
props
|
|
(if disabled?
|
|
(mf/spread-props props {:disabled true
|
|
:on-key-down handle-key-down-save
|
|
:type "submit"})
|
|
props)]
|
|
|
|
[:> button* props]))
|
|
|
|
(mf/defc form*
|
|
[{:keys [on-submit form children class]}]
|
|
(let [on-submit' (mf/use-fn
|
|
(mf/deps on-submit)
|
|
(fn [event]
|
|
(dom/prevent-default event)
|
|
(when (fn? on-submit)
|
|
(on-submit form event))))]
|
|
[:> (mf/provider context) {:value form}
|
|
[:form {:class class :on-submit on-submit'} children]]))
|