mirror of
https://github.com/penpot/penpot.git
synced 2025-01-21 06:02:32 -05:00
Add error state for invalid name
This commit is contained in:
parent
0c45d15fe7
commit
0830a26be9
4 changed files with 124 additions and 68 deletions
|
@ -34,7 +34,7 @@
|
|||
|
||||
(mf/defc labeled-input
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [label input-props auto-complete?]}]
|
||||
[{:keys [label input-props auto-complete? error?]}]
|
||||
(let [input-props (cond-> input-props
|
||||
:always camel-keys
|
||||
;; Disable auto-complete on form fields for proprietary password managers
|
||||
|
@ -42,6 +42,7 @@
|
|||
(not auto-complete?) (assoc "data-1p-ignore" true
|
||||
"data-lpignore" true
|
||||
: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]
|
||||
[:& :input input-props]]))
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.labeled-input-error {
|
||||
border: 1px solid var(--status-color-error-500) !important;
|
||||
}
|
||||
|
||||
.button {
|
||||
@extend .button-primary;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
[app.main.ui.workspace.tokens.common :as tokens.common]
|
||||
[app.main.ui.workspace.tokens.style-dictionary :as sd]
|
||||
[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]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
@ -36,8 +40,11 @@
|
|||
:else {:left (str (+ x 80) "px")
|
||||
:top (str (- y 70 overflow-fix) "px")})))
|
||||
|
||||
(def viewport
|
||||
(l/derived :vport refs/workspace-local))
|
||||
(defn use-viewport-position-style [x y position]
|
||||
(let [vport (-> (l/derived :vport refs/workspace-local)
|
||||
(mf/deref))]
|
||||
(-> (calculate-position vport position x y)
|
||||
(clj->js))))
|
||||
|
||||
(defn fields->map [fields]
|
||||
(->> (map (fn [{:keys [key] :as field}]
|
||||
|
@ -76,81 +83,119 @@
|
|||
debounced-fn (debounce callback-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/wrap-props false}
|
||||
[{:keys [token-type x y position fields token] :as args}]
|
||||
(let [tokens (sd/use-resolved-workspace-tokens {:debug? true})
|
||||
used-token-names (mf/use-memo
|
||||
(mf/deps tokens)
|
||||
(fn []
|
||||
(-> (into #{} (map (fn [[_ {:keys [name]}]] name) tokens))
|
||||
;; Allow setting token to already used name
|
||||
(disj (:name token)))))
|
||||
vport (mf/deref viewport)
|
||||
style (calculate-position vport position x y)
|
||||
[{:keys [x y position token] :as _args}]
|
||||
(let [wrapper-style (use-viewport-position-style x y position)
|
||||
|
||||
resolved-value (mf/use-state (get-in tokens [(:id token) :value]))
|
||||
;; Tokens
|
||||
tokens (sd/use-resolved-workspace-tokens)
|
||||
existing-token-names (mf/use-memo
|
||||
(mf/deps tokens)
|
||||
(fn []
|
||||
(-> (into #{} (map (fn [[_ {:keys [name]}]] name) tokens))
|
||||
;; Allow setting token to already used name
|
||||
(disj (:name token)))))
|
||||
|
||||
name (mf/use-var (or (:name token) ""))
|
||||
on-update-name #(reset! name (dom/get-target-val %))
|
||||
;; State
|
||||
state* (mf/use-state (merge {:name ""
|
||||
:value ""
|
||||
:description ""}
|
||||
token))
|
||||
state @state*
|
||||
|
||||
token-value (mf/use-var (or (:value token) ""))
|
||||
;; Name
|
||||
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))]
|
||||
|
||||
description (mf/use-var (or (:description token) ""))
|
||||
on-update-description #(reset! description (dom/get-target-val %))
|
||||
;; 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)))
|
||||
|
||||
initial-fields (mapv (fn [field]
|
||||
(assoc field :value (or (:value token) "")))
|
||||
fields)
|
||||
state (mf/use-state initial-fields)
|
||||
|
||||
debounced-update (use-promise-debounce sd/resolve-tokens+
|
||||
(fn [tokens]
|
||||
(let [value (get-in tokens [(:id token) :value])]
|
||||
(reset! resolved-value value)))
|
||||
#(reset! resolved-value nil))
|
||||
|
||||
on-update-state-field (fn [idx e]
|
||||
(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!)))]
|
||||
;; on-submit (fn [e]
|
||||
;; (dom/prevent-default e)
|
||||
;; (let [token-value (-> (fields->map state)
|
||||
;; (first)
|
||||
;; (val))
|
||||
;; token (cond-> {:name (:name state)
|
||||
;; :type (or (:type token) token-type)
|
||||
;; :value token-value
|
||||
;; :description (:description state)}
|
||||
;; (:id token) (assoc :id (:id token)))]
|
||||
;; (st/emit! (dt/add-token token))
|
||||
;; (modal/hide!)))]
|
||||
[:form
|
||||
{:class (stl/css :shadow)
|
||||
:style (clj->js style)
|
||||
:on-submit on-submit}
|
||||
:style wrapper-style
|
||||
#_#_:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
[:& tokens.common/labeled-input {:label "Name"
|
||||
:input-props {:default-value @name
|
||||
:auto-focus true
|
||||
:on-change on-update-name}}]
|
||||
(for [[idx {:keys [label type]}] (d/enumerate @state)]
|
||||
[:* {:key (str "form-field-" idx)}
|
||||
(case type
|
||||
:box-shadow [:p "TODO BOX SHADOW"]
|
||||
[:& tokens.common/labeled-input {:label "Value"
|
||||
:input-props {:default-value @token-value
|
||||
:on-change #(on-update-state-field idx %)}}])])
|
||||
(when (and @resolved-value
|
||||
(not= @resolved-value (:value (first @state))))
|
||||
[:div {:class (stl/css :resolved-value)}
|
||||
[:p @resolved-value]])
|
||||
[:& tokens.common/labeled-input {:label "Description"
|
||||
:input-props {:default-value @description
|
||||
:on-change #(on-update-description %)}}]
|
||||
[:div
|
||||
[:& tokens.common/labeled-input {:label "Name"
|
||||
:error? (:errors/name state)
|
||||
:input-props {:default-value (:name state)
|
||||
:auto-focus true
|
||||
:on-change on-update-name}}]
|
||||
(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)}
|
||||
(case type
|
||||
:box-shadow [:p "TODO BOX SHADOW"]
|
||||
[:& tokens.common/labeled-input {:label "Value"
|
||||
:input-props {:default-value value
|
||||
:on-change #(on-update-field idx %)}}])])
|
||||
;; (when (and @resolved-value
|
||||
;; (not= @resolved-value (:value (first @state*))))
|
||||
;; [:div {:class (stl/css :resolved-value)}
|
||||
;; [:p @resolved-value]])
|
||||
#_[:& tokens.common/labeled-input {:label "Description"
|
||||
:input-props {:default-value (:description state)
|
||||
:on-change #(on-update-description %)}}]
|
||||
[:div {:class (stl/css :button-row)}
|
||||
[:button {:class (stl/css :button)
|
||||
:type "submit"}
|
||||
:type "submit"
|
||||
:disabled disabled?}
|
||||
"Save"]]]]))
|
||||
|
|
|
@ -19,6 +19,12 @@
|
|||
gap: $s-8;
|
||||
}
|
||||
|
||||
.error {
|
||||
@include bodySmallTypography;
|
||||
margin-top: $s-6;
|
||||
color: var(--status-color-error-500);
|
||||
}
|
||||
|
||||
.resolved-value {
|
||||
@include bodySmallTypography;
|
||||
padding: $s-4 $s-6;
|
||||
|
|
Loading…
Add table
Reference in a new issue