0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-01 20:09:04 -05:00

Add error state for invalid name

This commit is contained in:
Florian Schroedl 2024-06-19 17:11:28 +02:00
parent 0c45d15fe7
commit 0830a26be9
4 changed files with 124 additions and 68 deletions

View file

@ -34,7 +34,7 @@
(mf/defc labeled-input (mf/defc labeled-input
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [label input-props auto-complete?]}] [{:keys [label input-props auto-complete? error?]}]
(let [input-props (cond-> input-props (let [input-props (cond-> input-props
:always camel-keys :always camel-keys
;; Disable auto-complete on form fields for proprietary password managers ;; Disable auto-complete on form fields for proprietary password managers
@ -42,6 +42,7 @@
(not auto-complete?) (assoc "data-1p-ignore" true (not auto-complete?) (assoc "data-1p-ignore" true
"data-lpignore" true "data-lpignore" true
:auto-complete "off"))] :auto-complete "off"))]
[:label {:class (stl/css :labeled-input)} [:label {:class (stl/css-case :labeled-input true
:error error?)}
[:span {:class (stl/css :label)} label] [:span {:class (stl/css :label)} label]
[:& :input input-props]])) [:& :input input-props]]))

View file

@ -18,6 +18,10 @@
} }
} }
.labeled-input-error {
border: 1px solid var(--status-color-error-500) !important;
}
.button { .button {
@extend .button-primary; @extend .button-primary;
} }

View file

@ -16,6 +16,10 @@
[app.main.ui.workspace.tokens.common :as tokens.common] [app.main.ui.workspace.tokens.common :as tokens.common]
[app.main.ui.workspace.tokens.style-dictionary :as sd] [app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.util.dom :as dom] [app.util.dom :as dom]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.core :as m]
[malli.error :as me]
[okulary.core :as l] [okulary.core :as l]
[promesa.core :as p] [promesa.core :as p]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -36,8 +40,11 @@
:else {:left (str (+ x 80) "px") :else {:left (str (+ x 80) "px")
:top (str (- y 70 overflow-fix) "px")}))) :top (str (- y 70 overflow-fix) "px")})))
(def viewport (defn use-viewport-position-style [x y position]
(l/derived :vport refs/workspace-local)) (let [vport (-> (l/derived :vport refs/workspace-local)
(mf/deref))]
(-> (calculate-position vport position x y)
(clj->js))))
(defn fields->map [fields] (defn fields->map [fields]
(->> (map (fn [{:keys [key] :as field}] (->> (map (fn [{:keys [key] :as field}]
@ -76,81 +83,119 @@
debounced-fn (debounce callback-fn)] debounced-fn (debounce callback-fn)]
debounced-fn)) debounced-fn))
(defn token-name-schema
"Generate a dynamic schema validation to check if a token name already exists.
`existing-token-names` should be a set of strings."
[existing-token-names]
(let [non-existing-token-schema
(m/-simple-schema
{:type :token/name-exists
:pred #(not (get existing-token-names %))
:type-properties {:error/fn #(str (:value %) " is an already existing token name")
:existing-token-names existing-token-names}})]
(m/schema
[:and
[:string {:min 1 :max 255}]
non-existing-token-schema])))
(comment
(-> (m/explain (token-name-schema #{"foo"}) nil)
(me/humanize))
nil)
(mf/defc tokens-properties-form (mf/defc tokens-properties-form
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [token-type x y position fields token] :as args}] [{:keys [x y position token] :as _args}]
(let [tokens (sd/use-resolved-workspace-tokens {:debug? true}) (let [wrapper-style (use-viewport-position-style x y position)
used-token-names (mf/use-memo
;; Tokens
tokens (sd/use-resolved-workspace-tokens)
existing-token-names (mf/use-memo
(mf/deps tokens) (mf/deps tokens)
(fn [] (fn []
(-> (into #{} (map (fn [[_ {:keys [name]}]] name) tokens)) (-> (into #{} (map (fn [[_ {:keys [name]}]] name) tokens))
;; Allow setting token to already used name ;; Allow setting token to already used name
(disj (:name token))))) (disj (:name token)))))
vport (mf/deref viewport)
style (calculate-position vport position x y)
resolved-value (mf/use-state (get-in tokens [(:id token) :value])) ;; State
state* (mf/use-state (merge {:name ""
:value ""
:description ""}
token))
state @state*
name (mf/use-var (or (:name token) "")) ;; Name
on-update-name #(reset! name (dom/get-target-val %)) finalize-name str/trim
name-schema (mf/use-memo
(mf/deps existing-token-names)
(fn []
(token-name-schema existing-token-names)))
on-update-name (fn [e]
(let [value (dom/get-target-val e)
errors (->> (finalize-name value)
(m/explain name-schema))]
(swap! state* merge {:name value
:errors/name errors})))
disabled? (or
(empty? (finalize-name (:name state)))
(:errors/name state))]
token-value (mf/use-var (or (:value token) "")) ;; on-update-name (fn [e]
;; (let [{:keys [errors] :as state} (mf/deref state*)
;; value (-> (dom/get-target-val e)
;; (str/trim))]
;; (cond-> @state*
;; ;; Remove existing name errors
;; :always (update :errors set/difference #{:empty})
;; (str/empty?) (conj))
;; (swap! state* assoc :name (dom/get-target-val e))))
;; on-update-description #(swap! state* assoc :description (dom/get-target-val %))
;; on-update-field (fn [idx e]
;; (let [value (dom/get-target-val e)]
;; (swap! state* assoc-in [idx :value] value)))
description (mf/use-var (or (:description token) ""))
on-update-description #(reset! description (dom/get-target-val %))
initial-fields (mapv (fn [field] ;; on-submit (fn [e]
(assoc field :value (or (:value token) ""))) ;; (dom/prevent-default e)
fields) ;; (let [token-value (-> (fields->map state)
state (mf/use-state initial-fields) ;; (first)
;; (val))
debounced-update (use-promise-debounce sd/resolve-tokens+ ;; token (cond-> {:name (:name state)
(fn [tokens] ;; :type (or (:type token) token-type)
(let [value (get-in tokens [(:id token) :value])] ;; :value token-value
(reset! resolved-value value))) ;; :description (:description state)}
#(reset! resolved-value nil)) ;; (:id token) (assoc :id (:id token)))]
;; (st/emit! (dt/add-token token))
on-update-state-field (fn [idx e] ;; (modal/hide!)))]
(let [value (dom/get-target-val e)]
(debounced-update)
(swap! state assoc-in [idx :value] value)))
on-submit (fn [e]
(dom/prevent-default e)
(let [token-value (-> (fields->map @state)
(first)
(val))
token (cond-> {:name @name
:type (or (:type token) token-type)
:value token-value}
@description (assoc :description @description)
(:id token) (assoc :id (:id token)))]
(st/emit! (dt/add-token token))
(modal/hide!)))]
[:form [:form
{:class (stl/css :shadow) {:class (stl/css :shadow)
:style (clj->js style) :style wrapper-style
:on-submit on-submit} #_#_:on-submit on-submit}
[:div {:class (stl/css :token-rows)} [:div {:class (stl/css :token-rows)}
[:div
[:& tokens.common/labeled-input {:label "Name" [:& tokens.common/labeled-input {:label "Name"
:input-props {:default-value @name :error? (:errors/name state)
:input-props {:default-value (:name state)
:auto-focus true :auto-focus true
:on-change on-update-name}}] :on-change on-update-name}}]
(for [[idx {:keys [label type]}] (d/enumerate @state)] (when-let [errors (:errors/name state)]
[:p {:class (stl/css :error)} (me/humanize errors)])]
#_(for [[idx {:keys [label type value]}] (d/enumerate (:fields state))]
[:* {:key (str "form-field-" idx)} [:* {:key (str "form-field-" idx)}
(case type (case type
:box-shadow [:p "TODO BOX SHADOW"] :box-shadow [:p "TODO BOX SHADOW"]
[:& tokens.common/labeled-input {:label "Value" [:& tokens.common/labeled-input {:label "Value"
:input-props {:default-value @token-value :input-props {:default-value value
:on-change #(on-update-state-field idx %)}}])]) :on-change #(on-update-field idx %)}}])])
(when (and @resolved-value ;; (when (and @resolved-value
(not= @resolved-value (:value (first @state)))) ;; (not= @resolved-value (:value (first @state*))))
[:div {:class (stl/css :resolved-value)} ;; [:div {:class (stl/css :resolved-value)}
[:p @resolved-value]]) ;; [:p @resolved-value]])
[:& tokens.common/labeled-input {:label "Description" #_[:& tokens.common/labeled-input {:label "Description"
:input-props {:default-value @description :input-props {:default-value (:description state)
:on-change #(on-update-description %)}}] :on-change #(on-update-description %)}}]
[:div {:class (stl/css :button-row)} [:div {:class (stl/css :button-row)}
[:button {:class (stl/css :button) [:button {:class (stl/css :button)
:type "submit"} :type "submit"
:disabled disabled?}
"Save"]]]])) "Save"]]]]))

View file

@ -19,6 +19,12 @@
gap: $s-8; gap: $s-8;
} }
.error {
@include bodySmallTypography;
margin-top: $s-6;
color: var(--status-color-error-500);
}
.resolved-value { .resolved-value {
@include bodySmallTypography; @include bodySmallTypography;
padding: $s-4 $s-6; padding: $s-4 $s-6;