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:
parent
0c45d15fe7
commit
0830a26be9
4 changed files with 124 additions and 68 deletions
|
@ -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]]))
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.labeled-input-error {
|
||||||
|
border: 1px solid var(--status-color-error-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@extend .button-primary;
|
@extend .button-primary;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]]]]))
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue