mirror of
https://github.com/penpot/penpot.git
synced 2025-02-01 20:09:04 -05:00
Add shared error handling
This commit is contained in:
parent
308fff05c3
commit
3a21643158
3 changed files with 106 additions and 69 deletions
22
frontend/src/app/main/ui/workspace/tokens/errors.cljs
Normal file
22
frontend/src/app/main/ui/workspace/tokens/errors.cljs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
(ns app.main.ui.workspace.tokens.errors)
|
||||||
|
|
||||||
|
(def error-codes
|
||||||
|
{:error.token/direct-self-reference
|
||||||
|
{:error/fn #(str "Token has self reference in name: " %)}
|
||||||
|
:error.token/invalid-color
|
||||||
|
{:error/fn #(str "Invalid color value: " %)}
|
||||||
|
:error.style-dictionary/missing-reference
|
||||||
|
{:error/fn #(str "Could not resolve reference token with name: " %)}
|
||||||
|
:error.style-dictionary/invalid-token-value
|
||||||
|
{:error/message "Invalid token value"}
|
||||||
|
:error/unknown
|
||||||
|
{:error/message "Unknown error"}})
|
||||||
|
|
||||||
|
(defn humanize-errors [v errors]
|
||||||
|
(->> errors
|
||||||
|
(map (fn [err]
|
||||||
|
(let [err' (get error-codes err err)]
|
||||||
|
(cond
|
||||||
|
(:error/fn err') ((:error/fn err') v)
|
||||||
|
(:error/message err') (:error/message err')
|
||||||
|
:else err'))))))
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.ui.workspace.tokens.form
|
(ns app.main.ui.workspace.tokens.form
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
|
[app.main.ui.workspace.tokens.errors :as wte]
|
||||||
["lodash.debounce" :as debounce]
|
["lodash.debounce" :as debounce]
|
||||||
[app.common.colors :as cc]
|
[app.common.colors :as cc]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
@ -25,7 +26,8 @@
|
||||||
[malli.core :as m]
|
[malli.core :as m]
|
||||||
[malli.error :as me]
|
[malli.error :as me]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]
|
||||||
|
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]))
|
||||||
|
|
||||||
;; Schemas ---------------------------------------------------------------------
|
;; Schemas ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -87,36 +89,39 @@ Token names should only contain letters and digits separated by . characters.")}
|
||||||
|
|
||||||
;; Component -------------------------------------------------------------------
|
;; Component -------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defn token-self-reference?
|
||||||
|
[token-name input]
|
||||||
|
(let [token-references (wtt/find-token-references input)
|
||||||
|
self-reference? (get token-references token-name)]
|
||||||
|
self-reference?))
|
||||||
|
|
||||||
(defn validate-token-value+
|
(defn validate-token-value+
|
||||||
"Validates token value by resolving the value `input` using `StyleDictionary`.
|
"Validates token value by resolving the value `input` using `StyleDictionary`.
|
||||||
Returns a promise of either resolved tokens or rejects with an error state."
|
Returns a promise of either resolved tokens or rejects with an error state."
|
||||||
[{:keys [input name-value token tokens]}]
|
[{:keys [input name-value token tokens]}]
|
||||||
(let [empty-input? (empty? (str/trim input))
|
(let [ ;; When creating a new token we dont have a token name yet,
|
||||||
;; Check if the given value contains a reference that is the current token-name
|
;; so we use a temporary token name that hopefully doesn't clash with any of the users token names
|
||||||
;; When creating a new token we dont have a token name yet,
|
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
|
||||||
;; so we use a temporary token name that hopefully doesn't clash with any of the users token names.
|
|
||||||
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)
|
|
||||||
token-references (wtt/find-token-references input)
|
|
||||||
direct-self-reference? (get token-references token-name)]
|
|
||||||
(cond
|
(cond
|
||||||
empty-input? (p/rejected nil)
|
(empty? (str/trim input))
|
||||||
direct-self-reference? (p/rejected :error/token-direct-self-reference)
|
(p/rejected {:errors #{:error/empty-input}})
|
||||||
:else (let [token-id (or (:id token) (random-uuid))
|
|
||||||
new-tokens (update tokens token-name merge {:id token-id
|
(token-self-reference? token-name input)
|
||||||
:value input
|
(p/rejected {:errors #{:error.token/direct-self-reference}})
|
||||||
:name token-name
|
|
||||||
:type (:type token)})]
|
:else
|
||||||
(-> (sd/resolve-tokens+ new-tokens {:names-map? true
|
(let [token-id (or (:id token) (random-uuid))
|
||||||
:debug? true})
|
new-tokens (update tokens token-name merge {:id token-id
|
||||||
(p/then
|
:value input
|
||||||
(fn [resolved-tokens]
|
:name token-name
|
||||||
(js/console.log "resolved-tokens" resolved-tokens)
|
:type (:type token)})]
|
||||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
|
(-> (sd/resolve-tokens+ new-tokens {:names-map? true})
|
||||||
(cond
|
(p/then
|
||||||
resolved-value (p/resolved resolved-token)
|
(fn [resolved-tokens]
|
||||||
(sd/missing-reference-error? errors) (p/rejected :error/token-missing-reference)
|
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
|
||||||
:else (p/rejected :error/unknown-error)))))
|
(cond
|
||||||
(p/catch js/console.log))))))
|
resolved-value (p/resolved resolved-token)
|
||||||
|
:else (p/rejected {:errors (or errors #{:error/unknown-error})}))))))))))
|
||||||
|
|
||||||
(defn use-debonced-resolve-callback
|
(defn use-debonced-resolve-callback
|
||||||
"Resolves a token values using `StyleDictionary`.
|
"Resolves a token values using `StyleDictionary`.
|
||||||
|
@ -139,9 +144,10 @@ Token names should only contain letters and digits separated by . characters.")}
|
||||||
:name-value @name-ref
|
:name-value @name-ref
|
||||||
:token token
|
:token token
|
||||||
:tokens tokens})
|
:tokens tokens})
|
||||||
(p/finally (fn [x err]
|
(p/finally
|
||||||
(when-not (timeout-outdated-cb?)
|
(fn [x err]
|
||||||
(callback (or err x))))))))
|
(when-not (timeout-outdated-cb?)
|
||||||
|
(callback (or err x))))))))
|
||||||
timeout))))]
|
timeout))))]
|
||||||
debounced-resolver-callback))
|
debounced-resolver-callback))
|
||||||
|
|
||||||
|
@ -149,7 +155,6 @@ Token names should only contain letters and digits separated by . characters.")}
|
||||||
|
|
||||||
(mf/defc ramp
|
(mf/defc ramp
|
||||||
[{:keys [color on-change]}]
|
[{:keys [color on-change]}]
|
||||||
(js/console.log "color" color)
|
|
||||||
(let [dragging? (mf/use-state)
|
(let [dragging? (mf/use-state)
|
||||||
[r g b] (cc/hex->rgb color)
|
[r g b] (cc/hex->rgb color)
|
||||||
[h s v] (cc/hex->hsv color)
|
[h s v] (cc/hex->hsv color)
|
||||||
|
@ -166,6 +171,20 @@ Token names should only contain letters and digits separated by . characters.")}
|
||||||
:on-finish-drag #(reset! dragging? false)
|
:on-finish-drag #(reset! dragging? false)
|
||||||
:on-change on-change'}]))
|
:on-change on-change'}]))
|
||||||
|
|
||||||
|
(mf/defc token-value-or-errors
|
||||||
|
[{:keys [result-or-errors]}]
|
||||||
|
(let [{:keys [errors]} result-or-errors
|
||||||
|
empty-message? (or (nil? result-or-errors)
|
||||||
|
(= errors #{:error/empty-input}))]
|
||||||
|
[:div {:class (stl/css-case :resolved-value true
|
||||||
|
:resolved-value-placeholder empty-message?
|
||||||
|
:resolved-value-error (seq errors))}
|
||||||
|
(cond
|
||||||
|
empty-message? "Enter token value"
|
||||||
|
errors (->> (wte/humanize-errors (:value result-or-errors) errors)
|
||||||
|
(str/join "\n"))
|
||||||
|
:else [:p result-or-errors])]))
|
||||||
|
|
||||||
(mf/defc form
|
(mf/defc form
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[{:keys [token token-type]}]
|
[{:keys [token token-type]}]
|
||||||
|
@ -219,11 +238,11 @@ Token names should only contain letters and digits separated by . characters.")}
|
||||||
token-resolve-result (mf/use-state (get-in resolved-tokens [(wtt/token-identifier token) :resolved-value]))
|
token-resolve-result (mf/use-state (get-in resolved-tokens [(wtt/token-identifier token) :resolved-value]))
|
||||||
set-resolve-value (mf/use-callback
|
set-resolve-value (mf/use-callback
|
||||||
(fn [token-or-err]
|
(fn [token-or-err]
|
||||||
(let [v (cond
|
(let [error? (:errors token-or-err)
|
||||||
(= token-or-err :error/token-direct-self-reference) token-or-err
|
v (if error?
|
||||||
(= token-or-err :error/token-missing-reference) token-or-err
|
token-or-err
|
||||||
(:resolved-value token-or-err) (:resolved-value token-or-err))]
|
(:resolved-value token-or-err))]
|
||||||
(when color? (reset! color v))
|
(when color? (reset! color (if error? nil v)))
|
||||||
(reset! token-resolve-result v))))
|
(reset! token-resolve-result v))))
|
||||||
on-update-value-debounced (use-debonced-resolve-callback name-ref token active-theme-tokens set-resolve-value)
|
on-update-value-debounced (use-debonced-resolve-callback name-ref token active-theme-tokens set-resolve-value)
|
||||||
on-update-value (mf/use-callback
|
on-update-value (mf/use-callback
|
||||||
|
@ -237,8 +256,7 @@ Token names should only contain letters and digits separated by . characters.")}
|
||||||
(fn [hex-value]
|
(fn [hex-value]
|
||||||
(reset! value-ref hex-value)
|
(reset! value-ref hex-value)
|
||||||
(on-update-value-debounced hex-value)))
|
(on-update-value-debounced hex-value)))
|
||||||
value-error? (when (keyword? @token-resolve-result)
|
value-error? (seq (:errors @token-resolve-result))
|
||||||
(= (namespace @token-resolve-result) "error"))
|
|
||||||
valid-value-field? (and
|
valid-value-field? (and
|
||||||
(not value-error?)
|
(not value-error?)
|
||||||
(valid-value? @token-resolve-result))
|
(valid-value? @token-resolve-result))
|
||||||
|
@ -317,24 +335,19 @@ Token names should only contain letters and digits separated by . characters.")}
|
||||||
:on-change on-update-value}
|
:on-change on-update-value}
|
||||||
:render-right (when color?
|
:render-right (when color?
|
||||||
(mf/fnc []
|
(mf/fnc []
|
||||||
[:div {:class (stl/css :color-bullet)
|
[:div {:class (stl/css :color-bullet)
|
||||||
:on-click #(swap! color-ramp-open? not)}
|
:on-click #(swap! color-ramp-open? not)}
|
||||||
(if @color
|
(if-let [hex (some-> @color tinycolor/valid-color tinycolor/->hex)]
|
||||||
[:& color-bullet {:color @color
|
[:& color-bullet {:color hex
|
||||||
:mini? true}]
|
:mini? true}]
|
||||||
[:div {:class (stl/css :color-bullet-placeholder)}])]))}]
|
[:div {:class (stl/css :color-bullet-placeholder)}])]))}]
|
||||||
(when @color-ramp-open?
|
(when @color-ramp-open?
|
||||||
[:& ramp {:color (or @token-resolve-result (:value token))
|
[:& ramp {:color (some-> (or @token-resolve-result (:value token))
|
||||||
|
(tinycolor/valid-color)
|
||||||
|
(tinycolor/->hex))
|
||||||
:on-change on-update-color}])
|
:on-change on-update-color}])
|
||||||
[:div {:class (stl/css-case :resolved-value true
|
[:& token-value-or-errors {:result-or-errors @token-resolve-result}]
|
||||||
:resolved-value-placeholder (nil? @token-resolve-result)
|
|
||||||
:resolved-value-error value-error?)}
|
|
||||||
(case @token-resolve-result
|
|
||||||
:error/token-direct-self-reference "Token has self reference"
|
|
||||||
:error/token-missing-reference "Token has missing reference"
|
|
||||||
:error/unknown-error ""
|
|
||||||
nil "Enter token value"
|
|
||||||
[:p @token-resolve-result])]
|
|
||||||
[:div
|
[:div
|
||||||
[:& tokens.common/labeled-input {:label "Description"
|
[:& tokens.common/labeled-input {:label "Description"
|
||||||
:input-props {:default-value @description-ref
|
:input-props {:default-value @description-ref
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
(:require
|
(:require
|
||||||
["@tokens-studio/sd-transforms" :as sd-transforms]
|
["@tokens-studio/sd-transforms" :as sd-transforms]
|
||||||
["style-dictionary$default" :as sd]
|
["style-dictionary$default" :as sd]
|
||||||
[app.common.data :refer [ordered-map]]
|
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
|
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
|
||||||
[app.main.ui.workspace.tokens.token :as wtt]
|
[app.main.ui.workspace.tokens.token :as wtt]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
|
@ -61,19 +61,15 @@
|
||||||
(js/console.log "Resolved tokens" resolved-tokens))
|
(js/console.log "Resolved tokens" resolved-tokens))
|
||||||
resolved-tokens))))))
|
resolved-tokens))))))
|
||||||
|
|
||||||
|
|
||||||
(defn humanize-errors [{:keys [errors value] :as _token}]
|
(defn humanize-errors [{:keys [errors value] :as _token}]
|
||||||
(->> (map (fn [err]
|
(->> (map (fn [err]
|
||||||
(case err
|
(case err
|
||||||
:style-dictionary/missing-reference (str "Could not resolve reference token with the name: " value)
|
:error.style-dictionary/missing-reference (str "Could not resolve reference token with the name: " value)
|
||||||
nil))
|
nil))
|
||||||
errors)
|
errors)
|
||||||
(str/join "\n")))
|
(str/join "\n")))
|
||||||
|
|
||||||
(defn missing-reference-error?
|
|
||||||
[errors]
|
|
||||||
(and (set? errors)
|
|
||||||
(get errors :style-dictionary/missing-reference)))
|
|
||||||
|
|
||||||
(defn resolve-tokens+
|
(defn resolve-tokens+
|
||||||
[tokens & {:keys [names-map? debug?] :as config}]
|
[tokens & {:keys [names-map? debug?] :as config}]
|
||||||
(p/let [sd-tokens (-> (wtt/token-names-tree tokens)
|
(p/let [sd-tokens (-> (wtt/token-names-tree tokens)
|
||||||
|
@ -85,15 +81,21 @@
|
||||||
(uuid (.-uuid (.-id cur))))
|
(uuid (.-uuid (.-id cur))))
|
||||||
{:keys [type] :as origin-token} (get tokens identifier)
|
{:keys [type] :as origin-token} (get tokens identifier)
|
||||||
value (.-value cur)
|
value (.-value cur)
|
||||||
parsed-value (case type
|
token-or-err (case type
|
||||||
:color (wtt/parse-token-color-value value)
|
:color (if-let [tc (tinycolor/valid-color value)]
|
||||||
(wtt/parse-token-value value))
|
{:value value :unit (tinycolor/color-format tc)}
|
||||||
resolved-token (if (not parsed-value)
|
{:errors #{:error.token/invalid-color}})
|
||||||
(assoc origin-token :errors [:style-dictionary/missing-reference])
|
(or (wtt/parse-token-value value)
|
||||||
(assoc origin-token
|
(if-let [references (seq (wtt/find-token-references value))]
|
||||||
:resolved-value (:value parsed-value)
|
{:errors #{:error.style-dictionary/missing-reference}
|
||||||
:resolved-unit (:unit parsed-value)))]
|
:references references}
|
||||||
(assoc acc (wtt/token-identifier resolved-token) resolved-token)))
|
{:errors #{:error.style-dictionary/invalid-token-value}})))
|
||||||
|
output-token (if (:errors token-or-err)
|
||||||
|
(merge origin-token token-or-err)
|
||||||
|
(assoc origin-token
|
||||||
|
:resolved-value (:value token-or-err)
|
||||||
|
:unit (:unit token-or-err)))]
|
||||||
|
(assoc acc (wtt/token-identifier output-token) output-token)))
|
||||||
{} sd-tokens)]
|
{} sd-tokens)]
|
||||||
(when debug?
|
(when debug?
|
||||||
(js/console.log "Resolved tokens" resolved-tokens))
|
(js/console.log "Resolved tokens" resolved-tokens))
|
||||||
|
|
Loading…
Add table
Reference in a new issue